<?php

declare(strict_types=1);

namespace Sabberworm\CSS\Value;

use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\ShortClassNameProvider;

use function Safe\preg_match;

/**
 * Abstract base class for specific classes of CSS values: `Size`, `Color`, `CSSString` and `URL`, and another
 * abstract subclass `ValueList`.
 */
abstract class Value implements CSSElement, Positionable
{
    use Position;
    use ShortClassNameProvider;

    /**
     * @param int<1, max>|null $lineNumber
     */
    public function __construct(?int $lineNumber = null)
    {
        $this->setPosition($lineNumber);
    }

    /**
     * @param array<non-empty-string> $listDelimiters
     *
     * @return Value|string
     *
     * @throws UnexpectedTokenException
     * @throws UnexpectedEOFException
     *
     * @internal since V8.8.0
     */
    public static function parseValue(ParserState $parserState, array $listDelimiters = [])
    {
        /** @var list<Value|string> $stack */
        $stack = [];
        $parserState->consumeWhiteSpace();
        //Build a list of delimiters and parsed values
        while (
        !($parserState->comes('}') || $parserState->comes(';') || $parserState->comes('!')
            || $parserState->comes(')')
            || $parserState->isEnd())
        ) {
            if (\count($stack) > 0) {
                $foundDelimiter = false;
                foreach ($listDelimiters as $delimiter) {
                    if ($parserState->comes($delimiter)) {
                        \array_push($stack, $parserState->consume($delimiter));
                        $parserState->consumeWhiteSpace();
                        $foundDelimiter = true;
                        break;
                    }
                }
                if (!$foundDelimiter) {
                    //Whitespace was the list delimiter
                    \array_push($stack, ' ');
                }
            }
            \array_push($stack, self::parsePrimitiveValue($parserState));
            $parserState->consumeWhiteSpace();
        }
        // Convert the list to list objects
        foreach ($listDelimiters as $delimiter) {
            $stackSize = \count($stack);
            if ($stackSize === 1) {
                return $stack[0];
            }
            $newStack = [];
            for ($offset = 0; $offset < $stackSize; ++$offset) {
                if ($offset === ($stackSize - 1) || $delimiter !== $stack[$offset + 1]) {
                    $newStack[] = $stack[$offset];
                    continue;
                }
                $length = 2; //Number of elements to be joined
                for ($i = $offset + 3; $i < $stackSize; $i += 2, ++$length) {
                    if ($delimiter !== $stack[$i]) {
                        break;
                    }
                }
                $list = new RuleValueList($delimiter, $parserState->currentLine());
                for ($i = $offset; $i - $offset < $length * 2; $i += 2) {
                    $list->addListComponent($stack[$i]);
                }
                $newStack[] = $list;
                $offset += $length * 2 - 2;
            }
            $stack = $newStack;
        }
        if (!isset($stack[0])) {
            throw new UnexpectedTokenException(
                " {$parserState->peek()} ",
                $parserState->peek(1, -1) . $parserState->peek(2),
                'literal',
                $parserState->currentLine()
            );
        }
        return $stack[0];
    }

    /**
     * @return CSSFunction|string
     *
     * @throws UnexpectedEOFException
     * @throws UnexpectedTokenException
     *
     * @internal since V8.8.0
     */
    public static function parseIdentifierOrFunction(ParserState $parserState, bool $ignoreCase = false)
    {
        $anchor = $parserState->anchor();
        $result = $parserState->parseIdentifier($ignoreCase);

        if ($parserState->comes('(')) {
            $anchor->backtrack();
            if ($parserState->streql('url', $result)) {
                $result = URL::parse($parserState);
            } elseif ($parserState->streql('calc', $result)) {
                $result = CalcFunction::parse($parserState);
            } else {
                $result = CSSFunction::parse($parserState, $ignoreCase);
            }
        }

        return $result;
    }

    /**
     * @return CSSFunction|CSSString|LineName|Size|URL|string
     *
     * @throws UnexpectedEOFException
     * @throws UnexpectedTokenException
     * @throws SourceException
     *
     * @internal since V8.8.0
     */
    public static function parsePrimitiveValue(ParserState $parserState)
    {
        $value = null;
        $parserState->consumeWhiteSpace();
        if (
            \is_numeric($parserState->peek())
            || ($parserState->comes('-.')
                && \is_numeric($parserState->peek(1, 2)))
            || (($parserState->comes('-') || $parserState->comes('.')) && \is_numeric($parserState->peek(1, 1)))
        ) {
            $value = Size::parse($parserState);
        } elseif ($parserState->comes('#') || $parserState->comes('rgb', true) || $parserState->comes('hsl', true)) {
            $value = Color::parse($parserState);
        } elseif ($parserState->comes("'") || $parserState->comes('"')) {
            $value = CSSString::parse($parserState);
        } elseif ($parserState->comes('progid:') && $parserState->getSettings()->usesLenientParsing()) {
            $value = self::parseMicrosoftFilter($parserState);
        } elseif ($parserState->comes('[')) {
            $value = LineName::parse($parserState);
        } elseif ($parserState->comes('U+')) {
            $value = self::parseUnicodeRangeValue($parserState);
        } else {
            $nextCharacter = $parserState->peek(1);
            try {
                $value = self::parseIdentifierOrFunction($parserState);
            } catch (UnexpectedTokenException $e) {
                if (\in_array($nextCharacter, ['+', '-', '*', '/'], true)) {
                    $value = $parserState->consume(1);
                } else {
                    throw $e;
                }
            }
        }
        $parserState->consumeWhiteSpace();

        return $value;
    }

    /**
     * @return array<string, bool|int|float|string|array<mixed>|null>
     *
     * @internal
     */
    public function getArrayRepresentation(): array
    {
        return [
            'class' => $this->getShortClassName(),
        ];
    }

    /**
     * @throws UnexpectedEOFException
     * @throws UnexpectedTokenException
     */
    private static function parseMicrosoftFilter(ParserState $parserState): CSSFunction
    {
        $function = $parserState->consumeUntil('(', false, true);
        $arguments = Value::parseValue($parserState, [',', '=']);
        return new CSSFunction($function, $arguments, ',', $parserState->currentLine());
    }

    /**
     * @throws UnexpectedEOFException
     * @throws UnexpectedTokenException
     */
    private static function parseUnicodeRangeValue(ParserState $parserState): string
    {
        $codepointMaxLength = 6; // Code points outside BMP can use up to six digits
        $range = '';
        $parserState->consume('U+');
        do {
            if ($parserState->comes('-')) {
                $codepointMaxLength = 13; // Max length is 2 six-digit code points + the dash(-) between them
            }
            $range .= $parserState->consume(1);
        } while (
            (\strlen($range) < $codepointMaxLength) && (preg_match('/[A-Fa-f0-9\\?-]/', $parserState->peek()) === 1)
        );

        return "U+{$range}";
    }
}
