<?php

namespace XF\Template\Compiler\Tag;

use XF\Template\Compiler;
use XF\Template\Compiler\ChoiceBuilder;
use XF\Template\Compiler\Syntax\AbstractSyntax;
use XF\Template\Compiler\Syntax\Tag;

use function in_array;

abstract class AbstractFormElement extends AbstractTag
{
	/**
	 * Specific attributes that will be treated as row-level.
	 * Key is name of the attribute. The value should be true if variables within
	 * should be escaped (because the value will be displayed raw) or false if
	 * the value will be escaped in all cases before displaying.
	 *
	 * @var array
	 */
	protected $defaultRowOptions = [
		'label' => true,
		'labelid' => false,
		'hint' => true,
		'explain' => true,
		'rowclass' => false,
		'rowid' => false,
		'rowtype' => false,
		'initialhtml' => true,
		'html' => true,
		'finalhtml' => true,
		'error' => true,
	];

	public function compileTextInput($functionBaseName, $isRowLevel, Tag $tag, Compiler $compiler, array $context)
	{
		$context['escape'] = true;

		[$controlOptions, $rowOptions] = $this->getOptionsFromAttributes($tag, $compiler, $context);

		foreach ($tag->children AS $child)
		{
			$foundExpected = (
				$this->compileRowOptionChild($child, $compiler, $context, $rowOptions)
			);
			if (!$foundExpected)
			{
				throw $child->exception(\XF::phrase('tag_x_contains_unexpected_child_element', ['name' => $tag->name]));
			}
		}

		return $this->getFinalInputElementCode(
			$functionBaseName,
			$isRowLevel,
			$compiler,
			$controlOptions,
			$rowOptions
		);
	}

	public function compileChoiceInput($functionBaseName, $isRowLevel, Tag $tag, Compiler $compiler, array $context, $allowDependent = false)
	{
		$context['escape'] = true;

		[$controlOptions, $rowOptions] = $this->getOptionsFromAttributes($tag, $compiler, $context);

		$choices = new ChoiceBuilder($compiler);

		foreach ($tag->children AS $child)
		{
			$foundExpected = (
				$this->compileRowOptionChild($child, $compiler, $context, $rowOptions)
				|| $this->compileExpectedChoice($choices, $tag, $child, $compiler, $context, $allowDependent)
			);
			if (!$foundExpected)
			{
				throw $child->exception(\XF::phrase('tag_x_contains_unexpected_child_element', ['name' => $tag->name]));
			}
		}

		return $this->getFinalChoiceElementCode(
			$functionBaseName,
			$isRowLevel,
			$compiler,
			$controlOptions,
			$choices,
			$rowOptions
		);
	}

	public function getOptionsFromAttributes(Tag $tag, Compiler $compiler, array $htmlContext, array $htmlAttributes = [])
	{
		$htmlContext['escape'] = true;

		$textContext = $htmlContext;
		$textContext['escape'] = false;

		$controlOptions = [];
		$rowOptions = [];
		foreach ($tag->attributes AS $name => $value)
		{
			if (isset($this->defaultRowOptions[$name]))
			{
				if ($this->defaultRowOptions[$name])
				{
					// allow html - escape variables now
					$rowOptions[$name] = $compiler->compileToArraySyntax($value, $name, $htmlContext);
				}
				else
				{
					// text only - escape at runtime
					$rowOptions[$name] = $compiler->compileToArraySyntax($value, $name, $textContext);
				}
			}
			else
			{
				if ($htmlAttributes && in_array($name, $htmlAttributes))
				{
					$controlOptions[$name] = $compiler->compileToArraySyntax($value, $name, $htmlContext);
				}
				else
				{
					$controlOptions[$name] = $compiler->compileToArraySyntax($value, $name, $textContext);
				}
			}
		}

		return [$controlOptions, $rowOptions];
	}

	public function compileRowOptionChild(
		AbstractSyntax $child,
		Compiler $compiler,
		array $context,
		array &$rowOptions
	)
	{
		if ($this->isNamedTag($child, ['label', 'hint', 'explain', 'initialhtml', 'html', 'finalhtml']))
		{
			/** @var $child Tag */
			$rowOptions[$child->name] = $compiler->compileToArraySyntax($child->children, $child->name, $context);
			return true;
		}

		if ($this->isEmptyString($child))
		{
			// ok, ignore
			return true;
		}

		return false;
	}

	public function compileExpectedChoice(
		ChoiceBuilder $choices,
		Tag $tag,
		AbstractSyntax $child,
		Compiler $compiler,
		array $context,
		$allowDependent = false
	)
	{
		$rawContext = $context;
		$rawContext['escape'] = false;

		if ($this->isNamedTag($child, 'option'))
		{
			/** @var $child Tag */
			$optionTag = $this->compileAttributesAsArray(
				$child->attributes,
				$compiler,
				$context,
				['label', 'hint'],
				['selected']
			);
			$this->compileOptionChildren($child->children, $compiler, $context, $optionTag, $allowDependent);
			$choices->handleOptionTag($optionTag);

			return true;
		}

		if ($this->isNamedTag($child, 'options'))
		{
			/** @var $child Tag */
			$child->assertAttribute('source');

			$choices->handleOptionsTag(
				$compiler->compileForcedExpression($child->attributes['source'], $rawContext)
			);

			return true;
		}

		if ($this->isNamedTag($child, 'optgroup'))
		{
			if ($choices->inOptGroup())
			{
				throw $child->exception(\XF::phrase('optgroup_tags_cannot_be_nested'));
			}

			/** @var $child Tag */
			$optgroupAttributes = $this->compileAttributesAsArray($child->attributes, $compiler, $context);
			$choices->startOptGroup($optgroupAttributes);

			foreach ($child->children AS $grandchild)
			{
				$success = $this->compileExpectedChoice($choices, $tag, $grandchild, $compiler, $context, $allowDependent);
				if (!$success)
				{
					throw $child->exception(\XF::phrase('tag_x_contains_unexpected_child_element', ['name' => $child->name]));
				}
			}

			$choices->endOptGroup();

			return true;
		}

		if ($this->isNamedTag($child, 'if'))
		{
			/** @var $child Tag */

			$choices->forceTempVariable();

			$callback = function ($type, array $elements) use ($child, $choices, $tag, $compiler, $context, $allowDependent)
			{
				if ($type == 'contentcheck')
				{
					throw $child->exception(\XF::phrase('contentcheck_based_if_tags_not_supported_with_choice_based_tags'));
				}

				foreach ($elements AS $element)
				{
					$success = $this->compileExpectedChoice($choices, $tag, $element, $compiler, $context, $allowDependent);
					if (!$success)
					{
						throw $child->exception(\XF::phrase('tag_x_contains_unexpected_child_element', ['name' => $child->name]));
					}
				}
			};

			/** @var IfTag $handler */
			$handler = $child->getTag($compiler);
			$handler->compileToCallback($child, $compiler, $context, $callback);

			return true;
		}

		if ($this->isNamedTag($child, 'foreach'))
		{
			/** @var $child Tag */

			$choices->forceTempVariable();

			$callback = function ($type, array $elements) use ($child, $choices, $tag, $compiler, $context, $allowDependent)
			{
				foreach ($elements AS $element)
				{
					$success = $this->compileExpectedChoice($choices, $tag, $element, $compiler, $context, $allowDependent);
					if (!$success)
					{
						throw $child->exception(\XF::phrase('tag_x_contains_unexpected_child_element', ['name' => $child->name]));
					}
				}
			};

			/** @var ForeachTag $handler */
			$handler = $child->getTag($compiler);
			$handler->compileToCallback($child, $compiler, $context, $callback);

			return true;
		}

		if ($this->isNamedTag($child, 'set'))
		{
			/** @var $child Tag */

			$choices->forceTempVariable();

			/** @var Set $handler */
			$handler = $child->getTag($compiler);
			$handler->compile($child, $compiler, $context, false);

			return true;
		}

		if ($this->isEmptyString($child))
		{
			// ok, ignore
			return true;
		}

		return false;
	}

	public function compileOptionChildren(array $children, Compiler $compiler, array $context, array &$optionTag, $allowDependent = false)
	{
		if (empty($optionTag['label']) && $children)
		{
			$hasTag = false;
			foreach ($children AS $child)
			{
				if ($child instanceof Tag)
				{
					$hasTag = true;
					break;
				}
			}

			if (!$hasTag)
			{
				// children are automatically the label if there are no other tags within
				$optionTag['label'] = $compiler->compileToArraySyntax($children, 'label', $context);
				return;
			}
		}

		/** @var $children Compiler\Syntax\AbstractSyntax[] */
		foreach ($children AS $child)
		{
			if ($this->isNamedTag($child, ['label', 'hint', 'html', 'afterhint', 'afterhtml']))
			{
				/** @var $child Tag */
				$optionTag[$child->name] = $compiler->compileToArraySyntax($child->children, $child->name, $context);
				continue;
			}

			if ($allowDependent)
			{
				if ($this->isNamedTag($child, [
					'assetupload', 'checkbox', 'dateinput', 'macro', 'radio', 'select',
					'textarea', 'textbox', 'tokeninput', 'numberbox', 'upload',
				]))
				{
					/** @var $child Tag */
					$optionTag['_dependent'][] = $child->compile($compiler, $context, true);
					continue;
				}

				if ($this->isNamedTag($child, 'dependent'))
				{
					/** @var $child Tag */
					$optionTag['_dependent'][] = $compiler->compileInlineList($child->children, $context);
					continue;
				}
			}

			if ($this->isEmptyString($child))
			{
				// ok, ignore
				continue;
			}

			throw $child->exception(\XF::phrase('option_tag_contains_unexpected_child_element'));
		}
	}

	public function getFinalChoiceElementCode(
		$functionBaseName,
		$isRowLevel,
		Compiler $compiler,
		array $controlOptions,
		ChoiceBuilder $choices,
		array $rowOptions
	)
	{
		$indent = $compiler->indent();
		$rowOptionCode = "array(" . implode('', $rowOptions) . "\n$indent)";
		$controlOptionCode = "array(" . implode('', $controlOptions) . "\n$indent)";
		$optionsCode = $choices->toInline();

		if ($isRowLevel)
		{
			return "{$compiler->templaterVariable}->form{$functionBaseName}Row($controlOptionCode, $optionsCode, $rowOptionCode)";
		}
		else
		{
			return "{$compiler->templaterVariable}->form{$functionBaseName}($controlOptionCode, $optionsCode)";
		}
	}

	public function getFinalInputElementCode(
		$functionBaseName,
		$isRowLevel,
		Compiler $compiler,
		array $controlOptions,
		array $rowOptions
	)
	{
		$indent = $compiler->indent();
		$rowOptionCode = "array(" . implode('', $rowOptions) . "\n$indent)";
		$controlOptionCode = "array(" . implode('', $controlOptions) . "\n$indent)";

		if ($isRowLevel)
		{
			return "{$compiler->templaterVariable}->form{$functionBaseName}Row($controlOptionCode, $rowOptionCode)";
		}
		else
		{
			return "{$compiler->templaterVariable}->form{$functionBaseName}($controlOptionCode)";
		}
	}
}
