<?php
namespace nur\v\base;

use nur\A;
use nur\b\ValueException;
use nur\func;
use nur\md;
use nur\str;
use nur\v\html5\Html5BasicErrorPage;
use nur\v\model\IPage;
use nur\v\model\IRouteManager;

class RouteManager implements IRouteManager {
  const DEFAULT_PAGE = Html5BasicErrorPage::class;

  function __construct(?array $routes=null) {
    $this->eroutes = [];
    $this->proutes = [];
    if ($routes !== null) $this->addRoute(...$routes);
  }

  private static function check_page_type($page): array {
    if (is_array($page) && array_key_exists(0, $page) && is_string($page[0])) {
      return [$page[0], $page];
    } elseif ($page instanceof IPage) {
      return [$page, $page];
    } elseif (is_string($page)) {
      return [$page, [$page]];
    }
    throw ValueException::unexpected_type(IPage::class, $page);
  }

  /** @param $args IPage|array */
  private static function get_page($args): IPage {
    if ($args instanceof IPage) return $args;
    else return func::cons(...$args);
  }

  /** @var IPage|array */
  protected $errorPage;

  function setErrorPage($page) {
    $this->errorPage = self::check_page_type($page)[1];
  }

  /**
   * @var array routes pour des chemins exacts. elles ont la priorité sur les
   * préfixes
   */
  protected $eroutes;

  /** @var array routes pour des préfixes de chemin */
  protected $proutes;

  private static function get_package(?string $class): ?string {
    if ($class === null) return null;
    str::del_prefix($class, "\\");
    if (($pos = strrpos($class, "\\")) === false) return "";
    else return substr($class, 0, $pos);
  }

  function addRoute(array ...$routes): void {
    $eroutes =& $this->eroutes;
    $proutes =& $this->proutes;
    foreach ($routes as $route) {
      md::ensure_schema($route, self::ROUTE_SCHEMA, null, false);
      str::del_prefix($route["path"], "/");
      $path = $route["path"];
      [$page, $cons_args] = self::check_page_type($route["page"]);
      $route["page"] = $page;
      $route["cons_args"] = $cons_args;
      $route["package"] = self::get_package($route["page"]);
      A::ensure_array($route["aliases"]);
      switch ($route["mode"]) {
      case self::MODE_EXACT:
        # route exacte
        $eroutes[$path] = $route;
        break;
      case self::MODE_PREFIX:
      case self::MODE_PACKAGE:
      case self::MODE_PACKAGE2:
        if ($path && !str::ends_with("/", $path)) {
          # chemin ne se terminant pas par '/': faire aussi la correspondance
          # exacte
          $eroutes[$path] = $route;
        }
        $proutes[$page] = $route;
        break;
      }
      # faire les aliases
      foreach ($route["aliases"] as $alias) {
        str::del_prefix($alias, "/");
        $route["path"] = $alias;
        $route["mode"] = self::MODE_EXACT;
        $eroutes[$alias] = $route;
      }
    }
  }

  /**
   * Calculer $page à partir de $path avec les règles suivantes:
   * - supprimer le suffixe '.php'
   * - remplacer '/' et '--' par '\\'
   * - pour chaque élément, transformer camel-case en CamelCase
   * - rajouter le suffixe Page
   *
   * voici comment est calculée la valeur de départ:
   * - si $path se termine par '.php', prendre le chemin SANS le suffixe '.php'
   * - si $path est la forme '*.php/SUFFIX', prendre SUFFIX comme valeur de
   * départ
   */
  private function computePackagePage(?string $path, string $package): string {
    str::del_prefix($path, "/");
    if (str::ends_with(".php", $path)) {
      str::del_suffix($path, ".php");
    } elseif (($pos = strpos($path, ".php/")) !== false) {
      $path = substr($path, $pos + 5);
    }
    $path = str_replace("--", "/", $path);

    $parts = explode("/", "$path-page");
    $last = count($parts) - 1;
    for ($i = 0; $i <= $last; $i++) {
      $parts[$i] = str::us2camel($parts[$i]);
      if ($i == $last) $parts[$i] = str::upper1($parts[$i]);
    }
    str::add_suffix($package, "\\", true);
    return $package.implode("\\", $parts);
  }

  /**
   * sur la base des routes définies, résoudre la classe à instancier pour
   * traiter le chemin spécifié
   *
   * @return IPage|array
   */
  function resolvePage(?string $path) {
    if ($path === null) $path = $_SERVER["PHP_SELF"];
    str::del_prefix($path, "/");

    # essayer d'abord des routes exactes
    $route = A::get($this->eroutes, $path);
    if ($route !== null) return $route["cons_args"];
    # puis, essayer dans l'ordre les routes basées sur des préfixes
    foreach ($this->proutes as $route) {
      switch ($route["mode"]) {
      case self::MODE_PREFIX:
        $prefix = $route["path"];
        if ($prefix) str::add_suffix($prefix, "/", true);
        if (str::starts_with($prefix, $path)) return $route["cons_args"];
        break;
      }
    }
    # chemin non trouvé, essayer de calculer à partir de $path
    $page = false;
    foreach ($this->proutes as $route) {
      switch ($route["mode"]) {
      case self::MODE_PACKAGE:
      case self::MODE_PACKAGE2:
        $prefix = $route["path"];
        if ($prefix) str::add_suffix($prefix, "/", true);
        if (str::starts_with($prefix, $path)) {
          $page = $this->computePackagePage(
            str::without_prefix($prefix, $path),
            $route["package"]);
          if (class_exists($page)) return [$page];
        }
        break;
      }
    }
    # sinon prendre la page par défaut (qui est en fait la page d'erreur)
    $error = $this->errorPage;
    if ($error === null) $error = [static::DEFAULT_PAGE];
    if (is_array($error)) {
      if ($page) {
        A::merge($error, ["$path: unable to find page class $page"]);
      } else {
        A::merge($error, ["$path: unable to find page class"]);
      }
    }
    return $error;
  }

  function getPage(?string $path=null): IPage {
    $page = $this->resolvePage($path);
    if ($page instanceof IPage) return $page;
    else return func::cons(...$page);
  }

  /**
   * Calculer $path à partir de $page avec les règles suivantes:
   * - supprimer le suffixe Page
   * - pour chaque élément, transformer CamelCase en camel-case
   * - remplacer '\\' par '/'
   * - rajouter le suffixe '.php'
   *
   * NB: cette fonction n'est pas la symétrique de {@link computePackagePage()}.
   * par exemple:
   * - computePackagePage() transforme 'p--do-it.php' en 'p\\DoItPage'
   * - computePackagePath() transforme 'p\\DoItPage' en 'p/do-it.php'
   * pour les cas particulier, il vaut donc mieux faire des routes exactes
   */
  private function computePackagePath(string $prefix, string $page, string $package, string $sep="/"): string {
    # préfixe
    str::add_suffix($prefix, "/", true);
    # classe
    if ($package) str::del_prefix($page, "$package\\");
    str::del_suffix($page, "Page");
    $parts = explode("\\", $page);
    $last = count($parts) - 1;
    for ($i = 0; $i <= $last; $i++) {
      if ($i == $last) $parts[$i] = str::lower1($parts[$i]);
      $parts[$i] = str::camel2us($parts[$i], false, "-");
    }
    $path = implode($sep, $parts);
    return "$prefix$path.php";
  }

  /**
   * obtenir le chemin correspondant à l'instance de {@link IPage} ou à la
   * classe spécifiée.
   *
   * @param string|IPage $page instance ou classe de la page dont on veut le
   * chemin depuis la racine de l'application
   */
  function getPath($page): string {
    if ($page instanceof IPage) {
      $page = get_class($page);
    } elseif (!is_string($page)) {
      throw ValueException::unexpected_type(["string", IPage::class], $page);
    }
    # d'abord les chemins exacts
    foreach ($this->eroutes as $route) {
      if ($page === $route["page"]) return $route["path"];
    }
    # puis les préfixes
    foreach ($this->proutes as $route) {
      switch ($route["mode"]) {
      case self::MODE_PREFIX:
        if ($page === $route["page"]) return $route["path"];
        break;
      }
    }
    # puis les packages
    foreach ($this->proutes as $route) {
      $mode = $route["mode"];
      switch ($mode) {
      case self::MODE_PACKAGE:
      case self::MODE_PACKAGE2:
        $package = $route["package"];
        if (str::starts_with($package, $page)) {
          if ($mode == self::MODE_PACKAGE) $sep = "/";
          else $sep = "--";
          return $this->computePackagePath($route["path"], $page, $package, $sep);
        }
        break;
      }
    }
    # pas trouvé
    throw new ValueException(": $page: unable to find path");
  }
}
