<?php

declare(strict_types=1);

namespace Rowbot\URL\Component\Host;

use Rowbot\URL\String\CodePoint;
use Rowbot\URL\String\StringIteratorInterface;
use Rowbot\URL\String\USVStringInterface;

use function intval;
use function strpbrk;

/**
 * @see https://url.spec.whatwg.org/#concept-ipv6-parser
 */
class IPv6AddressParser
{
    /**
     * @return \Rowbot\URL\Component\Host\IPv6Address|false
     */
    public static function parse(USVStringInterface $input)
    {
        // 1. Let address be a new IPv6 address whose IPv6 pieces are all 0.
        $address = [0, 0, 0, 0, 0, 0, 0, 0];

        // 2. Let pieceIndex be 0.
        $pieceIndex = 0;

        // 3. Let compress be null.
        $compress = null;

        // 4. Let pointer be a pointer for input.
        $iter = $input->getIterator();
        $iter->rewind();

        // 5. If c is U+003A (:), then:
        if ($iter->current() === ':') {
            // 5.1. If remaining does not start with U+003A (:), validation error, return failure.
            if ($iter->peek() !== ':') {
                return false;
            }

            // 5.2. Increase pointer by 2.
            $iter->seek(2);

            // 5.3. Increase pieceIndex by 1 and then set compress to pieceIndex.
            $compress = ++$pieceIndex;
        }

        // 6. While c is not the EOF code point:
        while ($iter->valid()) {
            // 6.1. If pieceIndex is 8, validation error, return failure.
            if ($pieceIndex === 8) {
                return false;
            }

            // 6.2. If c is U+003A (:), then:
            if ($iter->current() === ':') {
                // 6.2.1. If compress is non-null, validation error, return failure.
                if ($compress !== null) {
                    return false;
                }

                // 6.2.2. Increase pointer and pieceIndex by 1, set compress to pieceIndex, and then continue.
                $iter->next();
                $compress = ++$pieceIndex;

                continue;
            }

            // 6.3. Let value and length be 0.
            $value = 0;
            $length = 0;
            $current = $iter->current();

            // 6.4. While length is less than 4 and c is an ASCII hex digit, set value to value × 0x10 + c interpreted
            // as hexadecimal number, and increase pointer and length by 1.
            while ($length < 4 && strpbrk($current, CodePoint::HEX_DIGIT_MASK) === $current) {
                $value = ($value * 0x10) + intval($current, 16);
                $iter->next();
                ++$length;
                $current = $iter->current();
            }

            // 6.5. If c is U+002E (.), then:
            if ($iter->current() === '.') {
                // 6.5.1. If length is 0, validation error, return failure.
                if ($length === 0) {
                    return false;
                }

                // 6.5.2. Decrease pointer by length.
                $iter->seek(-$length);

                // 6.5.3. If pieceIndex is greater than 6, validation error, return failure.
                if ($pieceIndex > 6) {
                    return false;
                }

                $result = self::parseIPv4Address($iter, $address, $pieceIndex);

                if ($result === false) {
                    return false;
                }

                [$address, $pieceIndex] = $result;

                // 6.5.7. Break.
                break;
            }

            // 6.6. Otherwise, if c is U+003A (:):
            if ($iter->current() === ':') {
                // 6.6.1. Increase pointer by 1.
                $iter->next();

                // 6.2.2. If c is the EOF code point, validation error, return failure.
                if (!$iter->valid()) {
                    return false;
                }

            // 6.7. Otherwise, if c is not the EOF code point, validation error, return failure.
            } elseif ($iter->valid()) {
                return false;
            }

            // 6.8. Set address[pieceIndex] to value.
            // 6.9. Increase pieceIndex by 1.
            $address[$pieceIndex++] = $value;
        }

        // 7. If compress is non-null, then:
        if ($compress !== null) {
            // 7.1. Let swaps be pieceIndex − compress.
            $swaps = $pieceIndex - $compress;

            // 7.2. Set pieceIndex to 7.
            $pieceIndex = 7;

            // 7.3. While pieceIndex is not 0 and swaps is greater than 0, swap address[pieceIndex] with
            // address[compress + swaps − 1], and then decrease both pieceIndex and swaps by 1.
            while ($pieceIndex !== 0 && $swaps > 0) {
                $temp = $address[$pieceIndex];
                $address[$pieceIndex] = $address[$compress + $swaps - 1];
                $address[$compress + $swaps - 1] = $temp;
                --$pieceIndex;
                --$swaps;
            }

        // Otherwise, if compress is null and pieceIndex is not 8, validation error, return failure.
        } elseif ($pieceIndex !== 8) {
            return false;
        }

        // 9. Return address.
        return new IPv6Address($address);
    }

    /**
     * @param list<int> $address
     *
     * @return array{0: list<int>, 1: int}|false
     */
    private static function parseIPv4Address(
        StringIteratorInterface $iter,
        array $address,
        int $pieceIndex
    ) {
        // 6.5.4. Let numbersSeen be 0.
        $numbersSeen = 0;

        // 6.5.5. While c is not the EOF code point:
        do {
            // 6.5.5.1. Let ipv4Piece be null.
            $ipv4Piece = null;

            // 6.5.5.2. If numbersSeen is greater than 0, then:
            if ($numbersSeen > 0) {
                // 6.5.5.2.2 Otherwise, validation error, return failure.
                if ($iter->current() !== '.' || $numbersSeen >= 4) {
                    // Validation error.
                    return false;
                }

                // 6.5.5.2.1 If c is a U+002E (.) and numbersSeen is less than 4, then increase pointer by 1.
                $iter->next();
            }

            $current = $iter->current();

            // 6.5.5.3. If c is not an ASCII digit, validation error, return failure.
            if (strpbrk($current, CodePoint::ASCII_DIGIT_MASK) !== $current) {
                // Validation error.
                return false;
            }

            // 6.5.5.4. While c is an ASCII digit:
            do {
                // 6.5.5.4.1. Let number be c interpreted as decimal number.
                $number = (int) $current;

                // 6.5.5.4.2. If ipv4Piece is null, then set ipv4Piece to number.
                if ($ipv4Piece === null) {
                    $ipv4Piece = $number;

                // Otherwise, if ipv4Piece is 0, validation error, return failure.
                } elseif ($ipv4Piece === 0) {
                    // Validation error.
                    return false;

                // Otherwise, set ipv4Piece to ipv4Piece × 10 + number.
                } else {
                    $ipv4Piece = ($ipv4Piece * 10) + $number;
                }

                // 6.5.5.4.3. If ipv4Piece is greater than 255, validation error, return failure.
                if ($ipv4Piece > 255) {
                    // Validation error.
                    return false;
                }

                // 6.5.5.4.4. Increase pointer by 1.
                $iter->next();
                $current = $iter->current();
            } while (strpbrk($current, CodePoint::ASCII_DIGIT_MASK) === $current);

            // 6.5.5.5. Set address[pieceIndex] to address[pieceIndex] × 0x100 + ipv4Piece.
            $piece = $address[$pieceIndex];
            $address[$pieceIndex] = ($piece * 0x100) + $ipv4Piece;

            // 6.5.5.6. Increase numbersSeen by 1.
            ++$numbersSeen;

            // 6.5.5.7. If numbersSeen is 2 or 4, then increase pieceIndex by 1.
            if ($numbersSeen === 2 || $numbersSeen === 4) {
                ++$pieceIndex;
            }
        } while ($iter->valid());

        // 6.5.6. If numbersSeen is not 4, validation error, return failure.
        if ($numbersSeen !== 4) {
            // Validation error.
            return false;
        }

        return [$address, $pieceIndex];
    }
}
