<?php

declare (strict_types=1);
namespace DynamicOOOS\Sabberworm\CSS\Property;

use DynamicOOOS\Sabberworm\CSS\Comment\Comment;
use DynamicOOOS\Sabberworm\CSS\OutputFormat;
use DynamicOOOS\Sabberworm\CSS\Parsing\ParserState;
use DynamicOOOS\Sabberworm\CSS\Parsing\UnexpectedTokenException;
use DynamicOOOS\Sabberworm\CSS\Property\Selector\Combinator;
use DynamicOOOS\Sabberworm\CSS\Property\Selector\Component;
use DynamicOOOS\Sabberworm\CSS\Property\Selector\CompoundSelector;
use DynamicOOOS\Sabberworm\CSS\Renderable;
use DynamicOOOS\Sabberworm\CSS\Settings;
use DynamicOOOS\Sabberworm\CSS\ShortClassNameProvider;
use function DynamicOOOS\Safe\preg_match;
/**
 * Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this
 * class.
 */
class Selector implements Renderable
{
    use ShortClassNameProvider;
    /**
     * @internal since 8.5.2
     */
    public const SELECTOR_VALIDATION_RX = '/
        ^(
            # not whitespace only
            (?!\\s*+$)
            (?:
                # any sequence of valid unescaped characters, except quotes
                [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>,]++
                |
                # one or more escaped characters
                (?:\\\\.)++
                |
                # quoted text, like in `[id="example"]`
                (?:
                    # opening quote
                    ([\'"])
                    (?:
                        # sequence of characters except closing quote or backslash
                        (?:(?!\\g{-1}|\\\\).)++
                        |
                        # one or more escaped characters
                        (?:\\\\.)++
                    )*+ # zero or more times
                    # closing quote or end (unmatched quote is currently allowed)
                    (?:\\g{-1}|$)
                )
            )*+ # zero or more times
        )$
        /ux';
    /**
     * @var non-empty-list<Component>
     */
    private $components;
    /**
     * @internal since V8.8.0
     */
    public static function isValid(string $selector) : bool
    {
        // Note: We need to use `static::` here as the constant is overridden in the `KeyframeSelector` class.
        $numberOfMatches = preg_match(static::SELECTOR_VALIDATION_RX, $selector);
        return $numberOfMatches === 1;
    }
    /**
     * @param non-empty-string|non-empty-list<Component> $selector
     *        Providing a string is deprecated in version 9.2 and will not work from v10.0
     *
     * @throws UnexpectedTokenException if the selector is not valid
     */
    public final function __construct($selector)
    {
        if (\is_string($selector)) {
            $this->setSelector($selector);
        } else {
            $this->setComponents($selector);
        }
    }
    /**
     * @param list<Comment> $comments
     *
     * @return non-empty-list<Component>
     *
     * @throws UnexpectedTokenException
     */
    private static function parseComponents(ParserState $parserState, array &$comments = []) : array
    {
        // Whitespace is a descendent combinator, not allowed around a compound selector.
        // (It is allowed within, e.g. as part of a string or within a function like `:not()`.)
        // Gobble any up now to get a clean start.
        $parserState->consumeWhiteSpace($comments);
        $selectorParts = [];
        while (\true) {
            try {
                $selectorParts[] = CompoundSelector::parse($parserState, $comments);
            } catch (UnexpectedTokenException $e) {
                if ($selectorParts !== [] && \end($selectorParts)->getValue() === ' ') {
                    // The whitespace was not a descendent combinator, and was, in fact, arbitrary,
                    // after the end of the selector.  Discard it.
                    \array_pop($selectorParts);
                    break;
                } else {
                    throw $e;
                }
            }
            try {
                $selectorParts[] = Combinator::parse($parserState, $comments);
            } catch (UnexpectedTokenException $e) {
                // End of selector has been reached.
                break;
            }
        }
        return $selectorParts;
    }
    /**
     * @param list<Comment> $comments
     *
     * @throws UnexpectedTokenException
     *
     * @internal
     */
    public static function parse(ParserState $parserState, array &$comments = []) : self
    {
        $selectorParts = self::parseComponents($parserState, $comments);
        // Check that the selector has been fully parsed:
        if (!\in_array($parserState->peek(), ['{', '}', ',', ''], \true)) {
            throw new UnexpectedTokenException('`,`, `{`, `}` or EOF', $parserState->peek(5), 'literal', $parserState->currentLine());
        }
        return new static($selectorParts);
    }
    /**
     * @return non-empty-list<Component>
     */
    public function getComponents() : array
    {
        return $this->components;
    }
    /**
     * @param non-empty-list<Component> $components
     *        This should be an alternating sequence of `CompoundSelector` and `Combinator`, starting and ending with a
     *        `CompoundSelector`, and may be a single `CompoundSelector`.
     */
    public function setComponents(array $components) : self
    {
        $this->components = $components;
        return $this;
    }
    /**
     * @return non-empty-string
     *
     * @deprecated in version 9.2, will be removed in v10.0.  Use either `getComponents()` or `render()` instead.
     */
    public function getSelector() : string
    {
        return $this->render(new OutputFormat());
    }
    /**
     * @param non-empty-string $selector
     *
     * @throws UnexpectedTokenException if the selector is not valid
     *
     * @deprecated in version 9.2, will be removed in v10.0.  Use `setComponents()` instead.
     */
    public function setSelector(string $selector) : void
    {
        $parserState = new ParserState($selector, Settings::create());
        $components = self::parseComponents($parserState);
        // Check that the selector has been fully parsed:
        if (!$parserState->isEnd()) {
            throw new UnexpectedTokenException('EOF', $parserState->peek(5), 'literal');
        }
        $this->components = $components;
    }
    /**
     * @return int<0, max>
     */
    public function getSpecificity() : int
    {
        return \array_sum(\array_map(static function (Component $component) : int {
            return $component->getSpecificity();
        }, $this->components));
    }
    public function render(OutputFormat $outputFormat) : string
    {
        return \implode('', \array_map(static function (Component $component) use($outputFormat) : string {
            return $component->render($outputFormat);
        }, $this->components));
    }
    /**
     * @return array<string, bool|int|float|string|array<mixed>|null>
     *
     * @internal
     */
    public function getArrayRepresentation() : array
    {
        return ['class' => $this->getShortClassName(), 'components' => \array_map(static function (Component $component) : array {
            return $component->getArrayRepresentation();
        }, $this->components)];
    }
}
