<?php

/**
 * This file is part of the Latte (https://latte.nette.org)
 * Copyright (c) 2008 David Grudl (https://davidgrudl.com)
 */

namespace Latte;


/**
 * PHP code generator helpers.
 */
class PhpWriter extends Object
{
	/** @var MacroTokens */
	private $tokens;

	/** @var string */
	private $modifiers;

	/** @var Compiler */
	private $compiler;


	public static function using(MacroNode $node, Compiler $compiler = NULL)
	{
		return new static($node->tokenizer, $node->modifiers, $compiler);
	}


	public function __construct(MacroTokens $tokens, $modifiers = NULL, Compiler $compiler = NULL)
	{
		$this->tokens = $tokens;
		$this->modifiers = $modifiers;
		$this->compiler = $compiler;
	}


	/**
	 * Expands %node.word, %node.array, %node.args, %escape(), %modify(), %var, %raw, %word in code.
	 * @param  string
	 * @return string
	 */
	public function write($mask)
	{
		$mask = preg_replace('#%(node|\d+)\.#', '%$1_', $mask);
		$me = $this;
		$mask = preg_replace_callback('#%escape(\(([^()]*+|(?1))+\))#', function ($m) use ($me) {
			return $me->escapeFilter(new MacroTokens(substr($m[1], 1, -1)))->joinAll();
		}, $mask);
		$mask = preg_replace_callback('#%modify(\(([^()]*+|(?1))+\))#', function ($m) use ($me) {
			return $me->formatModifiers(substr($m[1], 1, -1));
		}, $mask);

		$args = func_get_args();
		$pos = $this->tokens->position;
		$word = strpos($mask, '%node_word') === FALSE ? NULL : $this->tokens->fetchWord();

		$code = preg_replace_callback('#([,+]\s*)?%(node_|\d+_|)(word|var|raw|array|args)(\?)?(\s*\+\s*)?()#',
		function ($m) use ($me, $word, & $args) {
			list(, $l, $source, $format, $cond, $r) = $m;

			switch ($source) {
				case 'node_':
					$arg = $word; break;
				case '':
					$arg = next($args); break;
				default:
					$arg = $args[$source + 1]; break;
			}

			switch ($format) {
				case 'word':
					$code = $me->formatWord($arg); break;
				case 'args':
					$code = $me->formatArgs(); break;
				case 'array':
					$code = $me->formatArray();
					$code = $cond && $code === 'array()' ? '' : $code; break;
				case 'var':
					$code = var_export($arg, TRUE); break;
				case 'raw':
					$code = (string) $arg; break;
			}

			if ($cond && $code === '') {
				return $r ? $l : $r;
			} else {
				return $l . $code . $r;
			}
		}, $mask);

		$this->tokens->position = $pos;
		return $code;
	}


	/**
	 * Formats modifiers calling.
	 * @param  string
	 * @return string
	 */
	public function formatModifiers($var)
	{
		$tokens = new MacroTokens(ltrim($this->modifiers, '|'));
		$tokens = $this->preprocess($tokens);
		$tokens = $this->modifiersFilter($tokens, $var);
		$tokens = $this->quoteFilter($tokens);
		return $tokens->joinAll();
	}


	/**
	 * Formats macro arguments to PHP code. (It advances tokenizer to the end as a side effect.)
	 * @return string
	 */
	public function formatArgs(MacroTokens $tokens = NULL)
	{
		$tokens = $this->preprocess($tokens);
		$tokens = $this->quoteFilter($tokens);
		return $tokens->joinAll();
	}


	/**
	 * Formats macro arguments to PHP array. (It advances tokenizer to the end as a side effect.)
	 * @return string
	 */
	public function formatArray(MacroTokens $tokens = NULL)
	{
		$tokens = $this->preprocess($tokens);
		$tokens = $this->expandFilter($tokens);
		$tokens = $this->quoteFilter($tokens);
		return $tokens->joinAll();
	}


	/**
	 * Formats parameter to PHP string.
	 * @param  string
	 * @return string
	 */
	public function formatWord($s)
	{
		return (is_numeric($s) || preg_match('#^\$|[\'"]|^true\z|^false\z|^null\z#i', $s))
			? $this->formatArgs(new MacroTokens($s))
			: '"' . $s . '"';
	}


	/**
	 * Preprocessor for tokens. (It advances tokenizer to the end as a side effect.)
	 * @return MacroTokens
	 */
	public function preprocess(MacroTokens $tokens = NULL)
	{
		$tokens = $tokens === NULL ? $this->tokens : $tokens;
		$tokens = $this->removeCommentsFilter($tokens);
		$tokens = $this->shortTernaryFilter($tokens);
		$tokens = $this->shortArraysFilter($tokens);
		return $tokens;
	}


	/**
	 * Removes PHP comments.
	 * @return MacroTokens
	 */
	public function removeCommentsFilter(MacroTokens $tokens)
	{
		$res = new MacroTokens;
		while ($tokens->nextToken()) {
			if (!$tokens->isCurrent(MacroTokens::T_COMMENT)) {
				$res->append($tokens->currentToken());
			}
		}
		return $res;
	}


	/**
	 * Simplified ternary expressions without third part.
	 * @return MacroTokens
	 */
	public function shortTernaryFilter(MacroTokens $tokens)
	{
		$res = new MacroTokens;
		$inTernary = array();
		while ($tokens->nextToken()) {
			if ($tokens->isCurrent('?')) {
				$inTernary[] = $tokens->depth;

			} elseif ($tokens->isCurrent(':')) {
				array_pop($inTernary);

			} elseif ($tokens->isCurrent(',', ')', ']') && end($inTernary) === $tokens->depth + !$tokens->isCurrent(',')) {
				$res->append(' : NULL');
				array_pop($inTernary);
			}
			$res->append($tokens->currentToken());
		}

		if ($inTernary) {
			$res->append(' : NULL');
		}
		return $res;
	}


	/**
	 * Simplified array syntax [...]
	 * @return MacroTokens
	 */
	public function shortArraysFilter(MacroTokens $tokens)
	{
		$res = new MacroTokens;
		$arrays = array();
		while ($tokens->nextToken()) {
			if ($tokens->isCurrent('[')) {
				if ($arrays[] = !$tokens->isPrev(']', ')', MacroTokens::T_SYMBOL, MacroTokens::T_VARIABLE, MacroTokens::T_KEYWORD)) {
					$res->append('array(');
					continue;

				}
			} elseif ($tokens->isCurrent(']')) {
				if (array_pop($arrays) === TRUE) {
					$res->append(')');
					continue;
				}
			}
			$res->append($tokens->currentToken());
		}
		return $res;
	}


	/**
	 * Pseudocast (expand).
	 * @return MacroTokens
	 */
	public function expandFilter(MacroTokens $tokens)
	{
		$res = new MacroTokens('array(');
		$expand = NULL;
		while ($tokens->nextToken()) {
			if ($tokens->isCurrent('(expand)') && $tokens->depth === 0) {
				$expand = TRUE;
				$res->append('),');
			} elseif ($expand && $tokens->isCurrent(',') && !$tokens->depth) {
				$expand = FALSE;
				$res->append(', array(');
			} else {
				$res->append($tokens->currentToken());
			}
		}

		if ($expand !== NULL) {
			$res->prepend('array_merge(')->append($expand ? ', array()' : ')');
		}
		return $res->append(')');
	}


	/**
	 * Quotes symbols to strings.
	 * @return MacroTokens
	 */
	public function quoteFilter(MacroTokens $tokens)
	{
		$res = new MacroTokens;
		while ($tokens->nextToken()) {
			$res->append($tokens->isCurrent(MacroTokens::T_SYMBOL)
				&& (!$tokens->isPrev() || $tokens->isPrev(',', '(', '[', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', '=', 'and', 'or', 'xor', '??'))
				&& (!$tokens->isNext() || $tokens->isNext(',', ';', ')', ']', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', 'and', 'or', 'xor', '??'))
				? "'" . $tokens->currentValue() . "'"
				: $tokens->currentToken()
			);
		}
		return $res;
	}


	/**
	 * Formats modifiers calling.
	 * @param  MacroTokens
	 * @param  string
	 * @throws CompileException
	 * @return MacroTokens
	 */
	public function modifiersFilter(MacroTokens $tokens, $var)
	{
		$inside = FALSE;
		$res = new MacroTokens($var);
		while ($tokens->nextToken()) {
			if ($tokens->isCurrent(MacroTokens::T_WHITESPACE)) {
				$res->append(' ');

			} elseif ($inside) {
				if ($tokens->isCurrent(':', ',')) {
					$res->append(', ');
					$tokens->nextAll(MacroTokens::T_WHITESPACE);

				} elseif ($tokens->isCurrent('|')) {
					$res->append(')');
					$inside = FALSE;

				} else {
					$res->append($tokens->currentToken());
				}
			} else {
				if ($tokens->isCurrent(MacroTokens::T_SYMBOL)) {
					if ($this->compiler && $tokens->isCurrent('escape')) {
						$res = $this->escapeFilter($res);
						$tokens->nextToken('|');
					} elseif (!strcasecmp($tokens->currentValue(), 'safeurl')) {
						$res->prepend('Latte\Runtime\Filters::safeUrl(');
						$inside = TRUE;
					} else {
						$res->prepend('$template->' . $tokens->currentValue() . '(');
						$inside = TRUE;
					}
				} else {
					throw new CompileException("Modifier name must be alphanumeric string, '{$tokens->currentValue()}' given.");
				}
			}
		}
		if ($inside) {
			$res->append(')');
		}
		return $res;
	}


	/**
	 * Escapes expression in tokens.
	 * @return MacroTokens
	 */
	public function escapeFilter(MacroTokens $tokens)
	{
		$tokens = clone $tokens;
		switch ($this->compiler->getContentType()) {
			case Compiler::CONTENT_XHTML:
			case Compiler::CONTENT_HTML:
				$context = $this->compiler->getContext();
				switch ($context[0]) {
					case Compiler::CONTEXT_SINGLE_QUOTED_ATTR:
					case Compiler::CONTEXT_DOUBLE_QUOTED_ATTR:
					case Compiler::CONTEXT_UNQUOTED_ATTR:
						if ($context[1] === Compiler::CONTENT_JS) {
							$tokens->prepend('Latte\Runtime\Filters::escapeJs(')->append(')');
						} elseif ($context[1] === Compiler::CONTENT_CSS) {
							$tokens->prepend('Latte\Runtime\Filters::escapeCss(')->append(')');
						}
						$tokens->prepend('Latte\Runtime\Filters::escapeHtml(')->append($context[0] === Compiler::CONTEXT_SINGLE_QUOTED_ATTR ? ', ENT_QUOTES)' : ', ENT_COMPAT)');
						if ($context[0] === Compiler::CONTEXT_UNQUOTED_ATTR) {
							$tokens->prepend("'\"', ")->append(", '\"'");
						}
						return $tokens;
					case Compiler::CONTEXT_COMMENT:
						return $tokens->prepend('Latte\Runtime\Filters::escapeHtmlComment(')->append(')');
					case Compiler::CONTENT_JS:
					case Compiler::CONTENT_CSS:
						return $tokens->prepend('Latte\Runtime\Filters::escape' . ucfirst($context[0]) . '(')->append(')');
					default:
						return $tokens->prepend('Latte\Runtime\Filters::escapeHtml(')->append(', ENT_NOQUOTES)');
				}

			case Compiler::CONTENT_XML:
				$context = $this->compiler->getContext();
				switch ($context[0]) {
					case Compiler::CONTEXT_COMMENT:
						return $tokens->prepend('Latte\Runtime\Filters::escapeHtmlComment(')->append(')');
					default:
						$tokens->prepend('Latte\Runtime\Filters::escapeXml(')->append(')');
						if ($context[0] === Compiler::CONTEXT_UNQUOTED_ATTR) {
							$tokens->prepend("'\"', ")->append(", '\"'");
						}
						return $tokens;
				}

			case Compiler::CONTENT_JS:
			case Compiler::CONTENT_CSS:
			case Compiler::CONTENT_ICAL:
				return $tokens->prepend('Latte\Runtime\Filters::escape' . ucfirst($this->compiler->getContentType()) . '(')->append(')');
			case Compiler::CONTENT_TEXT:
				return $tokens;
			default:
				return $tokens->prepend('$template->escape(')->append(')');
		}
	}

}
