<?php
namespace App\Utils\Views;

/**
 * XML Viewer class that displays an XML just like Firefox does.
 * Initially developed for the CSOB plugin, then as Sirena view, then converted much later to component
 *
 * @author Lucian Sirbu
 */
class XMLExplorer extends \App\Utils\Views\AbstractHtmlComponent
{
	/**
	 * The xml object
	 *
	 * @var \XmlReader
	 */
	protected $xml = null;

	/**
	 * Internal object id, used in js
	 *
	 * @var string
	 */
	protected $ID = null;

	/**
	 * Create a new XML viewer
	 */
	public function __construct(\XMLReader $xml)
	{
		$this->xml = $xml;

		$this->mergeOptions([
			'interactive' => true,
			'showEmptyLeafs' => true,
		]);

	}

	/**
	 * Get the current ID
	 */
	private function getId()
	{
		if ($this->ID) {
			return $this->ID;
		}
		$this->ID = 'XmlExplorer_' . md5(mt_rand());
		return $this->ID;
	}

	/**
	 * Render the XML only.
	 * Usually called from the template
	 * @return string
	 */
	public function renderHtml()
	{
		$output = "";
		$output.= sprintf("<div class='XMLExplorer %s %s' id=%s>", ($this->getOption('easySelect') ? 'select-easy' : ''), ($this->getOption('noSelect') ? 'select-none' : ''), $this->getId());

		list($output2, ) = $this->renderXMLNode($this->xml,
		[
			'depth' => 		0,
			'parents' => 	[],
		],
		[
			'nodes' => 0,
			'nodes_cdata' => 0,
		]);
		$output.= $output2;

		$output.= sprintf("</div>");

		return $output;
	}

	/**
	 * Recursivelly renders XML tags
	 */
	public function renderXMLNode($xml, $options, $stats)
	{
		$output = "";
		$istats = $stats;

		while ($xml->read()) {
			$nodeOutput = "";
			$replacements = [
				'__li_collapsible__' => '',
			];

			if ($xml->nodeType == \XMLReader::ELEMENT) {
				if (!$this->getOption('showEmptyLeafs') && $xml->isEmptyElement) {
					continue;
				}

				$name = $xml->name;
				$stats['nodes']++;

				$nodeOutput.= sprintf('<li class="node __li_collapsible__">');
				$nodeOutput.= sprintf('&lt;<span class="label start" title="%s">%s</span>',
					htmlentities(implode("/", $options['parents'])."/".$name),
					$this->toHtml($name)
				);

				if ($xml->hasAttributes) {
					$nodeOutput.= sprintf('<span class="attributes">');
					while ($xml->moveToNextAttribute()) {
						if (!$this->getOption('showEmptyLeafs') && $xml->value==='') {
							continue;
						}
						$nodeOutput.= sprintf(' <span class="attribute"><span class="label" title="%s">%s</span>="<span class="value">%s</span>"</span>',
							htmlentities(implode("/", $options['parents'])."/".$name."@".$xml->name),
							$this->toHtml($xml->name),
							$this->toHtml($xml->value)
						);
					}
					$nodeOutput.= sprintf('</span>');
				}

				$xml->moveToElement();
				if ($xml->isEmptyElement) {	// apparently in PHP 5.3 isEmptyElement is set when no attribtues & values are defined
					$nodeOutput.= sprintf(' /&gt;');
				}
				else {
					$nodeOutput.= sprintf('&gt;');

					$o = $options;
					$o['depth']++;
					$o['parents'][]= $xml->name;
					list($output2, $stats2) = $this->renderXMLNode($xml, $o, $stats);

					if (($stats2['nodes'] - $stats['nodes'])>1) {
						$replacements['__li_collapsible__'] = 'collapsible';
					}
					elseif (($stats2['nodes_cdata'] - $stats['nodes_cdata'])>0) {
						$replacements['__li_collapsible__'] = 'collapsible';
					}

					$stats = $stats2;

					$nodeOutput.= $output2;
					$nodeOutput.= sprintf('&lt;/<span class="label end">%s</span>&gt;', $this->toHtml($name));
				}

				$nodeOutput.= sprintf('</li>');
			}
			elseif ($xml->nodeType == \XMLReader::END_ELEMENT) {
				// retun to parent
				break;
			}
			elseif ($xml->nodeType == \XMLReader::TEXT) {
				$nodeOutput.= sprintf('<span class="value text">%s</span>', $this->toHtml($xml->value));
			}
			elseif ($xml->nodeType == \XMLReader::CDATA) {
				$nodeOutput.= sprintf('<span class="CDATA">%s</span>', sprintf('<span class="CDATA">%s</span><span class="value">%s</span><span class="CDATA">%s</span>', $this->toHtml('<![CDATA['), $this->toHtml($xml->value), $this->toHtml(']]>')));
				$stats['nodes_cdata']++;
			}
			elseif ($xml->nodeType == \XMLReader::COMMENT) {
				$nodeOutput.= sprintf('<li class="node comment">%s</li>', sprintf('<span class="comment">%s</span><span class="value">%s</span><span class="comment">%s</span>', $this->toHtml('<!--'), $this->toHtml($xml->value), $this->toHtml('-->')));
			}
			elseif ($xml->nodeType == \XMLReader::DOC_TYPE) {
				// ignore doctype declarations
			}
			elseif ($xml->nodeType == \XMLReader::SIGNIFICANT_WHITESPACE) {
				// ignore spaces
			}
			else {
				throw new \Exception(sprintf("Don't know how to handle nodeType: %d", $xml->nodeType));
			}

			if (!$this->getOption('interactive')) {
				$replacements['__li_collapsible__'] = '';
			}

			$output.= str_replace(array_keys($replacements), array_values($replacements), $nodeOutput);
		}

		if ($stats['nodes'] - $istats['nodes']) {
			$output = sprintf("<ul class='node'>%s</ul>", $output);
		}

		return [$output, $stats];
	}

	protected function toHtml($str)
	{
		return htmlentities($str, ENT_COMPAT, "UTF-8");
	}

	public function renderJs(\Engine\Theme\JSLoader\LoaderAbstract $JSLoader)
	{
		if (!$this->getOption('interactive')) {
			return '';
		}

		return <<<JS
			$('{$this->getId()}').on('click', 'li.node.collapsible >span.label', function(evt, elm) {
				var li = elm.up('li.node.collapsible');
				if (li.hasClassName('collapsed')) {
					li.removeClassName('collapsed');
				}
				else {
					li.addClassName('collapsed');
					if (!li.down('>span.collapsed')) {
						li.down('>ul.node, >span.CDATA').insert({
							before: "<span class='collapsed'></span>"
						});
					}
				}
			});
JS;
	}

	public function preloadCss(\Engine\Theme\CSSLoader\Complex $CSSLoader)
	{
		$CSSLoader->loadGroup("utils.views.xmlexplorer");
		return '';
	}

	public function preloadJs(\Engine\Theme\JSLoader\LoaderAbstract $JSLoader)
	{
		$JSLoader->loadGroup("utils.views.xmlexplorer");
		return '';
	}
}
