<?php

/**
 * This file is part of the MathExecutor package
 *
 * (c) Alexander Kiryukhin
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */
namespace Barn2\Plugin\WC_Product_Options\Dependencies\NXP\Classes;

use Barn2\Plugin\WC_Product_Options\Dependencies\NXP\Exception\IncorrectBracketsException;
use Barn2\Plugin\WC_Product_Options\Dependencies\NXP\Exception\UnknownOperatorException;
use RuntimeException;
use SplStack;
/**
 * @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
 */
class Tokenizer
{
    /** @var array<Token> */
    public array $tokens = [];
    private string $input = '';
    private string $numberBuffer = '';
    private string $stringBuffer = '';
    private bool $allowNegative = \true;
    /** @var array<Operator> */
    private array $operators = [];
    private bool $inSingleQuotedString = \false;
    private bool $inDoubleQuotedString = \false;
    /**
     * Tokenizer constructor.
     * @param Operator[] $operators
     */
    public function __construct(string $input, array $operators)
    {
        $this->input = $input;
        $this->operators = $operators;
    }
    public function tokenize(): self
    {
        $isLastCharEscape = \false;
        foreach (\str_split($this->input) as $ch) {
            switch (\true) {
                case $this->inSingleQuotedString:
                    if ('\\' === $ch) {
                        if ($isLastCharEscape) {
                            $this->stringBuffer .= '\\';
                            $isLastCharEscape = \false;
                        } else {
                            $isLastCharEscape = \true;
                        }
                        continue 2;
                    } elseif ("'" === $ch) {
                        if ($isLastCharEscape) {
                            $this->stringBuffer .= "'";
                            $isLastCharEscape = \false;
                        } else {
                            $this->tokens[] = new Token(Token::String, $this->stringBuffer);
                            $this->inSingleQuotedString = \false;
                            $this->stringBuffer = '';
                        }
                        continue 2;
                    }
                    if ($isLastCharEscape) {
                        $this->stringBuffer .= '\\';
                        $isLastCharEscape = \false;
                    }
                    $this->stringBuffer .= $ch;
                    continue 2;
                case $this->inDoubleQuotedString:
                    if ('\\' === $ch) {
                        if ($isLastCharEscape) {
                            $this->stringBuffer .= '\\';
                            $isLastCharEscape = \false;
                        } else {
                            $isLastCharEscape = \true;
                        }
                        continue 2;
                    } elseif ('"' === $ch) {
                        if ($isLastCharEscape) {
                            $this->stringBuffer .= '"';
                            $isLastCharEscape = \false;
                        } else {
                            $this->tokens[] = new Token(Token::String, $this->stringBuffer);
                            $this->inDoubleQuotedString = \false;
                            $this->stringBuffer = '';
                        }
                        continue 2;
                    }
                    if ($isLastCharEscape) {
                        $this->stringBuffer .= '\\';
                        $isLastCharEscape = \false;
                    }
                    $this->stringBuffer .= $ch;
                    continue 2;
                case '[' === $ch:
                    $this->tokens[] = new Token(Token::Function, 'array');
                    $this->allowNegative = \true;
                    $this->tokens[] = new Token(Token::LeftParenthesis, '');
                    continue 2;
                case ' ' == $ch || "\n" == $ch || "\r" == $ch || "\t" == $ch:
                    $this->emptyNumberBufferAsLiteral();
                    $this->emptyStrBufferAsVariable();
                    $this->tokens[] = new Token(Token::Space, '');
                    continue 2;
                case $this->isNumber($ch):
                    if ('' != $this->stringBuffer) {
                        $this->stringBuffer .= $ch;
                        continue 2;
                    }
                    $this->numberBuffer .= $ch;
                    $this->allowNegative = \false;
                    break;
                /** @noinspection PhpMissingBreakStatementInspection */
                case 'e' === \strtolower($ch):
                    if (\strlen($this->numberBuffer) && \false !== \strpos($this->numberBuffer, '.')) {
                        $this->numberBuffer .= 'e';
                        $this->allowNegative = \false;
                        break;
                    }
                // no break
                // Intentionally fall through
                case $this->isAlpha($ch):
                    if (\strlen($this->numberBuffer)) {
                        $this->emptyNumberBufferAsLiteral();
                        $this->tokens[] = new Token(Token::Operator, '*');
                    }
                    $this->allowNegative = \false;
                    $this->stringBuffer .= $ch;
                    break;
                case '"' == $ch:
                    $this->inDoubleQuotedString = \true;
                    continue 2;
                case "'" == $ch:
                    $this->inSingleQuotedString = \true;
                    continue 2;
                case $this->isDot($ch):
                    $this->numberBuffer .= $ch;
                    $this->allowNegative = \false;
                    break;
                case $this->isLP($ch):
                    if ('' != $this->stringBuffer) {
                        $this->tokens[] = new Token(Token::Function, $this->stringBuffer);
                        $this->stringBuffer = '';
                    } elseif (\strlen($this->numberBuffer)) {
                        $this->emptyNumberBufferAsLiteral();
                        $this->tokens[] = new Token(Token::Operator, '*');
                    }
                    $this->allowNegative = \true;
                    $this->tokens[] = new Token(Token::LeftParenthesis, '');
                    break;
                case $this->isRP($ch) || ']' === $ch:
                    $this->emptyNumberBufferAsLiteral();
                    $this->emptyStrBufferAsVariable();
                    $this->allowNegative = \false;
                    $this->tokens[] = new Token(Token::RightParenthesis, '');
                    break;
                case $this->isComma($ch):
                    $this->emptyNumberBufferAsLiteral();
                    $this->emptyStrBufferAsVariable();
                    $this->allowNegative = \true;
                    $this->tokens[] = new Token(Token::ParamSeparator, '');
                    break;
                default:
                    // special case for unary operations
                    if ('-' == $ch || '+' == $ch) {
                        if ($this->allowNegative) {
                            $this->allowNegative = \false;
                            $this->tokens[] = new Token(Token::Operator, '-' == $ch ? 'uNeg' : 'uPos');
                            continue 2;
                        }
                        // could be in exponent, in which case negative should be added to the numberBuffer
                        if ($this->numberBuffer && 'e' == $this->numberBuffer[\strlen($this->numberBuffer) - 1]) {
                            $this->numberBuffer .= $ch;
                            continue 2;
                        }
                    }
                    $this->emptyNumberBufferAsLiteral();
                    $this->emptyStrBufferAsVariable();
                    if ('$' != $ch) {
                        if (\count($this->tokens) > 0) {
                            if (Token::Operator === $this->tokens[\count($this->tokens) - 1]->type) {
                                $this->tokens[\count($this->tokens) - 1]->value .= $ch;
                            } else {
                                $this->tokens[] = new Token(Token::Operator, $ch);
                            }
                        } else {
                            $this->tokens[] = new Token(Token::Operator, $ch);
                        }
                    }
                    $this->allowNegative = \true;
            }
        }
        $this->emptyNumberBufferAsLiteral();
        $this->emptyStrBufferAsVariable();
        return $this;
    }
    /**
     * @throws IncorrectBracketsException
     * @throws UnknownOperatorException
     * @return Token[] Array of tokens in revers polish notation
     */
    public function buildReversePolishNotation(): array
    {
        $tokens = [];
        /** @var SplStack<Token> $stack */
        $stack = new SplStack();
        /**
         * @var SplStack<int> $paramCounter
         */
        $paramCounter = new SplStack();
        foreach ($this->tokens as $token) {
            switch ($token->type) {
                case Token::Literal:
                case Token::Variable:
                case Token::String:
                    $tokens[] = $token;
                    if ($paramCounter->count() > 0 && 0 === $paramCounter->top()) {
                        $paramCounter->push($paramCounter->pop() + 1);
                    }
                    break;
                case Token::Function:
                    if ($paramCounter->count() > 0 && 0 === $paramCounter->top()) {
                        $paramCounter->push($paramCounter->pop() + 1);
                    }
                    $stack->push($token);
                    $paramCounter->push(0);
                    break;
                case Token::LeftParenthesis:
                    $stack->push($token);
                    break;
                case Token::ParamSeparator:
                    while (Token::LeftParenthesis !== $stack->top()->type) {
                        if (0 === $stack->count()) {
                            throw new IncorrectBracketsException();
                        }
                        $tokens[] = $stack->pop();
                    }
                    $paramCounter->push($paramCounter->pop() + 1);
                    break;
                case Token::Operator:
                    if (!\array_key_exists($token->value, $this->operators)) {
                        throw new UnknownOperatorException($token->value);
                    }
                    $op1 = $this->operators[$token->value];
                    while ($stack->count() > 0 && Token::Operator === $stack->top()->type) {
                        if (!\array_key_exists($stack->top()->value, $this->operators)) {
                            throw new UnknownOperatorException($stack->top()->value);
                        }
                        $op2 = $this->operators[$stack->top()->value];
                        if ($op2->priority >= $op1->priority) {
                            $tokens[] = $stack->pop();
                            continue;
                        }
                        break;
                    }
                    $stack->push($token);
                    break;
                case Token::RightParenthesis:
                    while (\true) {
                        try {
                            $ctoken = $stack->pop();
                            if (Token::LeftParenthesis === $ctoken->type) {
                                break;
                            }
                            $tokens[] = $ctoken;
                        } catch (RuntimeException $e) {
                            throw new IncorrectBracketsException();
                        }
                    }
                    if ($stack->count() > 0 && Token::Function == $stack->top()->type) {
                        /**
                         * @var Token $f
                         */
                        $f = $stack->pop();
                        $f->paramCount = $paramCounter->pop();
                        $tokens[] = $f;
                    }
                    break;
                case Token::Space:
            }
        }
        while (0 !== $stack->count()) {
            if (Token::LeftParenthesis === $stack->top()->type || Token::RightParenthesis === $stack->top()->type) {
                throw new IncorrectBracketsException();
            }
            if (Token::Space === $stack->top()->type) {
                $stack->pop();
                continue;
            }
            $tokens[] = $stack->pop();
        }
        return $tokens;
    }
    private function isNumber(string $ch): bool
    {
        return $ch >= '0' && $ch <= '9';
    }
    private function isAlpha(string $ch): bool
    {
        return $ch >= 'a' && $ch <= 'z' || $ch >= 'A' && $ch <= 'Z' || '_' == $ch;
    }
    private function emptyNumberBufferAsLiteral(): void
    {
        if (\strlen($this->numberBuffer)) {
            $this->tokens[] = new Token(Token::Literal, $this->numberBuffer);
            $this->numberBuffer = '';
        }
    }
    private function isDot(string $ch): bool
    {
        return '.' == $ch;
    }
    private function isLP(string $ch): bool
    {
        return '(' == $ch;
    }
    private function isRP(string $ch): bool
    {
        return ')' == $ch;
    }
    private function emptyStrBufferAsVariable(): void
    {
        if ('' != $this->stringBuffer) {
            $this->tokens[] = new Token(Token::Variable, $this->stringBuffer);
            $this->stringBuffer = '';
        }
    }
    private function isComma(string $ch): bool
    {
        return ',' == $ch;
    }
}
