<?php
namespace ModulesAbstract\ElectronicInvoice;

/**
 * Simplexml class
 * @author Lucian Sirbu
 */
class SimpleXmlElementExtended extends \Engine\SimpleXmlElement
{
	/**
	 * Locally defined namespaces
	 *
	 * @return array
	 */
	public function getDefinedNamespaces()
	{
		return [
			null => 	null,
			'etsi' =>   'http://uri.etsi.org/01903/v1.3.2#',
			'ds' =>     'http://www.w3.org/2000/09/xmldsig#',
		];
	}

	/**
	 * Convert this XML to a localised canonical form
	 * By default it will do whatever the parent class does. This should be overwritten by extending classes
	 *
	 * @return string
	 */
	public function asCanonicalDefault()
	{
		$xmlStr = $this->asDOMElement()->C14N(false, false, null);
		return $xmlStr;
	}

	public function asCanonicalExclusive()
	{
		$xmlStr = $this->asDOMElement()->C14N(true, false, null);
		return $xmlStr;
	}

	/**
	 * Inject a namespace, return the new element with the new namespace
	 *
	 * @param string $ns
	 * @param string $uri
	 * @param string $nsNode
	 * @return static
	 */
	public function injectNamespace(string $ns, string $uri, ?string $nsNode=null)
	{
		if ($nsNode===null) {
			$nsNode = $this;
		}

		// doubling the xmlns tag is a kind of a hack in order to allow adding multiple namespaces. It works though
		if ($ns==='xmlns') {
			$nsNode->asDOMElement()->setAttributeNS('http://www.w3.org/2000/xmlns/', "$ns", $uri);
			return $this;
		}
		else {
			$nsNode->addAttribute("xmlns:xmlns:$ns", $uri);
		}

		$ret = $this->clone();
		if ($ret===null) {
			return $this;
		}
		return $ret;
	}

	/**
	 * 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($namespace, $tag) = explode(":", $name);
		if ($tag===null) {
			$tag = $namespace;
			$namespace = null;
		}

		return parent::addChild($name, $value, ($this->getDefinedNamespaces()[$namespace]?:$ns));
	}

	/**
	 * Add a child without a namespace
	 *
	 * @param string $name
	 * @param mixed $value
	 * @return static
	 */
	public function addWithoutNS($name, $value = NULL)
	{
		$newElm = new \DOMElement($name, $value);
		$this->asDOMElement()->appendChild($newElm);

		return simplexml_import_dom($newElm, get_class($this));
	}

	/**
	 * Specialised xpath call, handling local namespaces
	 * @param string $expression
	 * @return static[]|false|null
	 */
	public function xpath($expression) :array|null|false
	{
		foreach ($this->getDefinedNamespaces() as $ns=>$uri) {
			if ($uri) {
				$this->registerXPathNamespace($ns, $uri);
			}
		}

		return parent::xpath($expression);
	}

	/**
	 * Get a child, while handling namespaces
	 * Allows accesses like: $xml->get("ns1:tagName");
	 *
	 * @param string $name
	 * @return static
	 */
	public function get($name)
	{
		return $this->getRaw($name);
	}

	/**
	 * Get a child, while handling namespaces. When invalid tags are accessed null is returned
	 *
	 * Allows accesses like: $xml->get("ns1:tagName");
	 *
	 * @param string $name
	 * @return static
	 */
	public function getRaw($name)
	{
		list($namespace, $tag) = explode(":", $name);
		if ($tag===null) {
			$tag = $namespace;
			$namespace = null;
		}

		$ret = $this->children($namespace, true)->{$tag};

		return $ret;
	}

	/**
	 * Get a child, while handling namespaces. When invalid tags are accessed a blank element is returned
	 *
	 * Allows accesses like: $xml->get("ns1:tagName");
	 *
	 * @param string $name
	 * @return static
	 */
	public function getSafe($name)
	{
		$ret = $this->getRaw($name);

		if ($ret->count()==0) {
			$ret = new \ModulesAbstract\ElectronicInvoice\SimpleXmlElementExtended('<?xml version="1.0" encoding="UTF-8"?><empty/>');
		}

		return $ret;
	}

	/**
	 * Convert this to XML string
	 * @param string $filename
	 * @return string|bool
	 */
	public function asXML($filename = null) : string|bool
	{
		$xmlStr = parent::asXML();

		// forcefully put the UTF8 header
		$possibleHeaderReplacements = [
			'<?xml version="1.0"?>' => '<?xml version="1.0" encoding="UTF-8"?>',
		];

		foreach ($possibleHeaderReplacements as $search=>$replacement) {
			if (strpos($xmlStr, $search)===0) {
				$xmlStr = $replacement. substr($xmlStr, strlen($search));
			}
		}

		return $xmlStr;
	}

	/**
	 * Convert this to XML string
	 * NOTE: this is not used anywhere right now
	 *
	 * @param bool? $inlineHeader
	 * @param bool? $showHeader
	 * @return string
	 */
	public function asXMLAdvanced($inlineHeader=null, $showHeader=null) : string
	{
		$xmlStr = parent::asXML();

		// forcefully put the UTF8 header, inline
		$possibleHeaderReplacements = [];
		if ($inlineHeader===true) {
			$possibleHeaderReplacements["<?xml version=\"1.0\"?>\r\n"] = 	"<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
			$possibleHeaderReplacements["<?xml version=\"1.0\"?>\n"] = 		"<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
			$possibleHeaderReplacements["<?xml version=\"1.0\"?>\r"] = 		"<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
		}
		if ($inlineHeader===false) {
			$possibleHeaderReplacements["<?xml version=\"1.0\"?>\r\n"] = 	"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
			$possibleHeaderReplacements["<?xml version=\"1.0\"?>\n"] = 		"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
			$possibleHeaderReplacements["<?xml version=\"1.0\"?>\r"] = 		"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
		}

		$possibleHeaderReplacements['<?xml version="1.0"?>'] = '<?xml version="1.0" encoding="UTF-8"?>';

		$headerReplaced = false;
		foreach ($possibleHeaderReplacements as $search=>$replacement) {
			if (strpos($xmlStr, $search)===0) {
				$xmlStr = $replacement. substr($xmlStr, strlen($search));
				$headerReplaced = true;
				break;
			}
		}

		if ($showHeader===true && !$headerReplaced) {
			$xmlStr = '<?xml version="1.0" encoding="UTF-8"?>'.$xmlStr;
		}
		if ($showHeader===false && $headerReplaced) {
			$xmlStr = substr($xmlStr, strlen('<?xml version="1.0" encoding="UTF-8"?>'));
		}

		return $xmlStr;
	}

}
