<?php

/**
 * This file is part of the Latte (https://latte.nette.org)
 * Copyright (c) 2008 David Grudl (https://davidgrudl.com)
 */
declare (strict_types=1);
namespace Packetery\Latte\Macros;

use Packetery\Latte;
use Packetery\Latte\CompileException;
use Packetery\Latte\Helpers;
use Packetery\Latte\MacroNode;
use Packetery\Latte\PhpHelpers;
use Packetery\Latte\PhpWriter;
use Packetery\Latte\Runtime\Block;
use Packetery\Latte\Runtime\SnippetDriver;
use Packetery\Latte\Runtime\Template;
/**
 * Block macros.
 */
class BlockMacros extends MacroSet
{
    /** @var string */
    public $snippetAttribute = 'id';
    /** @var Block[][] */
    private $blocks;
    /** @var int  current layer */
    private $index;
    /** @var string|bool|null */
    private $extends;
    /** @var string[] */
    private $imports;
    /** @var array[] */
    private $placeholders;
    public static function install(\Packetery\Latte\Compiler $compiler) : void
    {
        $me = new static($compiler);
        $me->addMacro('include', [$me, 'macroInclude']);
        $me->addMacro('includeblock', [$me, 'macroIncludeBlock']);
        // deprecated
        $me->addMacro('import', [$me, 'macroImport'], null, null, self::ALLOWED_IN_HEAD);
        $me->addMacro('extends', [$me, 'macroExtends'], null, null, self::ALLOWED_IN_HEAD);
        $me->addMacro('layout', [$me, 'macroExtends'], null, null, self::ALLOWED_IN_HEAD);
        $me->addMacro('snippet', [$me, 'macroSnippet'], [$me, 'macroBlockEnd']);
        // must be before block
        $me->addMacro('block', [$me, 'macroBlock'], [$me, 'macroBlockEnd'], null, self::AUTO_CLOSE);
        $me->addMacro('define', [$me, 'macroDefine'], [$me, 'macroBlockEnd']);
        $me->addMacro('embed', [$me, 'macroEmbed'], [$me, 'macroEmbedEnd']);
        $me->addMacro('snippetArea', [$me, 'macroSnippetArea'], [$me, 'macroBlockEnd']);
        $me->addMacro('ifset', [$me, 'macroIfset'], '}');
        $me->addMacro('elseifset', [$me, 'macroIfset']);
    }
    /**
     * Initializes before template parsing.
     * @return void
     */
    public function initialize()
    {
        $this->blocks = [[]];
        $this->index = Template::LAYER_TOP;
        $this->extends = null;
        $this->imports = [];
        $this->placeholders = [];
    }
    /**
     * Finishes template parsing.
     */
    public function finalize()
    {
        $compiler = $this->getCompiler();
        foreach ($this->placeholders as $key => [$index, $blockName]) {
            $block = $this->blocks[$index][$blockName] ?? $this->blocks[Template::LAYER_LOCAL][$blockName] ?? null;
            $compiler->placeholders[$key] = $block && !$block->hasParameters ? 'get_defined_vars()' : '[]';
        }
        $meta = [];
        foreach ($this->blocks as $layer => $blocks) {
            foreach ($blocks as $name => $block) {
                $compiler->addMethod($method = $this->generateMethodName($name), '?>' . $compiler->expandTokens($block->code) . '<?php', 'array $ʟ_args', 'void', $block->comment);
                $meta[$layer][$name] = $block->contentType === $compiler->getContentType() ? $method : [$method, $block->contentType];
            }
        }
        if ($meta) {
            $compiler->addConstant('BLOCKS', $meta);
        }
        return [($this->extends === null ? '' : '$this->parentName = ' . $this->extends . ';') . \implode('', $this->imports)];
    }
    /********************* macros ****************d*g**/
    /**
     * {include [block] name [,] [params]}
     * @return string|false
     */
    public function macroInclude(MacroNode $node, PhpWriter $writer)
    {
        $node->validate(\true, [], \true);
        $node->replaced = \false;
        $tmp = $node->tokenizer->joinUntil('=');
        if ($node->tokenizer->isNext('=') && !$node->tokenizer->depth) {
            \trigger_error('The assignment in the {' . $node->name . ' ' . $tmp . '= ...} looks like an error.', \E_USER_NOTICE);
        }
        $node->tokenizer->reset();
        [$name, $mod] = $node->tokenizer->fetchWordWithModifier(['block', 'file', '#']);
        if (!$mod && \preg_match('~([\'"])[\\w-]+\\1$~DA', $name)) {
            \trigger_error("Change {include {$name}} to {include file {$name}} for clarity (on line {$node->startLine})", \E_USER_NOTICE);
        }
        if ($mod !== 'block' && $mod !== '#' && ($mod === 'file' || !$name || !\preg_match('~[\\w-]+$~DA', $name))) {
            return \false;
            // {include file}
        }
        if ($name === 'parent' && $node->modifiers !== '') {
            throw new CompileException('Filters are not allowed in {include parent}');
        }
        $noEscape = Helpers::removeFilter($node->modifiers, 'noescape');
        if ($node->modifiers && !$noEscape) {
            $node->modifiers .= '|escape';
        }
        if ($node->tokenizer->nextToken('from')) {
            $node->tokenizer->nextToken($node->tokenizer::T_WHITESPACE);
            return $writer->write('$this->createTemplate(%node.word, %node.array? + $this->params, "include")->renderToContentType(%raw, %word) %node.line;', $node->modifiers ? $writer->write('function ($s, $type) { $ʟ_fi = new LR\\FilterInfo($type); return %modifyContent($s); }') : PhpHelpers::dump($noEscape ? null : \implode('', $node->context)), $name);
        }
        $parent = $name === 'parent';
        if ($name === 'parent' || $name === 'this') {
            $item = $node->closest(['block', 'define'], function ($node) {
                return $node->data->name !== '';
            });
            if (!$item) {
                throw new CompileException("Cannot include {$name} block outside of any block.");
            }
            $name = $item->data->name;
        }
        $key = \uniqid() . '$iterator';
        // to fool CoreMacros::macroEndForeach
        $this->placeholders[$key] = [$this->index, $name];
        $phpName = $this->isDynamic($name) ? $writer->formatWord($name) : PhpHelpers::dump($name);
        return $writer->write('$this->renderBlock' . ($parent ? 'Parent' : '') . '(' . $phpName . ', ' . '%node.array? + ' . $key . ($node->modifiers ? ', function ($s, $type) { $ʟ_fi = new LR\\FilterInfo($type); return %modifyContent($s); }' : ($noEscape || $parent ? '' : ', ' . PhpHelpers::dump(\implode('', $node->context)))) . ') %node.line;');
    }
    /**
     * {includeblock "file"}
     * @deprecated
     */
    public function macroIncludeBlock(MacroNode $node, PhpWriter $writer) : string
    {
        \trigger_error("Macro {includeblock} is deprecated, use {include {$node->args} with blocks} or similar macro {import} (on line {$node->startLine})", \E_USER_DEPRECATED);
        $node->replaced = \false;
        $node->validate(\true);
        return $writer->write('
			ob_start(function () {});
			try {
				$this->createTemplate(%node.word, %node.array? + get_defined_vars(), "includeblock")->renderToContentType(%var) %node.line;
			} finally {
				echo rtrim(ob_get_clean());
			}', \implode('', $node->context));
    }
    /**
     * {import "file"}
     */
    public function macroImport(MacroNode $node, PhpWriter $writer) : string
    {
        $node->validate(\true);
        $file = $node->tokenizer->fetchWord();
        $this->checkExtraArgs($node);
        $code = $writer->write('$this->createTemplate(%word, $this->params, "import")->render() %node.line;', $file);
        if ($this->getCompiler()->isInHead()) {
            $this->imports[] = $code;
            return '';
        } elseif ($node->parentNode && $node->parentNode->name === 'embed') {
            return "} {$code} if (false) {";
        } else {
            return $code;
        }
    }
    /**
     * {extends none | $var | "file"}
     */
    public function macroExtends(MacroNode $node, PhpWriter $writer) : void
    {
        $node->validate(\true);
        if ($node->parentNode) {
            throw new CompileException($node->getNotation() . ' must not be inside other tags.');
        } elseif ($this->extends !== null) {
            throw new CompileException('Multiple ' . $node->getNotation() . ' declarations are not allowed.');
        } elseif ($node->args === 'none') {
            $this->extends = 'false';
        } else {
            $this->extends = $writer->write('%node.word%node.args');
        }
        if (!$this->getCompiler()->isInHead()) {
            throw new CompileException($node->getNotation() . ' must be placed in template head.');
        }
    }
    /**
     * {block [local] [name]}
     */
    public function macroBlock(MacroNode $node, PhpWriter $writer) : string
    {
        [$name, $local] = $node->tokenizer->fetchWordWithModifier('local');
        $layer = $local ? Template::LAYER_LOCAL : null;
        $data = $node->data;
        $data->name = \ltrim((string) $name, '#');
        $this->checkExtraArgs($node);
        if ($data->name === '') {
            if ($node->modifiers === '') {
                return '';
            }
            $node->modifiers .= '|escape';
            $node->closingCode = $writer->write('<?php } finally { $ʟ_fi = new LR\\FilterInfo(%var); echo %modifyContent(ob_get_clean()); } ?>', \implode('', $node->context));
            return $writer->write('ob_start(function () {}) %node.line; try {');
        }
        if (Helpers::startsWith((string) $node->context[1], \Packetery\Latte\Compiler::CONTEXT_HTML_ATTRIBUTE)) {
            $node->context[1] = '';
            $node->modifiers .= '|escape';
        } elseif ($node->modifiers) {
            $node->modifiers .= '|escape';
        }
        $renderArgs = $writer->write('get_defined_vars()' . ($node->modifiers ? ', function ($s, $type) { $ʟ_fi = new LR\\FilterInfo($type); return %modifyContent($s); }' : ''));
        if ($this->isDynamic($data->name)) {
            $node->closingCode = $writer->write('<?php $this->renderBlock($ʟ_nm, %raw); ?>', $renderArgs);
            return $this->beginDynamicBlockOrDefine($node, $writer, $layer);
        }
        if (!\preg_match('#^[a-z]#iD', $data->name)) {
            throw new CompileException("Block name must start with letter a-z, '{$data->name}' given.");
        }
        $extendsCheck = $this->blocks[Template::LAYER_TOP] || \count($this->blocks) > 1 || $node->parentNode;
        $block = $this->addBlock($node, $layer);
        $data->after = function () use($node, $block) {
            $this->extractMethod($node, $block);
        };
        return $writer->write(($extendsCheck ? '' : 'if ($this->getParentName()) { return get_defined_vars(); } ') . '$this->renderBlock(%var, %raw) %node.line;', $data->name, $renderArgs);
    }
    /**
     * {define [local] name}
     */
    public function macroDefine(MacroNode $node, PhpWriter $writer) : string
    {
        if ($node->modifiers) {
            // modifier may be union|type
            $node->setArgs($node->args . $node->modifiers);
            $node->modifiers = '';
        }
        $node->validate(\true);
        [$name, $local] = $node->tokenizer->fetchWordWithModifier('local');
        $layer = $local ? Template::LAYER_LOCAL : null;
        $data = $node->data;
        $data->name = \ltrim((string) $name, '#');
        if ($this->isDynamic($data->name)) {
            $node->closingCode = '<?php ?>';
            return $this->beginDynamicBlockOrDefine($node, $writer, $layer);
        }
        if (!\preg_match('#^[a-z]#iD', $data->name)) {
            throw new CompileException("Block name must start with letter a-z, '{$data->name}' given.");
        }
        $tokens = $node->tokenizer;
        $params = [];
        while ($tokens->isNext(...$tokens::SIGNIFICANT)) {
            if ($tokens->nextToken($tokens::T_SYMBOL, '?', 'null', '\\')) {
                // type
                $tokens->nextAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null');
            }
            $param = $tokens->consumeValue($tokens::T_VARIABLE);
            $default = $tokens->nextToken('=') ? $tokens->joinUntilSameDepth(',') : 'null';
            $params[] = $writer->write('%raw = $ʟ_args[%var] ?? $ʟ_args[%var] ?? %raw;', $param, \count($params), \substr($param, 1), $default);
            if ($tokens->isNext(...$tokens::SIGNIFICANT)) {
                $tokens->consumeValue(',');
            }
        }
        $extendsCheck = $this->blocks[Template::LAYER_TOP] || \count($this->blocks) > 1 || $node->parentNode;
        $block = $this->addBlock($node, $layer);
        $block->hasParameters = (bool) $params;
        $data->after = function () use($node, $block, $params) {
            $params = $params ? \implode('', $params) : null;
            $this->extractMethod($node, $block, $params);
        };
        return $extendsCheck ? '' : 'if ($this->getParentName()) { return get_defined_vars();} ';
    }
    private function beginDynamicBlockOrDefine(MacroNode $node, PhpWriter $writer, ?string $layer) : string
    {
        $this->checkExtraArgs($node);
        $data = $node->data;
        $func = $this->generateMethodName($data->name);
        $data->after = function () use($node, $func) {
            $node->content = \rtrim($node->content, " \t");
            $this->getCompiler()->addMethod($func, $this->getCompiler()->expandTokens("extract(\$ʟ_args); unset(\$ʟ_args);\n?>{$node->content}<?php"), 'array $ʟ_args', 'void', "{{$node->name} {$node->args}} on line {$node->startLine}");
            $node->content = '';
        };
        return $writer->write('$this->addBlock($ʟ_nm = %word, %var, [[$this, %var]], %var);', $data->name, \implode('', $node->context), $func, $layer);
    }
    /**
     * {snippet [name]}
     */
    public function macroSnippet(MacroNode $node, PhpWriter $writer) : string
    {
        $node->validate(null);
        $data = $node->data;
        $data->name = (string) $node->tokenizer->fetchWord();
        $this->checkExtraArgs($node);
        if ($node->prefix && isset($node->htmlNode->attrs[$this->snippetAttribute])) {
            throw new CompileException("Cannot combine HTML attribute {$this->snippetAttribute} with n:snippet.");
        } elseif ($node->prefix && isset($node->htmlNode->macroAttrs['ifcontent'])) {
            throw new CompileException('Cannot combine n:ifcontent with n:snippet.');
        } elseif ($this->isDynamic($data->name)) {
            return $this->beginDynamicSnippet($node, $writer);
        } elseif ($data->name !== '' && !\preg_match('#^[a-z]#iD', $data->name)) {
            throw new CompileException("Snippet name must start with letter a-z, '{$data->name}' given.");
        }
        if ($node->prefix && $node->prefix !== $node::PREFIX_NONE) {
            \trigger_error("Use n:snippet instead of {$node->getNotation()}", \E_USER_DEPRECATED);
        }
        $block = $this->addBlock($node, Template::LAYER_SNIPPET);
        $data->after = function () use($node, $writer, $data, $block) {
            if ($node->prefix === MacroNode::PREFIX_NONE) {
                // n:snippet -> n:inner-snippet
                $node->content = $node->innerContent;
            }
            $node->content = $writer->write('<?php $this->global->snippetDriver->enter(%word, %var);
				try { ?>%raw<?php } finally { $this->global->snippetDriver->leave(); } ?>', $data->name, SnippetDriver::TYPE_STATIC, \preg_replace('#(?<=\\n)[ \\t]+$#D', '', $node->content));
            $this->extractMethod($node, $block);
            if ($node->prefix === MacroNode::PREFIX_NONE) {
                $node->innerContent = $node->openingCode . $node->content . $node->closingCode;
                $node->closingCode = $node->openingCode = '<?php ?>';
            }
        };
        if ($node->prefix) {
            if (isset($node->htmlNode->macroAttrs['foreach'])) {
                throw new CompileException('Combination of n:snippet with n:foreach is invalid, use n:inner-foreach.');
            }
            $node->attrCode = $writer->write("<?php echo ' {$this->snippetAttribute}=\"' . htmlspecialchars(\$this->global->snippetDriver->getHtmlId(%var)) . '\"' ?>", $data->name);
            return $writer->write('$this->renderBlock(%var, [], null, %var)', $data->name, Template::LAYER_SNIPPET);
        }
        return $writer->write("?>\n<div {$this->snippetAttribute}=\"<?php echo htmlspecialchars(\$this->global->snippetDriver->getHtmlId(%0_var)) ?>\">" . '<?php $this->renderBlock(%0_var, [], null, %1_var) %node.line; ?>' . "\n</div><?php ", $data->name, Template::LAYER_SNIPPET);
    }
    private function beginDynamicSnippet(MacroNode $node, PhpWriter $writer) : string
    {
        $data = $node->data;
        $node->closingCode = '<?php } finally { $this->global->snippetDriver->leave(); } ?>';
        if ($node->prefix) {
            if ($node->prefix === MacroNode::PREFIX_NONE) {
                // n:snippet -> n:inner-snippet
                $data->after = function () use($node) {
                    $node->innerContent = $node->openingCode . $node->innerContent . $node->closingCode;
                    $node->closingCode = $node->openingCode = '<?php ?>';
                };
            }
            $node->attrCode = $writer->write("<?php echo ' {$this->snippetAttribute}=\"' . htmlspecialchars(\$this->global->snippetDriver->getHtmlId(\$ʟ_nm = %word)) . '\"' ?>", $data->name);
            return $writer->write('$this->global->snippetDriver->enter($ʟ_nm, %var) %node.line; try {', SnippetDriver::TYPE_DYNAMIC);
        }
        $node->closingCode .= "\n</div>";
        return $writer->write("?>\n<div {$this->snippetAttribute}=\"" . '<?php echo htmlspecialchars($this->global->snippetDriver->getHtmlId($ʟ_nm = %word)) ?>"' . '><?php $this->global->snippetDriver->enter($ʟ_nm, %var) %node.line; try {', $data->name, SnippetDriver::TYPE_DYNAMIC);
    }
    /**
     * {snippetArea [name]}
     */
    public function macroSnippetArea(MacroNode $node, PhpWriter $writer) : string
    {
        $node->validate(null);
        $data = $node->data;
        $data->name = (string) $node->tokenizer->fetchWord();
        $this->checkExtraArgs($node);
        $block = $this->addBlock($node, Template::LAYER_SNIPPET);
        $data->after = function () use($node, $writer, $data, $block) {
            $node->content = $writer->write('<?php $this->global->snippetDriver->enter(%var, %var);
				try { ?>%raw<?php } finally { $this->global->snippetDriver->leave(); } ?>', $data->name, SnippetDriver::TYPE_AREA, \preg_replace('#(?<=\\n)[ \\t]+$#D', '', $node->content));
            $this->extractMethod($node, $block);
        };
        return $writer->write('$this->renderBlock(%var, [], null, %var) %node.line;', $data->name, Template::LAYER_SNIPPET);
    }
    /**
     * {/block}
     * {/define}
     * {/snippet}
     * {/snippetArea}
     */
    public function macroBlockEnd(MacroNode $node, PhpWriter $writer) : string
    {
        if (isset($node->data->after)) {
            ($node->data->after)();
        }
        return $node->name === 'define' ? ' ' : '';
    }
    private function addBlock(MacroNode $node, ?string $layer = null) : Block
    {
        $data = $node->data;
        if ($layer === Template::LAYER_SNIPPET ? isset($this->blocks[$layer][$data->name]) : isset($this->blocks[Template::LAYER_LOCAL][$data->name]) || isset($this->blocks[$this->index][$data->name])) {
            throw new CompileException("Cannot redeclare {$node->name} '{$data->name}'");
        }
        $block = $this->blocks[$layer ?? $this->index][$data->name] = new Block();
        $block->contentType = \implode('', $node->context);
        $block->comment = "{{$node->name} {$node->args}} on line {$node->startLine}";
        return $block;
    }
    private function extractMethod(MacroNode $node, Block $block, ?string $params = null) : void
    {
        if (\preg_match('#\\$|n:#', $node->content)) {
            $node->content = '<?php extract(' . ($node->name === 'block' && $node->closest(['embed']) ? 'end($this->varStack)' : '$this->params') . ');' . ($params ?? 'extract($ʟ_args);') . 'unset($ʟ_args);?>' . $node->content;
        }
        $block->code = \preg_replace('#^\\n+|(?<=\\n)[ \\t]+$#D', '', $node->content);
        $node->content = \substr_replace($node->content, $node->openingCode . "\n", \strspn($node->content, "\n"), \strlen($block->code));
        $node->openingCode = '<?php ?>';
    }
    /**
     * {embed [block|file] name [,] [params]}
     */
    public function macroEmbed(MacroNode $node, PhpWriter $writer) : void
    {
        $node->validate(\true);
        $node->replaced = \false;
        $node->data->prevIndex = $this->index;
        $this->index = \count($this->blocks);
        $this->blocks[$this->index] = [];
        [$name, $mod] = $node->tokenizer->fetchWordWithModifier(['block', 'file']);
        if (!$mod && \preg_match('~([\'"])[\\w-]+\\1$~DA', $name)) {
            \trigger_error("Change {embed {$name}} to {embed file {$name}} for clarity (on line {$node->startLine})", \E_USER_NOTICE);
        }
        $mod = $mod ?? (\preg_match('~^[\\w-]+$~DA', $name) ? 'block' : 'file');
        $node->openingCode = $writer->write('<?php
			$this->enterBlockLayer(%0_var, get_defined_vars()) %node.line;
			if (false) { ?>', $this->index);
        if ($mod === 'file') {
            $node->closingCode = $writer->write('<?php }
				try { $this->createTemplate(%word, %node.array, "embed")->renderToContentType(%var) %node.line; }
				finally { $this->leaveBlockLayer(); } ?>' . "\n", $name, \implode('', $node->context));
        } else {
            $node->closingCode = $writer->write('<?php }
				$this->copyBlockLayer();
				try { $this->renderBlock(%raw, %node.array, %var) %node.line; }
				finally { $this->leaveBlockLayer(); } ?>' . "\n", $this->isDynamic($name) ? $writer->formatWord($name) : PhpHelpers::dump($name), \implode('', $node->context));
        }
    }
    /**
     * {/embed}
     */
    public function macroEmbedEnd(MacroNode $node, PhpWriter $writer) : void
    {
        $this->index = $node->data->prevIndex;
    }
    /**
     * {ifset block}
     * {elseifset block}
     * @return string|false
     */
    public function macroIfset(MacroNode $node, PhpWriter $writer)
    {
        $node->validate(\true);
        if (!\preg_match('~#|\\w~A', $node->args)) {
            return \false;
        }
        $list = [];
        while ([$name, $block] = $node->tokenizer->fetchWordWithModifier(['block', '#'])) {
            $list[] = $block || \preg_match('~\\w[\\w-]*$~DA', $name) ? '$this->hasBlock(' . $writer->formatWord($name) . ')' : 'isset(' . $writer->formatArgs(new \Packetery\Latte\MacroTokens($name)) . ')';
        }
        return $writer->write(($node->name === 'elseifset' ? '} else' : '') . 'if (%raw) %node.line {', \implode(' && ', $list));
    }
    private function generateMethodName(string $blockName) : string
    {
        $name = 'block' . \ucfirst(\trim(\preg_replace('#\\W+#', '_', $blockName), '_'));
        $lower = \strtolower($name);
        $methods = \array_change_key_case($this->getCompiler()->getMethods()) + ['block' => 1];
        $counter = null;
        while (isset($methods[$lower . $counter])) {
            $counter++;
        }
        return $name . $counter;
    }
    private function isDynamic(string $name) : bool
    {
        return \strpos($name, '$') !== \false || \strpos($name, ' ') !== \false;
    }
}
