<?php

declare(strict_types=1);

namespace App\Framework\View\Dom\Parser;

use App\Framework\View\Dom\CommentNode;
use App\Framework\View\Dom\DocumentNode;
use App\Framework\View\Dom\ElementNode;
use App\Framework\View\Dom\Node;
use App\Framework\View\Dom\TextNode;
use App\Framework\View\Lexer\HtmlLexer;
use App\Framework\View\Lexer\Token;
use App\Framework\View\Lexer\TokenType;

final class HtmlParser
{
    /** @var array<Token> */
    private array $tokens;

    private int $position = 0;

    /** @var array<ElementNode> */
    private array $openElements = [];

    public function parse(string $html): DocumentNode
    {
        $lexer = new HtmlLexer($html);
        $this->tokens = $lexer->tokenize();
        $this->position = 0;
        $this->openElements = [];

        $document = new DocumentNode();

        while ($this->position < count($this->tokens)) {
            $token = $this->current();

            match ($token->type) {
                TokenType::DOCTYPE => $this->handleDoctype($document, $token),
                TokenType::OPEN_TAG_START => $this->handleOpenTag($document, $token),
                TokenType::SELF_CLOSING_TAG => $this->handleSelfClosingTag($document, $token),
                TokenType::CLOSING_TAG => $this->handleClosingTag($token),
                TokenType::CONTENT => $this->handleContent($document, $token),
                TokenType::COMMENT => $this->handleComment($document, $token),
                TokenType::CDATA => $this->handleContent($document, $token),
                default => null,
            };

            $this->advance();
        }

        // Handle unclosed tags
        while (count($this->openElements) > 0) {
            array_pop($this->openElements);
        }

        return $document;
    }

    private function handleDoctype(DocumentNode $document, Token $token): void
    {
        $document->setDoctype(trim($token->content));
    }

    private function handleOpenTag(DocumentNode $document, Token $token): void
    {
        $element = $this->parseElement($token->content);

        $parent = $this->getCurrentParent($document);
        $parent->appendChild($element);

        // Push to stack if not a void element
        if (!$element->isVoidElement()) {
            $this->openElements[] = $element;
        }
    }

    private function handleSelfClosingTag(DocumentNode $document, Token $token): void
    {
        $element = $this->parseElement($token->content);

        $parent = $this->getCurrentParent($document);
        $parent->appendChild($element);

        // Don't push to stack - it's self-closing
    }

    private function handleClosingTag(Token $token): void
    {
        $tagName = $this->extractClosingTagName($token->content);

        // Pop elements until we find matching opening tag
        while (count($this->openElements) > 0) {
            $element = array_pop($this->openElements);

            if (strtolower($element->getTagName()) === strtolower($tagName)) {
                break;
            }
        }
    }

    private function handleContent(DocumentNode $document, Token $token): void
    {
        $text = $token->content;

        // Skip empty text nodes (only whitespace)
        if (trim($text) === '' && count($this->openElements) === 0) {
            return;
        }

        $textNode = new TextNode($text);
        $parent = $this->getCurrentParent($document);
        $parent->appendChild($textNode);
    }

    private function handleComment(DocumentNode $document, Token $token): void
    {
        // Extract comment content (remove <!-- and -->)
        $content = preg_replace('/^<!--(.*)-->$/s', '$1', $token->content) ?? '';

        $commentNode = new CommentNode($content);
        $parent = $this->getCurrentParent($document);
        $parent->appendChild($commentNode);
    }

    private function parseElement(string $tagContent): ElementNode
    {
        // Extract tag name and attributes
        // Example: <div class="foo" id="bar">

        preg_match('/<([a-z][a-z0-9-]*)/i', $tagContent, $matches);
        $tagName = $matches[1] ?? 'div';

        $element = new ElementNode($tagName);

        // Parse attributes
        $attributesString = substr($tagContent, strlen($tagName) + 1);
        $attributesString = rtrim($attributesString, '/>');
        $attributesString = trim($attributesString);

        if ($attributesString !== '') {
            $this->parseAttributes($element, $attributesString);
        }

        return $element;
    }

    private function parseAttributes(ElementNode $element, string $attributesString): void
    {
        // Parse HTML attributes with proper quote handling
        // Supports: attr="value", attr='value', attr=value, attr (boolean)

        $pattern = '/([a-z][a-z0-9_:-]*)\s*(?:=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+)))?/i';

        preg_match_all($pattern, $attributesString, $matches, PREG_SET_ORDER);

        foreach ($matches as $match) {
            $name = $match[1];
            $value = $match[2] ?? $match[3] ?? $match[4] ?? '';

            $element->setAttribute($name, $value);
        }
    }

    private function extractClosingTagName(string $closingTag): string
    {
        // Extract "div" from "</div>"
        preg_match('/<\/([a-z][a-z0-9-]*)/i', $closingTag, $matches);
        return $matches[1] ?? '';
    }

    private function getCurrentParent(DocumentNode $document): Node
    {
        return $this->openElements[count($this->openElements) - 1] ?? $document;
    }

    private function current(): Token
    {
        return $this->tokens[$this->position];
    }

    private function advance(): void
    {
        $this->position++;
    }
}
