<?php

/**
 * Active view data context stack for site route templates.
 *
 * Registers controller and nested partial view data and exposes it via
 * view_data(), with caller verification to prevent hijacking.
 *
 * @package    Framework
 * @subpackage View
 * @since      2.1.2
 */
namespace Kirki\Framework\View;

\defined('ABSPATH') || exit;
use Kirki\Framework\Supports\Arr;
use function Kirki\Framework\app;
class ViewContext
{
    /**
     * Stack of view contexts for the current request (root + nested partials).
     *
     * @var array
     *
     * @since 2.1.2
     */
    protected $stack = [];
    /**
     * Whether the shutdown clear hook was registered.
     *
     * @var bool
     *
     * @since 2.1.2
     */
    protected $shutdown_registered = \false;
    /**
     * Prepare and push view data for a site route view.
     *
     * @param View $view Controller view return value.
     * @param string $route_name Named route or empty string.
     * @param string $resolved_path Absolute template filesystem path.
     *
     * @return array Final data stored in the pushed context.
     *
     * @since 2.1.2
     */
    public function prepare(View $view, string $route_name, string $resolved_path)
    {
        $engine = app(TemplateEngine::class);
        $template = $view->get_template();
        $data = \array_merge($engine->get_shared(), $view->get_data());
        $this->push(['template' => $template, 'route_name' => $route_name, 'resolved_path' => $this->normalize_path($resolved_path), 'data' => $data]);
        return $data;
    }
    /**
     * Push a context frame onto the stack.
     *
     * @param array $context Context payload with template, route_name, resolved_path, data.
     *
     * @return void
     *
     * @since 2.1.2
     */
    public function push(array $context)
    {
        if (isset($context['resolved_path'])) {
            $context['resolved_path'] = $this->normalize_path((string) $context['resolved_path']);
        }
        $this->stack[] = $context;
        $this->ensure_shutdown_registered();
    }
    /**
     * Pop the top context frame from the stack.
     *
     * @return array|null The removed frame, or null when the stack is empty.
     *
     * @since 2.1.2
     */
    public function pop()
    {
        if ($this->stack === []) {
            return null;
        }
        return \array_pop($this->stack);
    }
    /**
     * Read a value from the innermost authorized context.
     *
     * Supports dot notation for nested keys (e.g. `product.name`).
     *
     * @param string|null $key Data key, or null for the full array.
     * @param mixed $default Default when missing or unauthorized.
     *
     * @return mixed
     *
     * @since 2.1.2
     */
    public function get($key = null, $default = null)
    {
        $frame = $this->authorized_frame();
        if ($frame === null) {
            return $key === null ? [] : $default;
        }
        if ($key === null) {
            return $frame['data'];
        }
        return Arr::get($frame['data'], $key, $default);
    }
    /**
     * Clear the entire context stack.
     *
     * @return void
     *
     * @since 2.1.2
     */
    public function clear()
    {
        $this->stack = [];
    }
    /**
     * Get the top context frame, or null when the stack is empty.
     *
     * @return array|null
     *
     * @since 2.1.2
     */
    public function get_active()
    {
        if ($this->stack === []) {
            return null;
        }
        return $this->stack[\count($this->stack) - 1];
    }
    /**
     * Find the innermost stack frame that authorizes the current caller.
     *
     * @return array|null
     *
     * @since 2.1.2
     */
    protected function authorized_frame()
    {
        if ($this->stack === []) {
            return null;
        }
        $trace_files = $this->trace_files();
        for ($index = \count($this->stack) - 1; $index >= 0; $index--) {
            $frame = $this->stack[$index];
            if (empty($frame['resolved_path'])) {
                continue;
            }
            if (isset($trace_files[$frame['resolved_path']])) {
                return $frame;
            }
        }
        return null;
    }
    /**
     * Collect normalized file paths from the current backtrace.
     *
     * @return array<string, bool> Map of normalized path => true.
     *
     * @since 2.1.2
     */
    protected function trace_files()
    {
        $files = [];
        $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
        foreach ($trace as $frame) {
            if (empty($frame['file'])) {
                continue;
            }
            $files[$this->normalize_path($frame['file'])] = \true;
        }
        return $files;
    }
    /**
     * Register a shutdown callback to clear the stack once.
     *
     * @return void
     *
     * @since 2.1.2
     */
    protected function ensure_shutdown_registered()
    {
        if ($this->shutdown_registered) {
            return;
        }
        $this->shutdown_registered = \true;
        add_action('shutdown', function () {
            $this->clear();
        }, 999);
    }
    /**
     * Normalize a filesystem path for comparison.
     *
     * @param string $path Absolute path.
     *
     * @return string
     *
     * @since 2.1.2
     */
    protected function normalize_path(string $path)
    {
        $real = \realpath($path);
        if ($real !== \false) {
            return $real;
        }
        return \str_replace('\\', '/', $path);
    }
}
