<?php

declare(strict_types=1);

namespace Rowbot\URL\State;

use Rowbot\URL\Component\PathSegment;
use Rowbot\URL\ParserContext;
use Rowbot\URL\ParserState;
use Rowbot\URL\String\CodePoint;
use Rowbot\URL\String\EncodeSet;
use Rowbot\URL\String\PercentEncoder;

/**
 * @see https://url.spec.whatwg.org/#path-state
 */
class PathState implements State
{
    /**
     * @see https://url.spec.whatwg.org/#double-dot-path-segment
     */
    private const DOUBLE_DOT_SEGMENT = [
        '..'     => '',
        '.%2e'   => '',
        '.%2E'   => '',
        '%2e.'   => '',
        '%2E.'   => '',
        '%2e%2e' => '',
        '%2E%2E' => '',
        '%2e%2E' => '',
        '%2E%2e' => '',
    ];

    /**
     * @see https://url.spec.whatwg.org/#single-dot-path-segment
     */
    private const SINGLE_DOT_SEGMENT = [
        '.'   => '',
        '%2e' => '',
        '%2E' => '',
    ];

    public function handle(ParserContext $context, string $codePoint): StatusCode
    {
        $percentEncoder = null;

        do {
            // 1. If one of the following is true:
            //      - c is the EOF code point or U+002F (/)
            //      - url is special and c is U+005C (\)
            //      - state override is not given and c is U+003F (?) or U+0023 (#)
            if (
                $codePoint === CodePoint::EOF
                || $codePoint === '/'
                || ($context->url->scheme->isSpecial() && $codePoint === '\\')
                || (!$context->isStateOverridden() && ($codePoint === '?' || $codePoint === '#'))
            ) {
                $urlIsSpecial = $context->url->scheme->isSpecial();

                // 1.1. If url is special and c is U+005C (\), validation error.
                if ($urlIsSpecial && $codePoint === '\\') {
                    // Validation error.
                    $context->logger?->notice('invalid-reverse-solidus', [
                        'input'  => (string) $context->input,
                        'column' => $context->iter->key() + 1,
                    ]);
                }

                $stringBuffer = (string) $context->buffer;

                // 1.2. If buffer is a double-dot path segment, then:
                if (isset(self::DOUBLE_DOT_SEGMENT[$stringBuffer])) {
                    // 1.2.1. Shorten url’s path.
                    $context->url->path->shorten($context->url->scheme);

                    // 1.2.2. If neither c is U+002F (/), nor url is special and c is U+005C (\), append the empty string
                    // to url’s path.
                    if ($codePoint !== '/' && !($urlIsSpecial && $codePoint === '\\')) {
                        $context->url->path->push(new PathSegment());
                    }

                // 1.3. Otherwise, if buffer is a single-dot path segment and if neither c is U+002F (/), nor url is special
                // and c is U+005C (\), append the empty string to url’s path.
                } elseif (
                    isset(self::SINGLE_DOT_SEGMENT[$stringBuffer])
                    && $codePoint !== '/'
                    && !($urlIsSpecial && $codePoint === '\\')
                ) {
                    $context->url->path->push(new PathSegment());

                // 1.4. Otherwise, if buffer is not a single-dot path segment, then:
                } elseif (!isset(self::SINGLE_DOT_SEGMENT[$stringBuffer])) {
                    // 1.4.1. If url’s scheme is "file", url’s path is empty, and buffer is a Windows drive letter, then
                    // replace the second code point in buffer with U+003A (:).
                    if (
                        $context->url->scheme->isFile()
                        && $context->url->path->isEmpty()
                        && $context->buffer->isWindowsDriveLetter()
                    ) {
                        // This is a (platform-independent) Windows drive letter quirk.
                        $context->buffer->setCodePointAt(1, ':');
                    }

                    // 1.4.2. Append buffer to url’s path.
                    $context->url->path->push($context->buffer->toPath());
                }

                // 1.5. Set buffer to the empty string.
                $context->buffer->clear();

                // 1.6. If c is U+003F (?), then set url’s query to the empty string and state to query state.
                if ($codePoint === '?') {
                    $context->url->query = '';
                    $context->state = ParserState::QUERY;

                // If c is U+0023 (#), then set url’s fragment to the empty string and state to fragment state.
                } elseif ($codePoint === '#') {
                    $context->url->fragment = '';
                    $context->state = ParserState::FRAGMENT;
                }

                return StatusCode::OK;
            }

            // 2. Otherwise, run these steps:
            // 2.1. If c is not a URL code point and not U+0025 (%), validation error.
            if ($codePoint !== '%' && !CodePoint::isUrlCodePoint($codePoint)) {
                // Validation error
                $context->logger?->notice('invalid-URL-unit', [
                    'input'  => (string) $context->input,
                    'column' => $context->iter->key() + 1,
                ]);

            // 2.2. If c is U+0025 (%) and remaining does not start with two ASCII hex digits, validation error.
            } elseif (
                $codePoint === '%'
                && !$context->input->substr($context->iter->key() + 1)->startsWithTwoAsciiHexDigits()
            ) {
                // Validation error
                $context->logger?->notice('invalid-URL-unit', [
                    'input'  => (string) $context->input,
                    'column' => $context->iter->key() + 1,
                ]);
            }

            // 2.3. UTF-8 percent-encode c using the path percent-encode set and append the result to buffer.
            $percentEncoder ??= new PercentEncoder();
            $context->buffer->append($percentEncoder->percentEncodeAfterEncoding('utf-8', $codePoint, EncodeSet::PATH));
            $context->iter->next();
            $codePoint = $context->iter->current();
        } while (true);
    }
}
