<?php
namespace ModulesAbstract\ElectronicInvoice\UBL30Exporter;

abstract class ExporterAbstract extends \ModulesAbstract\ElectronicInvoice\UBLExporter\Exporter
{
	/**
	 * Referenced invoice entity
	 *
	 * @var \Entity\Invoice
	 */
	protected $invoice = null;

	/**
	 * Referenced document entity
	 *
	 * @var \Entity\Document
	 */
	protected $document = null;

	/**
	 * The filled-in PEPPOL structure
	 *
	 * @var \ModulesAbstract\ElectronicInvoice\UBL30Structure\Invoice
	 */
	protected $peppolStructure = null;

	protected $namespaces = [
		'root' => 	'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
		'cac' => 	'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
		'cbc' => 	'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
	];

	public function __construct(\Entity\Invoice $invoice, \Entity\Document $document, \ModulesAbstract\ElectronicInvoice\UBL30Structure\AbstractPeppolElement $peppolStructure)
	{
		$this->options = [
			'hideEmptyChilds' => 	null,	// 'loose', 'strict', null
			'hideEmptyNodes' => 	null,	// 'loose', 'strict', null
		];

		$this->invoice = $invoice;
		$this->document = $document;
		$this->peppolStructure = $peppolStructure;
	}

	/**
	 * Get the internal invoice
	 *
	 * @return \Entity\Invoice
	 */
	public function getInvoice()
	{
		return $this->invoice;
	}

	/**
	 * Get the internal document
	 *
	 * @return \Entity\Document
	 */
	public function getDocument()
	{
		return $this->document;
	}

	/**
	 * Get the generated structure
	 *
	 * @return \ModulesAbstract\ElectronicInvoice\UBL30Structure\AbstractPeppolElement
	 */
	public function getStructure()
	{
		return $this->peppolStructure;
	}

	/**
	 * Generate the output XML
	 *
	 * @return \Engine\SimpleXmlElement
	 * @throws \Exception
	 */
	abstract public function generate();


	/**
	 * Generate an XML tag
	 * @param \Engine\SimpleXmlElement $node
	 * @param mixed $object
	 * @param array $parents
	 * @return void
	 * @throws \Exception
	 */
	protected function generateObjectTag(\Engine\SimpleXmlElement $node, $object, array $parents=[])
	{
		if ($this->options['hideEmptyChilds']) {
			if (!$this->objectHasValue($object, true, $this->options['hideEmptyChilds'], $parents)) {
				return;
			}
		}

		// render attributes
		foreach ($object->getAttributes() as $k=>$label) {
			$p = $parents;
			$p[]= $k;

			$v = $object->{$k};
			if ($v===null || is_scalar($v)) {
				// TODO: $label should never be null
				if ($this->options['hideEmptyNodes']) {
					if ($this->isValue($v, $this->options['hideEmptyNodes'], $object, $p)) {
						$node[$label] = $this->renderField($object, $k, $v, $parents);
					}
				}
				else {
					$node[$label] = $this->renderField($object, $k, $v, $parents);
				}
			}
			else {
				throw new \Exception("Trying to report an unsupported value!");
			}
		}

		// render child tags
		foreach ($object->getChilds() as $k=>$label) {
			$p = $parents;
			$p[]= $k;

			$v = $object->{$k};

			if (is_array($v)) {
				if ($label===null) {
					$node->asDOMElement()->nodeValue = $this->renderField($object, $k, $v, $parents);
				}
				else {
					foreach ($v as $child) {
						if ($this->options['hideEmptyNodes']) {
							if ($this->objectHasValue($child, true, $this->options['hideEmptyNodes'], $p)) {
								$this->generateObjectTag($node->addChild($label, null, $this->determineNamespace($child, $p)), $child, $p);
							}
						}
						else {
							$this->generateObjectTag($node->addChild($label, null, $this->determineNamespace($child, $p)), $child, $p);
						}
					}
				}
			}
			elseif ($v===null || is_scalar($v)) {
				$shouldAdd = false;
				if ($this->options['hideEmptyNodes']) {
					if ($this->isValue($v, $this->options['hideEmptyNodes'], $object, $p)) {
						$shouldAdd = true;
					}
				}
				else {
					$shouldAdd = true;
				}

				if ($shouldAdd) {
					if ($label===null) {
						$node->asDOMElement()->nodeValue = $this->renderField($object, $k, $v, $parents);
					}
					else {
						$node->addChild($label, $this->renderField($object, $k, $v, $parents), $this->determineNamespace($v, $p));
					}
				}
			}
			elseif (is_object($v)) {
				// TODO: $label should never be null
				if ($this->options['hideEmptyNodes']) {
					if ($this->objectHasValue($v, true, $this->options['hideEmptyNodes'], $p)) {
						$this->generateObjectTag($node->addChild($label, null, $this->determineNamespace($v, $p)), $v, $p);
					}
				}
				else {
					$this->generateObjectTag($node->addChild($label, null, $this->determineNamespace($v, $p)), $v, $p);
				}
			}
			else {
				throw new \Exception("Trying to report an unsupported value!");
			}
		}
	}

	/**
	 * Determine the namespace of an object (cbc, cac, etc)
	 * @param mixed $object
	 * @param mixed $parents
	 * @return string
	 */
	private function determineNamespace($object, $parents)
	{
		if ($object===null || is_scalar($object) || $object instanceof \ModulesAbstract\ElectronicInvoice\UBL30Structure\AbstractPeppolElementWithOneValue) {
			return $this->namespaces['cbc'];
		}
		return $this->namespaces['cac'];
	}

	/**
	 * Check if an object has a value that needs to be reported
	 * @param mixed $object
	 * @param bool $recurse
	 * @param string $comparisonMode
	 * @param array $parents
	 * @return bool
	 * @throws \Exception
	 */
	private function objectHasValue($object, bool $recurse=true, $comparisonMode='strict', array $parents=[])
	{
		if ($object instanceof \ModulesAbstract\ElectronicInvoice\UBL21Structure\TypeSignature) {
			return true;
		}
		if ($object instanceof \ModulesAbstract\ElectronicInvoice\UBL21Structure\TypeExtension) {
			return true;
		}


		$hasValue = false;
		foreach ($object as $k=>$v) {
			$p = $parents;
			$p[]= $k;

			if ($v instanceOf \ModulesAbstract\ElectronicInvoice\UBL21Structure\AbstractPeppolElement) {
				$hasChildsWithValue = $this->objectHasValue($v, $recurse, $comparisonMode, $p);
				if ($hasChildsWithValue) {
					$hasValue = true;
					break;
				}
			}
			elseif ($this->isValue($v, $comparisonMode, $object, $p)) {
				$hasValue = true;
				break;
			}
		}

		return $hasValue;
	}

	/**
	 * Check if an object is a value object
	 * @param mixed $value
	 * @param string $comparisonMode
	 * @param mixed $object
	 * @param array $parents
	 * @return bool
	 * @throws \Exception
	 */
	private function isValue($value, string $comparisonMode, $object, array $parents)
	{
		$ret = false;
		if ($object instanceof \ModulesAbstract\ElectronicInvoice\UBL21Structure\AbstractPeppolElementWithOneValue) {
			if ($value!==null) {
				$ret = true;
			}
		}
		else {
			$ret = $object->tagHasValue(end($parents), $value, $comparisonMode, $object, $parents);
		}

		// printf("<div style=''>/%s -> %s</div>", implode("/", $parents), ($ret?"<span style='color: #00c;'>true</span>":"<span style='color: #800;'>false</span>"));

		return $ret;
	}

	/**
	 * Overridable call, allowing to control how each field is written
	 * @param mixed $object
	 * @param mixed $k
	 * @param mixed $v
	 * @param mixed $parents
	 * @return mixed
	 */
	protected function renderField($object, $k, $v, $parents)
	{
		// v1: this should always be the default, but we sometimes need to override this
		return $object->renderField($k, $v);

		// v2: encode all utf8-chars to &#1610-like notation
		// @see https://stackoverflow.com/a/72195771/11301
		// $convmap = [0x80, 0xfffffff, 0, 0xfffffff];
		// return mb_encode_numericentity($object->renderField($k, $v), $convmap, 'UTF-8', false);

		// v3: remove all utf8-chars
		// return preg_replace("/[^\x01-\x7f]/u", "", $object->renderField($k, $v));
	}

}
