<?php
namespace nur\v\base;

use nur\A;
use nur\b\coll\BaseArray;
use nur\b\coll\GenericArray;
use nur\b\ExitError;
use nur\co;
use nur\config;
use nur\func;
use nur\v\html5\Html5BasicErrorPage;
use nur\v\model\IChildComponent;
use nur\v\model\IComponent;
use nur\v\model\IErrorPage;
use nur\v\model\IPage;
use nur\v\model\IPageContainer;
use nur\v\model\IPlugin;
use nur\v\page;
use nur\v\prefix;
use nur\v\vo;
use Throwable;

abstract class AbstractPageContainer implements IPageContainer {
  protected static function ensure_preparec(IComponent $c, bool $afterPrepare=false): bool {
    if (!$c->didPrepare()) {
      $c->beforePrepare();
      $c->prepare();
      if ($afterPrepare) $c->afterPrepare();
      return true;
    }
    return false;
  }
  
  protected static function ensure_configc(IComponent $c, array &$config, bool $afterConfig=false): bool {
    if (!$c->didConfig()) {
      $c->beforeConfig($config);
      $c->config($config);
      if ($afterConfig) $c->afterConfig();
      return true;
    }
    return false;
  }
  
  protected static function ensure_setupc(IComponent $c, bool $afterSetup=false): bool {
    if (!$c->didSetup()) {
      $c->beforeSetup();
      $c->setup();
      if ($afterSetup) $c->afterSetup();
      return true;
    }
    return false;
  }
  
  protected static function ensure_teardownc(IComponent $c, bool $afterTeardown=false): bool {
    if (!$c->didTeardown()) {
      $c->beforeTeardown();
      $c->teardown();
      if ($afterTeardown) $c->afterTeardown();
      return true;
    }
    return false;
  }
  
  protected static function printc(IComponent $c, ?array &$config): void {
    self::ensure_preparec($c, true);
    self::ensure_configc($c, $config, true);
    self::ensure_setupc($c, true);
    try {
      if ($c->haveContent()) vo::write($c);
    } finally {
      self::ensure_teardownc($c, true);
    }
  }

  #############################################################################

  /** configurer les classes utilisées dans le cadre de ce container */
  protected abstract function initToolkit(): void;

  function __construct(?array $config=null) {
    $this->initToolkit();
    $this->config = [
      "configure_options" => null,
      "error_page_class" => null,
      "self" => null,
      "prefix" => prefix::compute(null),
      "title" => null,
      "css" => [],
      "js" => [],
      "plugins" => [],
    ];
    $css = A::getdel($config, "css");
    $js = A::getdel($config, "js");
    $plugins = A::getdel($config, "plugins");
    A::merge_nz($this->config, $config);
    A::merge($this->config["css"], $css);
    A::merge($this->config["js"], $js);
    A::merge($this->config["plugins"], $plugins);
    $this->params = new GenericArray();
    $this->components = [];
  }

  /** @return array les options par défaut pour {@link config::configure()} */
  protected function CONFIGURE_OPTIONS(): ?array {
    return static::CONFIGURE_OPTIONS;
  } const CONFIGURE_OPTIONS = null;

  /** @return string la page utilisée pour afficher les erreurs */
  protected function ERROR_PAGE_CLASS(): string {
    return static::ERROR_PAGE_CLASS;
  } const ERROR_PAGE_CLASS = Html5BasicErrorPage::class;

  /**
   * @return string chemin du script correspondant à cette page, depuis la racine
   * de l'application e.g. index.php, ou null s'il faut utiliser la valeur par
   * défaut
   */
  protected function SELF(): ?string {
    return static::SELF;
  } const SELF = null;

  /** @return string titre de la page */
  protected function TITLE(): ?string {
    return static::TITLE;
  } const TITLE = null;

  /** @return ?string|array liste de feuilles CSS à charger dans la page */
  protected function CSS() {
    return static::CSS;
  } const CSS = null;

  /** @return ?string|array liste de scripts à charger dans la page */
  protected function JS() {
    return static::JS;
  } const JS = null;

  /** @return ?string|array liste de plugins à ajouter à cette page */
  protected function PLUGINS() {
    return static::PLUGINS;
  } const PLUGINS = null;

  /** @var array */
  protected $config;

  protected function initConfig(): void {
    A::replace_z($this->config, "configure_options", $this->CONFIGURE_OPTIONS());
    A::replace_z($this->config, "error_page_class", $this->ERROR_PAGE_CLASS());
    $self = $this->SELF();
    if ($self !== null) {
      A::merge($this->config, [
        "self" => $self,
        "prefix" => prefix::compute($self),
      ]);
    }
    A::replace_z($this->config, "title", $this->TITLE());
    A::merge($this->config["css"], $this->CSS());
    A::merge($this->config["js"], $this->JS());
    A::merge($this->config["plugins"], $this->PLUGINS());
  }

  function getConfig(): array { return $this->config; }
  function getErrorPage(): IErrorPage {
    # NB: cette fonction peut être appelée AVANT initConfig()
    $error_page_class = $this->config["error_page_class"];
    if ($error_page_class === null) $error_page_class = $this->ERROR_PAGE_CLASS();
    if ($error_page_class === null) $error_page_class = Html5BasicErrorPage::class;
    $error_page = new $error_page_class();
    $error_page->initContainer($this);
    return $error_page;
  }
  function getSelf(): ?string { return $this->config["self"]; }
  function getSelfRelativePrefix(): string { return $this->config["prefix"]; }
  function getTitle(): string { return $this->config["title"]; }
  function getCssUrls(): array { return $this->config["css"]; }
  function getJsUrls(): array { return $this->config["js"]; }
  function getPlugins(): array { return $this->config["plugins"]; }

  /** @var BaseArray */
  protected $params;

  function getParams(): BaseArray {
    return $this->params;
  }

  function haveError(): bool {
    return boolval($this->params["page_error"]);
  }

  function setError(?string $message, ?Throwable $exception=null): void {
    $this->params["page_error"] = [
      "message" => $message,
      "exception" => $exception,
    ];
  }

  function getError(): ?array {
    return $this->params["page_error"];
  }

  # null = init, 0 = prepare, 1 = config, 2 = setup, 3 = print, 4 = teardown
  const INIT_PHASE = null;
  const PREPARE_PHASE = 0;
  const CONFIG_PHASE = 1;
  const SETUP_PHASE = 2;
  const PRINT_PHASE = 3;
  const TEARDOWN_PHASE = 4;

  protected $phase;

  protected function ensure_phasec(IComponent $c) {
    $phase = $this->phase;
    if ($phase !== self::INIT_PHASE) {
      if ($phase >= self::PREPARE_PHASE) self::ensure_preparec($c);
      if ($phase >= self::CONFIG_PHASE) self::ensure_configc($c, $this->config);
      if ($phase >= self::SETUP_PHASE) self::ensure_setupc($c);
      if ($phase >= self::TEARDOWN_PHASE) self::ensure_teardownc($c);
    }
  }

  /** @var array composants enregistrés dans ce container, indexés par nom */
  protected $components;

  function addPlugin($plugin, ?string $name=null): IPlugin {
    if (is_string($plugin)) $plugin = new $plugin();
    elseif (is_array($plugin)) $plugin = func::cons(...$plugin);
    if ($plugin instanceof IChildComponent) $plugin->initContainer($this);
    if ($plugin instanceof IComponent) $this->ensure_phasec($plugin);
    A::set($this->components, $name, $plugin);
    return $plugin;
  }

  /** @var IPage composant de page à afficher dans ce container */
  protected $page;

  function setPage(IPage $page): void {
    if ($page instanceof IChildComponent) $page->initContainer($this);
    $this->page = $page;
    $this->haveOutput = false;
  }

  /** @var bool ce container a-t-il déjà commencé à afficher du contenu? */
  protected $haveOutput;

  function haveOutput(): bool {
    return $this->haveOutput;
  }

  function print(): void {
    $page = $this->page;
    page::set_current_page($page);

    try {
      $this->phase = self::PREPARE_PHASE;
      if (self::ensure_preparec($page)) {
        $this->overridePrepare($page);
        $page->afterPrepare();
      }

      $this->phase = self::CONFIG_PHASE;
      $this->initConfig();
      if (self::ensure_configc($page, $this->config)) {
        $this->overrideConfig($page);
        $page->afterConfig();
      }
      config::configure($this->config["configure_options"]);

      $this->phase = self::SETUP_PHASE;
      if (self::ensure_setupc($page)) {
        $this->overrideSetup($page);
        $page->afterSetup();
      }

      $this->phase = self::PRINT_PHASE;
      $this->overridePrint($page);

    } catch (Throwable $e) {
      if ($e instanceof ExitError && !$e->isError()) {
        # NOP
      } else {
        $this->setError(null, $e);
      }

    } finally {
      if ($this->haveError()) {
        $errorPage = $this->getErrorPage();
        if ($this->haveOutput()) {
          # n'afficher que l'erreur si on a déjà du contenu
          $errorPage->printError();
        } else {
          # sinon afficher la page entière
          $errorPage->print();
        }
      }
      if ($page->didSetup()) {
        $this->phase = self::TEARDOWN_PHASE;
        if (self::ensure_teardownc($page)) {
          $this->overrideTeardown($page);
          $page->afterTeardown();
        }
      }
    }
  }

  protected function overridePrepare(IPage $page): void {
  }

  protected function overrideConfig(IPage $page): void {
  }

  protected function overrideSetup(IPage $page): void {
  }

  protected function overridePrint(IPage $page): void {
    if ($page->haveContent()) {
      $this->haveOutput = true;
      co::_print([$page]);
    }
  }

  protected function overrideTeardown(IPage $page): void {
  }
}
