<?php

/**
* @package   s9e\TextFormatter
* @copyright Copyright (c) 2010-2022 The s9e authors
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Renderers;

use DOMNode;
use DOMXPath;
use RuntimeException;
use s9e\TextFormatter\Renderer;
use s9e\TextFormatter\Utils\XPath;

abstract class PHP extends Renderer
{
	/**
	* @var array[] Stack of dictionaries used by the Quick renderer [[attrName => attrValue]]
	*/
	protected $attributes;

	/**
	* @var array Dictionary of replacements used by the Quick renderer [id => [match, replace]]
	*/
	protected $dynamic;

	/**
	* @var bool Whether to enable the Quick renderer
	*/
	public $enableQuickRenderer = false;

	/**
	* @var string Renderer's output
	*/
	protected $out;

	/**
	* @var string Regexp that matches XML elements to be rendered by the quick renderer
	*/
	protected $quickRegexp = '((?!))';

	/**
	* @var string Regexp that matches nodes that SHOULD NOT be rendered by the quick renderer
	*/
	protected $quickRenderingTest = '((?<=<)[!?])';

	/**
	* @var array Dictionary of static replacements used by the Quick renderer [id => replacement]
	*/
	protected $static;

	/**
	* @var DOMXPath XPath object used to query the document being rendered
	*/
	protected $xpath;

	/**
	* Render given DOMNode
	*
	* @param  DOMNode $node
	* @return void
	*/
	abstract protected function renderNode(DOMNode $node);

	public function __sleep()
	{
		return ['enableQuickRenderer', 'params'];
	}

	/**
	* Render the content of given node
	*
	* Matches the behaviour of an xsl:apply-templates element
	*
	* @param  DOMNode $root  Context node
	* @param  string  $query XPath query used to filter which child nodes to render
	* @return void
	*/
	protected function at(DOMNode $root, $query = null)
	{
		if ($root->nodeType === XML_TEXT_NODE)
		{
			// Text nodes are outputted directly
			$this->out .= htmlspecialchars($root->textContent, ENT_NOQUOTES);
		}
		else
		{
			$nodes = (isset($query)) ? $this->xpath->query($query, $root) : $root->childNodes;
			foreach ($nodes as $node)
			{
				$this->renderNode($node);
			}
		}
	}

	/**
	* Test whether given XML can be rendered with the Quick renderer
	*
	* @param  string $xml
	* @return bool
	*/
	protected function canQuickRender($xml)
	{
		return ($this->enableQuickRenderer && !preg_match($this->quickRenderingTest, $xml) && substr($xml, -4) === '</r>');
	}

	/**
	* Ensure that a tag pair does not contain a start tag of itself
	*
	* Detects malformed matches such as <X><X></X>
	*
	* @param  string $id
	* @param  string $xml
	* @return void
	*/
	protected function checkTagPairContent($id, $xml)
	{
		if (strpos($xml, '<' . $id, 1) !== false)
		{
			throw new RuntimeException;
		}
	}

	/**
	* Return a parameter's value as an XPath expression
	*
	* @param  string $paramName
	* @return string
	*/
	protected function getParamAsXPath($paramName)
	{
		return (isset($this->params[$paramName])) ? XPath::export($this->params[$paramName]) : "''";
	}

	/**
	* Extract the text content from given XML
	*
	* NOTE: numeric character entities are decoded beforehand, we don't need to decode them here
	*
	* @param  string $xml Original XML
	* @return string      Text content, with special characters decoded
	*/
	protected function getQuickTextContent($xml)
	{
		return htmlspecialchars_decode(strip_tags($xml));
	}

	/**
	* Test whether given array has any non-null values
	*
	* @param  array $array
	* @return bool
	*/
	protected function hasNonNullValues(array $array)
	{
		foreach ($array as $v)
		{
			if (isset($v))
			{
				return true;
			}
		}

		return false;
	}

	/**
	* Capture and return the attributes of an XML element
	*
	* NOTE: XML character entities are left as-is
	*
	* @param  string $xml Element in XML form
	* @return array       Dictionary of [attrName => attrValue]
	*/
	protected function matchAttributes($xml)
	{
		if (strpos($xml, '="') === false)
		{
			return [];
		}

		// Match all name-value pairs until the first right bracket
		preg_match_all('(([^ =]++)="([^"]*))S', substr($xml, 0, strpos($xml, '>')), $m);

		return array_combine($m[1], $m[2]);
	}

	/**
	* Render an intermediate representation using the Quick renderer
	*
	* @param  string $xml Intermediate representation
	* @return string
	*/
	protected function renderQuick($xml)
	{
		$this->attributes = [];
		$xml = $this->decodeSMP($xml);
		$html = preg_replace_callback(
			$this->quickRegexp,
			[$this, 'renderQuickCallback'],
			substr($xml, 1 + strpos($xml, '>'), -4)
		);

		return str_replace('<br/>', '<br>', $html);
	}

	/**
	* Render a string matched by the Quick renderer
	*
	* This stub should be overwritten by generated renderers
	*
	* @param  string[] $m
	* @return string
	*/
	protected function renderQuickCallback(array $m)
	{
		if (isset($m[3]))
		{
			return $this->renderQuickSelfClosingTag($m);
		}

		if (isset($m[2]))
		{
			// Single tag
			$id = $m[2];
		}
		else
		{
			// Tag pair
			$id = $m[1];
			$this->checkTagPairContent($id, $m[0]);
		}

		if (isset($this->static[$id]))
		{
			return $this->static[$id];
		}
		if (isset($this->dynamic[$id]))
		{
			return preg_replace($this->dynamic[$id][0], $this->dynamic[$id][1], $m[0], 1);
		}

		return $this->renderQuickTemplate($id, $m[0]);
	}

	/**
	* Render a self-closing tag using the Quick renderer
	*
	* @param  string[] $m
	* @return string
	*/
	protected function renderQuickSelfClosingTag(array $m)
	{
		unset($m[3]);

		$m[0] = substr($m[0], 0, -2) . '>';
		$html = $this->renderQuickCallback($m);

		$m[0] = '</' . $m[2] . '>';
		$m[2] = '/' . $m[2];
		$html .= $this->renderQuickCallback($m);

		return $html;
	}

	/**
	* Render a string matched by the Quick renderer using a generated PHP template
	*
	* This stub should be overwritten by generated renderers
	*
	* @param  integer $id  Tag's ID (tag name optionally preceded by a slash)
	* @param  string  $xml Tag's XML or tag pair's XML including their content
	* @return string       Rendered template
	*/
	protected function renderQuickTemplate($id, $xml)
	{
		throw new RuntimeException('Not implemented');
	}

	/**
	* {@inheritdoc}
	*/
	protected function renderRichText($xml)
	{
		$this->setLocale();

		try
		{
			if ($this->canQuickRender($xml))
			{
				$html = $this->renderQuick($xml);
				$this->restoreLocale();

				return $html;
			}
		}
		catch (RuntimeException $e)
		{
			// Do nothing
		}

		$dom         = $this->loadXML($xml);
		$this->out   = '';
		$this->xpath = new DOMXPath($dom);
		$this->at($dom->documentElement);
		$html        = $this->out;
		$this->reset();
		$this->restoreLocale();

		return $html;
	}

	/**
	* Reset object properties that are populated during rendering
	*
	* @return void
	*/
	protected function reset()
	{
		unset($this->attributes);
		unset($this->out);
		unset($this->xpath);
	}
}