<?php

declare(strict_types=1);

namespace Laminas\Navigation\Page;

use Laminas\Mvc\Router as MvcRouter;
use Laminas\Navigation\Exception;
use Laminas\Permissions\Acl\Resource\ResourceInterface;
use Laminas\Router\RouteMatch;
use Laminas\Router\RouteStackInterface;

use function array_intersect_assoc;
use function array_merge;
use function count;
use function get_debug_type;
use function is_string;
use function sprintf;
use function strlen;

/**
 * Represents a page that is defined using controller, action, route
 * name and route params to assemble the href
 *
 * The two constants defined were originally provided via the laminas-mvc class
 * ModuleRouteListener; to remove the requirement on that component, they are
 * reproduced here.
 *
 * @final
 */
class Mvc extends AbstractPage
{
    public const MODULE_NAMESPACE    = '__NAMESPACE__';
    public const ORIGINAL_CONTROLLER = '__CONTROLLER__';

    /**
     * Action name to use when assembling URL
     *
     * @var string
     */
    protected $action;

    /**
     * Controller name to use when assembling URL
     *
     * @var string
     */
    protected $controller;

    /**
     * URL query part to use when assembling URL
     *
     * @var array|string
     */
    protected $query;

    /**
     * Params to use when assembling URL
     *
     * @see getHref()
     *
     * @var array
     */
    protected $params = [];

    /**
     * RouteInterface name to use when assembling URL
     *
     * @see getHref()
     *
     * @var string
     */
    protected $route;

    /**
     * Cached href
     *
     * The use of this variable minimizes execution time when getHref() is
     * called more than once during the lifetime of a request. If a property
     * is updated, the cache is invalidated.
     *
     * @var string
     */
    protected $hrefCache;

    /**
     * RouteInterface matches; used for routing parameters and testing validity
     *
     * @var RouteMatch
     */
    protected $routeMatch;

    /**
     * If true and set routeMatch than getHref will use routeMatch params
     * to assemble uri
     *
     * @var bool
     */
    protected $useRouteMatch = false;

    /**
     * Router for assembling URLs
     *
     * @see getHref()
     *
     * @var RouteStackInterface
     */
    protected $router;

    /**
     * Default router to be used if router is not given.
     *
     * @see getHref()
     *
     * @var RouteStackInterface
     */
    protected static $defaultRouter;

    /**
     * Default route name
     *
     * @var string
     */
    protected static $defaultRoute;

    // Accessors:

    /**
     * Returns whether page should be considered active or not
     *
     * This method will compare the page properties against the route matches
     * composed in the object.
     *
     * @param  bool $recursive  [optional] whether page should be considered
     *                          active if any child pages are active. Default is
     *                          false.
     * @return bool             whether page should be considered active or not
     */
    public function isActive($recursive = false)
    {
        if (! $this->active) {
            $reqParams = [];
            if ($this->routeMatch instanceof RouteMatch || $this->routeMatch instanceof MvcRouter\RouteMatch) {
                $reqParams = $this->routeMatch->getParams();

                if (isset($reqParams[self::ORIGINAL_CONTROLLER])) {
                    $reqParams['controller'] = $reqParams[self::ORIGINAL_CONTROLLER];
                }

                $pageParams = $this->params;
                if (null !== $this->controller) {
                    $pageParams['controller'] = $this->controller;
                }
                if (null !== $this->action) {
                    $pageParams['action'] = $this->action;
                }

                if (null !== $this->getRoute()) {
                    if (
                        $this->routeMatch->getMatchedRouteName() === $this->getRoute()
                        && (count(array_intersect_assoc($reqParams, $pageParams)) === count($pageParams))
                    ) {
                        $this->active = true;
                        return $this->active;
                    } else {
                        return parent::isActive($recursive);
                    }
                }
            }

            $pageParams = $this->params;

            if (null !== $this->controller) {
                $pageParams['controller'] = $this->controller;
            } else {
                /**
                 * @todo In Laminas1, this was configurable and pulled from the front controller
                 */
                $pageParams['controller'] = 'index';
            }

            if (null !== $this->action) {
                $pageParams['action'] = $this->action;
            } else {
                /**
                 * @todo In Laminas1, this was configurable and pulled from the front controller
                 */
                $pageParams['action'] = 'index';
            }

            if (count(array_intersect_assoc($reqParams, $pageParams)) === count($pageParams)) {
                $this->active = true;
                return true;
            }
        }

        return parent::isActive($recursive);
    }

    /**
     * Returns href for this page
     *
     * This method uses {@link RouteStackInterface} to assemble
     * the href based on the page's properties.
     *
     * @see RouteStackInterface
     *
     * @return string  page href
     * @throws Exception\DomainException If no router is set.
     */
    public function getHref()
    {
        if ($this->hrefCache) {
            return $this->hrefCache;
        }

        $router = $this->router;
        if (null === $router) {
            $router = static::$defaultRouter;
        }

        if (! $router instanceof RouteStackInterface && ! $router instanceof MvcRouter\RouteStackInterface) {
            throw new Exception\DomainException(
                __METHOD__
                . ' cannot execute as no Laminas\Router\RouteStackInterface instance is composed'
            );
        }

        if ($this->useRouteMatch() && $this->getRouteMatch()) {
            $rmParams = $this->getRouteMatch()->getParams();

            if (isset($rmParams[self::ORIGINAL_CONTROLLER])) {
                $rmParams['controller'] = $rmParams[self::ORIGINAL_CONTROLLER];
                unset($rmParams[self::ORIGINAL_CONTROLLER]);
            }

            if (isset($rmParams[self::MODULE_NAMESPACE])) {
                unset($rmParams[self::MODULE_NAMESPACE]);
            }

            $params = array_merge($rmParams, $this->getParams());
        } else {
            $params = $this->getParams();
        }

        if (($param = $this->getController()) !== null) {
            $params['controller'] = $param;
        }

        if (($param = $this->getAction()) !== null) {
            $params['action'] = $param;
        }

        $name = match (true) {
            $this->getRoute() !== null || static::getDefaultRoute() !== null
                => $this->getRoute() ?? static::getDefaultRoute(),
            $this->getRouteMatch() !== null
                => $this->getRouteMatch()->getMatchedRouteName(),
            default => throw new Exception\DomainException('No route name could be found'),
        };

        $options = ['name' => $name];

        // Add the fragment identifier if it is set
        $fragment = $this->getFragment();
        if (null !== $fragment) {
            $options['fragment'] = $fragment;
        }

        if (null !== ($query = $this->getQuery())) {
            $options['query'] = $query;
        }

        $url = $router->assemble($params, $options);

        return $this->hrefCache = $url;
    }

    /**
     * Sets action name to use when assembling URL
     *
     * @see getHref()
     *
     * @param  string $action             action name
     * @return Mvc   fluent interface, returns self
     * @throws Exception\InvalidArgumentException  If invalid $action is given.
     */
    public function setAction($action)
    {
        if (null !== $action && ! is_string($action)) {
            throw new Exception\InvalidArgumentException(
                'Invalid argument: $action must be a string or null'
            );
        }

        $this->action    = $action;
        $this->hrefCache = null;
        return $this;
    }

    /**
     * Returns action name to use when assembling URL
     *
     * @see getHref()
     *
     * @return string|null  action name
     */
    public function getAction()
    {
        return $this->action;
    }

    /**
     * Sets controller name to use when assembling URL
     *
     * @see getHref()
     *
     * @param  string|null $controller    controller name
     * @return Mvc   fluent interface, returns self
     * @throws Exception\InvalidArgumentException  If invalid controller name is given.
     */
    public function setController($controller)
    {
        if (null !== $controller && ! is_string($controller)) {
            throw new Exception\InvalidArgumentException(
                'Invalid argument: $controller must be a string or null'
            );
        }

        $this->controller = $controller;
        $this->hrefCache  = null;
        return $this;
    }

    /**
     * Returns controller name to use when assembling URL
     *
     * @see getHref()
     *
     * @return string|null  controller name or null
     */
    public function getController()
    {
        return $this->controller;
    }

    /**
     * Sets URL query part to use when assembling URL
     *
     * @see getHref()
     *
     * @param  array|string|null $query    URL query part
     * @return self   fluent interface, returns self
     */
    public function setQuery($query)
    {
        $this->query     = $query;
        $this->hrefCache = null;
        return $this;
    }

    /**
     * Returns URL query part to use when assembling URL
     *
     * @see getHref()
     *
     * @return array|string|null  URL query part (as an array or string) or null
     */
    public function getQuery()
    {
        return $this->query;
    }

    /**
     * Sets params to use when assembling URL
     *
     * @see getHref()
     *
     * @param  array|null $params [optional] page params. Default is null
     *                            which sets no params.
     * @return Mvc  fluent interface, returns self
     */
    public function setParams(?array $params = null)
    {
        $this->params    = $params ?? [];
        $this->hrefCache = null;
        return $this;
    }

    /**
     * Returns params to use when assembling URL
     *
     * @see getHref()
     *
     * @return array  page params
     */
    public function getParams()
    {
        return $this->params;
    }

    /**
     * Sets route name to use when assembling URL
     *
     * @see getHref()
     *
     * @param  string $route Route name to use when assembling URL.
     * @return Mvc Fluent interface, returns self.
     * @throws Exception\InvalidArgumentException If invalid $route is given.
     */
    public function setRoute($route)
    {
        if (null !== $route && (! is_string($route) || strlen($route) < 1)) {
            throw new Exception\InvalidArgumentException(
                'Invalid argument: $route must be a non-empty string or null'
            );
        }

        $this->route     = $route;
        $this->hrefCache = null;
        return $this;
    }

    /**
     * Returns route name to use when assembling URL
     *
     * @see getHref()
     *
     * @return string  route name
     */
    public function getRoute()
    {
        return $this->route;
    }

    /**
     * Get the route match.
     *
     * @return RouteMatch
     */
    public function getRouteMatch()
    {
        return $this->routeMatch;
    }

    /**
     * Set route match object from which parameters will be retrieved
     *
     * @param  RouteMatch|MvcRouter\RouteMatch $matches
     * @return Mvc fluent interface, returns self
     */
    public function setRouteMatch($matches)
    {
        if (! $matches instanceof RouteMatch && ! $matches instanceof MvcRouter\RouteMatch) {
            throw new Exception\InvalidArgumentException(sprintf(
                'RouteMatch passed to %s must be either a %s or a %s instance; received %s',
                __METHOD__,
                RouteMatch::class,
                MvcRouter\RouteMatch::class,
                get_debug_type($matches)
            ));
        }
        $this->routeMatch = $matches;
        return $this;
    }

    /**
     * Get the useRouteMatch flag
     *
     * @return bool
     */
    public function useRouteMatch()
    {
        return $this->useRouteMatch;
    }

    /**
     * Set whether the page should use route match params for assembling link uri
     *
     * @see getHref()
     *
     * @param bool $useRouteMatch [optional]
     * @return Mvc
     */
    public function setUseRouteMatch($useRouteMatch = true)
    {
        $this->useRouteMatch = (bool) $useRouteMatch;
        $this->hrefCache     = null;
        return $this;
    }

    /**
     * Get the router.
     *
     * @return null|RouteStackInterface|MvcRouter\RouteStackInterface
     */
    public function getRouter()
    {
        return $this->router;
    }

    /**
     * Sets router for assembling URLs
     *
     * @see getHref()
     *
     * @param  RouteStackInterface|MvcRouter\RouteStackInterface $router Router
     * @return Mvc Fluent interface, returns self
     */
    public function setRouter($router)
    {
        if (! $router instanceof RouteStackInterface && ! $router instanceof MvcRouter\RouteStackInterface) {
            throw new Exception\InvalidArgumentException(sprintf(
                'Router passed to %s must be either a %s or a %s instance; received %s',
                __METHOD__,
                RouteStackInterface::class,
                MvcRouter\RouteStackInterface::class,
                get_debug_type($router)
            ));
        }
        $this->router = $router;
        return $this;
    }

    /**
     * Sets the default router for assembling URLs.
     *
     * @see getHref()
     *
     * @param  RouteStackInterface $router Router
     * @return void
     */
    public static function setDefaultRouter($router)
    {
        static::$defaultRouter = $router;
    }

    /**
     * Gets the default router for assembling URLs.
     *
     * @return RouteStackInterface
     */
    public static function getDefaultRouter()
    {
        return static::$defaultRouter;
    }

    /**
     * Set default route name
     *
     * @param string $route
     * @return void
     */
    public static function setDefaultRoute($route)
    {
        static::$defaultRoute = $route;
    }

    /**
     * Get default route name
     *
     * @return string
     */
    public static function getDefaultRoute()
    {
        return static::$defaultRoute;
    }

    // Public methods:

    /**
     * Returns an array representation of the page
     *
     * @see ResourceInterface
     *
     * @return array
     * @psalm-return array{
     *     label: string|null,
     *     fragment: string|null,
     *     id: string|null,
     *     class: string|null,
     *     title: string|null,
     *     target: string|null,
     *     rel: array|null,
     *     rev: array|null,
     *     order: int|null,
     *     resource: ResourceInterface|string|null,
     *     privilege: string|null,
     *     permission: mixed|null,
     *     active: bool,
     *     visible: bool,
     *     pages: list<array>,
     *     action: string|null,
     *     controller: string|null,
     *     params: array,
     *     route: string,
     *     router: RouteStackInterface|MvcRouter\RouteStackInterface|null,
     *     route_match: RouteMatch,
     *     ...
     * }
     */
    public function toArray()
    {
        return array_merge(
            parent::toArray(),
            [
                'action'      => $this->getAction(),
                'controller'  => $this->getController(),
                'params'      => $this->getParams(),
                'route'       => $this->getRoute(),
                'router'      => $this->getRouter(),
                'route_match' => $this->getRouteMatch(),
            ]
        );
    }
}
