<?php

declare(strict_types=1);

namespace App\Framework\View;

use Dom\DocumentFragment;
use Dom\Element;
use Dom\HTMLDocument;
use Dom\Node;

/**
 * Wrapper-Klasse für HTMLDocument mit erweiterten Template-spezifischen Operationen
 */
final readonly class DomWrapper
{
    public function __construct(
        public HTMLDocument $document
    ) {
    }

    /**
     * Erstellt einen DomWrapper aus HTML-String
     */
    public static function fromString(string $html): self
    {
        libxml_use_internal_errors(true);
        $dom = HTMLDocument::createFromString($html);
        libxml_clear_errors();

        return new self($dom);
    }

    /**
     * Erstellt einen DomWrapper aus Datei
     */
    public static function fromFile(string $path): self
    {
        $dom = HTMLDocument::createFromFile($path);

        return new self($dom);
    }

    /**
     * Konvertiert zu HTML-String
     */
    public function toHtml(bool $bodyOnly = false): string
    {
        if ($bodyOnly && $this->document->body) {
            return $this->document->body->innerHTML;
        }

        return $this->document->saveHTML();
    }

    /**
     * Findet Elemente nach Tag-Name
     */
    public function getElementsByTagName(string $tagName): ElementCollection
    {
        $elements = iterator_to_array($this->document->getElementsByTagName($tagName));

        return new ElementCollection($elements, $this);
    }

    /**
     * Findet Element nach ID
     */
    public function getElementById(string $id): ?Element
    {
        return $this->document->getElementById($id);
    }

    /**
     * Findet Elemente nach Name-Attribut
     */
    public function getElementsByName(string $name): ElementCollection
    {
        $elements = [];
        $this->findElementsByNameRecursive($this->document->documentElement, $name, $elements);

        return new ElementCollection($elements, $this);
    }

    private function findElementsByNameRecursive(?Node $node, string $name, array &$elements): void
    {
        if (! $node || $node->nodeType !== XML_ELEMENT_NODE) {
            return;
        }

        if ($node instanceof Element && $node->getAttribute('name') === $name) {
            $elements[] = $node;
        }

        foreach ($node->childNodes as $child) {
            $this->findElementsByNameRecursive($child, $name, $elements);
        }
    }

    /**
     * Findet Elemente nach Attribut und Wert
     */
    public function getElementsByAttribute(string $attribute, ?string $value = null): ElementCollection
    {
        $elements = [];
        $this->findElementsByAttributeRecursive($this->document->documentElement, $attribute, $value, $elements);

        return new ElementCollection($elements, $this);
    }

    private function findElementsByAttributeRecursive(?Node $node, string $attribute, ?string $value, array &$elements): void
    {
        if (! $node || $node->nodeType !== XML_ELEMENT_NODE) {
            return;
        }

        if ($node instanceof Element) {
            if ($value === null && $node->hasAttribute($attribute)) {
                $elements[] = $node;
            } elseif ($value !== null && $node->getAttribute($attribute) === $value) {
                $elements[] = $node;
            }
        }

        foreach ($node->childNodes as $child) {
            $this->findElementsByAttributeRecursive($child, $attribute, $value, $elements);
        }
    }

    /**
     * Erstellt ein Document Fragment
     */
    public function createFragment(): DocumentFragment
    {
        return $this->document->createDocumentFragment();
    }

    /**
     * Erstellt ein Fragment aus HTML-String
     */
    public function createFragmentFromHtml(string $html): DocumentFragment
    {
        $fragment = $this->document->createDocumentFragment();
        $fragment->appendXML($html);

        return $fragment;
    }

    /**
     * Ersetzt ein Element durch HTML-Content
     */
    public function replaceElementWithHtml(Element $element, string $html): void
    {
        $parent = $element->parentNode;
        if (!$parent) {
            return;
        }

        // Create temporary wrapper to parse HTML
        $tempWrapper = HTMLDocument::createFromString('<div>' . $html . '</div>');
        $tempDiv = $tempWrapper->body->firstChild;

        if (!$tempDiv) {
            // Empty HTML, just remove element
            $parent->removeChild($element);
            return;
        }

        // Insert all children of temp div before the element
        while ($tempDiv->firstChild) {
            $importedNode = $this->document->importNode($tempDiv->firstChild, true);
            $parent->insertBefore($importedNode, $element);
        }

        // Remove the original element
        $parent->removeChild($element);
    }

    /**
     * Entfernt leere Textknoten
     */
    public function removeEmptyTextNodes(): self
    {
        $this->removeEmptyTextNodesRecursive($this->document->documentElement);

        return $this;
    }

    private function removeEmptyTextNodesRecursive(?Node $node): void
    {
        if (! $node) {
            return;
        }

        $childNodes = [];
        foreach ($node->childNodes as $child) {
            $childNodes[] = $child;
        }

        foreach ($childNodes as $child) {
            if ($child->nodeType === XML_TEXT_NODE && trim($child->textContent) === '') {
                $node->removeChild($child);
            } else {
                $this->removeEmptyTextNodesRecursive($child);
            }
        }
    }

    /**
     * Fügt CSS-Klasse zu einem Element hinzu
     */
    public function addClass(Element $element, string $className): void
    {
        $currentClass = $element->getAttribute('class');
        $classes = $currentClass ? explode(' ', $currentClass) : [];

        if (! in_array($className, $classes)) {
            $classes[] = $className;
            $element->setAttribute('class', implode(' ', $classes));
        }
    }

    /**
     * Entfernt CSS-Klasse von einem Element
     */
    public function removeClass(Element $element, string $className): void
    {
        $currentClass = $element->getAttribute('class');
        if (! $currentClass) {
            return;
        }

        $classes = array_filter(explode(' ', $currentClass), fn ($c) => $c !== $className);
        $element->setAttribute('class', implode(' ', $classes));
    }

    /**
     * Prüft ob Element eine CSS-Klasse hat
     */
    public function hasClass(Element $element, string $className): bool
    {
        $currentClass = $element->getAttribute('class');
        if (! $currentClass) {
            return false;
        }

        return in_array($className, explode(' ', $currentClass));
    }

    /**
     * Findet Elemente nach CSS-Klasse
     */
    public function getElementsByClassName(string $className): ElementCollection
    {
        $elements = [];
        $this->findElementsByClassRecursive($this->document->documentElement, $className, $elements);

        return new ElementCollection($elements, $this);
    }

    private function findElementsByClassRecursive(?Node $node, string $className, array &$elements): void
    {
        if (! $node || $node->nodeType !== XML_ELEMENT_NODE) {
            return;
        }

        if ($node instanceof Element && $this->hasClass($node, $className)) {
            $elements[] = $node;
        }

        foreach ($node->childNodes as $child) {
            $this->findElementsByClassRecursive($child, $className, $elements);
        }
    }

    /**
     * Setzt Attribut für ein Element
     */
    public function setAttribute(Element $element, string $name, string $value): void
    {
        $element->setAttribute($name, $value);
    }

    /**
     * Entfernt Attribut von einem Element
     */
    public function removeAttribute(Element $element, string $name): void
    {
        $element->removeAttribute($name);
    }
}
