<?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\Runtime;

use Packetery\Latte;
use Packetery\Latte\Engine;
use Packetery\Latte\Policy;
/**
 * Template.
 */
class Template
{
    use \Packetery\Latte\Strict;
    public const LAYER_TOP = 0, LAYER_SNIPPET = 'snippet', LAYER_LOCAL = 'local';
    protected const CONTENT_TYPE = Engine::CONTENT_HTML;
    protected const BLOCKS = [];
    /** @var \stdClass global accumulators for intermediate results */
    public $global;
    /** @var mixed[]  @internal */
    protected $params = [];
    /** @var FilterExecutor */
    protected $filters;
    /** @var string|false|null  @internal */
    protected $parentName;
    /** @var mixed[][] */
    protected $varStack = [];
    /** @var Block[][] */
    private $blocks;
    /** @var mixed[][] */
    private $blockStack = [];
    /** @var Engine */
    private $engine;
    /** @var string */
    private $name;
    /** @var Policy|null */
    private $policy;
    /** @var Template|null */
    private $referringTemplate;
    /** @var string|null */
    private $referenceType;
    /**
     * @param  mixed[]  $params
     * @param  mixed[]  $providers
     */
    public function __construct(Engine $engine, array $params, FilterExecutor $filters, array $providers, string $name, ?Policy $policy)
    {
        $this->engine = $engine;
        $this->params = $params;
        $this->filters = $filters;
        $this->name = $name;
        $this->policy = $policy;
        $this->global = (object) $providers;
        $this->initBlockLayer(self::LAYER_TOP);
        $this->initBlockLayer(self::LAYER_LOCAL);
        $this->initBlockLayer(self::LAYER_SNIPPET);
    }
    public function getEngine() : Engine
    {
        return $this->engine;
    }
    public function getName() : string
    {
        return $this->name;
    }
    /**
     * Returns array of all parameters.
     * @return mixed[]
     */
    public function getParameters() : array
    {
        return $this->params;
    }
    /** @deprecated */
    public function getParameter(string $name)
    {
        \trigger_error(__METHOD__ . '() is deprecated, use getParameters()', \E_USER_DEPRECATED);
        if (!\array_key_exists($name, $this->params)) {
            \trigger_error("The variable '{$name}' does not exist in template.", \E_USER_NOTICE);
        }
        return $this->params[$name];
    }
    /**
     * @param  int|string  $layer
     * @return string[]
     */
    public function getBlockNames($layer = self::LAYER_TOP) : array
    {
        return \array_keys($this->blocks[$layer] ?? []);
    }
    public function getContentType() : string
    {
        return static::CONTENT_TYPE;
    }
    public function getParentName() : ?string
    {
        return $this->parentName ?: null;
    }
    public function getReferringTemplate() : ?self
    {
        return $this->referringTemplate;
    }
    public function getReferenceType() : ?string
    {
        return $this->referenceType;
    }
    /**
     * Renders template.
     * @internal
     */
    public function render(?string $block = null) : void
    {
        $level = \ob_get_level();
        try {
            $this->prepare();
            if (!$this->doRender($block)) {
                $this->main();
            }
        } catch (\Throwable $e) {
            while (\ob_get_level() > $level) {
                \ob_end_clean();
            }
            throw $e;
        }
    }
    private function doRender(?string $block = null) : bool
    {
        if ($this->parentName === null && isset($this->global->coreParentFinder)) {
            $this->parentName = ($this->global->coreParentFinder)($this);
        }
        if (isset($this->global->snippetBridge) && !isset($this->global->snippetDriver)) {
            $this->global->snippetDriver = new SnippetDriver($this->global->snippetBridge);
        }
        Filters::$xhtml = (bool) \preg_match('#xml|xhtml#', static::CONTENT_TYPE);
        if ($this->referenceType === 'import') {
            if ($this->parentName) {
                throw new \Packetery\Latte\RuntimeException('Imported template cannot use {extends} or {layout}, use {import}');
            }
        } elseif ($this->parentName) {
            // extends
            \ob_start(function () {
            });
            $this->params = $this->main();
            \ob_end_clean();
            $this->createTemplate($this->parentName, $this->params, 'extends')->render($block);
        } elseif ($block !== null) {
            // single block rendering
            $this->renderBlock($block, $this->params);
        } elseif (isset($this->global->snippetDriver) && $this->global->snippetDriver->renderSnippets($this->blocks[self::LAYER_SNIPPET], $this->params)) {
            // nothing
        } else {
            return \false;
        }
        return \true;
    }
    /**
     * Renders template.
     * @param  mixed[]  $params
     * @internal
     */
    public function createTemplate(string $name, array $params, string $referenceType) : self
    {
        $name = $this->engine->getLoader()->getReferredName($name, $this->name);
        $referred = $referenceType === 'sandbox' ? (clone $this->engine)->setSandboxMode()->createTemplate($name, $params) : $this->engine->createTemplate($name, $params);
        $referred->referringTemplate = $this;
        $referred->referenceType = $referenceType;
        $referred->global = $this->global;
        if (\in_array($referenceType, ['extends', 'includeblock', 'import', 'embed'], \true)) {
            foreach ($referred->blocks[self::LAYER_TOP] as $nm => $block) {
                $this->addBlock($nm, $block->contentType, $block->functions);
            }
            $referred->blocks[self::LAYER_TOP] =& $this->blocks[self::LAYER_TOP];
            $this->blocks[self::LAYER_SNIPPET] += $referred->blocks[self::LAYER_SNIPPET];
            $referred->blocks[self::LAYER_SNIPPET] =& $this->blocks[self::LAYER_SNIPPET];
        }
        ($this->engine->probe)($referred);
        return $referred;
    }
    /**
     * @param  string|\Closure|null  $mod  content-type name or modifier closure
     * @internal
     */
    public function renderToContentType($mod, ?string $block = null) : void
    {
        $this->filter(function () use($block) {
            $this->render($block);
        }, $mod, static::CONTENT_TYPE, "'{$this->name}'");
    }
    /** @internal */
    public function prepare() : void
    {
    }
    /**
     * @internal
     * @return mixed[]
     */
    public function main() : array
    {
        return [];
    }
    /********************* blocks ****************d*g**/
    /**
     * Renders block.
     * @param  mixed[]  $params
     * @param  string|\Closure|null  $mod  content-type name or modifier closure
     * @param  int|string  $layer
     * @internal
     */
    public function renderBlock(string $name, array $params, $mod = null, $layer = null) : void
    {
        $block = $layer ? $this->blocks[$layer][$name] ?? null : $this->blocks[self::LAYER_LOCAL][$name] ?? $this->blocks[self::LAYER_TOP][$name] ?? null;
        if (!$block) {
            $hint = ($t = \Packetery\Latte\Helpers::getSuggestion($this->getBlockNames($layer), $name)) ? ", did you mean '{$t}'?" : '.';
            $name = $layer ? "{$layer} {$name}" : $name;
            throw new \Packetery\Latte\RuntimeException("Cannot include undefined block '{$name}'{$hint}");
        }
        $this->filter(function () use($block, $params) : void {
            \reset($block->functions)($params);
        }, $mod, $block->contentType, "block {$name}");
    }
    /**
     * Renders parent block.
     * @param  mixed[]  $params
     * @internal
     */
    public function renderBlockParent(string $name, array $params) : void
    {
        $block = $this->blocks[self::LAYER_LOCAL][$name] ?? $this->blocks[self::LAYER_TOP][$name] ?? null;
        if (!$block || ($function = \next($block->functions)) === \false) {
            throw new \Packetery\Latte\RuntimeException("Cannot include undefined parent block '{$name}'.");
        }
        $function($params);
        \prev($block->functions);
    }
    /**
     * Creates block if doesn't exist and checks if content type is the same.
     * @param  callable[]  $functions
     * @param  int|string  $layer
     * @internal
     */
    protected function addBlock(string $name, string $contentType, array $functions, $layer = null) : void
    {
        $block =& $this->blocks[$layer ?? self::LAYER_TOP][$name];
        $block = $block ?? new Block();
        if ($block->contentType === null) {
            $block->contentType = $contentType;
        } elseif ($block->contentType !== $contentType) {
            throw new \Packetery\Latte\RuntimeException(\sprintf("Overridden block {$name} with content type %s by incompatible type %s.", \strtoupper($contentType), \strtoupper($block->contentType)));
        }
        $block->functions = \array_merge($block->functions, $functions);
    }
    /**
     * @param  string|\Closure|null  $mod  content-type name or modifier closure
     */
    private function filter(callable $function, $mod, string $contentType, string $name) : void
    {
        if ($mod === null || $mod === $contentType) {
            $function();
        } elseif ($mod instanceof \Closure) {
            echo $mod($this->capture($function), $contentType);
        } elseif ($filter = Filters::getConvertor($contentType, $mod)) {
            echo $filter($this->capture($function));
        } else {
            throw new \Packetery\Latte\RuntimeException(\sprintf("Including {$name} with content type %s into incompatible type %s.", \strtoupper($contentType), \strtoupper($mod)));
        }
    }
    /**
     * Captures output to string.
     * @internal
     */
    public function capture(callable $function) : string
    {
        try {
            \ob_start(function () {
            });
            $function();
            return \ob_get_clean();
        } catch (\Throwable $e) {
            \ob_end_clean();
            throw $e;
        }
    }
    /**
     * @param  int|string  $staticId
     */
    private function initBlockLayer($staticId, ?int $destId = null) : void
    {
        $destId = $destId ?? $staticId;
        $this->blocks[$destId] = [];
        foreach (static::BLOCKS[$staticId] ?? [] as $nm => $info) {
            [$method, $contentType] = \is_array($info) ? $info : [$info, static::CONTENT_TYPE];
            $this->addBlock($nm, $contentType, [[$this, $method]], $destId);
        }
    }
    protected function enterBlockLayer(int $staticId, array $vars) : void
    {
        $this->blockStack[] = $this->blocks[self::LAYER_TOP];
        $this->initBlockLayer($staticId, self::LAYER_TOP);
        $this->varStack[] = $vars;
    }
    protected function copyBlockLayer() : void
    {
        foreach (\end($this->blockStack) as $nm => $block) {
            $this->addBlock($nm, $block->contentType, $block->functions);
        }
    }
    protected function leaveBlockLayer() : void
    {
        $this->blocks[self::LAYER_TOP] = \array_pop($this->blockStack);
        \array_pop($this->varStack);
    }
    public function hasBlock(string $name) : bool
    {
        return isset($this->blocks[self::LAYER_LOCAL][$name]) || isset($this->blocks[self::LAYER_TOP][$name]);
    }
    /********************* policy ****************d*g**/
    /**
     * @param  mixed  $callable
     * @return mixed
     * @internal
     */
    protected function call($callable)
    {
        if (!\is_callable($callable)) {
            throw new \Packetery\Latte\SecurityViolationException('Invalid callable.');
        } elseif (\is_string($callable)) {
            $parts = \explode('::', $callable);
            $allowed = \count($parts) === 1 ? $this->policy->isFunctionAllowed($parts[0]) : $this->policy->isMethodAllowed(...$parts);
        } elseif (\is_array($callable)) {
            $allowed = $this->policy->isMethodAllowed(\is_object($callable[0]) ? \get_class($callable[0]) : $callable[0], $callable[1]);
        } elseif (\is_object($callable)) {
            $allowed = $callable instanceof \Closure ? \true : $this->policy->isMethodAllowed(\get_class($callable), '__invoke');
        } else {
            $allowed = \false;
        }
        if (!$allowed) {
            \is_callable($callable, \false, $text);
            throw new \Packetery\Latte\SecurityViolationException("Calling {$text}() is not allowed.");
        }
        return $callable;
    }
    /**
     * @param  mixed  $obj
     * @param  mixed  $prop
     * @return mixed
     * @internal
     */
    protected function prop($obj, $prop)
    {
        $class = \is_object($obj) ? \get_class($obj) : $obj;
        if (\is_string($class) && !$this->policy->isPropertyAllowed($class, (string) $prop)) {
            throw new \Packetery\Latte\SecurityViolationException("Access to '{$prop}' property on a {$class} object is not allowed.");
        }
        return $obj;
    }
    /**
     * @return mixed
     */
    public function &__get(string $name)
    {
        if ($name === 'blocks') {
            // compatibility with nette/application < 3.0.8
            $tmp = static::BLOCKS[self::LAYER_TOP] ?? [];
            return $tmp;
        }
        throw new \LogicException('Attempt to read undeclared property ' . self::class . '::$' . $name);
    }
}
