<?php
namespace Engine;

/**
 * Simplexml class
 * @author Lucian Sirbu
 */
class SimpleXmlElement extends \SimpleXMLElement
{
	/**
	 * Get the first node matching the xpath route
	 *
	 * @param String $xpath
	 * @return static
	 */
	public function xpathFirstNode($xpath)
	{
		$nodes = $this->xpath($xpath);
		return $nodes[0];
	}

	/**
	 * Get the first node matching the xpath route
	 *
	 * @param String $xpath
	 * @return string|null
	 */
	public function xpathFirstNodeAsStr($xpath)
	{
		$nodes = $this->xpath($xpath);
		return ($nodes[0]?(string)$nodes[0]:null);
	}

	/**
	 * Apply a callback for every matched node, return results
	 *
	 * @param string $xpath
	 * @param callable $callback
	 * @return array
	 */
	public function xpathProcessNodes($xpath, $callback)
	{
		$nodes = $this->xpath($xpath);
		if (!$nodes) {
			return [];
		}
		return array_map($callback, $nodes);
	}

	/**
	 * Evaluate an xpath query
	 *
	 * @param string $xpathEval
	 * @return string
	 */
	public function xpathEvaluate($xpathEval)
	{
		$xpath = new \DOMXPath($this->asDOMElement()->ownerDocument);
		return $xpath->evaluate($xpathEval);
	}

	/**
	 * Debug helper. Display the XML structure
	 *
	 * @return static
	 */
	public function xpathDump($xpath)
	{
		$nodes = $this->xpath($xpath);

		$CSSLoader = \Engine\Theme\CSSLoader\CSSLoader::getInstance();
		$JSLoader = \Engine\Theme\JSLoader\JSLoader::getInstance();

		$CSSLoader->preload();
		$JSLoader->preload();

		printf("<div style='border: 1px solid #800; background-color: #fcfcfc; padding: 0.5em; margin: 0.5em;'>");
		foreach ($nodes as $node) {
			$xml = new \XMLReader();
			$xml->XML($node->asXML());

			$objectExplorer = new \App\Utils\Views\XMLExplorer($xml);
			$objectExplorer->setOption('interactive', true);
			$objectExplorer->setOption('showEmptyLeafs', true);

			echo $objectExplorer->preloadCss($CSSLoader);
			echo $objectExplorer->renderHtml();
			echo $objectExplorer->preloadJs($JSLoader);
		}
		printf("</div>");

		// $CSSLoader->render();
		// $JSLoader->render();

		// $CSSLoader->postload();
		// $JSLoader->postload();

		return $this;
	}

	/**
	 * Specialised addChild() function, handling local namespaces
	 *
	 * @param string $name
	 * @param string|null $value
	 * @param string|null $ns
	 * @return $this
	 */
	public function add($name, $value = NULL, $ns = NULL)
	{
		list(, $tag) = explode(":", $name);
		if ($tag!==null) {
			// you might want to overwrite this call and see how namespaces could be handled in \ModulesAbstract\ElectronicInvoice\SimpleXmlElementExtended()
			throw new \Exception("Namespace is not allowed in here!");
		}

		return parent::addChild($name, $value, $ns);
	}


	/**
	 * Set the tag value
	 *
	 * @return static
	 */
	public function setValue(string $value)
	{
		$this->asDOMElement()->nodeValue = $value;
		return $this;
	}

	/**
	 * Get the tag value
	 *
	 * @return string
	 */
	public function getValueStr()
	{
		return (string)$this->asDOMElement()->nodeValue;
	}

	/**
	 * Remove the SimpleXML element from the DOM tree, discard it completly
	 *
	 * @return static
	 */
	public function remove()
	{
		$domElm = $this->asDOMElement();
		$domElm->parentNode->removeChild($domElm);
		return $this;
	}

	/**
	 * Remove the SimpleXML element from the DOM tree, and return it
	 *
	 * @return static
	 */
	public function removeAndGetNode()
	{
		$domElm = $this->asDOMElement();
		$ret = simplexml_load_string($this->asStr(), get_class($this));
		$domElm->parentNode->removeChild($domElm);
		return $ret;
	}

	/**
	 * Replace this node with another node
	 *
	 * @param \Engine\SimpleXMLElement $element
	 * @return static
	 */
	public function replaceWith(\Engine\SimpleXMLElement $element) {
        $dom     = $this->asDOMElement();
        $import  = $dom->ownerDocument->importNode(
            $element->asDOMElement(),
            TRUE
        );
        $dom->parentNode->replaceChild($import, $dom);

		return $this;
    }

	/**
	 * Replace a child of this element with another one
	 *
	 * @param \Engine\SimpleXMLElement $node
	 * @param \Engine\SimpleXMLElement $element
	 * @return static
	 */
	public function replaceChild(\Engine\SimpleXMLElement $node, \Engine\SimpleXMLElement $element) {
        $dom     = $node->asDOMElement();
        $import  = $dom->ownerDocument->importNode(
            $element->asDOMElement(),
            TRUE
        );
        $dom->parentNode->replaceChild($import, $dom);

		return $this;
    }

	 /**
	  * Add a child before another node
	  *
	  * @param string $qualifiedName
	  * @param string|null $value
	  * @param string|null $namespace
	  * @param \DOMElement $referenceNode
	  * @return static
	  * @throws \Exception
	  */
    public function addChildBefore(string $qualifiedName, \DOMElement $referenceNode, ?string $value = null, ?string $namespace = null)
	{
		$elm = $this->add($qualifiedName, $value, $namespace);
		$referenceNode->parentNode->insertBefore($elm->asDOMElement(), $referenceNode);
		return $elm;
	}

	/**
	 * Add a child node after a node
	 *
	 * @param string $qualifiedName
	 * @param string|null $value
	 * @param string|null $namespace
	 * @param \DOMElement $referenceNode
	 * @return static
	 */
	public function addChildAfter(string $qualifiedName, \DOMElement $referenceNode, ?string $value = null, ?string $namespace = null)
	{
		// NOTE: there is no insertAfter in DOM, so a workaround is to insert the node before the next sibling
		$elm = $this->add($qualifiedName, $value, $namespace);
		$referenceNode->parentNode->insertBefore($elm->asDOMElement(), $referenceNode->nextSibling);
		return $elm;
	}

	/**
	 * Strip all XML namespaces, allowing for an easier access pattern via simplexml
	 *
	 * @return static
	 */
	public function stripNamespaces()
	{
		return $this->stripNamespaces_correct($this);
	}


	/**
	 * stripNamespaces, v1
	 * @param \Engine\SimpleXMLElement $xmlElement
	 * @return static
	 */
	public function stripNamespaces_fast(\Engine\SimpleXMLElement $xmlElement)
	{
		// remove comments
		$xml = $xmlElement->asStr();
		$xml = preg_replace('/<!--.*?-->/mis', '', $xml);

		// skip comment removal. This is the faster version, but simplexml_load_string might fail for some inputs
		// $xml = $xmlElement->asStr();

		return simplexml_load_string($this->stripNamespaces_stripTagsWithRegexps($xml), get_class($this));
	}

	/**
	 * stripNamespaces, v2
	 * @param \Engine\SimpleXMLElement $xmlElement
	 * @return static
	 */
	public function stripNamespaces_correct(\Engine\SimpleXMLElement $xmlElement)
	{
		$xml = (@$xmlElement->asCanonical()?:$xmlElement->asStr());	// this will remove comments, but might fail for manual SOAP requests (as Farelogix does)

		return simplexml_load_string($this->stripNamespaces_stripTagsWithRegexps($xml), get_class($this));
	}

	/**
	 * stripNamespaces helper
	 * @param string $xml
	 * @return string
	 */
	protected function stripNamespaces_stripTagsWithRegexps(string $xml): string
	{
		/**
		 * this function uses regexp to build a list of replacements like this:
		 * 	- looks up namespaces derived from the tag names (ex: "<xsi:root" will be replaced with "<root")
		 * 	- looks up namespaces used for attributes (ex: "<root [...] xsi:attr='22'" will be replaced with "<root [...] attr='22'")
		 */

		// build a namespaced-tags replacements list
		$nstags = [];
		preg_match_all("/\<(?P<tag>[^:\s\/\<\>]+:[^\s\/\<\>]+[\s]*)/i", $xml, $nstags);
		$repl = [];
		$nstags['tag'] = array_unique($nstags['tag']);
		foreach ($nstags['tag'] as $nstag) {
			$tag = preg_replace("/([^:\s\/\<]+):([^\s\/\<]+)/i", "$2", $nstag);
			$repl[sprintf("<%s", $nstag)] = sprintf("<%s", $tag);
			$repl[sprintf("</%s", rtrim($nstag))] = sprintf("</%s", rtrim($tag));
		}

		// build a namespaced-attributes replacements list
		//  lookup atrributes like "ns:asdasdasd="
		$alphaNumRegexpPattern = "\p{L}\p{N}_\-";	// allow any attributes that contain UTF-8 letters & numbers
		preg_match_all("/[\s](?P<tag>[{$alphaNumRegexpPattern}]+:[{$alphaNumRegexpPattern}]+[\s]*)=/u", $xml, $nstags);
		$nstags['tag'] = array_unique($nstags['tag']);
		foreach ($nstags['tag'] as $nstag) {
			$tag = preg_replace("/([^:\s\/]+):([^\s]+)/i", "$2", $nstag);
			$repl[sprintf(" %s=", $nstag)] = sprintf(" %s=", $tag);
		}

		$replregexp = [];
		$namespaces = $this->getNamespaces(true);
		foreach ($namespaces as $var=>$ns) {
			$replregexp[sprintf("/xmlns:%s=('|\")%s('|\")/i", preg_quote($var, "/"), preg_quote($ns, "/"))] = "";
		}

		$replregexp[sprintf("/[\s]+xsi:[^\s:]+=('|\").*?('|\")/i")] = " ";
		$replregexp[sprintf("/[\s]+xmlns:[^\s:]+=('|\").*?('|\")/i")] = " ";
		$replregexp[sprintf("/[\s]+xmlns[\s]*=('|\").*?('|\")/i")] = " ";


		// sort by length, name, reversed, to avoid mistakes while replacing
		uksort($repl, function($a, $b) {
			$ret = 0;

			if (!$ret) {
				$ret = strlen($b) - strlen($a);
			}

			if (!$ret) {
				$ret = strcmp($a, $b);
			}

			return $ret;
		});

		// sort by length, name, reversed, to avoid mistakes while replacing
		uksort($replregexp, function($a, $b) {
			$ret = 0;

			if (!$ret) {
				$ret = strlen($b) - strlen($a);
			}

			if (!$ret) {
				$ret = strcmp($a, $b);
			}

			return $ret;
		});

		// do the actual replacements
		foreach ($replregexp as $key=>$value) {
			$xml = preg_replace($key, $value, $xml);
		}

		// do the actual replacements
		$xml = str_replace(array_keys($repl), array_values($repl), $xml);
		return $xml;
	}




	/**
	 * The xml content, without any xml headers
	 *
	 * @return string
	 */
	public function asStr()
	{
		$str = preg_replace("/^(\<\?xml([\s]+version=\"1\.0\")?[\s]*([\s]*encoding=\"[^\"]+\")?[\s]*\?\>)/i", "", $this->asXML());
		return trim($str);
	}

	/**
	 * The xml content as beautified for display
	 *
	 * @return string
	 */
	public function asPretty()
	{
		$thisDocument = $this->asDOMElement()->ownerDocument;
		$dom = new \DOMDocument($thisDocument->xmlVersion, ($thisDocument->xmlEncoding??'UTF8'));
		$dom->preserveWhiteSpace = true;
		$dom->formatOutput = true;
		$dom->loadXML($this->asXml());
		return $dom->saveXML();
	}

	/**
	 * Remove spaces & newlines
	 *
	 * @return string
	 */
	public function asMinified()
	{
		$thisDocument = $this->asDOMElement()->ownerDocument;
		$dom = new \DOMDocument($thisDocument->xmlVersion, ($thisDocument->xmlEncoding??'UTF8'));
		$dom->preserveWhiteSpace = false;
		$dom->formatOutput = false;
		$dom->loadXML($this->asXML());
		return $dom->saveXML();
	}

	/**
	 * Canonicalize (or C14N) an XML document
	 * @see https://www.php.net/manual/en/domnode.c14n.php
	 *
	 * @return string
	 */
	public function asCanonical()
	{
		return $this->asDOMElement()->C14N(false, false, null);
	}

	/**
	 * Return the simpleXML element as a \DOMElement node.
	 * The return is a reference!
	 *
	 * @return \DOMElement
	 */
	public function asDOMElement()
	{
		return dom_import_simplexml($this);
	}

	/**
	 * Return the simpleXML element as a \DOMElement cloned node.
	 * The return is a complete clone
	 *
	 * @return \DOMDocument
	 */
	public function asDOMDocumentClone()
	{
		$thisDocument = $this->asDOMElement()->ownerDocument;
		$dom = new \DOMDocument($thisDocument->xmlVersion, ($thisDocument->xmlEncoding??'UTF8'));
		$dom->loadXML($this->asCanonical());
		return $dom;
	}

	/**
	 * Convert this object to another object type. It should extend the SimpleXml class
	 *
	 * @param string $classRef
	 * @return static
	 */
	public function asObject(string $classRef)
	{
		return simplexml_import_dom($this->asDOMDocumentClone(), $classRef);
	}

	/**
	 * Clone the current SimplXML node
	 *
	 * @return static
	 */
	public function clone()
	{
		return simplexml_import_dom($this->asDOMDocumentClone(), get_class($this));
	}

	/**
	 * Validate xml against the xsd
	 *
	 * @param string $xsdFilePath
	 * @param \App\MessagesCollector $msgs
	 * @return \App\MessagesCollector
	 */
	public function validateAgainstXsd($xsdFilePath, \App\MessagesCollector $msgs)
	{
		libxml_use_internal_errors(true);

		$doc = new \DOMDocument();
		$doc->loadXML($this->asStr(), LIBXML_NOBLANKS);

		if( !$doc->schemaValidate($xsdFilePath) ) {
			$errors = libxml_get_errors();
			foreach ($errors as $error) {
				switch ($error->level) {
					case LIBXML_ERR_WARNING:
						$msgs->addWarning(sprintf("XML validation warning: %s", $error->message));
					break;
					case LIBXML_ERR_ERROR:
					case LIBXML_ERR_FATAL:
						$msgs->addError(sprintf("XML validation error: %s", $error->message));
					break;
				}
			}
		}

		return $msgs;
	}

}
