<?php
namespace nulib\schema;

use ArrayAccess;
use IteratorAggregate;
use nulib\php\func;
use nulib\schema\input\Input;
use nulib\schema\types\IType;
use nulib\ValueException;

abstract class Wrapper implements ArrayAccess, IteratorAggregate {
  protected WrapperContext $context;

  /** changer les paramètres de gestion des valeurs */
  function resetParams(?array $params): void {
    $this->context->resetParams($params);
  }

  protected function resetContext(bool $resetSelectedKey): void {
    $context = $this->context;
    $type = $context->schema->type;
    if (is_array($type)) $type = $type[0];
    if (is_string($type)) $type = types::get($context->schema->nullable, $type);
    $context->type = $type;
    $context->result->reset();
    $context->analyzed = false;
    $context->normalized = false;
  }

  protected function afterModify(?array $params, bool $resetSelectedKey=false): void {
    $context = $this->context;
    $this->resetContext($resetSelectedKey);
    if ($params["analyze"] ?? $context->analyze) {
      $this->analyze($params);
    }
    if ($context->analyzed && ($params["normalize"] ?? $context->normalize)) {
      $this->normalize($params);
    }
  }

  protected function newInput(&$value): Input {
    return new Input($value);
  }

  /**
   * spécifier la valeur destination gérée par cet objet.
   *
   * @param ?array $params paramètres spécifique à cet appel, qui peuvent être
   * différent des paramètres par défaut
   */
  function reset(&$value, $valueKey=null, ?array $params=null): Wrapper {
    $context = $this->context;
    if ($value instanceof Input) $input = $value;
    else $input = $this->newInput($value);
    $context->input = $input;
    $context->valueKey = $valueKey;
    $this->afterModify($params, true);
    return $this;
  }

  /** analyser la valeur */
  abstract static function _analyze(WrapperContext $context, Wrapper $wrapper, ?array $params): int;

  function analyze(?array $params=null): bool {
    $context = $this->context;
    $reanalyze = $params["reanalyze"] ?? false;
    if ($context->analyzed && !$reanalyze) return false;

    static::_analyze($context, $this, $params);
    $context->analyzed = true;
    return true;
  }

  /** normaliser la valeur */
  abstract static function _normalize(WrapperContext $context, Wrapper $wrapper, ?array $params): bool;

  protected function getConsolidatedResult(): Result {
    return $this->context->result;
  }

  function normalize(?array $params=null): bool {
    $context = $this->context;

    // il faut que la valeur soit analysée avant de la normaliser
    static::analyze($params);
    if (!$context->analyzed) return false;

    $renormalize = $params["renormalize"] ?? false;
    if ($renormalize || !$context->normalized) {
      $modified = static::_normalize($context, $this, $params);
      $context->normalized = true;
    } else {
      $modified = false;
    }

    $result = $this->getConsolidatedResult();
    if (!$result->valid) {
      $result->throw($params["throw"] ?? $context->throw);
    }

    return $modified;
  }

  /**
   * Obtenir la liste des clés valides pour les valeurs accessibles via cet
   * objet
   */
  abstract function getKeys(): array;

  /**
   * sélectionner le wrapper associé à la clé spécifiée
   *
   * $key peut valoir:
   * - false pour la clé courante (ne pas changer la sélection)
   * - null ou "" le wrapper de la valeur principale
   * - ou toute autre valeur présente dans {@link getKeys()} pour les valeurs
   * accessible via cet objet
   *
   * @param string|int|null $key
   * @return Wrapper $this
   */
  abstract function select($key=null): Wrapper;

  function getIterator() {
    foreach ($this->getKeys() as $key) {
      yield $key => $this->select($key);
    }
    $this->select();
  }

  /**
   * obtenir le résultat de l'analyse de la valeur du wrapper sélectionné
   *
   * cette fonction doit être appelée après {@link set()} ou {@link unset()} et
   * après que le wrapper aie été sélectionné avec {@link select()}
   *
   * $key peut valoir:
   * - false pour la clé sélectionnée avec {@link select()}
   * - null pour le résultat consolidé
   * - "" pour le résultat de l'analyse de la valeur principale
   * - ou toute autre valeur présente dans {@link getKeys()} pour le résultat
   * de l'analyse des valeurs correspondantes
   */
  function getResult($key=false): Result {
    if ($key === false || $key === "") return $this->context->result;
    if ($key === null) return $this->getConsolidatedResult();
    throw ValueException::invalid_key($key);
  }

  /** retourner true si la valeur existe */
  function isPresent($key=false): bool {
    return $this->getResult($key)->present;
  }

  /** retourner le type associé à la valeur */
  function getType($key=false): IType {
    return $this->context->type;
  }

  /** retourner true si la valeur est disponible */
  function isAvailable($key=false): bool {
    return $this->getResult($key)->available;
  }

  /** retourner true si la valeur est nulle */
  function isNull($key=false): bool {
    return $this->getResult($key)->null;
  }

  /** retourner true si la valeur est valide */
  function isValid($key=false): bool {
    return $this->getResult($key)->valid;
  }

  /** retourner true si la valeur est dans sa forme normalisée */
  function isNormalized($key=false): bool {
    return $this->getResult($key)->normalized;
  }

  function get($default=null, $key=false) {
    $context = $this->context;
    if (!$context->result->available) return $default;
    return $context->input->get($context->valueKey);
  }

  function set($value, ?array $params=null, $key=false): self {
    $context = $this->context;
    $context->input->set($value, $context->valueKey);
    $this->afterModify($params);
    return $this;
  }

  function unset(?array $params=null, $key=false): self {
    $context = $this->context;
    $context->input->unset($context->valueKey);
    $this->afterModify($params);
    return $this;
  }

  protected function _format(WrapperContext $context, $format=null): string {
    $value = $context->input->get($context->valueKey);
    /** @var func $formatterFunc */
    $formatterFunc = $context->schema->formatterFunc;
    if ($formatterFunc !== null) {
      # la fonction formatter n'a pas forcément accès au format de la définition
      # le lui fournir ici
      $format ??= $context->schema->format;
      return $formatterFunc->invoke([$value, $format, $context, $this]);
    } else {
      # on assume que le type a été initialisé avec le format de la définition
      # le cas échéant
      return $context->type->format($value, $format);
    }
  }

  /** formatter la valeur pour affichage */
  function format($format=null, $key=false): string {
    return $this->_format($this->context, $format);
  }

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

  function offsetExists($offset): bool {
    return in_array($offset, $this->getKeys());
  }

  function offsetGet($offset) {
    return $this->get(null, $offset);
  }

  function offsetSet($offset, $value): void {
    $this->set($value, null, $offset);
  }

  function offsetUnset($offset): void {
    $this->unset(null, $offset);
  }
}
