<?php

/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Router;

use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Router\Exception\RouteNotFoundException;
use Joomla\CMS\Uri\Uri;
use Joomla\Router\Route;
use Joomla\Router\Router;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Joomla! API Router class
 *
 * @since  4.0.0
 */
class ApiRouter extends Router
{
    /**
     * The application object
     *
     * @var    CMSApplicationInterface
     * @since  4.0.0
     */
    protected $app;

    /**
     * Constructor.
     *
     * @param   CMSApplicationInterface  $app   The application object
     * @param   array                    $maps  An optional array of route maps
     *
     * @since   1.0
     */
    public function __construct(CMSApplicationInterface $app, array $maps = [])
    {
        $this->app = $app;

        parent::__construct($maps);
    }

    /**
     * Creates routes map for CRUD
     *
     * @param   string  $baseName    The base name of the component.
     * @param   string  $controller  The name of the controller that contains CRUD functions.
     * @param   array   $defaults    An array of default values that are used when the URL is matched.
     * @param   bool    $publicGets  Allow the public to make GET requests.
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function createCRUDRoutes($baseName, $controller, $defaults = [], $publicGets = false)
    {
        $getDefaults = array_merge(['public' => $publicGets], $defaults);

        $routes = [
            new Route(['GET'], $baseName, $controller . '.displayList', [], $getDefaults),
            new Route(['GET'], $baseName . '/:id', $controller . '.displayItem', ['id' => '(\d+)'], $getDefaults),
            new Route(['POST'], $baseName, $controller . '.add', [], $defaults),
            new Route(['PATCH'], $baseName . '/:id', $controller . '.edit', ['id' => '(\d+)'], $defaults),
            new Route(['DELETE'], $baseName . '/:id', $controller . '.delete', ['id' => '(\d+)'], $defaults),
        ];

        $this->addRoutes($routes);
    }

    /**
     * Parse the given route and return the name of a controller mapped to the given route.
     *
     * @param   string  $method  Request method to match. One of GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE or PATCH
     *
     * @return  array   An array containing the controller and the matched variables.
     *
     * @since   4.0.0
     * @throws  \InvalidArgumentException
     */
    public function parseApiRoute($method = 'GET')
    {
        $method = strtoupper($method);

        $validMethods = ["GET", "POST", "PUT", "DELETE", "HEAD", "TRACE", "PATCH"];

        if (!\in_array($method, $validMethods)) {
            throw new \InvalidArgumentException(sprintf('%s is not a valid HTTP method.', $method));
        }

        // Get the path from the route and remove and leading or trailing slash.
        $routePath = $this->getRoutePath();

        $query = Uri::getInstance()->getQuery(true);

        // Remove the public key as it is only supported coming from the route definition
        if (array_key_exists('public', $query)) {
            unset($query['public']);
        }

        // Iterate through all of the known routes looking for a match.
        foreach ($this->routes as $route) {
            if (\in_array($method, $route->getMethods())) {
                if (preg_match($route->getRegex(), ltrim($routePath, '/'), $matches)) {
                    // If we have gotten this far then we have a positive match.
                    $vars = $route->getDefaults();

                    foreach ($route->getRouteVariables() as $i => $var) {
                        $vars[$var] = $matches[$i + 1];
                    }

                    $controller = preg_split("/[.]+/", $route->getController());

                    /** @deprecated  4.3  Query parameters will not be merged into route variables from 5.0 */
                    $vars       = array_merge($vars, $query);

                    return [
                        'controller' => $controller[0],
                        'task'       => $controller[1],
                        'vars'       => $vars
                    ];
                }
            }
        }

        throw new RouteNotFoundException(sprintf('Unable to handle request for route `%s`.', $routePath));
    }

    /**
     * Get the path from the route and remove and leading or trailing slash.
     *
     * @return string
     *
     * @since 4.0.0
     */
    public function getRoutePath()
    {
        // Get the path from the route and remove and leading or trailing slash.
        $uri  = Uri::getInstance();
        $path = urldecode($uri->getPath());

        /**
         * In some environments (e.g. CLI we can't form a valid base URL). In this case we catch the exception thrown
         * by URI and set an empty base URI for further work.
         * @todo: This should probably be handled better
         */
        try {
            $baseUri = Uri::base(true);
        } catch (\RuntimeException $e) {
            $baseUri = '';
        }

        // Remove the base URI path.
        $path = substr_replace($path, '', 0, \strlen($baseUri));

        // Transform the route
        $path = $this->removeIndexPhpFromPath($path);

        return $path;
    }

    /**
     * Removes the index.php from the route's path.
     *
     * @param   string  $path  The path
     *
     * @return  string
     *
     * @since   4.0.0
     */
    private function removeIndexPhpFromPath(string $path): string
    {
        // Normalize the path
        $path = ltrim($path, '/');

        // We can only remove index.php if it's present in the beginning of the route
        if (strpos($path, 'index.php') !== 0) {
            return $path;
        }

        // Edge case: the route is index.php without a trailing slash. Bad idea but we can still map it to a null route.
        if ($path === 'index.php') {
            return '';
        }

        // Remove the "index.php/" part of the route and return the result.
        return substr($path, 10);
    }

    /**
     * Extract routes matching current route from all known routes.
     *
     * @return \Joomla\Router\Route[]
     *
     * @since 4.0.0
     */
    public function getMatchingRoutes()
    {
        $routePath = $this->getRoutePath();

        // Extract routes matching $routePath from all known routes.
        return array_filter(
            $this->routes,
            function ($route) use ($routePath) {
                return preg_match($route->getRegex(), ltrim($routePath, '/')) === 1;
            }
        );
    }
}
