<?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_Regex extends Core_Router_Route_Abstract
{
    const SEPARATOR_HOSTNAME  = '.';

    /**
     * @var string Regex modifiers
     */
    protected $_modifiers;

    /**
     * @var string Regex tokens
     */
    protected $_tokens;

    /**
     * @param string $modifiers Route regex modifiers
     *
     * @return Core_Router_Route_Regex
     */
    public function setModifiers($modifiers)
    {
        $this->_modifiers = $modifiers;
        return $this;
    }

    /**
     * @return string Route regex modifiers
     */
    public function getModifiers()
    {
        if (false === strpos($this->_modifiers, 'i'))  {
            $this->_modifiers .= 'i';
        }
        return $this->_modifiers;
    }

    /**
     * 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);
        }

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

    /**
     * @oaram string  $separator Use this as the path separator
     * @param array   $params    Route params to set, if any.
     *
     * @return string Assembled path.
     */
    public function path($separator, $params = null)
    {
        $unused = array();
        $path   = $this->getBase() . $this->_injectParams($params, $this->getRegex(), $separator, $unused);
        return $this->_appendQuery($path, $unused);
    }

    /**
     * When setting the route pattern, nullify the pattern tokens (if previously set)
     *
     * @param string $pattern
     *
     * @return Core_Router_Route_Regex
     */
    public function setPattern($pattern)
    {
        $this->_tokens = null;
        return parent::setPattern($pattern);
    }

    /**
     * @param string $regex
     *
     * @return Core_Router_Route_Regex
     */
    public function setRegex($regex)
    {
        return $this->setPattern($regex);
    }

    /**
     * @return string
     */
    public function getRegex($separator = '/')
    {
        return $this->getPattern();
    }

    /**
     * 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)
    {
        // this should be a valid regex, but no leading / trailing separator
        // use @ as regex separator if route separator is /, else use /
        $regexSeparator = ('/' == $separator) ? '@' : '/';

        return $this->_matchRegex($path, $this->getRegex(), $regexSeparator);
    }

    /**
     * @param string $string
     * @param string $regex
     * @param string $regexSeparator
     *
     * @return type
     */
    protected function _matchRegex($string, $regex, $regexSeparator)
    {
        // construct the regex
        $regex   = $regexSeparator . $regex . $regexSeparator . $this->getModifiers();

        $matches = array();
        if (true === ($match = (bool) preg_match($regex, $string, $matches))) {
            // route match found!
            foreach ($matches as $key => $value) {
                if (!is_numeric($key)) {
                    $this->setValue($key, $value);
                }
            }
        }
        return $match;
    }

    /**
     * Tokenize the regex.
     *
     * This method is nasty because it uses regex to parse regex which makes the parsing regex complex.
     *
     * @param string $regex     Regex to tokenize
     * @param string $separator Use this as the path separator
     *
     * @return array Keys "values" and "fill" (used to prefill $params prior to building the URL)
     */
    protected function _tokenizeRegex($regex, $separator)
    {
        if (!isset($this->_tokens[$regex]) || null === $this->_tokens[$regex]) {
            // find all placeholders in the regex
            $separator = preg_quote($separator, '@');

            /*
             * optional tokens (/ == $separator)
             *  (?P<param>.*)?
             *  (/(?P<param>.*))?
             *  ((?P<param>.*)/)?
             */
            $matcher   = "@
                # match (?P<param>.*)
                \(?
                ". $separator ."?
                \(
                    # ?P<name> or ?P'name' (P is optional)
                    \?P?[<'](?<name>[^>']+)[>']

                    # whatever until ')' (closing the block)
                    (?<matcher>[^)]+)
                \)
                ". $separator ."?
                \)?
            (?<optional>\?)?
            @x";

            $tokens    = array();
            $matches   = array();
            if (preg_match_all($matcher, $regex, $matches, PREG_SET_ORDER)) {
                $tokens['mandatory'] = array();
                $tokens['urlencode'] = array();
                foreach ($matches as $match) {
                    $name                  = $match['name'];
                    $tokens['fill'][$name] = null;
                    if (!isset($match['optional'])) {
                        $tokens['mandatory'][] = $name;
                    }
                    if (false === strpos($match['matcher'], '.')) {
                        $tokens['urlencode'][] = $name;
                    }
                }
            }
            $this->_tokens[$regex] = $tokens;
        }
        return $this->_tokens[$regex];
    }

    /**
     * @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, $expectedHostname, '@');
    }

    /**
     *
     * @param string $params    Params to inject
     * @param string $regex     Regex to inject the params to
     * @param string $separator Path separator to use
     * @param array  $unused    Unused params will be placed here
     *
     * @return string
     * @throws InvalidArgumentException
     */
    protected function _injectParams($params, $regex, $separator, &$unused = array())
    {
        $params         = (array) $params;
        
        $tokens         = $this->_tokenizeRegex($regex, $separator);

        // we expect params to always resemble an array
        $defaults       = $this->getDefaults();
        $defaultsDiff   = array_diff_key($defaults, $params);
        $params         = array_merge($defaults, $params);

        // stripping optional start/end anchors
        $patterns       = array('/^\^?/', '/\$?$/', '/\.\?$/');
        $replacements   = array('', '', '');

        // prefill params with tokens to nullify empty values
        if ($tokens) {
            $unused = array_diff_key($params, $tokens['fill']);
            $diff   = array_diff($tokens['mandatory'], array_keys($params));
            if (!empty($diff)) {
                // mandatory params for route not set
                $message = 'Failed building route "%s", values for mandatory params ("%s") not set';
                throw new InvalidArgumentException(sprintf($message, $this->getName(), implode('", "', $diff)));
            }
            $params = array_merge($tokens['fill'], $params);

            // replace all placeholder tokens with values
            foreach ($params as $key => $value) {
                if (is_array($value) || is_object($value)) {
                    $value = http_build_query($value);
                }
                $patterns[]     = "#\(\?P?[<']". $key ."[>'][^)]+\)\??#i";
                if (in_array($key, $tokens['urlencode'])) {
                    $replacements[] = urlencode($value);
                } else {
                    $replacements[] = $value;
                }
            }
        } else {
            $unused = $params;
        }
        
        if ($unused) {
            if ($defaults) {
                $unused = array_diff_assoc($unused, $defaults);
            }
            if ($defaultsDiff) {
                $unused = array_diff_key($unused, $defaultsDiff);
            }
        }

        // strip optional separators without attached params
        // ie. (/)?
        $patterns[]     = '/\('. preg_quote($separator, '/') .'\)\?/';
        $replacements[] = null;

        // extract non-empty groups from optional grouping
        // (something)? => something
        $patterns[]     = '/\(([^)]+)\)\??/';
        $replacements[] = '\\1';

        // expand trailing separator
        // ie. /?
        $patterns[]     = '/'. preg_quote($separator, '/') .'\??$/';
        $replacements[] = $separator;

        // replace the  patterns with replacements inside the regex
        return preg_replace($patterns, $replacements, $regex);
    }
}
