<?php

namespace XF\BbCode;

use function call_user_func, in_array, is_array, is_scalar, strlen;

class RuleSet
{
	public const OPTION_KEYS_NO = 0;
	public const OPTION_KEYS_ONLY = 1;
	public const OPTION_KEYS_BOTH = 2;

	protected $tags = [];

	protected $context = null;
	protected $subContext = null;

	public function __construct($context, $subContext = null, $addDefault = true)
	{
		$this->context = $context;
		$this->subContext = $subContext;

		if ($addDefault)
		{
			$this->addDefaultTags();
		}
	}

	public function getContext()
	{
		return $this->context;
	}

	public function getSubContext()
	{
		return $this->subContext;
	}

	public function addDefaultTags()
	{
		$this->addTag('b', ['hasOption' => false]);
		$this->addTag('i', ['hasOption' => false]);
		$this->addTag('u', ['hasOption' => false]);
		$this->addTag('s', ['hasOption' => false]);

		$this->addTag('color', [
			'hasOption' => true,
			'optionMatch' => '/^(rgb\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*\)|#[a-f0-9]{6}|#[a-f0-9]{3}|[a-z]+)$/i',
		]);

		$this->addTag('font', [
			'hasOption' => true,
			'optionMatch' => '/^[a-z0-9 \-]+$/i', // regex matched to HTML->BB code regex
		]);

		$this->addTag('size', [
			'hasOption' => true,
			'optionMatch' => '/^[0-9]+(px)?$/i',
		]);

		$this->addTag('url', [
			'supportOptionKeys' => RuleSet::OPTION_KEYS_BOTH,
			'parseValidate' => [$this, 'parseValidateLink'],
			'stopAutoLink' => true,
		]);
		$this->addTag('email', [
			'parseValidate' => [$this, 'parseValidateLink'],
			'stopAutoLink' => true,
		]);

		$this->addTag('embed', [
			'supportOptionKeys' => RuleSet::OPTION_KEYS_ONLY,
			'stopAutoLink' => true,
		]);

		$this->addTag('left', [
			'hasOption' => false,
		]);
		$this->addTag('center', [
			'hasOption' => false,
		]);
		$this->addTag('right', [
			'hasOption' => false,
		]);
		$this->addTag('justify', [
			'hasOption' => false,
		]);

		$this->addTag('indent', [
			'optionMatch' => '/^[0-9]+$/',
		]);

		$this->addTag('heading', [
			'optionMatch' => '/^[0-9]+$/',
		]);

		$this->addTag('hr', [
			'hasOption' => false,
		]);

		$this->addTag('img', [
			'supportOptionKeys' => RuleSet::OPTION_KEYS_ONLY,
			'plain' => true,
			'stopSmilies' => true,
			'stopAutoLink' => true,
		]);

		$this->addTag('quote', []);

		$this->addTag('code', [
			'parseValidate' => [$this, 'parseValidateCode'],
			'stopSmilies' => true,
			'stopAutoLink' => true,
		]);

		$this->addTag('icode', [
			'parseValidate' => [$this, 'parseValidateCode'],
			'stopSmilies' => true,
			'stopAutoLink' => true,
		]);

		$this->addTag('php', [
			'hasOption' => false,
			'plain' => true,
			'stopSmilies' => true,
			'stopAutoLink' => true,
		]);

		$this->addTag('html', [
			'hasOption' => false,
			'plain' => true,
			'stopSmilies' => true,
			'stopAutoLink' => true,
		]);

		$this->addTag('list', []);

		$this->addTag('plain', [
			'hasOption' => false,
			'plain' => true,
			'stopSmilies' => true,
			'stopAutoLink' => true,
		]);

		$this->addTag('media', [
			'hasOption' => true,
			'plain' => true,
			'stopSmilies' => true,
			'stopAutoLink' => true,
		]);

		$this->addTag('spoiler', []);

		$this->addTag('ispoiler', []);

		$this->addTag('attach', [
			'supportOptionKeys' => RuleSet::OPTION_KEYS_BOTH,
			'plain' => true,
			'stopSmilies' => true,
			'stopAutoLink' => true,
		]);

		$this->addTag('user', [
			'hasOption' => true,
			'plain' => true,
			'stopSmilies' => true,
			'stopAutoLink' => true,
		]);

		$this->addTag('table', [
			'supportOptionKeys' => RuleSet::OPTION_KEYS_ONLY,
		]);
		$this->addTag('tr', [
			'validParents' => ['table'],
		]);
		$this->addTag('th', [
			'supportOptionKeys' => RuleSet::OPTION_KEYS_ONLY,
			'validParents' => ['tr'],
		]);
		$this->addTag('td', [
			'supportOptionKeys' => RuleSet::OPTION_KEYS_ONLY,
			'validParents' => ['tr'],
		]);
	}

	public function addTag($tag, array $config)
	{
		$this->tags[$tag] = $config;
	}

	public function modifyTag($tag, array $modification)
	{
		if (isset($this->tags[$tag]))
		{
			$this->tags[$tag] = array_merge($this->tags[$tag], $modification);
		}
	}

	public function removeTag($tag)
	{
		unset($this->tags[$tag]);
	}

	public function getTag($tag)
	{
		return $this->tags[$tag] ?? null;
	}

	public function getTags()
	{
		return $this->tags;
	}

	public function getCustomTagConfig(array $tag)
	{
		$output = [];

		if ($tag['has_option'] == 'yes')
		{
			$output['hasOption'] = true;
		}
		else if ($tag['has_option'] == 'no')
		{
			$output['hasOption'] = false;
		}

		if (strlen($tag['option_regex']))
		{
			$output['optionMatch'] = $tag['option_regex'];
		}

		if ($tag['disable_smilies'])
		{
			$output['stopSmilies'] = true;
		}

		if ($tag['disable_autolink'] || $tag['plain_children'])
		{
			$output['stopAutoLink'] = true;
		}

		if ($tag['plain_children'])
		{
			$output['plain'] = true;
		}

		return $output;
	}

	public function validateTag($tag, $option = null, &$parsingModifiers = [], array $tagStack = [])
	{
		$parsingModifiers = [];

		$definition = $this->getTag($tag);
		if (!is_array($definition))
		{
			return false;
		}

		if (!empty($definition['validParents']))
		{
			if (!$tagStack)
			{
				return false;
			}

			$lastTag = $tagStack[0];
			if (!in_array($lastTag['tag'], $definition['validParents']))
			{
				return false;
			}
		}

		if (!$this->validateOption($option, $definition))
		{
			return false;
		}

		if ($option !== null && !empty($definition['optionMatch']) && is_scalar($option))
		{
			if (!preg_match($definition['optionMatch'], $option))
			{
				return false;
			}
		}

		$parsingModifiers = $definition;

		if (isset($definition['parseValidate']))
		{
			$validated = call_user_func($definition['parseValidate'], $tag, $option, $tagStack);
			if ($validated === false)
			{
				return false;
			}

			if (is_array($validated))
			{
				$parsingModifiers = array_merge($parsingModifiers, $validated);
			}
		}

		return true;
	}

	public function validateOption($option, array $definition)
	{
		if (isset($definition['hasOption']))
		{
			if (!$definition['hasOption'])
			{
				// no option permitted
				return $option === null ? true : false;
			}
			else if ($option === null)
			{
				// option required but not set
				return false;
			}
			else
			{
				return $this->validateOptionKeys($option, $definition);
			}
		}
		else if ($option !== null)
		{
			return $this->validateOptionKeys($option, $definition);
		}
		else
		{
			return true;
		}
	}

	public function validateOptionKeys($option, array $definition)
	{
		if (isset($definition['supportOptionKeys']))
		{
			switch ($definition['supportOptionKeys'])
			{
				case RuleSet::OPTION_KEYS_BOTH:
					return is_array($option) || is_scalar($option);

				case RuleSet::OPTION_KEYS_ONLY:
					return is_array($option);

				case RuleSet::OPTION_KEYS_NO:
				default:
					return is_scalar($option);
			}
		}
		else
		{
			return is_scalar($option);
		}
	}

	public function parsePlainNoOption($tag, $option)
	{
		if ($option === null)
		{
			return ['plain' => true];
		}

		return true;
	}

	public function parseValidateLink($tag, $option, array $tagStack)
	{
		foreach ($tagStack AS $tagAbove)
		{
			switch ($tagAbove['tag'])
			{
				case 'url':
				case 'email':
					// can't nest these
					return false;
			}
		}

		return self::parsePlainNoOption($tag, $option);
	}

	public function parseValidateCode($tag, $option)
	{
		if ($option && strtolower($option) == 'rich')
		{
			return true;
		}

		return ['plain' => true];
	}
}
