<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2007 - 2017 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flRouter Class
 *
 * Class to match paths to routes and dispatch controllers and actions.
 *
 * @package fusionLib
 */
class flRouter {
	/**
	 * Array of currently set routes.
	 *
	 * @var array
	 */
	private $routes = array();

	/**
	 * The name of the matched route.
	 *
	 * @var string
	 */
	private $currentRouteName = null;

	/**
	 * The request object.
	 *
	 * @param flRequest
	 */
	private $request;

	/**
	 * The current controller.
	 *
	 * @var flController
	 */
	private $currentController = null;

	/**
	 * Construct the router.
	 *
	 * @param flRequest $request The request object.
	 */
	function __construct($request) {
		$this->request = $request;
	}

	/**
	 * Add a route.
	 *
	 * @param string $name The name of the route.
	 * @param flRouterRoute $route The route to add.
	 */
	function addRoute($name, $route) {
		$route->setRequest($this->request);
		$this->routes[$name] = $route;
	}

	/**
	 * Get the named route.
	 *
	 * @param string $name The name of the route to get.
	 * @return flRouterRoute The route or null if the route doesn't exist.
	 */
	function getRoute($name) {
		return isset($this->routes[$name]) ? $this->routes[$name] : null;
	}

	/**
	 * Get the name of the current route.
	 *
	 * @return string The current route name or null.
	 */
	function getCurrentRouteName() {
		return $this->currentRouteName;
	}

	/**
	 * Match the request against the routes and populate the action, controller and module parameters.
	 *
	 * @return flRequest The updated request object, if routing fails issues a 404 and never returns.
	 * @throws flExceptionNotFound
	 */
	function route() {
		// Make sure controller, module and action are set by us
		$this->request->setParam('controller', null)->setParam('module', null)->setParam('action', null);

		// Route
		$segments = $this->request->getSegments();
		foreach($this->routes as $name => $route) {
			if($params = $route->match($segments)) {

				// Store current route, store params in the request
				$this->currentRouteName = $name;
				foreach($params as $k => $v)
					$this->request->setParam($k, $v);

				// Validate controller and action parameters
				$controller = $this->request->getParam('controller');
				$action = $this->request->getParam('action');
				if($controller === null || $action === null ||
				   !preg_match('/^[a-z][a-zA-Z0-9]+$/', $controller) || !preg_match('/^[a-z][a-z0-9]+$/', $action))
					throw new flExceptionNotFound();

				return $this->request;
			}
		}

		// No route matched
		throw new flExceptionNotFound();
	}

	/**
	 * Get the controller based on the results of the previous call to flRouter::route.
	 *
	 * If the controller can't be loaded then an exception is thrown.
	 *
	 * @return flController The controller object.
	 * @throws flExceptionNotFound.
	 */
	function getController() {
		// If the controller not loaded then load it; else return it
		if($this->currentController === null) {
			try {
				$controller = $this->request->getParam('controller') . 'Controller';
				if(($module = $this->request->getParam('module')) !== null)
					$controller = $module . ucfirst($controller);
				class_exists($controller);
				return new $controller($this, $this->request);
			} catch(flExceptionLoadClass $e) {
				throw new flExceptionNotFound();
			}
		}
		else
			return $this->currentController;
	}

	/**
	 * Get the action to invoke on the controller.
	 *
	 * @return string The action to route.
	 */
	function getAction() {
		return $this->request->getParam('action');
	}

	/**
	 * Get the module holding the controller.
	 *
	 * @return string The module to route to.
	 */
	function getModule() {
		return $this->request->getParam('module');
	}

	/**
	 * Assemble a URI using the routing information as a template.
	 *
	 * @param string $name The name of the route to use.
	 * @param array $params To populate the route template with.
	 * @param bool $absolute true to create an absolute URI else false for relative.
	 * @param bool $secure tue to use https, false for http or null for as per request.
	 * @return string The composed URI.
	 * @throws flRouterException
	 */
	function assemble($name, $params, $absolute = false, $secure = null) {
		if(!isset($this->routes[$name]))
			throw new flRouterException("Unknown route '$name'.");

		return $this->routes[$name]->assemble($params, $absolute, $secure);
	}

	/**
	 * Load route information from routing files.
	 *
	 * @param string $path The path and file pattern to load route files from.
	 * @return flRouter This object.
	 */
	function loadRouteFiles($path) {
		foreach(glob($path) as $route)
			include $route;
		return $this;
	}
}
