<?php
/**
 * core.framework
 *
 * @category  Core
 * @package   Core_Router
 * @copyright Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */

/**
 * @uses      Core_Request
 * @uses      Core_Loader_PluginLoader
 * @category  Core
 * @package   Core_Router
 * @copyright Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license   proprietary
 */
class Core_Router
{
    /**
     * @var array
     */
    protected $_defaults = array();
    
    /**
     * Hostname which will be used when generating URLs from routes without a hostname set.
     *
     * @var string
     */
    protected $_defaultHostname;

    /**
     * Prefix which will be stripped from path.
     *
     * @var string
     */
    protected $_base;

    /**
     * Used to separate route tokens
     *
     * @var string
     */
    protected $_separator = '/';

    /**
     * Contain all routes, instances of Core_Router_Route_Interface
     *
     * @var array
     */
    protected $_routes    = array();

    /**
     * @var Core_Router_Route_Interface
     */
    protected $_currentRoute;

    /**
     * @var Core_Request
     */
    protected $_request;

    /**
     * @var Core_Loader_PluginLoader
     */
    protected $_pluginLoader;

    /**
     * Class constructor
     *
     * @param array|Zend_Config $options
     */
    public function __construct($options = null)
    {
        if (is_array($options)) {
            $this->setOptions($options);
        } elseif ($options instanceof Zend_Config) {
            $this->setConfig($options);
        }
    }

    /**
     * @param string $defaultHostname
     *
     * @return \Core_Router
     */
    public function setDefaultHostname($defaultHostname)
    {
        $this->_defaultHostname = $defaultHostname;
        return $this;
    }

    /**
     * @throws RuntimeException
     * @return string
     */
    public function getDefaultHostname()
    {
        if (null === $this->_defaultHostname) {
            throw new RuntimeException('Failed fetching router default hostname, none is set');
        }
        return $this->_defaultHostname;
    }

    /**
     * @param Core_Request $request
     *
     * @return Core_Router
     */
    public function setRequest(Core_Request $request)
    {
        $this->_request = $request;
        return $this;
    }

    /**
     * If not set, fetch from Core_Application
     *
     * @return Core_Request
     */
    public function getRequest()
    {
        if (null === $this->_request) {
            $this->setRequest(Core_Application::get('Request'));
        }
        return $this->_request;
    }

    /**
     * @param Core_Loader_PluginLoader $pluginLoader
     *
     * @return Core_Router
     */
    public function setPluginLoader(Core_Loader_PluginLoader $pluginLoader)
    {
        $this->_pluginLoader = $pluginLoader;
        return $this;
    }

    /**
     * If not set, fetch from Core_Application
     *
     * @return Core_Loader_PluginLoader
     */
    public function getPluginLoader()
    {
        if (null === $this->_pluginLoader) {
            $this->setPluginLoader(Core_Application::get('PluginLoader'));
        }
        return $this->_pluginLoader;
    }

    /**
     * @param string $base Prefix to strip from path before processing
     *
     * @return Core_Router
     */
    public function setBase($base)
    {
        $this->_base = $base;
        return $this;
    }

    /**
     * Path base (prefix), null by default
     *
     * @return string
     */
    public function getBase()
    {
        return rtrim($this->_base, $this->getSeparator());
    }

    /**
     * @param Core_Router_Route_Interface $currentRoute
     *
     * @return Core_Router
     */
    public function setCurrentRoute(Core_Router_Route_Interface $currentRoute)
    {
        $this->_currentRoute = $currentRoute;
        return $this;
    }

    /**
     * Matched route. Will be null until match() is called.
     *
     * @return Core_Router_Route_Interface
     */
    public function getCurrentRoute()
    {
        return $this->_currentRoute;
    }

    /**
     * Set current route to null.
     *
     * @return Core_Router
     */
    public function resetCurrentRoute()
    {
        $this->_currentRoute = null;
        return $this;
    }

    /**
     * Used to determine what's the string which separates route tokens.
     *
     * @param string $separator
     *
     * @return Core_Router
     */
    public function setSeparator($separator)
    {
        $this->_separator = $separator;
        return $this;
    }

    /**
     * @return string Route token separator.
     */
    public function getSeparator()
    {
        return $this->_separator;
    }

    /**
     * Returns true if we have a route by that name, false otherwise.
     *
     * @param string $name Name of route to check.
     *
     * @return bool
     */
    public function hasRoute($name)
    {
        return isset($this->_routes[$name]);
    }

    /**
     * Add a new route to the router.
     *
     * @param Core_Router_Route_Interface $route
     *
     * @return Core_Router
     */
    public function addRoute(Core_Router_Route_Interface $route)
    {
        $name                 = $route->getName();
        $this->_routes[$name] = $route;

        $route->setRouter($this);

        return $this;
    }

    /**
     * Adds multiple routes to the router. Can be Core_Router_Route_Interface
     * instances or route specs.
     *
     * @param array $routes
     *
     * @throws InvalidArgumentException
     * @return Core_Router
     */
    public function addRoutes(Array $routes)
    {
        foreach ($routes as $name => $route) {
            if ($route instanceof Core_Router_Route_Interface) {
                $this->addRoute($route);
            } else {
                if (!isset($route['type'])) {
                    throw new InvalidArgumentException('Failed adding route, no type set for route "'. $name .'"');
                }

                $options = isset($route['options']) ? $route['options'] : null;
                $this->createRoute($route['type'], $name, $options);
            }
        }
        return $this;
    }

    /**
     * Create a new route from spec.
     *
     * @param string $type    Route type
     * @param string $name    Route name
     * @param array  $options Route options
     *
     * @return Core_Router_Route_Interface
     */
    public function createRoute($type, $name, $options = array())
    {
        $route      = $this->getPluginLoader()->initializeRouterRoutePlugin(ucfirst($type), $name, $options);
        $this->addRoute($route);

        return $route;
    }

    /**
     * Fetch a route by name.
     *
     * @param string $name Name of route to fetch
     *
     * @return Core_Router_Route_Interface
     * @throws InvalidArgumentException If no such route found
     */
    public function getRoute($name)
    {
        if (!$this->hasRoute($name)) {
            throw new InvalidArgumentException(sprintf('Route "%s" not defined', $name));
        }
        return $this->_routes[$name];
    }

    /**
     * fetch all routes.
     *
     * @return array All routes in a named array.
     * @throws LogicException If no routes are defined
     */
    public function getRoutes()
    {
        if (empty($this->_routes)) {
            throw new LogicException('Routing failed, no routes defined');
        }
        return $this->_routes;
    }

    /**
     * Removes a route by name.
     *
     * @param string $name Name of route to remove
     *
     * @return Core_Router
     * @throws InvalidArgumentException If trying to remove an undefined route.
     */
    public function removeRoute($name)
    {
        if (!$this->hasRoute($name)) {
            throw new InvalidArgumentException(sprintf('Cannot remove undefined route "%s"', $name));
        }
        unset($this->_routes[$name]);
        return $this;
    }

    /**
     * Do the actual routing. If a route does match and has values, copy those
     * values to Request as params.
     *
     * @param string $path   Path to match routes against.
     * @param string $method HTTP method to match routes against. Default to current method.
     *
     * @return Core_Router_Route_Interface Matched route
     * @throws RuntimeException If no route matched.
     */
    public function route($path, $method = null)
    {
        $tokens     = parse_url($path);
        if ($tokens && isset($tokens['path'])) {
            $path   = $tokens['path'];
            if (isset($tokens['host'])) {
                // Copy/paste from Core_Request
                if (!isset($tokens['port']) ||
                    ('http' == $tokens['scheme'] && $tokens['port'] == 80) ||
                    ('https' == $tokens['scheme'] && $tokens['port'] == 443)
                ) {
                    $host = $tokens['host'];
                } else {
                    $host = $tokens['host'] .':'. $tokens['port'];
                }

                $hostname = $tokens['scheme'] .'://'. $host;
            }
        }
        $path       = str_replace($this->getBase(), '', $path);
        $routeFound = false;
        $separator  = $this->getSeparator();
        $request    = $this->getRequest();
        if (null === $method) {
            $method = $request->getMethod();
        }
        if (!isset($hostname)) {
            $hostname = $request->getUriBase();
        }
        $this->resetCurrentRoute();
        foreach ($this->getRoutes() as $route) {
            if ($route->match($path, $separator, $method, $hostname)) {
                $routeFound = true;
                $this->setCurrentRoute($route);
                break;
            }
        }

        if (!$routeFound) {
            $message = sprintf('Routing failed, no route matching path %s found!', $path);
            throw new Core_Application_NotFoundException($message);
        }

        // set route params to request
        $values = $route->getValues();
        if (!empty($values)) {
            foreach ($values as $name => $value) {
                $request->setParam($name, $value);
            }
        }

        return $route;
    }

    /**
     * Build an path using attached routes.
     *
     * @param array  $params Route params
     * @param string $name   Route name to use, default to current route.
     * @param bool   $reset  Do we wish not to use the values from the current route as defaults?
     *
     * @return string Built path.
     */
    public function path($params = null, $name = null, $reset = false)
    {
        $route = $this->_findRoute($name, $params);
        if (null === $name && true !== $reset) {
            $routeValues = $route->getValues();
            if ($routeValues) {
                $params = array_merge($routeValues, (array) $params);                
            }
        }
        return $route->path($this->getSeparator(), $params);
    }

    /**
     * Build an URL using attached routes.
     *
     * @param array  $params Route params
     * @param string $name   Route name to use, default to current route.
     * @param bool   $reset  Do we wish not to use the values from the current route as defaults?
     *
     * @return string Built URL.
     */
    public function url($params = null, $name = null, $reset = false)
    {
        $route = $this->_findRoute($name, $params);
        if (null === $name && true !== $reset) {
            $routeValues = $route->getValues();
            if ($routeValues) {
                $params = array_merge($routeValues, (array) $params);                
            }
        }
        return $route->url($this->getSeparator(), $params);
    }

    /**
     * Set router options. Will skip "request", "pluginLoader", "currentRoute"
     * and "options".
     *
     * @param array $options
     *
     * @return Core_Router
     * @throws InvalidArgumentException if an invalid option passed.
     */
    public function setOptions(Array $options)
    {
        if (isset($options['routes'])) {
            $this->addRoutes($options['routes']);
            unset($options['routes']);
        }

        $forbidden = array('Request', 'PluginLoader', 'CurrentRoute', 'Options');

        foreach ($options as $key => $value) {
            $normalized = ucfirst($key);
            if (in_array($normalized, $forbidden)) {
                continue;
            }

            $method = 'set' . $normalized;
            if (method_exists($this, $method)) {
                $this->$method($value);
            } else {
                throw new InvalidArgumentException(sprintf('Invalid option "%s" passed', $key));
            }
        }
        return $this;
    }

    /**
     * @param Zend_Config $config
     *
     * @return Core_Router
     */
    public function setConfig(Zend_Config $config)
    {
        return $this->setOptions($config->toArray());
    }
    

    /**
     * @param array $defaults
     *
     * @return Core_Router
     */
    public function setDefaults(array $defaults)
    {
        foreach ($defaults as $name => $value) {
            $this->setDefault($name, $value);
        }
        return $this;
    }

    /**
     * @param string $name  Name of the variable
     * @param mixed  $value Value to set for the variable
     *
     * @return Core_Router
     */
    public function setDefault($name, $value)
    {
        $this->_defaults[$name] = $value;
        return $this;
    }

    /**
     * @return array Router default values
     */
    public function getDefaults()
    {
        return $this->_defaults;
    }

    /**
     * @param string $name
     *
     * @return mixed
     */
    public function getDefault($name)
    {
        if (!isset($this->_defaults[$name])) {
            return null;
        }
        return $this->_defaults[$name];
    }

    /**
     * @param string $name
     *
     * @return \Core_Router_Route_Interface
     * @throws InvalidArgumentException
     */
    protected function _findRoute($name)
    {
        if (null !== $name) {
            $route = $this->getRoute($name);
        } else {
            $route = $this->getCurrentRoute();
        }

        if (!$route instanceof Core_Router_Route_Interface) {
            throw new InvalidArgumentException('Failed generating path, no route specified and no current route available');
        }
        return $route;
    }
}