<?php

namespace Rize\UriTemplate;

use Rize\UriTemplate\Node\Abstraction;
use Rize\UriTemplate\Node\Expression;
use Rize\UriTemplate\Node\Variable;
use Rize\UriTemplate\Operator\UnNamed;

class Parser
{
    private const REGEX_VARNAME = '[A-z0-9.]|%[0-9a-fA-F]{2}';

    /**
     * Parses URI Template and returns nodes.
     *
     * @return Node\Abstraction[]
     */
    public function parse(string $template): array
    {
        $parts = preg_split('#(\{[^}]+})#', $template, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
        $nodes = [];

        foreach ($parts as $part) {
            $node = $this->createNode($part);

            // if current node has dot separator that requires a forward lookup
            // for the previous node iff previous node's operator is UnNamed
            if ($node instanceof Expression && $node->getOperator()->id === '.') {
                if (count($nodes) > 0) {
                    $previousNode = $nodes[count($nodes) - 1];
                    if ($previousNode instanceof Expression && $previousNode->getOperator() instanceof UnNamed) {
                        $previousNode->setForwardLookupSeparator($node->getOperator()->id);
                    }
                }
            }

            $nodes[] = $node;
        }

        return $nodes;
    }

    protected function createNode(string $token): Abstraction
    {
        // literal string
        if ($token[0] !== '{') {
            $node = $this->createLiteralNode($token);
        } else {
            // remove `{}` from expression and parse it
            $node = $this->parseExpression(substr($token, 1, -1));
        }

        return $node;
    }

    protected function parseExpression(string $expression): Expression
    {
        $token  = $expression;
        $prefix = $token[0];

        // not a valid operator?
        if (!Operator\Abstraction::isValid($prefix)) {
            // not valid chars?
            if (!preg_match('#' . self::REGEX_VARNAME . '#', $token)) {
                throw new \InvalidArgumentException("Invalid operator [{$prefix}] found at {$token}");
            }

            // default operator
            $prefix = null;
        }

        // remove operator prefix if exists e.g. '?'
        if ($prefix) {
            $token = substr($token, 1);
        }

        // parse variables
        $vars = [];
        foreach (explode(',', $token) as $var) {
            $vars[] = $this->parseVariable($var);
        }

        return $this->createExpressionNode(
            $token,
            $this->createOperatorNode($prefix),
            $vars,
        );
    }

    protected function parseVariable(string $var): Variable
    {
        $var      = trim($var);
        $val      = null;
        $modifier = null;

        // check for prefix (:) / explode (*) / array (%) modifier
        if (str_contains($var, ':')) {
            $modifier = ':';
            [$varname, $val] = explode(':', $var);

            // error checking
            if (!is_numeric($val)) {
                throw new \InvalidArgumentException("Value for `:` modifier must be numeric value [{$varname}:{$val}]");
            }
        }

        switch ($last = substr($var, -1)) {
            case '*':
            case '%':
                // there can be only 1 modifier per var
                if ($modifier) {
                    throw new \InvalidArgumentException("Multiple modifiers per variable are not allowed [{$var}]");
                }

                $modifier = $last;
                $var = substr($var, 0, -1);

                break;
        }

        return $this->createVariableNode(
            $var,
            ['modifier' => $modifier, 'value' => $val],
        );
    }

    protected function createVariableNode($token, $options = []): Variable
    {
        return new Variable($token, $options);
    }

    protected function createExpressionNode($token, ?Operator\Abstraction $operator = null, array $vars = []): Expression
    {
        return new Expression($token, $operator, $vars);
    }

    protected function createLiteralNode(string $token): Node\Literal
    {
        return new Node\Literal($token);
    }

    protected function createOperatorNode($token): Operator\Abstraction
    {
        return Operator\Abstraction::createById($token);
    }
}
