<?php

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

use DOMElement;

class OptimizeChoose extends AbstractChooseOptimization
{
	/**
	* Adopt the children of given element's only child
	*
	* @param  DOMElement $branch
	* @return void
	*/
	protected function adoptChildren(DOMElement $branch)
	{
		while ($branch->firstChild->firstChild)
		{
			$branch->appendChild($branch->firstChild->removeChild($branch->firstChild->firstChild));
		}
		$branch->removeChild($branch->firstChild);
	}

	/**
	* Test whether all branches of current xsl:choose element share a common firstChild/lastChild
	*
	* @param  string $childType Either firstChild or lastChild
	* @return bool
	*/
	protected function matchBranches($childType)
	{
		$branches = $this->getBranches();
		if (!isset($branches[0]->$childType))
		{
			return false;
		}

		$childNode = $branches[0]->$childType;
		foreach ($branches as $branch)
		{
			if (!isset($branch->$childType) || !$this->isEqualNode($childNode, $branch->$childType))
			{
				return false;
			}
		}

		return true;
	}

	/**
	* Test whether all branches of current xsl:choose element have a single child with the same start tag
	*
	* @return bool
	*/
	protected function matchOnlyChild()
	{
		$branches = $this->getBranches();
		if (!isset($branches[0]->firstChild))
		{
			return false;
		}

		$firstChild = $branches[0]->firstChild;
		if ($this->isXsl($firstChild, 'choose'))
		{
			// Abort on xsl:choose because we can't move it without moving its children
			return false;
		}

		foreach ($branches as $branch)
		{
			if ($branch->childNodes->length !== 1 || !($branch->firstChild instanceof DOMElement))
			{
				return false;
			}
			if (!$this->isEqualTag($firstChild, $branch->firstChild))
			{
				return false;
			}
		}

		return true;
	}

	/**
	* Move the firstChild of each branch before current xsl:choose
	*
	* @return void
	*/
	protected function moveFirstChildBefore()
	{
		$branches = $this->getBranches();
		$this->choose->parentNode->insertBefore(array_pop($branches)->firstChild, $this->choose);
		foreach ($branches as $branch)
		{
			$branch->removeChild($branch->firstChild);
		}
	}

	/**
	* Move the lastChild of each branch after current xsl:choose
	*
	* @return void
	*/
	protected function moveLastChildAfter()
	{
		$branches = $this->getBranches();
		$node     = array_pop($branches)->lastChild;
		if (isset($this->choose->nextSibling))
		{
			$this->choose->parentNode->insertBefore($node, $this->choose->nextSibling);
		}
		else
		{
			$this->choose->parentNode->appendChild($node);
		}
		foreach ($branches as $branch)
		{
			$branch->removeChild($branch->lastChild);
		}
	}

	/**
	* {@inheritdoc}
	*/
	protected function optimizeChoose()
	{
		if ($this->hasOtherwise())
		{
			$this->optimizeCommonFirstChild();
			$this->optimizeCommonLastChild();
			$this->optimizeCommonOnlyChild();
			$this->optimizeEmptyBranch();
			$this->optimizeEmptyOtherwise();
		}
		if ($this->isEmpty())
		{
			$this->choose->parentNode->removeChild($this->choose);
		}
		else
		{
			$this->optimizeSingleBranch();
		}
	}

	/**
	* Optimize current xsl:choose by moving out the first child of each branch if they match
	*
	* @return void
	*/
	protected function optimizeCommonFirstChild()
	{
		while ($this->matchBranches('firstChild'))
		{
			$this->moveFirstChildBefore();
		}
	}

	/**
	* Optimize current xsl:choose by moving out the last child of each branch if they match
	*
	* @return void
	*/
	protected function optimizeCommonLastChild()
	{
		while ($this->matchBranches('lastChild'))
		{
			$this->moveLastChildAfter();
		}
	}

	/**
	* Optimize current xsl:choose by moving out only child of each branch if they match
	*
	* This will reorder xsl:choose/xsl:when/div into div/xsl:choose/xsl:when if every branch has
	* the same only child (excluding the child's own descendants)
	*
	* @return void
	*/
	protected function optimizeCommonOnlyChild()
	{
		while ($this->matchOnlyChild())
		{
			$this->reparentChild();
		}
	}

	/**
	* Switch the logic of an xsl:otherwise if the only other branch is empty
	*
	* @return void
	*/
	protected function optimizeEmptyBranch()
	{
		$query = 'count(xsl:when) = 1 and count(xsl:when/node()) = 0 and xsl:otherwise';
		if (!$this->xpath->evaluate($query, $this->choose))
		{
			return;
		}

		// test="@foo" becomes test="not(@foo)"
		$when = $this->xpath('xsl:when', $this->choose)[0];
		$when->setAttribute('test', 'not(' . $when->getAttribute('test') . ')');

		$otherwise = $this->xpath('xsl:otherwise', $this->choose)[0];
		while ($otherwise->firstChild)
		{
			$when->appendChild($otherwise->removeChild($otherwise->firstChild));
		}
	}

	/**
	* Optimize away the xsl:otherwise child of current xsl:choose if it's empty
	*
	* @return void
	*/
	protected function optimizeEmptyOtherwise()
	{
		$query = 'xsl:otherwise[count(node()) = 0]';
		foreach ($this->xpath($query, $this->choose) as $otherwise)
		{
			$this->choose->removeChild($otherwise);
		}
	}

	/**
	* Replace current xsl:choose with xsl:if if it has only one branch
	*
	* @return void
	*/
	protected function optimizeSingleBranch()
	{
		$query = 'count(xsl:when) = 1 and not(xsl:otherwise)';
		if (!$this->xpath->evaluate($query, $this->choose))
		{
			return;
		}
		$when = $this->xpath('xsl:when', $this->choose)[0];
		$if   = $this->createElement('xsl:if');
		$if->setAttribute('test', $when->getAttribute('test'));
		while ($when->firstChild)
		{
			$if->appendChild($when->removeChild($when->firstChild));
		}

		$this->choose->parentNode->replaceChild($if, $this->choose);
	}

	/**
	* Reorder the current xsl:choose tree to make it a child of the first child of its first branch
	*
	* This will reorder xsl:choose/xsl:when/div into div/xsl:choose/xsl:when
	*
	* @return void
	*/
	protected function reparentChild()
	{
		$branches  = $this->getBranches();
		$childNode = $branches[0]->firstChild->cloneNode();
		$childNode->appendChild($this->choose->parentNode->replaceChild($childNode, $this->choose));

		foreach ($branches as $branch)
		{
			$this->adoptChildren($branch);
		}
	}
}