<?php

/**
* Class that resolves a path in the URI or other part of the request to a controller/action.
* Also allows the type of response that is desired to be controlled based on input.
* Individual matches can make modifications to the routing path to be passed to other rules.
*
* Rules will continue matching until a {@link XenForo_RouteMatch} object is returned that
* has a {@link XenForo_RouteMatch::$controllerName controller name} specified.
*
* @package XenForo_Mvc
*/
class XenForo_Router
{
	/**
	* Stack of rules to match against. Once a match is found, further matching stops.
	*
	* @see addRules()
	* @var XenForo_Route_Interface[]
	*/
	protected $_rules = array();

	/**
	* The default response type, if it's not modified based on input.
	*
	* @var string
	*/
	protected $_defaultResponseType = 'html';

	/**
	* The route path to apply if the path ever becomes empty.
	*
	* @var string
	*/
	protected $_routePathIfEmpty = 'index';

	/**
	* Match against the rules stack. If no match can be found, the {@link getNotFoundError()}
	* handler is invoked.
	*
	* @param Zend_Controller_Request_Http $request Request object
	 * @param string|null $routePath If specified, the route path to route to
	*
	* @return XenForo_RouteMatch|boolean Final information (including controller and action) about where to route to
	*/
	public function match(Zend_Controller_Request_Http $request, $routePath = null)
	{
		if ($routePath === null)
		{
			$routePath = $this->getRoutePath($request);
		}

		if (strlen($routePath) && strpos($routePath, '/') === false)
		{
			$routePath .= '/';
		}

		$request->setParam('_origRoutePath', $routePath);

		$responseType = $this->_defaultResponseType;
		$finalRouteMatch = null;

		foreach ($this->_rules AS $rule)
		{
			if ($routePath === '')
			{
				// must always have a route path of some sort
				$routePath = $this->_routePathIfEmpty;
			}

			/** @var $match XenForo_RouteMatch */
			if (!$match = $rule->match($routePath, $request, $this))
			{
				continue;
			}

			if ($match->getResponseType())
			{
				$responseType = $match->getResponseType();
			}

			if ($match->getControllerName())
			{
				$finalRouteMatch = $match;
				$request->setParam('_matchedRoutePath', $routePath);
				break;
			}

			if ($match->getModifiedRoutePath() !== null)
			{
				$routePath = $match->getModifiedRoutePath();
			}
		}

		// a bit of magic - if the query string specifies a _xfResponseType parameter,
		// use THAT as the extension, overriding anything else.
		if ($request->has('_xfResponseType') && is_string($request->get('_xfResponseType')))
		{
			$responseType = $request->get('_xfResponseType');
		}

		if ($finalRouteMatch)
		{
			$finalRouteMatch->setResponseType($responseType);
			return $finalRouteMatch;
		}
		else
		{
			$match = $this->getRouteMatch();
			$match->setResponseType($responseType);
			return $match;
		}
	}

	/**
	* Gets the path the to be routed based on the URL of the request
	*
	* @param Zend_Controller_Request_Http Request object
	*
	* @return string Routing path
	*/
	public function getRoutePath(Zend_Controller_Request_Http $request)
	{
		$baseUrl = $request->getBaseUrl();
		$requestUri = $request->getRequestUri();

		$result = null;

		if (substr($requestUri, 0, strlen($baseUrl)) == $baseUrl)
		{
			$routeBase = substr($requestUri, strlen($baseUrl));

			if (preg_match('#^/([^?]+)(\?|$)#U', $routeBase, $match))
			{
				// rewrite approach (starts with /). Must be non-empty rewrite up to query string.
				$result = urldecode($match[1]);
			}
			else if (preg_match('#\?([^=&]+)(&|$)#U', $routeBase, $match))
			{
				// query string approach. Must start with non-empty, non-named param.
				$result = urldecode($match[1]);
			}
		}

		if ($result === null)
		{
			$namedRouteVar = $request->getParam('_');
			if ($namedRouteVar !== null && is_string($namedRouteVar))
			{
				$result = $namedRouteVar;
			}
		}

		if ($result !== null)
		{
			return ltrim($result, '/');
		}

		return '';
	}

	/**
	* Adds a new routing rule to the end of the chain or overwrites an existing rule by name.
	*
	* @param XenForo_Route_Interface Routing rule
	* @param string               Name of the rule. If it already exists, it is overwritten.
	*
	* @return XenForo_Router Fluent interface ($this)
	*/
	public function addRule(XenForo_Route_Interface $route, $name)
	{
		$this->_rules[$name] = $route;

		return $this;
	}

	/**
	* Get the current routing rules
	*
	* @return array
	*/
	public function getRules()
	{
		return $this->_rules;
	}

	/**
	* Reset (remove) all routing rules.
	*
	* @return XenForo_Router Fluent interface ($this)
	*/
	public function resetRules()
	{
		$this->_rules = array();

		return $this;
	}

	/**
	 * Resolves the action from a route that looks like name.123/action and sets
	 * the "123" param into the specified parameter name.
	 *
	 * Supports name.123/action1/action2 (returns "action1/action2"). If given
	 * "action1/action2", this will return the full string as the action as long as action1
	 * does not have a "." in it.
	 *
	 * @param string $routePath Full path to route against. This should not include a prefix.
	 * @param Zend_Controller_Request_Http $request Request object
	 * @param string $paramName Name of the parameter to be registered with the request object (if found)
	 * @param string $defaultActionWithParam If there's no action and there is an int param, use this as the default action
	 *
	 * @return string The requested action
	 */
	public function resolveActionWithIntegerParam($routePath, Zend_Controller_Request_Http $request, $paramName, $defaultActionWithParam = '')
	{
		$parts = explode('/', $routePath, 2);
		$action = isset($parts[1]) ? $parts[1] : '';

		$paramParts = explode(XenForo_Application::URL_ID_DELIMITER, $parts[0]);
		$paramId = end($paramParts);

		if (count($paramParts) > 1
			|| $paramId === strval(intval($paramId))
		)
		{
			$request->setParam($paramName, intval($paramId));
			if ($action === '')
			{
				$action = $defaultActionWithParam;
			}
			return $action;
		}
		else
		{
			return $routePath;
		}
	}

	/**
	 * Resolves an action with an integer or a string parameter, such as in situations
	 * where a slug is an optional component. Note that <title>.<int>/<action> and
	 * <string>/<action> will work, but <action> on its own will be matched as a string.
	 *
	 * This method is not an ideal function to use when you are not guaranteed to have data.
	 *
	 * @param string $routePath
	 * @param Zend_Controller_Request_Http $request
	 * @param string $intParamName Name of the parameter to set if int is found
	 * @param string $stringParamName Name of the parameter to set if string is found
	 */
	public function resolveActionWithIntegerOrStringParam($routePath, Zend_Controller_Request_Http $request, $intParamName, $stringParamName)
	{
		$parts = explode('/', $routePath, 2);
		$action = isset($parts[1]) ? $parts[1] : '';

		$paramParts = explode(XenForo_Application::URL_ID_DELIMITER, $parts[0]);
		$paramId = end($paramParts);

		if (count($paramParts) > 1
			|| $paramId === strval(intval($paramId))
		)
		{
			$request->setParam($intParamName, intval($paramId));
			return $action;
		}
		else
		{
			if ($paramId != '-') // special case: not set
			{
				$request->setParam($stringParamName, $paramId);
			}
			return $action;
		}
	}

	/**
	 * Resolves the action from a route that may have a string parameter. If there
	 * are no slashes, then an action is assumed. If there is a slash, then the first
	 * item is considered the string param.
	 *
	 * For example: list => "list" action. blah/list => "list" action, with "blah" param.
	 *
	 * @param string $routePath Full path to route against. This should not include a prefix.
	 * @param Zend_Controller_Request_Http $request Request object
	 * @param string $paramName Name of the parameter to be registered with the request object (if found)
	 *
	 * @return string The requested action
	 */
	public function resolveActionWithStringParam($routePath, Zend_Controller_Request_Http $request, $paramName)
	{
		$components = explode('/', $routePath);
		if (isset($components[1]))
		{
			// param comes first but we have an action: <param>/<action...>
			$request->setParam($paramName, $components[0]);

			unset($components[0]);
			return implode('', $components);
		}
		else
		{
			return $routePath;
		}
	}

	/**
	 * Checks for the presence of 'page-x' as the action component of a route
	 *
	 * @param string $action
	 * @param Zend_Controller_Request_Http $request
	 *
	 * @return string
	 */
	public function resolveActionAsPageNumber($action, Zend_Controller_Request_Http $request)
	{
		if (preg_match('#^page-(\d+)$#i', $action, $match))
		{
			$action = '';
			$request->setParam('page', $match[1]);
		}

		return $action;
	}

	/**
	 * Gets a route match object.
	 *
	 * @param string $controllerName
	 * @param string|boolean $action
	 * @param string $majorSection
	 * @param string $minorSection
	 *
	 * @return XenForo_RouteMatch
	 */
	public function getRouteMatch($controllerName = '', $action = false, $majorSection = '', $minorSection = '')
	{
		return new XenForo_RouteMatch($controllerName, $action, $majorSection, $minorSection);
	}

	/**
	 * Prepares routing for a link in the form of <sub component>/<data.id>/<action>. Note that
	 * the prefix has already been removed from this link.
	 *
	 * @param array $subComponents
	 * @param string $routePath
	 * @param Zend_Controller_Request_Http $request
	 * @param string $controllerOverride Current controller passed in by referenced; overridden if sub-component chooses
	 *
	 * @return string|boolean String of action if sub-component matched; false otherwise
	 */
	public function getSubComponentAction(array $subComponents, $routePath, Zend_Controller_Request_Http $request, &$controllerOverride)
	{
		$action = false;

		$parts = explode('/', $routePath, 2);
		$subComponentName = strtolower($parts[0]);
		if (!isset($parts[1]))
		{
			$parts[1] = '';
		}

		foreach ($subComponents AS $key => $subComponent)
		{
			if ($key == $subComponentName)
			{
				$action = (isset($subComponent['actionPrefix']) ? $subComponent['actionPrefix'] : '');

				if (isset($subComponent['intId']))
				{
					$action .= $this->resolveActionWithIntegerParam($parts[1], $request, $subComponent['intId']);
				}
				else if (isset($subComponent['stringId']))
				{
					$action .= $this->resolveActionWithStringParam($parts[1], $request, $subComponent['stringId']);
				}
				else
				{
					$action .= $parts[1];
				}

				if (isset($subComponent['controller']))
				{
					$controllerOverride = $subComponent['controller'];
				}
				break;
			}
		}

		return $action;
	}
}