<?php

namespace Planaday\Html\Parsers;

use DOMAttr;
use DOMNode;
use Planaday\Html\Nodes\NodeCouldNotBeCreatedException;
use Planaday\Html\Nodes\NodeData;
use Planaday\Html\Nodes\ParserNode;
use Planaday\Html\Nodes\StdParserDocument;
use Planaday\Html\Nodes\StdParserElement;
use Planaday\Html\Nodes\StdParserFragment;
use Planaday\Html\NodeTags\Core\ForEachTag\ForEachTag;
use Planaday\Html\NodeTags\Core\IfTag\IfTag;
use Planaday\Html\NodeTags\Core\XmlFragmentTag\XmlFragmentTag;
use Planaday\Html\NodeTags\Tag;
use Planaday\Html\NodeTags\TagNotFullyInitializedException;
use Planaday\Html\Parsers\DynamicDataParsers\DynamicDataParser;
use Planaday\Html\Parsers\NodeParsers\ConfigInjector;
use Planaday\Html\Parsers\NodeParsers\GeneralTagDataInjector;
use Planaday\Html\Parsers\NodeParsers\NodeAttributeParser;
use Planaday\Html\Parsers\NodeParsers\NodeParser;
use Planaday\Html\Parsers\NodeParsers\ParserGeneratedId;
use Planaday\Html\Parsers\NodeParsers\RelativeSrcFixer;
use Planaday\Html\Parsers\NodeParsers\TextNodeContentParser;
use Planaday\Html\Parsers\TagNameParsers\TagNameParser;
use Planaday\Utility\Debug\Log\Logging;
use Planaday\Utility\DependencyInjection\Attributes\InjectionMethod;
use Planaday\Utility\DependencyInjection\Attributes\Scoped;
use Planaday\Utility\DependencyInjection\Container;
use Planaday\Utility\DependencyInjection\InjectionImpossibleException;
use Planaday\Utility\DependencyInjection\Scope;

#[Scoped]
class StdDOMParser {
	use Logging;

	protected const parserRegistry = [
		"if" => IfTag::class,
		"for" => ForEachTag::class,
		"xml" => XmlFragmentTag::class,
	];

	protected NodeData $startData;

	#[InjectionMethod]
	protected function injectDefaultNodeParsers(
		NodeAttributeParser $nodeAttributeParser,
		TextNodeContentParser $textNodeContentParser,
		GeneralTagDataInjector $generalTagDataInjector,
		ConfigInjector $configInjector,
		ParserGeneratedId $parserGeneratedId,
		RelativeSrcFixer $relativeSrcFixer
	): void {
		$this->nodeParsers = [
			$nodeAttributeParser,
			$textNodeContentParser,
			$generalTagDataInjector,
			$configInjector,
			$parserGeneratedId,
			$relativeSrcFixer
		];
	}

	/**
	 * @param DynamicDataParser $dataParser
	 * @param TagNameParser $tagNameParser
	 * @param Container $registry
	 * @param Scope $scope
	 * @param NodeParser[] $nodeParsers
	 */
	public function __construct(
		protected DynamicDataParser $dataParser,
		protected TagNameParser $tagNameParser,
		protected Container $registry,
		protected Scope $scope,
		protected array $nodeParsers = [],
	) {
		$this->tagNameParser->setBaseRegistry( self::parserRegistry);
	}

	public function preloadData(NodeData $data): static{
		$this->startData = $data;
		return $this;
	}

	/**
	 * @param string $html
	 *
	 * @return string
	 * @throws ParseFailedException
	 */
	public function parse(string $html): string {
		if (!isset($this->startData)){
			$this->startData = new NodeData();
		}

		$this->logDebug( "Preparing parse");
		$htmlFragment = new StdParserFragment();
		$doc = $this->prepareDomDocument($html, $htmlFragment);

		$this->logDebug( "Parsing nodes");

		$this->doNode($htmlFragment, $this->startData);

		$this->logDebug( "Saving html");

		$doc->normalizeDocument();

		$output = $doc->saveHTML($htmlFragment);

		return preg_replace('/(\s)\s+/', '$1', $output);
	}

	/**
	 * @param string $toParseHtml
	 * @param StdParserFragment $htmlFragment
	 *
	 * @return StdParserDocument
	 * @throws ParseFailedException
	 */
	protected function prepareDomDocument(string $toParseHtml, StdParserFragment &$htmlFragment): StdParserDocument {
		$doc = new StdParserDocument();

		libxml_use_internal_errors(true);
		//The root tag is used to easily find only the HTML wanted;
		if (!$doc->loadHTML("<root>" . $toParseHtml . "</root>")){
			libxml_use_internal_errors(false);
			throw new ParseFailedException( "Could not parse html: " . $toParseHtml );
		}
		libxml_use_internal_errors(false);

		$this->logDebug( "loaded html" );

		/** @var StdParserElement $rootNode */
		$rootNode = $doc->getElementsByTagName("root")->item(0);
		try {
			/** @var StdParserElement $htmlFragmentElement */
			$htmlFragmentElement = $rootNode->getUsableDocumentFragment();
			$htmlFragment = $htmlFragmentElement->getAsDocumentFragment();
		} catch ( NodeCouldNotBeCreatedException $e ) {
			throw new ParseFailedException( "Could not create usable document fragment: " . $e->getMessage() );
		}

		$rootNode->parentNode->replaceChild($htmlFragment, $rootNode);

		for($i = count($rootNode->childNodes); $i > 0; $i--) {
			$htmlFragment->prepend($rootNode->childNodes->item($i - 1));
		}

		return $doc;
	}

	protected function doNode(DOMNode&ParserNode $node, NodeData $nodeData): void {
		$this->logDebug("starting Node: " . $node->nodeName);
		$nodeData = $node->getExtendedNodeData() ?? $nodeData;
		$node->setExtendedNodeData(clone $nodeData);

		if ($node instanceof StdParserElement){
			$this->prepareElement($node, $nodeData);
		}

		foreach ($this->nodeParsers as $nodeParser){
			$nodeParser->setNodeData($nodeData);
			$nodeParser->modifyNode($node);
		}

		$tag = $node->getAssociatedTag();
		if ($node instanceof StdParserElement){
			if ($node->tagName === "document-fragment") {
				$this->logDebug("Node is document-fragment, preparing...");
				try {
					$newNode = $node->getAsDocumentFragment();
				} catch ( NodeCouldNotBeCreatedException $e ) {
					$comment = $node->ownerDocument->createComment( "node could not be created: " . $e->getMessage() );
					$node->prepend( $comment );
				}
			}

			if ($tag !== null){
				try{
					$tag->setNodeData($nodeData);
					$this->logDebug("Node is associated with tag, running: " . get_class($tag));
					$newNode = $tag->getNewNode();
				} catch (NodeCouldNotBeCreatedException|TagNotFullyInitializedException $e){
					$comment = $node->ownerDocument->createComment("Tag was not initialized: " . $e->getMessage());
					$node->prepend($comment);
				}
			}
		}

		$this->logDebug("Node is done: " . $node->nodeName);

		$this->doChildNodes($newNode ?? $node, $nodeData);

		if ($node instanceof StdParserElement && isset($newNode)){
			if ($newNode instanceof StdParserElement){
				try {
					$newNode = $newNode->getAsDocumentFragment();
				} catch (NodeCouldNotBeCreatedException) {}
			}

			$this->logDebug("Replacing Node: " . $node->tagName);
			$node->replaceWith($newNode);
		}

		$tag?->destroy();
	}

	protected function prepareElement(StdParserElement $node, NodeData $nodeData): void {
		$tagClass = $this->tagNameParser->parseTagName($node->tagName, $nodeData->tagRegistry);

		if ($tagClass === null) return;

		$this->logDebug("Tag class was found: " . $tagClass);

		try {
			/** @var Tag $newTagInstance */
			$newTagInstance = $this->registry->getInstanceTransient($tagClass, $this->scope);
			$this->logDebug("Tag instance was created ($tagClass)");
		} catch (InjectionImpossibleException $e) {
			$comment = $node->ownerDocument->createComment("Could not create tag: " . $e->getMessage());
			$node->prepend($comment);
			return;
		}

		$newTagInstance->setNodeData($nodeData);
		$newTagInstance->setOriginalNode($node);

		/** @var DOMAttr $attribute */
		foreach ($node->attributes as $attribute){
			// This is the basic copy of the attributes to the tag, more actions can be done with the nodeParsers
			$newTagInstance->setAttribute($attribute->name, $attribute->value);
		}

		$node->setAssociatedTag( $newTagInstance );
	}

	protected function doChildNodes(DOMNode&ParserNode $nodeToLoopThrough, NodeData $nodeData): void {
		$nodeData = $nodeToLoopThrough->getExtendedNodeData() ?? $nodeData;

		$childCount = count($nodeToLoopThrough->childNodes);
		for ( $i = $childCount; $i > 0; $i--){
			$childNode = $nodeToLoopThrough->childNodes->item($i - 1);
			/** @noinspection PhpParamsInspection */
			$this->doNode($childNode, clone $nodeData);
		}
	}
}