<?php
namespace nulib\schema;

use ArrayAccess;
use nulib\AccessException;
use nulib\cl;
use nulib\php\types\varray;
use nulib\php\types\vbool;
use nulib\php\types\vcontent;
use nulib\php\types\vfunc;
use nulib\php\types\vpkey;
use nulib\ref\schema\ref_schema;
use nulib\ref\schema\ref_types;
use nulib\schema\_assoc\AssocSchema;
use nulib\schema\_list\ListSchema;
use nulib\schema\_scalar\ScalarSchema;
use nulib\schema\types\IType;
use nulib\schema\types\trawstring;

/**
 * Class Schema
 *
 * @property-read array|IType $type
 * @property-read mixed $default
 * @property-read string|null $title
 * @property-read bool $required
 * @property-read bool $nullable
 * @property-read string|array|null $desc
 * @property-read callable|null $analyzerFunc
 * @property-read callable|null $extractorFunc
 * @property-read callable|null $parserFunc
 * @property-read callable|null $normalizerFunc
 * @property-read array|null $messages
 * @property-read callable|null $formatterFunc
 * @property-read mixed $format
 * @property-read array $nature
 * @property-read array|null $schema
 * @property-read string|int|null $name
 * @property-read string|array|null $pkey
 * @property-read string|null $header
 * @property-read bool|null $computed
 */
abstract class Schema implements ArrayAccess {
  /**
   * créer le cas échéant une nouvelle instance de cette classe à partir d'une
   * définition de schéma
   *
   * - si $schema est une instance de schéma, la retourner
   * - si $schema est un array, c'est une définition, et elle est remplacée par
   * l'instance de Schema nouvelle créée
   * - sinon, prendre $definition comme définition
   */
  static function ns($definition=null, $definitionKey=null, &$schema=null, bool $normalize=true): self {
    if (is_array($schema)) {
      $definition = $schema;
      $schema = null;
    }
    if ($schema === null) {
      if (AssocSchema::isa_definition($definition)) {
        $schema = new AssocSchema($definition, $definitionKey, $normalize);
      } elseif (ListSchema::isa_definition($definition)) {
        $schema = new ListSchema($definition, $definitionKey, $normalize);
      } elseif (ScalarSchema::isa_definition($definition)) {
        $schema = new ScalarSchema($definition, $definitionKey, $normalize);
      } else {
        throw SchemaException::invalid_schema();
      }
    }
    return $schema;
  }

  /**
   * Créer une nouvelle instance de {@link Wrapper} qui référence la
   * variable $value (si $valueKey===null) ou $value[$valueKey] si $valueKey
   * n'est pas null
   */
  static function nw(&$value=null, $valueKey=null, $definition=null, &$schema=null, ?Wrapper &$wrapper=null): Wrapper {
    if ($definition === null) {
      # bien que techniquement, $definition peut être null (il s'agit alors du
      # schéma d'un scalaire quelconque), on ne l'autorise pas ici
      throw SchemaException::invalid_schema("definition is required");
    }
    return self::ns($definition, null, $schema)->getWrapper($value, $valueKey, null, $wrapper);
  }

  protected static function have_nature(array $definition, ?string &$nature=null): bool {
    $definitionNature = $definition[""] ?? null;
    if (is_string($definitionNature)) {
      $nature = $definitionNature;
      return true;
    }
    if (is_array($definitionNature)
      && array_key_exists(0, $definitionNature)
      && is_string($definitionNature[0])) {
      $nature = $definitionNature;
      return true;
    }
    return false;
  }

  protected static function _normalize_definition(&$definition, $definitionKey=null, ?array $natureMetaschema=null): void {
    if (!is_array($definition)) $definition = [$definition];
    # s'assurer que toutes les clés existent avec leur valeur par défaut
    $index = 0;
    foreach (array_keys(ref_schema::VALUE_METASCHEMA) as $key) {
      if (!array_key_exists($key, $definition)) {
        if (array_key_exists($index, $definition)) {
          $definition[$key] = $definition[$index];
          unset($definition[$index]);
          $index++;
        } else {
          $definition[$key] = ref_schema::VALUE_METASCHEMA[$key][1];
        }
      }
    }
    # réordonner les clés numériques
    if (cl::have_num_keys($definition)) {
      $keys = array_keys($definition);
      $index = 0;
      foreach ($keys as $key) {
        if (!is_int($key)) continue;
        if ($key !== $index) {
          $definition[$index] = $definition[$key];
          unset($definition[$key]);
        }
        $index++;
      }
    }
    # type
    $deftype = $definition["type"];
    $nullable = $definition["nullable"] ?? false;
    if ($deftype === null) {
      $types[] = null;
      $nullable = true;
    } else {
      if (!is_array($deftype)) {
        if (!is_string($deftype)) throw SchemaException::invalid_type($deftype);
        $deftype = explode("|", $deftype);
      }
      $types = [];
      $unionTypes = [];
      $index = 0;
      foreach ($deftype as $key => $type) {
        $args = null;
        if ($key === $index) {
          $index++;
        } else {
          $args = $type;
          $type = $key;
        }
        if ($type !== null) $type = trim($type);
        if ($type === null || $type === "null") {
          $nullable = true;
          continue;
        }
        if (!is_string($type)) throw SchemaException::invalid_type($type);
        if (substr($type, 0, 1) == "?") {
          $type = substr($type, 1);
          $nullable = true;
        }
        if ($type === "") throw SchemaException::invalid_type($type);
        $type = cl::get(ref_types::ALIASES, $type, $type);
        if ($args === null) {
          $unionTypes = array_merge($unionTypes, explode("|", $type));
        } else {
          $types = array_merge($types, [$type => $args]);
        }
      }
      if (!$types && !$unionTypes) throw SchemaException::invalid_schema("scalar: type is required");
      foreach ($unionTypes as $type) {
        if (!array_key_exists($type, $types)) {
          $types[] = $type;
        }
      }
    }
    $definition["type"] = $types;
    $definition["nullable"] = $nullable;
    # nature
    $nature = $definition[""];
    varray::ensure($nature);
    $natureMetaschema ??= ref_schema::NATURE_METASCHEMA;
    foreach (array_keys($natureMetaschema) as $key) {
      if (!array_key_exists($key, $nature)) {
        $nature[$key] = $natureMetaschema[$key][1];
      }
    }
    $definition[""] = $nature;
    # name, pkey, header
    $name = $definition["name"];
    $pkey = $definition["pkey"];
    $header = $definition["header"];
    if ($name === null) $name = $definitionKey;
    trawstring::ensure_nstring($name);
    vpkey::ensuren($pkey);
    trawstring::ensure_nstring($header);
    if ($pkey === null) $pkey = $name;
    if ($header === null) $header = $name;
    $definition["name"] = $name;
    $definition["pkey"] = $pkey;
    $definition["header"] = $header;
    # autres éléments
    varray::ensuren($definition["schema"]);
    trawstring::ensure_nstring($definition["title"]);
    vbool::ensure($definition["required"]);
    vbool::ensure($definition["nullable"]);
    vcontent::ensuren($definition["desc"]);
    vfunc::ensuren($definition["analyzer_func"]);
    vfunc::ensuren($definition["extractor_func"]);
    vfunc::ensuren($definition["parser_func"]);
    vfunc::ensuren($definition["normalizer_func"]);
    varray::ensuren($definition["messages"]);
    vfunc::ensuren($definition["formatter_func"]);
    vbool::ensuren($definition["computed"]);

    switch ($nature[0] ?? null) {
    case "assoc":
      foreach ($definition["schema"] as $key => &$keydef) {
        self::_normalize_definition($keydef, $key);
      }; unset($keydef);
      break;
    case "list":
      self::_normalize_definition($definition["schema"]);
      break;
    }
  }

  protected static function _ensure_nature(array $definition, string $expectedNature, ?string $expectedType=null): void {
    $nature = $definition[""];
    if (!array_key_exists(0, $nature) || $nature[0] !== $expectedNature) {
      throw SchemaException::invalid_schema("$nature: invalid nature. expected $expectedNature");
    }
    if ($expectedType !== null) {
      $types = $definition["type"];
      if (count($types) !== 1 || $types[0] !== $expectedType) {
        throw new SchemaException("{$types[O]}: invalide type. expected $expectedType");
      }
    }
  }

  protected static function _ensure_type(array &$definition): void {
    $types = $definition["type"];
    $nullable = $definition["nullable"];
    # s'il n'y a qu'une seul type, l'instancier tout de suite
    if (is_array($types) && count($types) == 1 && cl::first($types) !== null) {
      foreach ($types as $key => $name) {
        if ($key === 0) {
          $args = null;
        } else {
          $args = $name;
          $name = $key;
        }
        $definition["type"] = types::get($nullable, $name, $args, $definition);
      }
    }
    switch ($definition[""][0]) {
    case "assoc":
      foreach ($definition["schema"] as &$keydef) {
        self::_ensure_type($keydef);
      }; unset($keydef);
      break;
    case "list":
      self::_ensure_type($definition["schema"]);
      break;
    }
  }

  protected static function _ensure_schema_instances(array &$definition): void {
    switch ($definition[""][0]) {
    case "assoc":
      foreach ($definition["schema"] as &$keydef) {
        self::_ensure_schema_instances($keydef);
        Schema::ns(null, null, $keydef, false);
      }; unset($keydef);
      break;
    case "list":
      Schema::ns(null, null, $definition["schema"], false);
      break;
    }
  }

  /**
   * @var array définition du schéma, à redéfinir le cas échéant dans une classe
   * dérivée
   */
  const SCHEMA = null;

  protected array $_definition;

  protected array $definition;

  function getDefinition(): array {
    return $this->_definition;
  }

  /**
   * retourner la liste des clés valides pour l'accès aux valeurs et résultats
   */
  abstract function getKeys(): array;

  abstract function getSchema($key=false): Schema;

  abstract protected function newWrapper(): Wrapper;

  abstract function getWrapper(&$value=null, $valueKey=null, ?array $params=null, ?Wrapper &$wrapper=null): Wrapper;

  #############################################################################
  # key & properties

  function offsetExists($offset): bool {
    return array_key_exists($offset, $this->definition);
  }
  function offsetGet($offset) {
    if (!array_key_exists($offset, $this->definition)) return null;
    else return $this->definition[$offset];
  }
  function offsetSet($offset, $value): void {
    throw AccessException::read_only(null, $offset);
  }
  function offsetUnset($offset): void {
    throw AccessException::read_only(null, $offset);
  }

  const _PROPERTY_PKEYS = [
    "analyzerFunc" => "analyzer_func",
    "extractorFunc" => "extractor_func",
    "parserFunc" => "parser_func",
    "normalizerFunc" => "normalizer_func",
    "formatterFunc" => "formatter_func",
    "nature" => ["", 0],
  ];

  function __get($name) {
    $pkey = cl::get(static::_PROPERTY_PKEYS, $name, $name);
    return cl::pget($this->definition, $pkey);
  }
}
