<?php

/**
 * Part of the Joomla Framework Router Package
 *
 * @copyright  Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

namespace Joomla\Router;

use function Opis\Closure\{serialize, unserialize};

/**
 * An object representing a route definition.
 *
 * @since  2.0.0
 */
class Route implements \Serializable
{
    /**
     * The controller which handles this route
     *
     * @var    mixed
     * @since  2.0.0
     */
    private $controller;

    /**
     * The default variables defined by the route
     *
     * @var    array
     * @since  2.0.0
     */
    private $defaults = [];

    /**
     * The HTTP methods this route supports
     *
     * @var    string[]
     * @since  2.0.0
     */
    private $methods;

    /**
     * The route pattern to use for matching
     *
     * @var    string
     * @since  2.0.0
     */
    private $pattern;

    /**
     * The path regex this route processes
     *
     * @var    string
     * @since  2.0.0
     */
    private $regex;

    /**
     * The variables defined by the route
     *
     * @var    array
     * @since  2.0.0
     */
    private $routeVariables = [];

    /**
     * An array of regex rules keyed using the route variables
     *
     * @var    array
     * @since  2.0.0
     */
    private $rules = [];

    /**
     * Constructor.
     *
     * @param   array   $methods     The HTTP methods this route supports
     * @param   string  $pattern     The route pattern to use for matching
     * @param   mixed   $controller  The controller which handles this route
     * @param   array   $rules       An array of regex rules keyed using the route variables
     * @param   array   $defaults    The default variables defined by the route
     *
     * @since   2.0.0
     */
    public function __construct(array $methods, string $pattern, $controller, array $rules = [], array $defaults = [])
    {
        $this->setMethods($methods);
        $this->setPattern($pattern);
        $this->setController($controller);
        $this->setRules($rules);
        $this->setDefaults($defaults);
    }

    /**
     * Parse the route's pattern to extract the named variables and build a proper regular expression for use when parsing the routes.
     *
     * @return  void
     *
     * @since   2.0.0
     */
    protected function buildRegexAndVarList(): void
    {
        // Sanitize and explode the pattern.
        $pattern = explode('/', trim(parse_url($this->getPattern(), PHP_URL_PATH), ' /'));

        // Prepare the route variables
        $vars = [];

        // Initialize regular expression
        $regex = [];

        // Loop on each segment
        foreach ($pattern as $segment) {
            if ($segment == '*') {
                // Match a splat with no variable.
                $regex[] = '.*';
            } elseif (isset($segment[0]) && $segment[0] == '*') {
                // Match a splat and capture the data to a named variable.
                $vars[]  = substr($segment, 1);
                $regex[] = '(.*)';
            } elseif (isset($segment[0]) && $segment[0] == '\\' && $segment[1] == '*') {
                // Match an escaped splat segment.
                $regex[] = '\*' . preg_quote(substr($segment, 2));
            } elseif ($segment == ':') {
                // Match an unnamed variable without capture.
                $regex[] = '([^/]*)';
            } elseif (isset($segment[0]) && $segment[0] == ':') {
                // Match a named variable and capture the data.
                $varName = substr($segment, 1);
                $vars[]  = $varName;

                // Use the regex in the rules array if it has been defined.
                $regex[] = array_key_exists($varName, $this->getRules()) ? '(' . $this->getRules()[$varName] . ')' : '([^/]*)';
            } elseif (isset($segment[0]) && $segment[0] == '\\' && $segment[1] == ':') {
                // Match a segment with an escaped variable character prefix.
                $regex[] = preg_quote(substr($segment, 1));
            } else {
                // Match the standard segment.
                $regex[] = preg_quote($segment);
            }
        }

        $this->setRegex(\chr(1) . '^' . implode('/', $regex) . '$' . \chr(1));
        $this->setRouteVariables($vars);
    }

    /**
     * Retrieve the controller which handles this route
     *
     * @return  mixed
     *
     * @since   2.0.0
     */
    public function getController()
    {
        return $this->controller;
    }

    /**
     * Retrieve the default variables defined by the route
     *
     * @return  array
     *
     * @since   2.0.0
     */
    public function getDefaults(): array
    {
        return $this->defaults;
    }

    /**
     * Retrieve the HTTP methods this route supports
     *
     * @return  string[]
     *
     * @since   2.0.0
     */
    public function getMethods(): array
    {
        return $this->methods;
    }

    /**
     * Retrieve the route pattern to use for matching
     *
     * @return  string
     *
     * @since   2.0.0
     */
    public function getPattern(): string
    {
        return $this->pattern;
    }

    /**
     * Retrieve the path regex this route processes
     *
     * @return  string
     *
     * @since   2.0.0
     */
    public function getRegex(): string
    {
        if (!$this->regex) {
            $this->buildRegexAndVarList();
        }

        return $this->regex;
    }

    /**
     * Retrieve the variables defined by the route
     *
     * @return  array
     *
     * @since   2.0.0
     */
    public function getRouteVariables(): array
    {
        if (!$this->regex) {
            $this->buildRegexAndVarList();
        }

        return $this->routeVariables;
    }

    /**
     * Retrieve the regex rules keyed using the route variables
     *
     * @return  array
     *
     * @since   2.0.0
     */
    public function getRules(): array
    {
        return $this->rules;
    }

    /**
     * Set the controller which handles this route
     *
     * @param   mixed  $controller  The controller which handles this route
     *
     * @return  $this
     *
     * @since   2.0.0
     */
    public function setController($controller): self
    {
        $this->controller = $controller;

        return $this;
    }

    /**
     * Set the default variables defined by the route
     *
     * @param   array  $defaults  The default variables defined by the route
     *
     * @return  $this
     *
     * @since   2.0.0
     */
    public function setDefaults(array $defaults): self
    {
        $this->defaults = $defaults;

        return $this;
    }

    /**
     * Set the HTTP methods this route supports
     *
     * @param   array  $methods  The HTTP methods this route supports
     *
     * @return  $this
     *
     * @since   2.0.0
     */
    public function setMethods(array $methods): self
    {
        $this->methods = $this->methods = array_map('strtoupper', $methods);

        return $this;
    }

    /**
     * Set the route pattern to use for matching
     *
     * @param   string  $pattern  The route pattern to use for matching
     *
     * @return  $this
     *
     * @since   2.0.0
     */
    public function setPattern(string $pattern): self
    {
        $this->pattern = $pattern;

        $this->setRegex('');
        $this->setRouteVariables([]);

        return $this;
    }

    /**
     * Set the path regex this route processes
     *
     * @param   string  $regex  The path regex this route processes
     *
     * @return  $this
     *
     * @since   2.0.0
     */
    public function setRegex(string $regex): self
    {
        $this->regex = $regex;

        return $this;
    }

    /**
     * Set the variables defined by the route
     *
     * @param   array  $routeVariables  The variables defined by the route
     *
     * @return  $this
     *
     * @since   2.0.0
     */
    public function setRouteVariables(array $routeVariables): self
    {
        $this->routeVariables = $routeVariables;

        return $this;
    }

    /**
     * Set the regex rules keyed using the route variables
     *
     * @param   array  $rules  The rules defined by the route
     *
     * @return  $this
     *
     * @since   2.0.0
     */
    public function setRules(array $rules): self
    {
        $this->rules = $rules;

        return $this;
    }

    /**
     * Serialize the route.
     *
     * @return  string  The serialized route.
     *
     * @since   2.0.0
     */
    public function serialize()
    {
        return serialize($this->__serialize());
    }

    /**
     * Serialize the route.
     *
     * @return  array  The data to be serialized
     *
     * @since   2.0.0
     */
    public function __serialize()
    {
        $controller = $this->getController();

        if ($controller instanceof \Closure) {
            if (!function_exists('\Opis\Closure\serialize')) {
                throw new \RuntimeException(
                    \sprintf(
                        'Cannot serialize the route for pattern "%s" because the controller is a Closure. '
                        . 'Install the "opis/closure" package to serialize Closures.',
                        $this->getPattern()
                    )
                );
            }
        }

        return [
            'controller'     => $controller,
            'defaults'       => $this->getDefaults(),
            'methods'        => $this->getMethods(),
            'pattern'        => $this->getPattern(),
            'regex'          => $this->getRegex(),
            'routeVariables' => $this->getRouteVariables(),
            'rules'          => $this->getRules(),
        ];
    }

    /**
     * Unserialize the route.
     *
     * @param   string  $serialized  The serialized route.
     *
     * @return  void
     *
     * @since   1.0
     */
    public function unserialize($serialized)
    {
        $this->__unserialize(unserialize($serialized));
    }

    /**
     * Unserialize the route.
     *
     * @param   array  $data  The serialized route.
     *
     * @return  void
     *
     * @since   2.0.0
     */
    public function __unserialize(array $data)
    {
        $this->controller     = $data['controller'];
        $this->defaults       = $data['defaults'];
        $this->methods        = $data['methods'];
        $this->pattern        = $data['pattern'];
        $this->regex          = $data['regex'];
        $this->routeVariables = $data['routeVariables'];
        $this->rules          = $data['rules'];
    }
}
