<?php

declare (strict_types=1);
namespace OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Lexer\Token;

use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\Iterable\ArrayClosingBracketMissing;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\Iterable\ArrayCommaMissing;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\Iterable\ArrayExpectedCommaOrClosingBracket;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\Iterable\ArrayMissingSubType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\Iterable\ShapedArrayClosingBracketMissing;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\Iterable\ShapedArrayColonTokenMissing;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\Iterable\ShapedArrayCommaMissing;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\Iterable\ShapedArrayElementTypeMissing;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\Iterable\ShapedArrayUnexpectedTokenAfterSealedType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Exception\Iterable\ShapedArrayWithoutElementsWithSealedType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Type;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Types\ArrayKeyType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Types\ArrayType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Types\IntegerValueType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Types\IterableType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Types\NonEmptyArrayType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Types\ShapedArrayElement;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Types\ShapedArrayType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Types\StringValueType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Types\UnionType;
use OCA\Talk\Vendor\CuyZ\Valinor\Type\Types\UnresolvableType;
use function array_map;
use function implode;
/** @internal */
final class ArrayToken implements TraversingToken
{
    private static self $array;
    private static self $nonEmptyArray;
    private static self $iterable;
    private function __construct(
        /** @var class-string<ArrayType|NonEmptyArrayType|IterableType> */
        private string $arrayType,
        private string $symbol
    )
    {
    }
    public static function array(): self
    {
        return self::$array ??= new self(ArrayType::class, 'array');
    }
    public static function nonEmptyArray(): self
    {
        return self::$nonEmptyArray ??= new self(NonEmptyArrayType::class, 'non-empty-array');
    }
    public static function iterable(): self
    {
        return self::$iterable ??= new self(IterableType::class, 'iterable');
    }
    public function traverse(TokenStream $stream): Type
    {
        if ($stream->done()) {
            return $this->arrayType::native();
        }
        if ($stream->next() instanceof OpeningBracketToken) {
            $stream->forward();
            return $this->arrayType($stream);
        }
        if ($this->arrayType === ArrayType::class && $stream->next() instanceof OpeningCurlyBracketToken) {
            return $this->shapedArrayType($stream);
        }
        return $this->arrayType::native();
    }
    public function symbol(): string
    {
        return $this->symbol;
    }
    private function arrayType(TokenStream $stream): Type
    {
        if ($stream->done()) {
            throw new ArrayMissingSubType($this->symbol . '<');
        }
        $type = $stream->read();
        if ($stream->done()) {
            throw new ArrayExpectedCommaOrClosingBracket($this->symbol, $type);
        }
        $token = $stream->forward();
        if ($token instanceof ClosingBracketToken) {
            return new $this->arrayType(ArrayKeyType::default(), $type);
        }
        if (!$token instanceof CommaToken) {
            throw new ArrayCommaMissing($this->symbol, $type);
        }
        $keyType = $type;
        if (!$keyType instanceof ArrayKeyType) {
            $keyTypes = $type instanceof UnionType ? $type->types() : [$type];
            try {
                $keyType = ArrayKeyType::from($keyTypes);
            } catch (InvalidType $exception) {
                $subTypes = implode('|', array_map(static fn(Type $type) => $type->toString(), $keyTypes));
                $keyType = new UnresolvableType($subTypes, $exception->getMessage());
            }
        } else {
            $keyTypes = $keyType->types;
        }
        if ($stream->done()) {
            throw new ArrayMissingSubType($this->symbol . '<' . $keyType->toString() . ',');
        }
        $subType = $stream->read();
        if ($keyType instanceof UnresolvableType) {
            return $keyType->forArrayType($this->symbol, $keyTypes, $subType);
        }
        $arrayType = new $this->arrayType($keyType, $subType);
        if ($stream->done() || !$stream->forward() instanceof ClosingBracketToken) {
            throw new ArrayClosingBracketMissing($arrayType);
        }
        return $arrayType;
    }
    private function shapedArrayType(TokenStream $stream): Type
    {
        $stream->forward();
        $elements = [];
        $index = 0;
        $isUnsealed = false;
        $unsealedType = null;
        while (!$stream->done()) {
            if ($stream->next() instanceof ClosingCurlyBracketToken) {
                $stream->forward();
                break;
            }
            if (!empty($elements) && !$stream->forward() instanceof CommaToken) {
                throw new ShapedArrayCommaMissing($elements);
            }
            if ($stream->done()) {
                throw new ShapedArrayClosingBracketMissing($elements);
            }
            if ($stream->next() instanceof ClosingCurlyBracketToken) {
                $stream->forward();
                break;
            }
            $optional = false;
            if ($stream->next() instanceof TripleDotsToken) {
                $isUnsealed = true;
                $stream->forward();
            }
            if ($stream->done()) {
                throw new ShapedArrayClosingBracketMissing($elements, unsealedType: false);
            }
            $keyToken = $stream->next();
            if ($isUnsealed && ($keyToken instanceof ClosingCurlyBracketToken || $keyToken instanceof CommaToken)) {
                $stream->forward();
                break;
            } else {
                $type = $stream->read();
            }
            if ($isUnsealed) {
                $unsealedType = $type;
                if ($elements === []) {
                    throw new ShapedArrayWithoutElementsWithSealedType($unsealedType);
                }
                if ($stream->done()) {
                    throw new ShapedArrayClosingBracketMissing($elements, $unsealedType);
                } elseif (!$stream->next() instanceof ClosingCurlyBracketToken) {
                    $unexpected = [];
                    while (!$stream->done() && !$stream->next() instanceof ClosingCurlyBracketToken) {
                        $unexpected[] = $stream->forward();
                    }
                    throw new ShapedArrayUnexpectedTokenAfterSealedType($elements, $unsealedType, $unexpected);
                }
                continue;
            }
            if ($stream->done()) {
                $elements[] = new ShapedArrayElement(new IntegerValueType($index), $type);
                throw new ShapedArrayClosingBracketMissing($elements);
            }
            if ($stream->next() instanceof NullableToken) {
                $stream->forward();
                $optional = true;
                if ($stream->done()) {
                    throw new ShapedArrayColonTokenMissing($elements, $type);
                }
            }
            if ($stream->next() instanceof ColonToken) {
                $stream->forward();
                $key = $type;
                $type = null;
                if (!$key instanceof StringValueType && !$key instanceof IntegerValueType) {
                    $key = new StringValueType($keyToken->symbol());
                }
                if ($key instanceof IntegerValueType) {
                    $index++;
                }
            } else {
                if ($optional) {
                    throw new ShapedArrayColonTokenMissing($elements, $type);
                }
                $key = new IntegerValueType($index++);
            }
            if (!$type) {
                if ($stream->done()) {
                    throw new ShapedArrayElementTypeMissing($elements, $key, $optional);
                }
                $type = $stream->read();
            }
            $elements[] = new ShapedArrayElement($key, $type, $optional);
            if ($stream->done()) {
                throw new ShapedArrayClosingBracketMissing($elements);
            }
        }
        return ShapedArrayType::from($elements, $isUnsealed, $unsealedType);
    }
}