<?php

namespace Linksy\SearchAndReplace\Plugli\Standard\Helpers;

class HTMLParser
{
    private $html;
    private $length;
    private $pos = 0;
    const SELF_CLOSING_TAGS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
    public function __construct($html)
    {
        $this->html = $html;
        $this->length = strlen($html);
    }
    public function parse()
    {
        $root = ['type' => 'root', 'children' => []];
        $stack = [&$root];
        $text_buffer = '';
        while ($this->pos < $this->length) {
            $char = $this->html[$this->pos];
            if ($char === '<') {
                // Handle any buffered text before processing the tag
                if ($text_buffer !== '') {
                    $this->add_text_node($stack[count($stack) - 1]['children'], $text_buffer);
                    $text_buffer = '';
                }
                if ($this->pos + 1 < $this->length) {
                    $next_char = $this->html[$this->pos + 1];
                    if ($next_char === '/') {
                        $this->handle_closing_tag($stack);
                    } elseif ($next_char === '!') {
                        if (substr($this->html, $this->pos, 4) === '<!--') {
                            $this->handle_comment($stack[count($stack) - 1]['children']);
                        } else {
                            // Handle other cases (like <!DOCTYPE>) as text
                            $text_buffer .= $char;
                            $this->pos++;
                        }
                    } else {
                        $this->handle_opening_tag($stack);
                    }
                } else {
                    // Handle lone '<' at the end of the document
                    $text_buffer .= $char;
                    $this->pos++;
                }
            } else {
                $text_buffer .= $char;
                $this->pos++;
            }
        }
        // Handle any remaining buffered text
        if ($text_buffer !== '') {
            $this->add_text_node($stack[count($stack) - 1]['children'], $text_buffer);
        }
        return $root['children'];
    }
    public static function stringify($nodes)
    {
        // If it's an array of nodes, process each one
        if (is_array($nodes) && isset($nodes[0])) {
            $result = '';
            foreach ($nodes as $node) {
                $result .= self::stringify_single_node($node);
            }
            return $result;
        }
        return self::stringify_single_node($nodes);
    }
    private static function stringify_single_node($node)
    {
        if (!isset($node['type'])) {
            return '';
        }
        if ($node['type'] === 'comment') {
            return '<!--' . $node['content'] . '-->';
        }
        if ($node['type'] === 'element') {
            $result = '<' . $node['name'];
            // Add attributes
            if (!empty($node['attributes'])) {
                foreach ($node['attributes'] as $attr => $value) {
                    $result .= ' ' . $attr . '="' . htmlspecialchars($value, \ENT_QUOTES) . '"';
                }
            }
            $result .= '>';
            // Self-closing tags
            if (in_array($node['name'], self::SELF_CLOSING_TAGS) && empty($node['children'])) {
                return $result;
            }
            // Add children
            if (!empty($node['children'])) {
                $result .= self::stringify($node['children']);
            }
            // Close tag
            $result .= '</' . $node['name'] . '>';
            return $result;
        }
        // Handle text nodes
        if ($node['type'] === 'text') {
            if (strpos(trim($node['content']), '<!') === 0) {
                return $node['content'];
            }
            return htmlspecialchars($node['content'], \ENT_QUOTES);
        }
        return '';
    }
    private function handle_opening_tag(&$stack)
    {
        $tag_start = $this->pos;
        $tag_name_start = $this->pos + 1;
        $tag_end = $this->find_char(' ', '>');
        $tag_name = strtolower(substr($this->html, $tag_name_start, $tag_end - $tag_name_start));
        $element = ['type' => 'element', 'name' => $tag_name, 'attributes' => $this->parse_attributes($tag_end), 'children' => [], 'start' => $tag_start, 'end' => null];
        $current_parent =& $stack[count($stack) - 1];
        $current_parent['children'][] =& $element;
        if (!in_array($tag_name, self::SELF_CLOSING_TAGS) && !$this->is_self_closing($tag_end)) {
            $stack[] =& $element;
        } else {
            $element['end'] = $this->find_char('>') + 1;
        }
        $this->pos = is_null($element['end']) ? $this->find_char('>') + 1 : $element['end'];
    }
    private function handle_closing_tag(&$stack)
    {
        $tag_start = $this->pos;
        $tag_name_start = $this->pos + 2;
        $tag_end = $this->find_char('>');
        $tag_name = strtolower(substr($this->html, $tag_name_start, $tag_end - $tag_name_start));
        while (count($stack) > 1 && end($stack)['name'] !== $tag_name) {
            $popped = array_pop($stack);
            $popped['end'] = $tag_start;
            // Set end position for any unclosed tags
        }
        if (count($stack) > 1) {
            $closed_element = array_pop($stack);
            $closed_element['end'] = $tag_end + 1;
        }
        $this->pos = $tag_end + 1;
    }
    private function handle_comment(&$children)
    {
        $comment_start = $this->pos;
        $comment_content_start = $this->pos + 4;
        // Skip '<!--'
        $comment_end = $this->find_string('-->');
        $children[] = ['type' => 'comment', 'content' => substr($this->html, $comment_content_start, $comment_end - $comment_content_start), 'start' => $comment_start, 'end' => $comment_end + 3];
        $this->pos = $comment_end + 3;
    }
    private function add_text_node(&$children, $text)
    {
        $trimmed_text = trim($text);
        if ($trimmed_text !== '') {
            $start = $this->pos - strlen($text);
            $end = $this->pos;
            $children[] = ['type' => 'text', 'content' => $trimmed_text, 'start' => $start, 'end' => $end];
        }
    }
    private function handle_text(&$children)
    {
        $text_start = $this->pos;
        $text_end = $this->find_char('<');
        $text = trim(substr($this->html, $text_start, $text_end - $text_start));
        if ($text !== '') {
            $children[] = ['type' => 'text', 'content' => $text];
        }
        $this->pos = $text_end;
    }
    private function parse_attributes($start)
    {
        $attributes = [];
        $this->pos = $start;
        while ($this->pos < $this->length) {
            $this->pos = $this->find_non_space($this->pos);
            if ($this->html[$this->pos] === '>' || $this->html[$this->pos] === '/' && $this->html[$this->pos + 1] === '>') {
                break;
            }
            $name_start = $this->pos;
            $this->pos = $this->find_char('=', ' ', '>', '/');
            $name = substr($this->html, $name_start, $this->pos - $name_start);
            if ($this->html[$this->pos] === '=') {
                $this->pos++;
                $quote = $this->html[$this->pos];
                if ($quote === '"' || $quote === "'") {
                    $value_start = ++$this->pos;
                    $this->pos = $this->find_char($quote);
                    $value = substr($this->html, $value_start, $this->pos - $value_start);
                    $this->pos++;
                } else {
                    $value_start = $this->pos;
                    $this->pos = $this->find_char(' ', '>', '/');
                    $value = substr($this->html, $value_start, $this->pos - $value_start);
                }
            } else {
                $value = \true;
            }
            $attributes[$name] = $value;
        }
        return $attributes;
    }
    private function is_self_closing($pos)
    {
        return $this->html[$pos - 1] === '/' || $this->html[$pos] === '/';
    }
    private function find_char()
    {
        $pos = $this->pos;
        while ($pos < $this->length && !in_array($this->html[$pos], func_get_args())) {
            $pos++;
        }
        return $pos;
    }
    private function find_string($str)
    {
        $pos = strpos($this->html, $str, $this->pos);
        return $pos === \false ? $this->length : $pos;
    }
    private function find_non_space($start)
    {
        $pos = $start;
        while ($pos < $this->length && $this->html[$pos] === ' ') {
            $pos++;
        }
        return $pos;
    }
}
