<?php

namespace WellRESTed\Routing\Route;

/**
 * @internal
 */
class TemplateRoute extends Route
{
    /** Regular expression matching a URI template variable (e.g., {id}) */
    public const URI_TEMPLATE_EXPRESSION_RE = '/{([+.\/]?[a-zA-Z0-9_,]+\*?)}/';
    /**
     * Regular expression matching 1 or more unreserved characters.
     * ALPHA / DIGIT / "-" / "." / "_" / "~"
     */
    private const RE_UNRESERVED = '[0-9a-zA-Z\-._\~%]*';

    /** @var array */
    private $pathVariables = [];
    /** @var array */
    private $explosions = [];

    public function getType(): int
    {
        return Route::TYPE_PATTERN;
    }

    public function getPathVariables(): array
    {
        return $this->pathVariables;
    }

    /**
     * Examines a request target to see if it is a match for the route.
     *
     * @param string $requestTarget
     * @return bool
     */
    public function matchesRequestTarget(string $requestTarget): bool
    {
        $this->pathVariables = [];
        $this->explosions = [];

        if (!$this->matchesStartOfRequestTarget($requestTarget)) {
            return false;
        }

        $matchingPattern = $this->getMatchingPattern();

        if (preg_match($matchingPattern, $requestTarget, $captures)) {
            $this->pathVariables = $this->processMatches($captures);
            return true;
        }
        return false;
    }

    private function matchesStartOfRequestTarget(string $requestTarget): bool
    {
        $firstVarPos = strpos($this->target, '{');
        if ($firstVarPos === false) {
            return $requestTarget === $this->target;
        }
        return substr($requestTarget, 0, $firstVarPos) === substr($this->target, 0, $firstVarPos);
    }

    private function processMatches(array $matches): array
    {
        $variables = [];

        // Isolate the named captures.
        $keys = array_filter(array_keys($matches), 'is_string');

        // Store named captures to the variables.
        foreach ($keys as $key) {
            $value = $matches[$key];

            if (isset($this->explosions[$key])) {
                $values = explode($this->explosions[$key], $value);
                $variables[$key] = array_map('urldecode', $values);
            } else {
                $value = urldecode($value);
                $variables[$key] = $value;
            }
        }

        return $variables;
    }

    private function getMatchingPattern(): string
    {
        // Convert the template into the pattern
        $pattern = $this->target;

        // Escape allowable characters with regex meaning.
        $escape = [
            '.' => '\\.',
            '-' => '\\-',
            '+' => '\\+',
            '*' => '\\*'
        ];
        $pattern = str_replace(array_keys($escape), array_values($escape), $pattern);
        $unescape = [
            '{\\+' => '{+',
            '{\\.' => '{.',
            '\\*}' => '*}'
        ];
        $pattern = str_replace(array_keys($unescape), array_values($unescape), $pattern);

        // Surround the pattern with delimiters.
        $pattern = "~^{$pattern}$~";

        $pattern = preg_replace_callback(
            self::URI_TEMPLATE_EXPRESSION_RE,
            [$this, 'uriVariableReplacementCallback'],
            $pattern
        );

        return $pattern;
    }

    private function uriVariableReplacementCallback(array $matches): string
    {
        $name = $matches[1];
        $pattern = self::RE_UNRESERVED;

        $prefix = '';
        $delimiter = ',';
        $explodeDelimiter = ',';

        // Read the first character as an operator. This determines which
        // characters to allow in the match.
        $operator = $name[0];

        // Read the last character as the modifier.
        $explosion = (substr($name, -1, 1) === '*');

        switch ($operator) {
            case '+':
                $name = substr($name, 1);
                $pattern = '.*';
                break;
            case '.':
                $name = substr($name, 1);
                $prefix = '\\.';
                $delimiter = '\\.';
                $explodeDelimiter = '.';
                break;
            case '/':
                $name = substr($name, 1);
                $prefix = '\\/';
                $delimiter = '\\/';
                if ($explosion) {
                    $pattern = '[0-9a-zA-Z\-._\~%,\/]*'; // Unreserved + "," and "/"
                    $explodeDelimiter = '/';
                }
                break;
        }

        // Explosion
        if ($explosion) {
            $name = substr($name, 0, -1);
            if ($pattern === self::RE_UNRESERVED) {
                $pattern = '[0-9a-zA-Z\-._\~%,]*'; // Unreserved + ","
            }
            $this->explosions[$name] = $explodeDelimiter;
        }

        $names = explode(',', $name);
        $results = [];
        foreach ($names as $name) {
            $results[] = "(?<{$name}>{$pattern})";
        }
        return $prefix . join($delimiter, $results);
    }
}
