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

/**
 * The route abstract.
 *
 * @category   Core
 * @package    Core_Router
 * @subpackage Route
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 */
abstract class Core_Router_Route_Abstract implements Core_Router_Route_Interface
{
    /**
     * @var string
     */
    protected $_pattern;

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

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

    /**
     * @var string route name
     */
    protected $_name;

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

    /**
     * @var string Default route values
     */
    protected $_defaults = array();

    /**
     * @var string Values found in route
     */
    protected $_plugins = array();

    /**
     * @var array
     */
    protected $_values = array();

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

    /**
     * @var Core_Router
     */
    protected $_router;

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

    /**
     * @var boolean
     */
    protected $_useQuery = false;

    /**
     * Examine if this route matches a path (in the global routing process)
     *
     * @param string $path      Path against we match the route against
     * @param string $separator Use this as the path separator
     *
     * @return bool True if route matches path, false otherwise.
     */
    abstract protected function _match($path, $separator);

    /**
     * @param string $name    Route name
     * @param array  $options Route options
     */
    public function __construct($name = null, $options = null)
    {
        $this->setName($name);

        if (is_array($options)) {
            $this->setOptions($options);
        }
    }

    /**
     * @param string $name
     *
     * @return Core_Router_Route_Abstract
     */
    public function setName($name)
    {
        $this->_name = $name;
        return $this;
    }

    /**
     * @return string
     */
    public function getName()
    {
        if (null === $this->_name) {
            $this->_name = get_class($this);
        }
        return $this->_name;
    }

    /**
     * @param string $pattern
     *
     * @return Core_Router_Route_Abstract
     */
    public function setPattern($pattern)
    {
        $this->_pattern = $pattern;
        return $this;
    }

    /**
     * @return string
     */
    public function getPattern()
    {
        return $this->_pattern;
    }

    /**
     * @param string $method
     *
     * @return Core_Router_Route_Abstract
     */
    public function setMethod($method)
    {
        $this->_method = $method;
        return $this;
    }

    /**
     * @return string
     */
    public function getMethod()
    {
        return $this->_method;
    }

    /**
     * @param string $hostname
     *
     * @return Core_Router_Route_Abstract
     */
    public function setHostname($hostname)
    {
        $this->_hostname = $hostname;
        return $this;
    }

    /**
     * @return string
     */
    public function getHostname()
    {
        return $this->_hostname;
    }

    /**
     * @param boolean $useQuery
     *
     * @return \Core_Router_Route_Abstract
     */
    public function setUseQuery($useQuery)
    {
        $this->_useQuery = (bool) $useQuery;
        return $this;
    }

    /**
     * @return boolean
     */
    public function isUseQuery()
    {
        return $this->_useQuery;
    }

    /**
     * @param array $options
     *
     * @return Core_Router_Route_Abstract
     * @throws InvalidArgumentException
     */
    public function setOptions(array $options)
    {
        if (isset($options['plugins'])) {
            $this->addPlugins($options['plugins']);
            unset($options['plugins']);
        }

        foreach ($options as $key => $value) {
            $normalized = ucfirst($key);

            $callable = array($this, 'set' . $normalized);
            if (is_callable($callable)) {
                call_user_func($callable, $value);
            } else {
                throw new InvalidArgumentException(sprintf('Invalid option "%s" passed', $key));
            }
        }
        return $this;
    }

    /**
     * @param string $name  Value name
     * @param mixed  $value Value
     *
     * @return \Core_Router_Route_Abstract
     */
    public function setValue($name, $value)
    {
        $this->_values[$name] = $value;
        return $this;
    }

    /**
     * @param string $name
     *
     * @return \Core_Router_Route_Abstract
     */
    public function removeValue($name) {
        if (isset($this->_values[$name])) {
            unset($this->_values[$name]);
        }
        return $this;
    }

    /**
     * @param array Values merged to route values.
     *
     * @return Core_Router_Route_abstract
     */
    public function addValues(array $values)
    {
        foreach($values as $name => $value) {
            $this->setValue($name, $value);
        }
        return $this;
    }

    /**
     * @return array
     */
    public function getValues()
    {
        return array_merge($this->_defaults, $this->_values);
    }

    /**
     * @param array $defaults
     *
     * @return Core_Router_Route_Abstract
     */
    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_Route_Abstract
     */
    public function setDefault($name, $value)
    {
        $this->_defaults[$name] = $value;
        return $this;
    }

    /**
     * @return array Route default values
     */
    public function getDefaults()
    {
        $router = $this->getRouter();
        if (null !== $router) {
            $routerDefaults = $router->getDefaults();
            return array_merge($routerDefaults, $this->_defaults);
        }
        return $this->_defaults;
    }

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

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

    /**
     * @param Core_Router $router
     *
     * @return \Core_Router_Route_Abstract
     */
    public function setRouter(Core_Router $router)
    {
        $this->_router = $router;
        return $this;
    }

    /**
     * @return \Core_Router_Route_Abstract
     */
    public function getRouter()
    {
        return $this->_router;
    }

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

    /**
     * @return string
     */
    public function getBase()
    {
        if (null === $this->_base) {
            $router = $this->getRouter();
            if (!$router) {
                return null;
            }
            return $router->getBase();
        }
        return $this->_base;
    }

    /**
     * @param Core_Loader_PluginLoader $pluginLoader
     *
     * @return Core_Router_Route_Abstract
     */
    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;
    }

    /**
     * @return array(Core_Router_Route_Plugin_Interface)
     * @throws InvalidArgumentException
     */
    public function getPlugins()
    {
        return $this->_plugins;
    }

    /**
     * @param string $name
     *
     * @return boolean
     */
    public function hasPlugin($name)
    {
        return array_key_exists(strtolower($name), $this->_plugins);
    }

    /**
     * @param string $name
     *
     * @return Core_Router_Route_Plugin_Interface
     * @throws InvalidArgumentException
     */
    public function getPlugin($name)
    {
        if (!$this->hasPlugin($name)) {
            throw new InvalidArgumentException(sprintf('Fetching route plugin "%s" failed: no such plugin', $name));
        }
        return $this->_plugins[strtolower($name)];
    }

    /**
     * @param string $type    Plugin type
     * @param string $name    Plugin name
     * @param array  $options Plugin options
     *
     * @return \Core_Router_Route_Abstract
     */
    public function createPlugin($type, $name, array $options = array())
    {
        $options['name'] = $name;
        $type            = ucfirst(strtolower($type));
        $plugin          = $this->getPluginLoader()->initializeRouterRoutePluginPlugin($type, $options);
        $this->addPlugin($plugin);
        return $this;
    }

    /**
     * @param Core_Router_Route_Plugin_Interface $plugin
     *
     * @return \Core_Router_Route_Abstract
     */
    public function addPlugin(Core_Router_Route_Plugin_Interface $plugin)
    {
        $name                  = strtolower($plugin->getName());
        $this->_plugins[$name] = $plugin;

        $plugin->setRoute($this);
        return $this;
    }

    /**
     * @param array $plugins
     *
     * @return \Core_Router_Route_Abstract
     */
    public function addPlugins(array $plugins)
    {
        foreach ($plugins as $name => $plugin) {
            if ($plugin instanceof Core_Router_Route_Plugin_Interface) {
                $this->addPlugin($plugin);
            } else if (is_array($plugin)) {
                $type    = $plugin['type'];
                $options = array();
                if (isset($plugin['options'])) {
                    $options = $plugin['options'];
                }
                $this->createPlugin($type, $name, $options);
            }
        }
        return $this;
    }

    /**
     * Generate an URL matching this route.
     *
     * @param string $separator Use this as the path separator
     * @param array  $params    Values to use when generating a route
     *
     * @return string URL matching route and params
     */
    public function url($separator, $params = array())
    {
        $hostname = $this->getHostname();
        if (null === $hostname) {
            $hostname = $this->getRouter()->getDefaultHostname();
        }
        return $hostname . $this->path($separator, $params);
    }

    /**
     * Examine if this route matches a path (in the global routing process)
     *
     * @param string $path      Path against we match the route against
     * @param string $separator Use this as the path separator
     * @param string $method    If set, the request method (GET, POST, etc.) must also match for the route to match
     * @param string $hostname  If set, the current hostname must match the specified hostname.
     *
     * @return bool True if route matches path, false otherwise.
     */
    public function match($path, $separator, $method = null, $hostname = null)
    {
        // Does the expected method match?
        $expectedMethod   = $this->getMethod();
        if (null !== $expectedMethod && 0 !== strcasecmp($expectedMethod, $method)) {
            return false;
        }

        // Does the expected hostname match?
        if (!$this->_matchHostname($hostname)) {
            return false;
        }

        // Does the path match?
        $match = $this->_match($path, $separator);
        if ($match) {
            foreach($this->getPlugins() as $plugin) {
                $headers = $this->_fetchRequestedHeaders($plugin->getHeadersRequest());
                $match   = ($match && $plugin->match($this->getValues(), $headers));
            }
        }
        if ($this->isUseQuery()) {
            // add values from query string (but only if not already in values)
            // for example, for /{+category}/
            // /foo/?category=bar, will get category: foo, not category: bar
            $query  = $this->getRequest()->getQuery();
            $values = array_diff_key($query, $this->getValues());
            $this->addValues($values);
        }
        return (bool) $match;
    }

    /**
     * @param string $hostname
     *
     * @return boolean
     */
    protected function _matchHostname($hostname)
    {
        $expectedHostname = $this->getHostname();
        if (null === $expectedHostname) {
            // No expected hostname, all hostnames are OK
            return true;
        }

        // expected tokens
        $expectedTokens = $this->_tokenizeHostname($expectedHostname);

        // current tokens
        // what ever is not set in current is said to match expected
        $tokens         = array_merge($expectedTokens, $this->_tokenizeHostname($hostname));

        // any difference is a failure
        return !(bool) array_diff_assoc($expectedTokens, $tokens);
    }

    /**
     * @param string $hostname
     *
     * @return array
     */
    protected function _tokenizeHostname($hostname)
    {
        $tokens = (array) parse_url($hostname);
        if (isset($tokens['path'])) {
            // Non FQHN with scheme
            $tokens['host'] = $tokens['path'];
            unset($tokens['path']);
        }
        if (isset($tokens['host']) && 0 === strpos($tokens['host'], '//')) {
            // buggy parse_url will not strip // from //example.com
            $tokens['host'] = substr($tokens['host'], 2);
        }
        return $tokens;
    }

    /**
     * @param array $requestedHeaders
     *
     * @return array
     */
    protected function _fetchRequestedHeaders(array $requestedHeaders)
    {
        $headers = array();
        if ($requestedHeaders) {
            $request = $this->getRequest();
            foreach ($requestedHeaders as $requestedHeader) {
                $callable = array($request, 'get'. $requestedHeader);
                if (is_callable($callable)) {
                    $value = call_user_func($callable);
                } else {
                    $value = $request->getHeader($requestedHeader);
                }
                $headers[$requestedHeader] = $value;
            }
        }
        // var_dump($headers);
        return $headers;
    }

    /**
     * @param string $path   Path
     * @param array  $params Params to append to path if useQuery is enabled
     *
     * @return string
     */
    protected function _appendQuery($path, array $params)
    {
        $queryString = null;
        if ($this->isUseQuery() && $params) {
            $queryString = http_build_query($params);
        }
        return $path .($queryString ? '?'. $queryString : null);
    }
}
