<?php
/**
 * Smarty Internal Plugin Compile Block Plugin
 * Compiles code for the execution of block plugin
 *


 * @author     Uwe Tews
 */

namespace Smarty\Compile;

use Smarty\Compiler\Template;
use Smarty\CompilerException;
use Smarty\Exception;
use Smarty\Smarty;

/**
 * Smarty Internal Plugin Compile Block Plugin Class
 *
 */
class BlockCompiler extends Base {

	/**
	 * Attribute definition: Overwrites base class.
	 *
	 * @var array
	 * @see BasePlugin
	 */
	protected $optional_attributes = ['_any'];

	/**
	 * nesting level
	 *
	 * @var int
	 */
	private $nesting = 0;


	/**
	 * Compiles code for the execution of block plugin
	 *
	 * @param array $args array with attributes from parser
	 * @param Template $compiler compiler object
	 * @param array $parameter array with compilation parameter
	 * @param string $tag name of block plugin
	 * @param string $function PHP function name
	 *
	 * @return string compiled code
	 * @throws CompilerException
	 * @throws Exception
	 */
	public function compile($args, Template $compiler, $parameter = [], $tag = null, $function = null) {

		if (!isset($tag[5]) || substr($tag, -5) !== 'close') {
			$output = $this->compileOpeningTag($compiler, $args, $tag, $function);
		} else {
			$output = $this->compileClosingTag($compiler, $tag, $parameter, $function);
		}
		return $output;
	}

	/**
	 * Compiles code for the {$smarty.block.child} property
	 *
	 * @param Template $compiler compiler object
	 *
	 * @return string compiled code
	 * @throws CompilerException
	 */
	public function compileChild(\Smarty\Compiler\Template $compiler) {

		if (!isset($compiler->_cache['blockNesting'])) {
			$compiler->trigger_template_error(
				"'{\$smarty.block.child}' used outside {block} tags ",
				$compiler->getParser()->lex->taglineno
			);
		}
		$compiler->_cache['blockParams'][$compiler->_cache['blockNesting']]['callsChild'] = true;
		$compiler->has_code = true;
		$compiler->suppressNocacheProcessing = true;

		$output = "<?php \n";
		$output .= '$_smarty_tpl->getInheritance()->callChild($_smarty_tpl, $this' . ");\n";
		$output .= "?>\n";
		return $output;
	}

	/**
	 * Compiles code for the {$smarty.block.parent} property
	 *
	 * @param Template $compiler compiler object
	 *
	 * @return string compiled code
	 * @throws CompilerException
	 */
	public function compileParent(\Smarty\Compiler\Template $compiler) {

		if (!isset($compiler->_cache['blockNesting'])) {
			$compiler->trigger_template_error(
				"'{\$smarty.block.parent}' used outside {block} tags ",
				$compiler->getParser()->lex->taglineno
			);
		}
		$compiler->has_code = true;
		$compiler->suppressNocacheProcessing = true;

		$output = "<?php \n";
		$output .= '$_smarty_tpl->getInheritance()->callParent($_smarty_tpl, $this' . ");\n";
		$output .= "?>\n";
		return $output;
	}

	/**
	 * Returns true if this block is cacheable.
	 *
	 * @param Smarty $smarty
	 * @param $function
	 *
	 * @return bool
	 */
	protected function blockIsCacheable(\Smarty\Smarty $smarty, $function): bool {
		return $smarty->getBlockHandler($function)->isCacheable();
	}

	/**
	 * Returns the code used for the isset check
	 *
	 * @param string $tag tag name
	 * @param string $function base tag or method name
	 *
	 * @return string
	 */
	protected function getIsCallableCode($tag, $function): string {
		return "\$_smarty_tpl->getSmarty()->getBlockHandler(" . var_export($function, true) . ")";
	}

	/**
	 * Returns the full code used to call the callback
	 *
	 * @param string $tag tag name
	 * @param string $function base tag or method name
	 *
	 * @return string
	 */
	protected function getFullCallbackCode($tag, $function): string {
		return "\$_smarty_tpl->getSmarty()->getBlockHandler(" . var_export($function, true) . ")->handle";
	}

	/**
	 * @param Template $compiler
	 * @param array $args
	 * @param string|null $tag
	 * @param string|null $function
	 *
	 * @return string
	 */
	private function compileOpeningTag(Template $compiler, array $args, ?string $tag, ?string $function): string {

		// check and get attributes
		$_attr = $this->getAttributes($compiler, $args);
		$this->nesting++;
		unset($_attr['nocache']);
		$_params = 'array(' . implode(',', $this->formatParamsArray($_attr)) . ')';

		if (!$this->blockIsCacheable($compiler->getSmarty(), $function)) {
			$compiler->tag_nocache = true;
		}

		if ($compiler->tag_nocache) {
			// push a {nocache} tag onto the stack to prevent caching of this block
			$this->openTag($compiler, 'nocache');
		}

		$this->openTag($compiler, $tag, [$_params, $compiler->tag_nocache]);

		// compile code
		$output = "<?php \$_block_repeat=true;
if (!" . $this->getIsCallableCode($tag, $function) .") {\nthrow new \\Smarty\\Exception('block tag \'{$tag}\' not callable or registered');\n}\n
echo " . $this->getFullCallbackCode($tag, $function) . "({$_params}, null, \$_smarty_tpl, \$_block_repeat);
while (\$_block_repeat) {
  ob_start();
?>";

		return $output;
	}

	/**
	 * @param Template $compiler
	 * @param string $tag
	 * @param array $parameter
	 * @param string|null $function
	 *
	 * @return string
	 * @throws CompilerException
	 * @throws Exception
	 */
	private function compileClosingTag(Template $compiler, string $tag, array $parameter, ?string $function): string {

		// closing tag of block plugin, restore nocache
		$base_tag = substr($tag, 0, -5);
		[$_params, $nocache_pushed] = $this->closeTag($compiler, $base_tag);

		// compile code
		if (!isset($parameter['modifier_list'])) {
			$mod_pre = $mod_post = $mod_content = '';
			$mod_content2 = 'ob_get_clean()';
		} else {
			$mod_content2 = "\$_block_content{$this->nesting}";
			$mod_content = "\$_block_content{$this->nesting} = ob_get_clean();\n";
			$mod_pre = "ob_start();\n";
			$mod_post = 'echo ' . $compiler->compileModifier($parameter['modifier_list'], 'ob_get_clean()')
				. ";\n";
		}
		$output = "<?php {$mod_content}\$_block_repeat=false;\n{$mod_pre}";
		$callback = $this->getFullCallbackCode($base_tag, $function);
		$output .= "echo {$callback}({$_params}, {$mod_content2}, \$_smarty_tpl, \$_block_repeat);\n";
		$output .= "{$mod_post}}\n?>";

		if ($nocache_pushed) {
			// pop the pushed virtual nocache tag
			$this->closeTag($compiler, 'nocache');
			$compiler->tag_nocache = true;
		}

		return $output;
	}

}
