<?php
namespace nur\data\types;

use nur\A;
use nur\b\params\IParametrable;
use nur\b\ValueException;
use nur\base;
use nur\md;
use nur\types;

/**
 * Class Metadata: gestion de données conformes à un schéma
 *
 * un schéma est un tableau d'éléments {$key => $sfield} où $sfield est un
 * tableau conforme au schéma {@link md::MSFIELD_SCHEMA}. Si $sfield n'est pas
 * un tableau, alors c'est la valeur par défaut
 *
 * l'élément {"" => $sinfos}, où $sinfos est un tableau conforme au schéma
 * {@link MSINFOS_SCHEMA}, est particulier: il définit des caractéristiques qui
 * concernent l'objet tout entier.
 *
 * --- ancienne doc à migrer --------------------------------------------------
 * Un méta-schéma sert à décrire les champs d'un schéma de données, e.g
 * ~~~
 * const FIELD_METASCHEMA = [
 *   "name" => [null, null, "nom de la colonne", true],
 *   "title" => [null, null, "libellé de la colonne", false],
 * ];
 * ~~~
 * Ce méta-schéma permettrait de décrire les champs d'un schéma comme celui-là:
 * ~~~
 * const DATA_SCHEMA = [
 *   "nom" => ["name" => "sn", "title" => "nom"],
 *   "prenom" => ["name" => "givenName", "title" => "prénom"],
 *   "pass" => ["name" => "password", "title" => "mot de passe"],
 * ]
 * ~~~
 *
 * Bien entendu, un méta-schéma peut servir aussi de schéma simple pour un
 * tableau de données
 */
class Metadata implements IParametrable {
  use _Tparametrable1;

  private static function itype($type): IType {
    return $type;
  }
  private static function citype($type): AbstractCompositeType {
    return $type;
  }

  static final function with($schema): Metadata {
    if ($schema instanceof Metadata) return $schema;
    elseif (is_array($schema)) return new static($schema);
    else throw ValueException::unexpected_type([Metadata::class, "array"], $schema);
  }

  const ENSURE_KEYS = true;
  const ORDER_KEYS = true;
  const ENSURE_TYPES = true;
  const CHECK_REQUIRED = true;

  const PARAMETRABLE_PARAMS_SCHEMA = [
    "ensure_keys" => ["bool", null, "toutes les clés doivent-elles être présentes?"],
    "order_keys" => ["bool", null, "les clés doivent-elle être dans l'ordre du schéma?"],
    "ensure_types" => ["bool", null, "les valeurs doivent-elles être du bon type?",
      "desc" => "cela ne concerne que les types simples standard",
    ],
    "check_required" => ["bool", null, "vérifier que les valeurs requises sont présentes?"],
  ];

  function __construct(array $schema, ?array $params=null, ?IIncarnation $incarnation=null) {
    if ($incarnation === null) $incarnation = types::manager();
    $this->incarnation = $incarnation;
    $this->schema = self::_normalize_schema($schema);

    $this->initParametrableParams($params);
    base::update_n($this->ppEnsureKeys, static::ENSURE_KEYS);
    base::update_n($this->ppOrderKeys, static::ORDER_KEYS);
    base::update_n($this->ppEnsureTypes, static::ENSURE_TYPES);
    base::update_n($this->ppCheckRequired, static::CHECK_REQUIRED);
  }

  /** @var bool */
  protected $ppEnsureKeys;

  /** @var bool */
  protected $ppOrderKeys;

  /** @var bool */
  protected $ppEnsureTypes;

  /** @var bool */
  protected $ppCheckRequired;

  const SINFOS_SCHEMA = [
    "ctypes" => [
      "type" => "?array",
      "default" => null,
      "title" => "liste de types composites à appliquer à l'objet",
      "required" => false,
      "key" => "ctypes",
      "header" => "ctypes",
      "desc" => null,
      "schema" => null,
      "composite" => false,
    ],
    "apply2items" => [
      "type" => "bool",
      "default" => false,
      "title" => "le schéma s'applique-t-il aux éléments d'une liste séquentielle",
      "required" => false,
      "key" => "apply2items",
      "header" => "apply2items",
      "desc" => null,
      "schema" => null,
      "composite" => false,
    ],
  ];
  const SINFOS_INDEXES = ["ctypes" => 0, "apply2items" => 1];

  protected static function _normalize_schema(array $schema): array {
    if (count($schema) == 1 && array_key_exists(0, $schema)) {
      $apply2items = true;
      $schema = $schema[0];
    } else {
      $apply2items = false;
    }
    $sfields = [];
    $sinfos = ["ctypes" => null, "apply2items" => $apply2items]; # doit être conforme au schéma ci-dessus
    $cokeys = [];
    $cikeys = [];
    foreach ($schema as $key => $sfield) {
      if ($key !== "") {
        md::_ensure_msfield($sfield, $key);

        $type = $sfield["type"];
        $is_array_type = in_array($type, md::ARRAY_TYPES);
        if ($sfield["schema"] !== null) {
          if ($is_array_type) {
            $sfield["schema"] = self::_normalize_schema($sfield["schema"]);
            if (in_array($type, md::APPLY2ITEMS_TYPES)) {
              $sfield["schema"]["sinfos"]["apply2items"] = true;
            }
          } else {
            $sfield["schema"] = null;
          }
        }
        if ($sfield["composite"]) $cokeys[] = $key;
        $sfields[$key] = $sfield;
      } else {
        $sinfos = $sfield;
        md::_ensure_schema($sinfos, self::SINFOS_SCHEMA, self::SINFOS_INDEXES);
        $sinfos["apply2items"] = $apply2items;
        $citypes = $sinfos["ctypes"];
        if ($citypes !== null) $cikeys = array_keys($citypes);
      }
    }; unset($sfield);
    return [
      "sfields" => $sfields,
      "sinfos" => $sinfos,
      "indexes" => array_flip(array_keys($sfields)),
      "cokeys" => $cokeys,
      "cikeys" => $cikeys,
      "types" => null,
    ];
  }

  /** @var IIncarnation */
  protected $incarnation;

  const schema_SCHEMA = [
    "sfields" => ["array", null, "liste des schémas des champs"],
    "sinfos" => ["array", null, "informations sur l'objet entier",
      "schema" => self::SINFOS_SCHEMA,
    ],
    "indexes" => ["array", null, "liste des indexes des clés, de la forme {key => index}"],
    "cokeys" => ["array", null, "liste des clés de champs composantes"],
    "cikeys" => ["array", null, "liste des clés de champs composites"],
    "types" => ["?array", null, "liste des types pour chaque élément de [skeys]"],
  ];

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

  /** obtenir le schéma normalisé */
  function getSchema(): array {
    $schema = ["" => $this->schema["sinfos"]];
    $schema = array_merge($schema, $this->schema["sfields"]);
    if ($this->schema["sinfos"]["apply2items"]) $schema = [$schema];
    return $schema;
  }

  /** retourner la définition des champs */
  function getSfields(): array {
    return $this->schema["sfields"];
  }

  /** retourner les clés des champs non composantes */
  function getSikeys(): array {
    $allKeys = array_keys($this->schema["sfields"]);
    return array_diff($allKeys, $this->schema["cokeys"]);
  }

  /** retourner les clés des champs composantes */
  function getCokeys(): array {
    return $this->schema["cokeys"];
  }

  /** retourner toutes les clés simples */
  function getKeys(): array {
    return array_keys($this->schema["sfields"]);
  }

  /** tester si $key est une clé simple valide */
  function isKey(string $key): bool {
    return in_array($key, $this->getKeys());
  }

  /** retourner les clés des champs composites */
  function getCikeys(): array {
    return $this->schema["cikeys"];
  }

  /** retourner toutes les clés (les clés simples et les clés composites) */
  function getAllKeys(): array {
    $schema = $this->schema;
    return array_merge(array_keys($schema["sfields"]), $schema["cikeys"]);
  }

  /** obtenir l'instance de type correspondant au champ spécifié */
  function getType(string $key, bool $required=true): ?IType {
    $types = $this->_getTypes($this->schema);
    $type = A::get($types, $key);
    if ($type === null && $required) {
      throw ValueException::invalid_value($key, "type");
    }
    return $type;
  }

  protected function _getTypes(array &$schema): array {
    if ($schema["types"] === null) {
      $types = [];
      foreach ($schema["sfields"] as $key => &$sfield) {
        $types[$key] = $this->_buildType($sfield["type"], $sfield);
        if ($sfield["schema"] !== null) {
          $this->_getTypes($sfield["schema"]);
        }
      }
      $ctypes = $schema["sinfos"]["ctypes"];
      if ($ctypes !== null) {
        foreach ($ctypes as $key => $type) {
          $types[$key] = $this->_buildType($type);
        }
      }
      $schema["types"] = $types;
    }
    return $schema["types"];
  }

  private function _buildType($type, $sfield=null): IType {
    if (!$this->incarnation->hasType($type) && is_array($type)) {
      $tkey = array_key_first($type);
      if (count($type) == 1) {
        # syntaxe [tkey => Type], dans ce cas c'est la définition du
        # champ qui est passé en paramètres au type
        $type = [$tkey => $type[$tkey], $sfield];
      }
    }
    return $this->incarnation->getType($type);
  }

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

  /**
   * s'assurer que $item est conforme à la structure du schéma, c'est à dire que
   * tous les champs sont présents dans l'ordre.
   *
   * de plus, par défaut, si les champs ont des types connus, ils sont honorés.
   * les autres champs sont laissés en l'état.
   *
   * $item peut être un tableau séquentiel ou associatif (ou un mix des deux).
   * s'assurer que toutes les clés définies dans le schéma existent dans $item
   * en les créant avec la valeur par défaut le cas échéant.
   *
   * soit une clé $key du schéma:
   * - avec le type par défaut, si $item[$key] === false alors on considère que
   * cette clé n'existe pas et elle est remplacée par la valeur par défaut.
   * Si un type est défini, le comportement peut être différent. cf la doc de
   * {@link md::MSFIELD_SCHEMA} pour les détails
   * - si la clé n'existe pas dans $item (même pas avec la valeur false),
   * alors chercher la première valeur séquentielle qui n'a pas encore été
   * analysée, et prendre cela comme valeur. bien entendu, si la valeur est
   * false, les mêmes règles que ci-dessus s'appliquent
   *
   * après l'appel de cette fonction, $item est un tableau associatif. les
   * clés séquentielles qui n'ont pas été utilisées sont gardées mais
   * renumérotées
   * ~~~
   * $item = [1, 2, 3];
   * $md = new Metadata(["a" => "x", "b" => "y"]);
   * $md->ensureSchema($item);
   * // maintenant, $item vaut ["a" => 1, "b" => 2, 3];
   * ~~~
   *
   * si $recursive==true, alors les éléments ayant un sous-schéma sont examinés
   * et corrigés aussi le cas échéant.
   *
   * Comme cas particulier, si $schema contient un unique élément de type array
   * avec l'index 0, alors $sfield est transformé en tableau et chacun de ses
   * éléments est corrigé pour être conforme au schema $schema[0]. Pour cela,
   * il faut que $recursive==true
   *
   * $key est utilisé dans le cas où les données proviennent d'un tableau de la
   * forme [..., $key => $sfield, ...]
   * si $key n'est pas null, alors on considère que c'est la première valeur de
   * du tableau $sfield. Par exemple, les deux ensembles de commandes suivants
   * sont équivalents:
   * ~~~
   * $schema = ["a" => "x", "b" => "y"];
   * # sans $key
   * $sfield = [1, 2];
   * metaschema::ensure_schema($sfield, $schema);
   * // $sfield vaut maintenant ["a" => 1, "b" => 2]
   * # avec $key
   * $sfield = [2];
   * metaschema::ensure_schema($sfield, $schema, 1);
   * // $sfield vaut maintenant ["a" => 1, "b" => 2]
   * ~~~
   */
  function ensureSchema(&$item, $key=null, ?array $params=null): void {
    $ensureKeys = A::get($params, "ensure_keys", $this->ppEnsureKeys);
    $orderKeys = A::get($params, "order_keys", $this->ppOrderKeys);
    $ensureTypes = A::get($params, "ensure_types", $this->ppEnsureTypes);
    $checkRequired = A::get($params, "check_required", $this->ppCheckRequired);
    $recursive = A::get($params, "recursive", true);

    $schema = $this->schema;
    $this->_ensureArrayItem($item, $schema, $key);
    $this->_ensureSchema($item, $schema, $ensureKeys, $ensureTypes, $recursive);
    if ($orderKeys) $this->orderKeys($item, $recursive);
    if ($checkRequired) $this->checkRequired($item, $recursive);
  }

  private function _ensureArrayItem(&$item, array $schema, $itemKey) {
    if ($itemKey !== null) {
      if (is_array($item)) {
        # n'utiliser key que si la première clé du schéma n'existe pas
        $sfields = $schema["sfields"];
        $first_key = array_key_first($sfields);
        if (!array_key_exists($first_key, $item)) {
          $tmp = [$itemKey];
          A::merge3($tmp, $item);
          $item = $tmp;
        }
      } else {
        $item = [$itemKey, $item];
      }
    } elseif ($item !== null && !is_array($item)) {
      $item = [$item];
    }
  }

  private function _ensureSchema(?array &$item, array $schema, bool $ensureKeys, bool $ensureTypes, bool $recursive, ?bool $apply2items=null): void {
    [
      "sfields" => $sfields,
      "sinfos" => $sinfos,
      "indexes" => $indexes,
      #"cokeys" => $cokeys,
      #"cikeys" => $cikeys,
      #"types" => $types,
    ] = $schema;
    if ($apply2items === null) $apply2items = $sinfos["apply2items"];
    if ($apply2items) {
      $items =& $item; unset($item);
      $index = 0;
      foreach ($items as $key => &$item) {
        if ($key === $index) {
          $index++;
          $key = null;
        }
        $this->_ensureArrayItem($item, $schema, $key);
        $this->_ensureSchema($item, $schema, $ensureKeys, $ensureTypes, $recursive, false);
      }; unset($item);
      return;
    }

    if ($item === null) $item = [];
    $src = $item;
    $keys = array_keys($sfields);
    $dones = array_fill(0, count($keys), false);
    # d'abord les clés associatives
    $inputIndex = 0;
    foreach ($src as $key => $value) {
      if ($key === $inputIndex) {
        # clé séquentielle
        $inputIndex++;
      } else {
        # clé associative
        $is_schema_key = array_key_exists($key, $sfields);
        if ($ensureTypes && $is_schema_key) {
          self::_ensure_type($sfields[$key], $value, true);
        }
        $item[$key] = $value;
        if ($is_schema_key) $dones[$indexes[$key]] = true;
      }
    }
    # ensuite les clés séquentielles
    $inputIndex = 0;
    $outputIndex = 0;
    foreach ($src as $index => $value) {
      if ($index === $inputIndex) {
        # clé séquentielle
        $inputIndex++;
        unset($item[$index]);
        $found = false;
        foreach ($keys as $kindex => $key) {
          if (!$dones[$kindex]) {
            if ($ensureTypes) {
              self::_ensure_type($sfields[$key], $value, true);
            }
            $item[$key] = $value;
            $dones[$kindex] = true;
            $found = true;
            break;
          }
        }
        if (!$found) {
          $item[$outputIndex++] = $value;
        }
      }
    }
    # puis mettre les valeurs par défaut des clés qui restent
    if ($ensureKeys) {
      foreach ($dones as $dindex => $done) {
        if (!$done) {
          $key = $keys[$dindex];
          $sfield = $sfields[$key];
          $value = $sfield["default"];
          if ($ensureTypes) {
            self::_ensure_type($sfield, $value, false);
          }
          $item[$key] = $value;
        }
      }
    }

    if ($recursive) {
      foreach ($sfields as $key => $sfield) {
        $schema2 = $sfield["schema"];
        if ($schema2 === null) continue;
        switch ($sfield["type"]) {
        case "?array":
          if ($item[$key] === null) continue 2;
        case "array":
          $this->_ensureArrayItem($item[$key], $schema2, null);
          $this->_ensureSchema($item[$key], $schema2, $ensureKeys, $ensureTypes, true, false);
          break;
        case "?array[]":
          if ($item[$key] === null) continue 2;
        case "array[]":
          $this->_ensureSchema($item[$key], $schema2, $ensureKeys, $ensureTypes, true, true);
          break;
        }
      }
    }
  }

  private static function _ensure_type($sfield, &$value, bool $exists): void {
    $type = $sfield["type"];
    $default = $sfield["default"];
    if (md::_check_known_type($type, $value, $default, $exists)) {
      md::_convert_value($type, $value);
    }
  }

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

  /** ordonner les champs de item selon l'ordre du schéma */
  function orderKeys(array &$item, bool $recursive=true): void {
    $sfields = $this->schema["sfields"];
    $indexes = $this->schema["indexes"];
    if ($this->schema["sinfos"]["apply2items"]) {
      $this->_eachOrderKeys($sfields, $indexes, $item, $recursive);
    } else {
      $this->_orderKeys($sfields, $indexes, $item, $recursive);
    }
  }

  function _eachOrderKeys(array $sfields, array $indexes, array &$items, bool $recursive) {
    foreach ($items as &$item) {
      $this->_orderKeys($sfields, $indexes, $item, $recursive);
    }; unset($item);
  }

  function _orderKeys(array $sfields, array $indexes, array &$item, bool $recursive) {
    $maxIndex = count($sfields);
    $index = 0;
    $ordered = true;
    foreach (array_keys($item) as $key) {
      if (!key_exists($key, $sfields) || $indexes[$key] !== $index) {
        $ordered = false;
        break;
      }
      $index++;
      if ($index == $maxIndex) break;
    }
    if ($ordered) return;

    $remaining = $item;
    $ordered = [];
    foreach (array_keys($sfields) as $key) {
      if (array_key_exists($key, $remaining)) {
        $ordered[$key] = $remaining[$key];
        unset($remaining[$key]);
      }
    }
    $item = [];
    A::merge2($item, $ordered, $remaining);

    if ($recursive) {
      foreach ($sfields as $key => $sfield) {
        $schema2 = $sfield["schema"];
        if ($schema2 === null || $item[$key] === null) continue;
        $sfields2 = $schema2["sfields"];
        $indexes2 = $schema2["indexes"];
        if ($schema2["sinfos"]["apply2items"]) {
          $this->_eachOrderKeys($sfields2, $indexes2, $item[$key], $recursive);
        } else {
          $this->_orderKeys($sfields2, $indexes2, $item[$key], $recursive);
        }
      }
    }
  }

  /**
   * vérifier que tous les champs marqués comme requis dans le schéma n'ont pas
   * une valeur null. sinon lancer une exception {@link ValueException}
   *
   * $item n'a pas besoin d'être conforme au schéma au préalable
   */
  function checkRequired(array $item, bool $recursive=true): void {
    $sfields = $this->schema["sfields"];
    if ($this->schema["sinfos"]["apply2items"]) {
      $this->_eachCheckRequired($sfields, "", $item, $recursive);
    } else {
      $this->_checkRequired($sfields, "", $item, $recursive);
    }
  }

  private function _eachCheckRequired(array $sfields, $keyPrefix, array $items, bool $recursive): void {
    foreach ($items as $key => $item) {
      $this->_checkRequired($sfields, "$keyPrefix$key.", $item, $recursive);
    }
  }

  private function _checkRequired(array $sfields, $keyPrefix, array $item, bool $recursive, ?bool $apply2items=null): void {
    foreach ($sfields as $key => $sfield) {
      if ($sfield["required"]) {
        if (!array_key_exists($key, $item) || $item[$key] === null) {
          throw new ValueException("$keyPrefix$key is required");
        }
      }
      $schema2 = $sfield["schema"];
      if ($schema2 !== null && $recursive) {
        A::ensure_array($item[$key]);
        $sfields2 = $schema2["sfields"];
        $keyPrefix = "$keyPrefix$key.";
        if ($schema2["sinfos"]["apply2items"]) {
          $this->_eachCheckRequired($sfields2, $keyPrefix, $item[$key], $recursive);
        } else {
          $this->_checkRequired($sfields2, $keyPrefix, $item[$key], $recursive);
        }
      }
    }
  }

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

  /**
   * Vérifier si la clé $key existe dans $item, en tenant compte des
   * informations du schéma
   *
   * $item n'a pas besoin d'être conforme au schéma: il n'est pas nécessaire
   * que toutes les clés soient présentes, et $item peut être séquentiel
   *
   * les clés composites ne sont pas supportées (le résultat est toujours false
   * même si le tableau contient une clé du nom de la clé composite)
   *
   * XXX si le type n'est pas bool, interpréter false comme "non présent"
   */
  function has($item, $key): bool {
    $cikeys = $this->schema["cikeys"];
    $sfields = $this->schema["sfields"];
    if (in_array($key, $cikeys)) {
      # valeur composite
      return false;
    } elseif (!array_key_exists($key, $sfields)) {
      return is_array($item) && array_key_exists($key, $item);
    } else {
      if (!is_array($item)) $item = [$item];
      if (array_key_exists($key, $item)) return true;
      $index = $this->schema["indexes"][$key];
      return array_key_exists($index, $item);
    }
  }

  /**
   * obtenir la valeur de la clé $key depuis $item en tenant compte des
   * informations du schéma. si la clé n'existe pas, retourner la valeur par
   * défaut: soit $default s'il n'est pas null, soit la valeur par défaut du
   * schéma.
   *
   * $item n'a pas besoin d'être conforme au schéma: il n'est pas nécessaire
   * que toutes les clés soient présentes, et $item peut être séquentiel
   *
   * les clés composites sont supportées et sont retournées sous forme de
   * tableau
   */
  function get($item, $key, $default=null, bool $ensure_type=true) {
    $cikeys = $this->schema["cikeys"];
    $sfields = $this->schema["sfields"];
    if (in_array($key, $cikeys)) {
      # valeur composite
      if (!is_array($item)) $item = [$item];
      $values = [];
      $type = self::citype($this->getType($key));
      foreach ($type->getComponents() as $cvalue) {
        $key = $cvalue["key"];
        $sfield = $sfields[$key];
        $values[$key] = $this->_get($sfield, $item, $key, null, $ensure_type);
      }
      return $values;
    } elseif (!array_key_exists($key, $sfields)) {
      if (is_array($item) && array_key_exists($key, $item)) {
        return $item[$key];
      } else {
        return $default;
      }
    } else {
      if (!is_array($item)) $item = [$item];
      $sfield = $sfields[$key];
      return $this->_get($sfield, $item, $key, $default, $ensure_type);
    }
  }

  private function _get(array $sfield, ?array $item, $key, $default=null, bool $ensureType=true) {
    if (array_key_exists($key, $item)) {
      $exists = true;
      $value = $item[$key];
    } else {
      $index = $this->schema["indexes"][$key];
      if (array_key_exists($index, $item)) {
        $exists = true;
        $value = $item[$index];
      } elseif ($default !== null) {
        $exists = true;
        $value = $default;
      } else {
        $exists = false;
        $value = $sfield["default"];
      }
    }

    if ($ensureType) {
      self::_ensure_type($sfield, $value, $exists);
      $schema2 = $sfield["schema"];
      if ($schema2 !== null) $this->_ensureSchema($value, $schema2, $this->ppEnsureKeys, $this->ppEnsureTypes, true);
    }
    return $value;
  }

  /**
   * spécifier la valeur de la clé $key dans $item en tenant compte des
   * informations du schéma.
   *
   * les clés composites sont supportées et doivent être fournies sous forme de
   * tableau
   */
  function set(?array &$item, $key, $value): void {
    $cikeys = $this->schema["cikeys"];
    $sfields = $this->schema["sfields"];
    if (in_array($key, $cikeys)) {
      # valeur composite
      if ($value === null) return;
      $type = self::citype($this->getType($key));
      if (!is_array($value)) {
        throw ValueException::unexpected_type("array", $value);
      }
      foreach ($type->getComponents() as $cname => $cvalue) {
        $ctype = $type->getCtype($cname);
        $key = $cvalue["key"];
        if (array_key_exists($key, $value)) {
          $item[$key] = $ctype->with($this->get($value, $key));
        }
      }
    } elseif (!array_key_exists($key, $sfields)) {
      $item[$key] = $value;
    } else {
      $type = $this->getType($key);
      $item[$key] = $type->with($value);
    }
  }

  /**
   * sélectionner dans $items les valeurs du schéma, et les retourner dans
   * l'ordre du schéma sous forme de tableau associatif.
   *
   * $item doit aussi être un tableau associatif. idéalement, il a été au
   * préalable rendu conforme au schéma
   *
   * si $item n'est pas conforme au schéma, les champs ne sont reconnus que si
   * l'on utilise les clés associatives. il n'est pas nécessaire que toutes les
   * clés du schéma soient présentes. dans ce cas, seuls les clés présentes sont
   * dans le tableau résultat. dans ce cas de figure, $ensureType==true permet
   * de s'assurer aussi que les valeurs sont dans le bon type
   */
  function getValues(?array $item, bool $ensureType=false): array {
    if ($item === null) return [];
    $values = [];
    foreach ($this->schema["sfields"] as $key => $sfield) {
      if (!array_key_exists($key, $item)) continue;
      $value = $item[$key];
      if ($ensureType) {
        self::_ensure_type($sfield, $value, true);
      }
      $values[$key] = $value;
    }
    return $values;
  }

  /**
   * complément de {@link getValues()}: retourner les clés qui n'ont pas été
   * sélectionnées
   */
  function getOthers(?array $item): array {
    if ($item === null) return [];
    $sfields = $this->schema["sfields"];
    $others = [];
    foreach ($item as $key => $value) {
      if (array_key_exists($key, $sfields)) continue;
      $others[$key] = $value;
    }
    return $others;
  }

  /**
   * formatter chacun des champs de $item. on assume que $item est déjà conforme
   * au schéma
   */
  function format($item): array {
    $cikeys = $this->schema["cikeys"];
    $types = $this->_getTypes($this->schema);
    $result = [];
    foreach ($this->getAllKeys() as $key) {
      $type = self::itype($types[$key]);
      if (in_array($key, $cikeys)) {
        $value = $type->format($item);
      } else {
        $value = $type->format($item[$key]);
      }
      $result[$key] = $value;
    }
    return $result;
  }

  /**
   * vérifier et corriger chaque champ de $item. on assume que $item est déjà
   * conforme au moins à la structure du schéma
   */
  function verifix(&$item, ?array &$results=null, bool $throw=false, bool $recursive=false): void {
    $this->_getTypes($this->schema);
    $this->_verifix($item, $results, $throw, $this->schema, $recursive);
  }

  function _verifix(&$item, ?array &$results, bool $throw, array $schema, bool $recursive): void {
    foreach ($schema["sfields"] as $key => $sfield) {
      if (!$sfield["composite"]) {
        $type = self::itype($schema["types"][$key]);
        $type->verifix($item[$key], $results[$key], $throw);
      }
      if ($recursive && $sfield["schema"] !== null) {
        $this->_verifix($item[$key], $results[$key], $throw, $sfield["schema"], true);
      }
    }
    foreach ($schema["cikeys"] as $key) {
      $type = self::itype($schema["types"][$key]);
      $type->verifix($item, $results[$key], $throw);
    }
  }

  /**
   * méthode de convenance pour corriger de façon récursive tous les types, et
   * lancer une exception en cas d'erreur
   */
  function ensureTypes(&$item): void {
    $this->verifix($item, $results, true, true);
  }

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

  function eachEnsureSchema(?iterable &$items, ?array $params=null): void {
    if ($items === null) return;
    $index = 0;
    foreach ($items as $key => &$item) {
      if ($key === $index) {
        $key = null;
        $index++;
      }
      $this->ensureSchema($item, $key, $params);
    }; unset($item);
  }

  function eachEnsureTypes(?iterable &$items): void {
    if ($items === null) return;
    foreach ($items as &$item) {
      $this->ensureTypes($item);
    }; unset($item);
  }

  function eachCheckRequired(?iterable $items, bool $recursive=true): void {
    if ($items === null) return;
    foreach ($items as $item) {
      $this->checkRequired($item, $recursive);
    }
  }

  function eachFormat(?iterable $items): ?array {
    if ($items === null) return null;
    $result = [];
    foreach ($items as $item) {
      $result[] = $this->format($item);
    }
    return $result;
  }

  function eachVerifix(?iterable &$items, ?array &$itemResults=null, bool $throw=false, bool $recursive=false): void {
    if ($items === null) return;
    $itemResults = [];
    foreach ($items as $key => &$item) {
      $this->verifix($item, $results, $throw, $recursive);
      $itemResults[$key] = $results;
    }; unset($item);
  }
}
