<?php

/**
 * @see       https://github.com/laminas/laminas-mvc for the canonical source repository
 * @copyright https://github.com/laminas/laminas-mvc/blob/master/COPYRIGHT.md
 * @license   https://github.com/laminas/laminas-mvc/blob/master/LICENSE.md New BSD License
 */

namespace Laminas\Mvc\Controller\Plugin;

use Laminas\Http\Header\Accept\FieldValuePart\AbstractFieldValuePart;
use Laminas\Http\Request;
use Laminas\Mvc\Exception\DomainException;
use Laminas\Mvc\Exception\InvalidArgumentException;
use Laminas\Mvc\InjectApplicationEventInterface;
use Laminas\Mvc\MvcEvent;
use Laminas\View\Model\ModelInterface;

/**
 * Controller Plugin to assist in selecting an appropriate View Model type based on the
 * User Agent's accept header.
 */
class AcceptableViewModelSelector extends AbstractPlugin
{
    /**
     *
     * @var string the Key to inject the name of a viewmodel with in an Accept Header
     */
    const INJECT_VIEWMODEL_NAME = '_internalViewModel';

    /**
     *
     * @var \Laminas\Mvc\MvcEvent
     */
    protected $event;

    /**
     *
     * @var \Laminas\Http\Request
     */
    protected $request;

    /**
     * Default array to match against.
     *
     * @var Array
     */
    protected $defaultMatchAgainst;

    /**
     *
     * @var string Default ViewModel
     */
    protected $defaultViewModelName = 'Laminas\View\Model\ViewModel';

    /**
     * Detects an appropriate viewmodel for request.
     *
     * @param array $matchAgainst (optional) The Array to match against
     * @param bool $returnDefault (optional) If no match is available. Return default instead
     * @param AbstractFieldValuePart|null $resultReference (optional) The object that was matched
     * @throws InvalidArgumentException If the supplied and matched View Model could not be found
     * @return ModelInterface|null
     */
    public function __invoke(
        array $matchAgainst = null,
        $returnDefault = true,
        & $resultReference = null
    ) {
        return $this->getViewModel($matchAgainst, $returnDefault, $resultReference);
    }

    /**
     * Detects an appropriate viewmodel for request.
     *
     * @param array $matchAgainst (optional) The Array to match against
     * @param bool $returnDefault (optional) If no match is available. Return default instead
     * @param AbstractFieldValuePart|null $resultReference (optional) The object that was matched
     * @throws InvalidArgumentException If the supplied and matched View Model could not be found
     * @return ModelInterface|null
     */
    public function getViewModel(
        array $matchAgainst = null,
        $returnDefault = true,
        & $resultReference = null
    ) {
        $name = $this->getViewModelName($matchAgainst, $returnDefault, $resultReference);

        if (!$name) {
            return;
        }

        if (!class_exists($name)) {
            throw new InvalidArgumentException('The supplied View Model could not be found');
        }

        return new $name();
    }

    /**
     * Detects an appropriate viewmodel name for request.
     *
     * @param array $matchAgainst (optional) The Array to match against
     * @param bool $returnDefault (optional) If no match is available. Return default instead
     * @param AbstractFieldValuePart|null $resultReference (optional) The object that was matched.
     * @return ModelInterface|null Returns null if $returnDefault = false and no match could be made
     */
    public function getViewModelName(
        array $matchAgainst = null,
        $returnDefault = true,
        & $resultReference = null
    ) {
        $res = $this->match($matchAgainst);
        if ($res) {
            $resultReference = $res;
            return $this->extractViewModelName($res);
        }

        if ($returnDefault) {
            return $this->defaultViewModelName;
        }
    }

    /**
     * Detects an appropriate viewmodel name for request.
     *
     * @param array $matchAgainst (optional) The Array to match against
     * @return AbstractFieldValuePart|null The object that was matched
     */
    public function match(array $matchAgainst = null)
    {
        $request        = $this->getRequest();
        $headers        = $request->getHeaders();

        if ((!$matchAgainst && !$this->defaultMatchAgainst) || !$headers->has('accept')) {
            return;
        }

        if (!$matchAgainst) {
            $matchAgainst = $this->defaultMatchAgainst;
        }

        $matchAgainstString = '';
        foreach ($matchAgainst as $modelName => $modelStrings) {
            foreach ((array) $modelStrings as $modelString) {
                $matchAgainstString .= $this->injectViewModelName($modelString, $modelName);
            }
        }

        /** @var $accept \Laminas\Http\Header\Accept */
        $accept = $headers->get('Accept');
        if (($res = $accept->match($matchAgainstString)) === false) {
            return;
        }

        return $res;
    }

    /**
     * Set the default View Model (name) to return if no match could be made
     * @param string $defaultViewModelName The default View Model name
     * @return AcceptableViewModelSelector provides fluent interface
     */
    public function setDefaultViewModelName($defaultViewModelName)
    {
        $this->defaultViewModelName = (string) $defaultViewModelName;
        return $this;
    }

    /**
     * Set the default View Model (name) to return if no match could be made
     * @return string
     */
    public function getDefaultViewModelName()
    {
        return $this->defaultViewModelName;
    }

    /**
     * Set the default Accept Types and View Model combinations to match against if none are specified.
     *
     * @param array $matchAgainst (optional) The Array to match against
     * @return AcceptableViewModelSelector provides fluent interface
     */
    public function setDefaultMatchAgainst(array $matchAgainst = null)
    {
        $this->defaultMatchAgainst = $matchAgainst;
        return $this;
    }

    /**
     * Get the default Accept Types and View Model combinations to match against if none are specified.
     *
     * @return array|null
     */
    public function getDefaultMatchAgainst()
    {
        return $this->defaultMatchAgainst;
    }

    /**
     * Inject the viewmodel name into the accept header string
     *
     * @param string $modelAcceptString
     * @param string $modelName
     * @return string
     */
    protected function injectViewModelName($modelAcceptString, $modelName)
    {
        $modelName = str_replace('\\', '|', $modelName);
        return $modelAcceptString . '; ' . self::INJECT_VIEWMODEL_NAME . '="' . $modelName . '", ';
    }

    /**
     * Extract the viewmodel name from a match
     * @param AbstractFieldValuePart $res
     * @return string
     */
    protected function extractViewModelName(AbstractFieldValuePart $res)
    {
        $modelName = $res->getMatchedAgainst()->params[self::INJECT_VIEWMODEL_NAME];
        return str_replace('|', '\\', $modelName);
    }

    /**
     * Get the request
     *
     * @return Request
     * @throws DomainException if unable to find request
     */
    protected function getRequest()
    {
        if ($this->request) {
            return $this->request;
        }

        $event = $this->getEvent();
        $request = $event->getRequest();
        if (!$request instanceof Request) {
            throw new DomainException(
                    'The event used does not contain a valid Request, but must.'
            );
        }

        $this->request = $request;
        return $request;
    }

    /**
     * Get the event
     *
     * @return MvcEvent
     * @throws DomainException if unable to find event
     */
    protected function getEvent()
    {
        if ($this->event) {
            return $this->event;
        }

        $controller = $this->getController();
        if (!$controller instanceof InjectApplicationEventInterface) {
            throw new DomainException(
                    'A controller that implements InjectApplicationEventInterface '
                  . 'is required to use ' . __CLASS__
            );
        }

        $event = $controller->getEvent();
        if (!$event instanceof MvcEvent) {
            $params = $event->getParams();
            $event = new MvcEvent();
            $event->setParams($params);
        }
        $this->event = $event;

        return $this->event;
    }
}
