<?php

namespace WordpressModels\Page;

use WordpressModels\Assets;

/**
 * Abstract page service class.
 *
 * This class is used to create WordPress admin pages. It wraps a couple of hooks and provides a basic structure for
 * page services.
 *
 * This class makes it easier for developers to create WordPress admin pages, as no manual hooking is required.
 * By default, this class renders an empty div with the page ID as the class name. This can be overridden by
 * extending this class and implementing the renderPage method. This can be done in different ways, for example by
 * using the Timber library to render a Twig template, or the wc_get_template function to render a WooCommerce template.
 *
 * The developer should provide and implement the buildContext method to provide the context for the page, or to localize
 * scripts. This method is called when the page is rendered and the context is passed to the script localization function.
 *
 * Using Dependency Injection, any subclass of the AbstractPage class will be automatically tagged as a service with the
 * 'wpm_page' tag. This allows the PageRegistry to automatically collect all page services and register them with WordPress.
 *
 * The {@see self::buildContext()} method is also used by the {@see AsyncPageData} service to provide data to the
 * client-side application. This provides asynchoronous data fetching for the client-side application, and authorization
 * checks are done in the {@see AsyncPageData} service. See the docs on client-side admin pages for more information.
 *
 * Other instances of the AbstractPage class can be used as parent pages, which prevents developers from having to
 * manually set the parent page slug. This is done by setting the parent property to an instance of AbstractPage.
 *
 * @changelog 0.6.0 - moved menu item registration to {@see PageRegistry}
 */
abstract class AbstractPage implements PageInterface
{
    protected string|false $pageLoadHook;
    protected string|array|\Closure $renderCallback;

    /**
     * @param string $pageId The page identifier.
     * @param string $menuSlug The WordPress admin menu slug.
     * @param string $title The page title, used for both the menu item and the page title.
     * @param string|AbstractPage $parent The parent page slug or an instance of AbstractPage.
     * @param string $capability The capability required to view the page.
     * @param int|string|null $position The position in the menu.
     * @param string $icon The icon for the menu item.
     * @param array $additionalScripts Additional script IDs to enqueue.
     * @param Assets|null $assets The assets service, required for script enqueuing.
     */
    public function __construct(
        protected string              $pageId,
        protected string              $menuSlug,
        protected string              $title,
        protected string|AbstractPage $parent = 'toplevel',
        protected string              $capability = 'administrator',
        protected int|string|null     $position = null,
        protected string              $icon = '',
        protected array               $additionalScripts = [],
        protected readonly ?Assets    $assets = null
    )
    {
        $this->renderCallback = [$this, 'renderPage'];
    }

    /**
     * Initializes the page.
     *
     * This method is automatically registered with the WordPress load hook for the page.
     *
     * @return void
     * @hooked load-{parent}_page_{slug}
     */
    public function init(): void
    {
        add_action('admin_enqueue_scripts', $this->enqueueScripts(...));
    }

    /**
     * Renders the page.
     *
     * @changelog 0.6.0 Removed the dependency on Timber, as it is not necessary for the page to be rendered and
     *                  clutters the dependency graph.
     *
     * @return void
     */
    public function renderPage(): void
    {
        echo "<div id='root' class='$this->pageId'></div>";
    }

    /**
     * Build the page context;
     *
     * @changelog 0.6.0 This method now always returns an array.
     *                  This method was previously used to build the page context for a Timber page, this has
     *                  been changed to a context builder for script localization.
     *
     * @param array $parameters Base context provided with Timber::context(), the page ID and title.
     * @return array -- an associative array of context variables.
     */
    abstract public function buildContext(array $parameters = []): array;

    /**
     * Enqueue scripts based on the page ID. Also enqueues additional scripts (as entry ids).
     *
     * @return void
     * @see Assets::enqueueCompiledScript()
     */
    public function enqueueScripts(): void
    {
        if ($this->assets) {
            $this->assets->enqueueCompiledScript($this->pageId);

            wp_localize_script($this->pageId, 'page', [
                'id' => $this->pageId,
                'title' => $this->title,
                'baseUrl' => $_SERVER['REQUEST_URI'],
                'async_data_nonce' => wp_create_nonce('wpm_load_page_data_nonce'),
                ...$this->buildContext()
            ]);

            foreach ($this->additionalScripts as $script) {
                $this->assets->enqueueCompiledScript($script);
            }
        }

        $this->doEnqueue();
    }


    /**
     * @return void
     * @deprecated 0.6.0 Override {@see self::enqueueScripts()} instead. This method is deemed unnecessary.
     */
    public function doEnqueue(): void
    {
    }

    /**
     * @return string
     */
    public function getPageId(): string
    {
        return $this->pageId;
    }

    /**
     * @return string
     */
    public function getParent(): string
    {
        return $this->parent instanceof AbstractPage ? $this->parent->menuSlug : $this->parent;
    }

    /**
     * @return string
     */
    public function getMenuSlug(): string
    {
        return $this->menuSlug;
    }

    /**
     * @return string
     */
    public function getCapability(): string
    {
        return $this->capability;
    }

    /**
     * @return string
     */
    public function getTitle(): string
    {
        return $this->title;
    }

    /**
     * @return float|int|null
     */
    public function getPosition(): float|int|null
    {
        return $this->position;
    }

    /**
     * @return string
     */
    public function getIcon(): string
    {
        return $this->icon;
    }

    public function getRenderCallback(): string|array|\Closure
    {
        return $this->renderCallback;
    }

    /**
     * Convenience method to get the URL of the page.
     *
     * @return string
     * @todo use decorator to get the URL via AbstractPageStack, prevent circular dependency
     */
    public function getUrl(): string
    {
        return $this->parent instanceof AbstractPageStack
            ? $this->parent->getRouteUrl($this->pageId)
            : get_admin_url(path: 'admin.php?page=' . $this->menuSlug);
    }

    public function getScriptUrl(): ?string
    {
        return $this->assets ? $this->assets->getAssetsUrls($this->pageId)['js'] : null;
    }


    // protected setters used by the AbstractPageStack class to override properties

    protected function setRenderCallback(string|array|\Closure $renderCallback): void
    {
        $this->renderCallback = $renderCallback;
    }

    protected function setParent(AbstractPage|string $parent): void
    {
        $this->parent = $parent;
    }

    protected function setMenuSlug(string $menuSlug): void
    {
        $this->menuSlug = $menuSlug;
    }

    protected function setAdditionalScripts(array $additionalScripts): void
    {
        $this->additionalScripts = $additionalScripts;
    }

}
