<?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
 */

/**
 * @category   Core
 * @package    Core_Router
 * @subpackage Route
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 */
class Core_Router_Route_Token extends Core_Router_Route_Regex
{
    const TOKEN_TYPE_STATIC   = 'static';
    const TOKEN_TYPE_VARIABLE = 'variable';

    protected $_regexes = array();

    /**
     * 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())
    {
        if (null === ($hostname = $this->getHostname())) {
            return parent::url($separator, $params);
        }

        $regex = $this->_hostnamePatternToRegex($hostname);
        return $this->_injectParams($params, $regex, self::SEPARATOR_HOSTNAME) . $this->path($separator, $params);
    }

    /**
     * @return string
     */
    public function getRegex($separator = '/')
    {
        if (!array_key_exists($separator, $this->_regexes)) {
            $this->_regexes[$separator] = $this->_pattenToRegex($this->getPattern(), $separator);
        }
        return $this->_regexes[$separator];
    }

    /**
     * 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.
     */
    protected function _match($path, $separator)
    {
        // normalize the path to always include the trailing slash
        $path .= ($separator != substr($path, -(strlen($separator))) ? $separator : null);

        return parent::_match($path, $separator);
    }

    /**
     * @param string $pattern
     *
     * @return string Token parsed to a regex
     */
    protected function _pattenToRegex($pattern, $separator, $anchor = true)
    {
        $tokens    = $this->_tokenizePattern($pattern, $separator);

        $regex     = ($anchor ? '^'. $separator : '');
        foreach ($tokens as $token) {
            switch($token['type']) {
                case self::TOKEN_TYPE_VARIABLE:
                    if ($token['mandatory']) {
                        $regex  .=  '(?P<'. $token['name'] .'>'. $token['matcher'] .')' . $separator;
                    } else {
                        $regex  .= '((?P<'. $token['name'] .'>'. $token['matcher'] .')'. $separator .')?';
                    }
                    break;

                case self::TOKEN_TYPE_STATIC:
                    $regex   .= $token['value'] . $separator;
                    break;
            }
        }
        // add an "optional" modifier (to allow for the trailing separator to work):
        // i.e. /site and /site/
        //
        // - if the pattern is not-empty
        // - if the last token DOES NOT have a custom matcher
        // - if the last token DOES have a custom matcher which is "catch-all"
        // - if the last token DOES have a custom matcher, but it excludes the separator
        $trailing  = $pattern && (!isset($token['matcher']) || $token['matcher'] != '.+' || false !== strpos($token['matcher'], $separator));

        $regex    .= ($trailing ? '?' : null);
        $regex    .= ($anchor   ? '$' : null);

        return $regex;
    }

    /**
     * @param string $pattern
     *
     * @return array Token parsed to an array
     */
    protected function _tokenizePattern($pattern, $separator)
    {
        $pattern      = trim($pattern, $separator);
        if (empty($pattern)) {
            return array();
        }

        $placeholders = preg_split('@(?<!\{)'. preg_quote($separator) .'(?!\})@', $pattern);

        // variable matching regex
        $quote  = ('/' === $separator ? '@' : '/');
        $regex  = $quote .'{(?P<optional>\+)?(?P<name>[^}]+)}'. $quote;

        $tokens = array();
        foreach ($placeholders as $placeholder) {
            $matches = array();
            $matcher = '[^'. preg_quote($separator, $quote) .']+';
            if (preg_match($regex, $placeholder, $matches)) {
                if (false !== ($idx = strpos($matches['name'], ':')))  {
                    $matcher         = substr($matches['name'], $idx + 1) .'+';
                    $matches['name'] = substr($matches['name'], 0, $idx);
                }
                $tokens[] = array(
                    'type'        =>  self::TOKEN_TYPE_VARIABLE,
                    'name'        =>  $matches['name'],
                    'mandatory'   => !$matches['optional'],
                    'matcher'     =>  $matcher,
                );
            } else {
                $tokens[] = array(
                    'type'  => self::TOKEN_TYPE_STATIC,
                    'value' => $placeholder,
                );
            }
        }
        return $tokens;
    }


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

        return $this->_matchRegex($hostname, $this->_hostnamePatternToRegex($expectedHostname), '@');
    }

    /**
     * @param string $hostname
     *
     * @return string
     */
    protected function _hostnamePatternToRegex($hostname)
    {
        /*
         * hostname examples:
         * -          host.example.com
         * -          host.example.com:port
         * -        //host.example.com
         * -        //host.example.com:port
         * - scheme://host.example.com
         * - scheme://host.example.com:port
         */
        $scheme = null;
        if (false !== strpos($hostname, '//')) {
            list($scheme, $hostname) = explode('//', $hostname);
        }
        $regex  = $this->_pattenToRegex($hostname, self::SEPARATOR_HOSTNAME, false);
        if (null !== $scheme) {
            $regex = $scheme .'//'. $regex;
        }
        return '^'. $regex .'$';
    }
}
