<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license       GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/

namespace phpbb\routing;

use phpbb\routing\resources_locator\resources_locator_interface;
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper;
use Symfony\Component\Routing\Generator\CompiledUrlGenerator;
use Symfony\Component\Routing\Generator\UrlGenerator;
use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
use Symfony\Component\Routing\Matcher\CompiledUrlMatcher;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RouterInterface;

/**
 * Integration of all pieces of the routing system for easier use.
 */
class router implements RouterInterface
{
	/**
	 * @var ContainerInterface
	 */
	protected $container;

	/**
	 * @var resources_locator_interface
	 */
	protected $resources_locator;

	/**
	 * @var LoaderInterface
	 */
	protected $loader;

	/**
	 * PHP file extensions
	 *
	 * @var string
	 */
	protected $php_ext;

	/**
	 * @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
	 */
	protected $matcher;

	/**
	 * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
	 */
	protected $generator;

	/**
	 * @var RequestContext
	 */
	protected $context;

	/**
	 * @var RouteCollection
	 */
	protected $route_collection;

	/**
	 * @var string
	 */
	protected $cache_dir;

	/**
	 * @var string
	 */
	protected $debug_url_generator;

	/**
	 * @var string
	 */
	protected $debug_url_matcher;

	/**
	 * Construct method
	 *
	 * @param ContainerInterface $container DI container
	 * @param resources_locator_interface $resources_locator Resources locator
	 * @param LoaderInterface $loader Resources loader
	 * @param string $php_ext PHP file extension
	 * @param string $cache_dir phpBB cache directory
	 * @param string $debug_url_generator Debug url generator
	 * @param string $debug_url_matcher Debug url matcher
	 */
	public function __construct(ContainerInterface $container, resources_locator_interface $resources_locator, LoaderInterface $loader, string $php_ext, string $cache_dir, string $debug_url_generator, string $debug_url_matcher)
	{
		$this->container			= $container;
		$this->resources_locator	= $resources_locator;
		$this->loader				= $loader;
		$this->php_ext				= $php_ext;
		$this->context				= new RequestContext();
		$this->cache_dir			= $cache_dir;
		$this->debug_url_generator	= $debug_url_generator;
		$this->debug_url_matcher    = $debug_url_matcher;
	}

	/**
	 * Get the list of routes
	 *
	 * @return RouteCollection Get the route collection
	 */
	public function get_routes()
	{
		if ($this->route_collection === null /*|| $this->route_collection->count() === 0*/)
		{
			$this->route_collection = new RouteCollection;
			foreach ($this->resources_locator->locate_resources() as $resource)
			{
				if (is_array($resource))
				{
					$this->route_collection->addCollection($this->loader->load($resource[0], $resource[1]));
				}
				else
				{
					$this->route_collection->addCollection($this->loader->load($resource));
				}
			}

			$this->resolveParameters($this->route_collection);
		}

		return $this->route_collection;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getRouteCollection(): RouteCollection
	{
		return $this->get_routes();
	}

	/**
	 * {@inheritdoc}
	 */
	public function setContext(RequestContext $context): void
	{
		$this->context = $context;

		if ($this->matcher !== null)
		{
			$this->get_matcher()->setContext($context);
		}
		if ($this->generator !== null)
		{
			$this->get_generator()->setContext($context);
		}
	}

	/**
	 * {@inheritdoc}
	 */
	public function getContext(): RequestContext
	{
		return $this->context;
	}

	/**
	 * {@inheritdoc}
	 */
	public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
	{
		return $this->get_generator()->generate($name, $parameters, $referenceType);
	}

	/**
	 * {@inheritdoc}
	 */
	public function match(string $pathinfo): array
	{
		// Remove query string
		$pathinfo = parse_url($pathinfo, PHP_URL_PATH);

		if ($pathinfo === null)
		{
			throw new RuntimeException('Malformed pathinfo given to the router.');
		}

		// Remove index.php if present
		$pathinfo = preg_replace('/^\/index\.php\//', '/', $pathinfo);

		return $this->get_matcher()->match($pathinfo);
	}

	/**
	 * Gets the UrlMatcher instance associated with this Router.
	 *
	 * @return \Symfony\Component\Routing\Matcher\UrlMatcherInterface A UrlMatcherInterface instance
	 */
	public function get_matcher()
	{
		if ($this->matcher !== null)
		{
			return $this->matcher;
		}

		$this->create_dumped_url_matcher();

		return $this->matcher;
	}

	/**
	 * Creates a new dumped URL Matcher (dump it if necessary)
	 */
	protected function create_dumped_url_matcher()
	{
		try
		{
			$cache = new ConfigCache("{$this->cache_dir}url_matcher.{$this->php_ext}", $this->debug_url_matcher);
			if (!$cache->isFresh())
			{
				$dumper = new CompiledUrlMatcherDumper($this->get_routes());
				$cache->write($dumper->dump(), $this->get_routes()->getResources());
			}

			$compiled_routes = require_once($cache->getPath());
			$this->matcher = new CompiledUrlMatcher($compiled_routes, $this->context);
		}
		catch (IOException $e)
		{
			$this->create_new_url_matcher();
		}
	}

	/**
	 * Creates a new URL Matcher
	 */
	protected function create_new_url_matcher()
	{
		$this->matcher = new UrlMatcher($this->get_routes(), $this->context);
	}

	/**
	 * Gets the UrlGenerator instance associated with this Router.
	 *
	 * @return \Symfony\Component\Routing\Generator\UrlGeneratorInterface A UrlGeneratorInterface instance
	 */
	public function get_generator()
	{
		if ($this->generator !== null)
		{
			return $this->generator;
		}

		$this->create_dumped_url_generator();

		return $this->generator;
	}

	/**
	 * Creates a new dumped URL Generator (dump it if necessary)
	 */
	protected function create_dumped_url_generator()
	{
		try
		{
			$cache = new ConfigCache("{$this->cache_dir}url_generator.{$this->php_ext}", $this->debug_url_generator);
			if (!$cache->isFresh())
			{
				$dumper = new CompiledUrlGeneratorDumper($this->get_routes());
				$cache->write($dumper->dump(), $this->get_routes()->getResources());
			}

			$compiled_routes = require_once($cache->getPath());
			$this->generator = new CompiledUrlGenerator($compiled_routes, $this->context);
		}
		catch (IOException $e)
		{
			$this->create_new_url_generator();
		}
	}

	/**
	 * Creates a new URL Generator
	 */
	protected function create_new_url_generator()
	{
		$this->generator = new UrlGenerator($this->get_routes(), $this->context);
	}

	/**
	 * Replaces placeholders with service container parameter values in:
	 * - the route defaults,
	 * - the route requirements,
	 * - the route path,
	 * - the route host,
	 * - the route schemes,
	 * - the route methods.
	 *
	 * @param RouteCollection $collection
	 */
	protected function resolveParameters(RouteCollection $collection)
	{
		/** @var \Symfony\Component\Routing\Route $route */
		foreach ($collection as $route)
		{
			foreach ($route->getDefaults() as $name => $value)
			{
				$route->setDefault($name, $this->resolve($value));
			}

			$requirements = $route->getRequirements();
			unset($requirements['_scheme']);
			unset($requirements['_method']);

			foreach ($requirements as $name => $value)
			{
				$route->setRequirement($name, $this->resolve($value));
			}

			$route->setPath($this->resolve($route->getPath()));
			$route->setHost($this->resolve($route->getHost()));

			$schemes = array();
			foreach ($route->getSchemes() as $scheme)
			{
				$schemes = array_merge($schemes, explode('|', $this->resolve($scheme)));
			}

			$route->setSchemes($schemes);
			$methods = array();
			foreach ($route->getMethods() as $method)
			{
				$methods = array_merge($methods, explode('|', $this->resolve($method)));
			}

			$route->setMethods($methods);
			$route->setCondition($this->resolve($route->getCondition()));
		}
	}

	/**
	 * Recursively replaces placeholders with the service container parameters.
	 *
	 * @param mixed $value The source which might contain "%placeholders%"
	 *
	 * @return mixed The source with the placeholders replaced by the container
	 *               parameters. Arrays are resolved recursively.
	 *
	 * @throws ParameterNotFoundException When a placeholder does not exist as a container parameter
	 * @throws RuntimeException           When a container value is not a string or a numeric value
	 */
	private function resolve($value)
	{
		if (is_array($value))
		{
			foreach ($value as $key => $val)
			{
				$value[$key] = $this->resolve($val);
			}

			return $value;
		}

		if (!is_string($value))
		{
			return $value;
		}

		$container = $this->container;
		$escapedValue = preg_replace_callback('/%%|%([^%\s]++)%/', function ($match) use ($container, $value)
		{
			// skip %%
			if (!isset($match[1]))
			{
				return '%%';
			}

			$resolved = $container->getParameter($match[1]);
			if (is_string($resolved) || is_numeric($resolved))
			{
				return (string) $resolved;
			}

			throw new RuntimeException(sprintf(
					'The container parameter "%s", used in the route configuration value "%s", '.
					'must be a string or numeric, but it is of type %s.',
					$match[1],
					$value,
					gettype($resolved)
				)
			);
		}, $value);

		return str_replace('%%', '%', $escapedValue);
	}
}
