<?php

namespace Planaday\Html;

use DOMDocument;
use DOMElement;
use DOMException;
use DOMNode;
use DOMXPath;
use Exception;
use Planaday\Html\Tags\TagController;
use Planaday\Html\Tags\TagNotFoundException;
use Planaday\Platform\PlatformAbstraction\LocalizationController;
use Planaday\Utility\DateAndTimeUtility;
use Planaday\Utility\Debug\Log\Logging;
use Planaday\Utility\DependencyInjection\InjectionImpossibleException;
use Planaday\Utility\Timer\Timer;
use Planaday\Utility\Timer\TimerFactory;

class HtmlParser {
	use Logging;

	protected ?string $beingParsedName = null;
	protected array $data = [];
	protected array $conditionals = [];
	protected array $unsafeData = [];
	protected array $scripts = [];
	protected array $styles = [];
	protected array $injectedAttributes = [];
	protected array $context = [];
	protected array $usedStyleSheets = [];
	protected array $usedScripts = [];
	protected array $tagNestingHistory = [];
	public const MAX_DEPTH = 20;
	protected ?Timer $timer = null;

	public function __construct(
		protected TagController $tagController,
		protected DateAndTimeUtility $dateAndTimeUtility,
		protected TimerFactory $timerFactory,
		protected LocalizationController $localizationController
	) {}

	/**
	 * @param array<mixed> $parserContext
	 */
	public function setParserContext( ?array $parserContext ): void {
		if ($parserContext === null) {
			return;
		}

		if (is_array($parserContext['tagNestingHistory'] ?? null)) $this->tagNestingHistory =& $parserContext['tagNestingHistory'];
		if (($parserContext['timer'] ?? null) instanceof Timer) $this->timer =& $parserContext['timer'];
		if (is_array($parserContext['usedScripts'] ?? null)) $this->usedScripts =& $parserContext['usedScripts'];
		if (is_array($parserContext['usedStyleSheets'] ?? null)) $this->usedStyleSheets =& $parserContext['usedStyleSheets'];
	}

	public function setBeingParsedName(string $name): void {
		$this->beingParsedName = $name;
	}

	public function setData(string $name, float|int|string|null $value): void {
		if (is_float($value)) {
			$value = number_format($value, 2);
		}

		$this->data[strtolower($name)] = htmlspecialchars('' . $value);
	}

	public function getParserUniqueId(): string {
		if (!isset($this->data['uniqueid'])) {
			try {
				$this->setData( 'uniqueId', (string) random_int( 0, 65535 ) );
			} catch ( Exception ) {
				$this->setData( 'uniqueId', substr( $this->dateAndTimeUtility->getNowMillisecondsString(), - 3 ) );
			}
		}

		return $this->data['uniqueid'];
	}

	public function setCondition(string $name, bool $value): void {
		$this->conditionals[strtolower($name)] = $value;
	}

	public function setUnsafeData(string $name, ?string $value): void {
		$this->unsafeData[strtolower($name)] = $value ?? '';
	}

	public function injectAttribute(string $name, int|string|null $value): void {
		$this->injectedAttributes[$name] = $value;
	}

	public function addStylesheet(string $pathToStylesheet): void {
		if (!in_array($pathToStylesheet, $this->usedStyleSheets)){
			$this->styles[] = $pathToStylesheet;
			$this->usedStyleSheets[]  = $pathToStylesheet;
		}
	}

	public function addScript(string $pathToScript): void {
		if (!in_array($pathToScript, $this->usedScripts)){
			$this->scripts[] = $pathToScript;
			$this->usedScripts[] = $pathToScript;
		}
	}

	public function addContext(string $name, mixed $value):void {
		$this->context[$name] = $value;
	}

	public function parse(string $html, string $nameThingBeingParsed = ''):string {
		if (count($this->tagNestingHistory) == 0){
			$this->timer = $this->timerFactory->getTimer('Full <b>'. $nameThingBeingParsed .'</b> parse sequence');
			$html = "<!--Planaday " . $nameThingBeingParsed ."-->" . $html;
		}

		if (!empty($nameThingBeingParsed)){
			$this->beingParsedName = $nameThingBeingParsed;
		}

		if (count($this->tagNestingHistory) > self::MAX_DEPTH){
			return $this->getTagDepthError();
		}

		$node = $this->parseToDom($html);

		if ($node->ownerDocument !== null){
			$html = $node->ownerDocument->saveHTML($node);
		} elseif($node instanceof DOMDocument) {
			$html = $node->saveHTML();
		} else {
			$this->logError('This should never be reached');
			return '';
		}

		if ($html === false) $html = '';
		$html = str_replace(array('<temp-body-pad>', '</temp-body-pad>'), '', $html);

		if (count($this->tagNestingHistory) == 0){
			$this->logInfo("Finished Parse with following times :(data)", $this->timer->getTimeStamps());
		}

		return $html;
	}

	public function parseToDom(string $html):DOMNode {
		$this->timer->addTimeStamp(str_repeat('-', count($this->tagNestingHistory)) . 'Tag Logic');

		$this->getParserUniqueId();
		$html = $this->parseForUnsafeData($html);
		$html = $this->parseForData($html);
		$html = $this->parseForTranslateTags($html);

		if (count($this->scripts) > 0) {
			$script = $this->buildScriptBlock();
		}

		if (count($this->styles) > 0) {
			$style = $this->buildStyleBlock();
		}

		$this->timer->addTimeStamp(str_repeat('-', count($this->tagNestingHistory)) . 'Data parse');

		$html = '<temp-body-pad>' . ($style ?? '') . $html . ($script ?? '') . '</temp-body-pad>';

		libxml_use_internal_errors(true);

		$dom = new DOMDocument();
		$dom->loadHTML(htmlspecialchars_decode(htmlentities( $html, ENT_COMPAT, 'UTF-8' )));
		$this->parseForConditionals($dom);
		$this->parseForInvertedConditionals($dom);
		$this->parseForTags($dom);

		$this->timer->addTimeStamp(str_repeat('-', count($this->tagNestingHistory)) . 'Tags');

		$domNode = $dom->getElementsByTagName('temp-body-pad')->item(0);
		if ($domNode === null) {
			$this->logError("no temp-body-pad tag found");
			return $dom;
		}

		return $domNode;
	}

	private function buildStyleBlock():string {
		$style = '<style>';

		foreach ($this->styles as $path){
			if (file_exists($path)) $fileContents = file_get_contents( $path );

			if (!isset($fileContents) || $fileContents === false){
				$fileContents = "\n/* could not load style " . str_replace('*/', '* /' , $path) . " */";
				$this->logError("could not load style $path");
			}

			$style .= $fileContents;
		}

		$style .= '</style>';
		return $style;
	}

	private function buildScriptBlock():string {
		$script = '<script type="module">';

		foreach ($this->scripts as $path){
			if (file_exists($path)) $fileContents = file_get_contents( $path );

			if (!isset($fileContents) || $fileContents === false){
				$fileContents = "\n/* could not load script " . str_replace('*/', '* /' , $path) . " */";
				$this->logError("could not load script $path");
			}

			$script .= $fileContents;
		}

		$script .= '</script>';
		return $script;
	}

	private function parseForData(string $html):string {
		preg_match_all("@\{\{pad-([^}]+)}}@", $html, $matches, PREG_SET_ORDER);

		foreach ($matches as $match){
			if (key_exists(strtolower($match[1]), $this->data)){
				$html = str_replace($match[0], $this->data[strtolower($match[1])], $html);
				continue;
			}

			$html = str_replace($match[0], '', $html);
		}

		return $html;
	}

	private function parseForUnsafeData(string $html):string {
		preg_match_all("@\{\{pad-([^}]+)}}@", $html, $matches, PREG_SET_ORDER);

		foreach ($matches as $match){
			if (isset($this->unsafeData[strtolower($match[1])])){
				$html = str_replace($match[0], $this->unsafeData[strtolower($match[1])], $html);
			}
		}

		return $html;
	}


	private function parseForTags( DOMDocument $dom):void {
		$xpath = new DOMXPath($dom);
		$query = '//*[starts-with(local-name(),"pad-")]';
		$tags = $xpath->query($query);

		if ( ! $tags ){
			return;
		}

		$this->timer->setDepth($this->timer->getDepth() + 1);

		for ( $i = 0; $i < $tags->length; $i ++ ){
			$node = $tags->item($i);
			if ($node === null){
				continue;
			}
			$parentNode = $node->parentNode;
			if ($parentNode === null){
				continue;
			}

			$tagName = str_replace('pad-', '', $node->localName ?? '');

			if ($tagName == 'translate'){
				$this->timer->addTimeStamp(str_repeat('-', count($this->tagNestingHistory)) . 'translate skip');
				continue;
			}

			if (in_array($tagName, $this->tagNestingHistory)){
				$this->logInfo('recursive tag detected ' . $tagName);
			}

			$this->tagNestingHistory[] = $tagName;

			foreach ($this->injectedAttributes as $attributeKey => $attributeValue){
				if ($node instanceof DOMElement && $node->getAttribute($attributeKey) == ''){
					$node->setAttribute($attributeKey, $attributeValue);
				}
			}

			try {
				$tag = $this->tagController->getTag($node);
				$tag->setContext($this->context);
				$tag->setParserContext(
					[
						'tagNestingHistory' => &$this->tagNestingHistory,
						'timer' => &$this->timer,
						'usedScripts' => &$this->usedScripts,
						'usedStyleSheets' => &$this->usedStyleSheets,
					]
				);


				$parentNode->replaceChild($tag->buildDomNode($node), $node);
			} catch ( TagNotFoundException ) {
			} catch ( InjectionImpossibleException ) {
				try {
					$errorSpan = $dom->createElement('span', $node->localName . ' failed to load due to a dependency injection problem');
					$errorSpan->setAttribute('class', 'pad-fatal-tag-error');

					$parentNode->replaceChild($errorSpan, $node);
				} catch ( DOMException ) {
					$parentNode->removeChild($node);
				}

			}
			array_pop($this->tagNestingHistory);

			$this->timer->addTimeStamp(str_repeat('-', count($this->tagNestingHistory)) . '"' . $tagName . '" tag parse');
		}
		$this->timer->setDepth($this->timer->getDepth() - 1);
	}

	private function parseForConditionals( DOMDocument $dom):void {
		$xpath = new DOMXPath($dom);
		$query = '//*[starts-with(local-name(),"if-pad-")]';
		$tags = $xpath->query($query);

		if ( ! $tags ){
			return;
		}

		for ( $i = $tags->length -1; $i >= 0; $i -- ){
			$node = $tags->item($i);
			if ($node === null){
				continue;
			}
			$parent = $node->parentNode ?? $dom;
			$conditionalName = str_replace('if-pad-', '', $node->localName ?? '');
			$replacementNode = $dom->createDocumentFragment();

			if (isset($this->conditionals[$conditionalName]) && $this->conditionals[$conditionalName]){
				while ( $node->childNodes->length > 0 ) {
					$child = $node->childNodes->item(0);
					if ($child === null ) break;
					$replacementNode->appendChild($child);
				}

				$parent->replaceChild($replacementNode, $node);
				continue;
			}
			$parent->removeChild($node);
		}
	}

	private function parseForInvertedConditionals( DOMDocument $dom):void {
		$xpath = new DOMXPath($dom);
		$query = '//*[starts-with(local-name(),"if-not-pad-")]';
		$tags = $xpath->query($query);

		if ( ! $tags ){
			return;
		}

		for ( $i = $tags->length -1; $i >= 0; $i -- ){
			$node = $tags->item($i);
			if ($node === null){
				continue;
			}
			$parent = $node->parentNode ?? $dom;
			$conditionalName = str_replace('if-not-pad-', '', $node->localName ?? '');
			$replacementNode = $dom->createDocumentFragment();

			if (isset($this->conditionals[$conditionalName]) && !$this->conditionals[$conditionalName]){
				while ( $node->childNodes->length > 0 ) {
					$child = $node->childNodes->item(0);
					if ($child === null ) break;
					$replacementNode->appendChild($child);
				}

				$parent->replaceChild($replacementNode, $node);
				continue;
			}
			$parent->removeChild($node);
		}
	}

	private function parseForTranslateTags(string $html):string {
		preg_match_all("@<pad-translate>(.*?)</pad-translate>@", $html, $matches, PREG_SET_ORDER);

		foreach ($matches as $match){
			$html = str_replace(
				$match[0],
				$this->localizationController->translate(
					$match[1], $this->beingParsedName !== null ? $this->beingParsedName . '(Templates)' : null
				) ?? '',
				$html
			);
		}

		return $html;
	}

	private function getTagDepthError(): string {
		$output = 'The maximum tag depth was reached with the following path: <br>';

		for ( $i = 0; $i < count($this->tagNestingHistory); $i ++ ) {
			$output .= '[' . $i . ']: ' . $this->tagNestingHistory[$i] . '<br>';
		}

		$this->logError('Tag nesting depth error', $this->tagNestingHistory);

		return $this->buildError($output);
	}

	private function buildError(string $errorMessage): string {
		$this->unsafeData['error-message'] = $errorMessage;
		$html = file_get_contents(__DIR__ . '/ParserErrorMessage.html');
		if ($html === false){
			return '';
		}
		$html = $this->parseForUnsafeData($html);
		return $this->parseForTranslateTags($html);
	}
}