<?php
namespace nulib\schema\_assoc;

use nulib\cl;
use nulib\ref\schema\ref_analyze;
use nulib\schema\_scalar\ScalarWrapper;
use nulib\schema\ConsolidatedResult;
use nulib\schema\input\Input;
use nulib\schema\Result;
use nulib\schema\types\IType;
use nulib\schema\Wrapper;
use nulib\schema\WrapperContext;
use nulib\ValueException;

class AssocWrapper extends Wrapper {
  function __construct(AssocSchema $schema, &$value=null, $valueKey=null, ?array $params=null) {
    $this->context = $context = new AssocWrapperContext($schema, null, null, $params);
    $paramsNoThrow = cl::merge($params, [
      "throw" => false,
    ]);

    $keys = $schema->getKeys();
    $keyWrappers = [];
    foreach ($keys as $key) {
      $keyDummy = null;
      $keyWrappers[$key] = $schema->getSchema($key)->getWrapper($keyDummy, null, $paramsNoThrow);
    }
    $context->keys = $keys;
    $context->keyWrappers = $keyWrappers;

    $arrayDummy = null;
    $context->arrayWrapper = new ScalarWrapper($schema, $arrayDummy, null, $paramsNoThrow, $context);

    $context->consolidatedResult = new ConsolidatedResult();

    if ($value !== null) {
      # n'initialiser que si $value n'est pas null
      $this->reset($value, $valueKey);
    }
  }

  /** @var AssocWrapperContext  */
  protected WrapperContext $context;

  protected function resetContext(bool $resetSelectedKey): void {
    parent::resetContext($resetSelectedKey);
    $context = $this->context;
    $context->arrayWrapper->getResult()->reset();
    foreach ($context->keyWrappers as $wrapper) {
      $wrapper->getResult()->reset();
    }
    if ($resetSelectedKey) $context->selectedKey = null;
  }

  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;
    foreach ($context->keyWrappers as $key => $keyWrapper) {
      $keyInput = $input->addKey($valueKey);
      $keyWrapper->reset($keyInput, $key, ["analyze" => false]);
    }
    $this->afterModify($params, true);
    return $this;
  }

  function getKeys(): array {
    return $this->context->keys;
  }

  protected function _getWrapper($key): Wrapper {
    $context = $this->context;
    if ($key === null || $key === "") return $context->arrayWrapper;
    $wrapper = $context->keyWrappers[$key] ?? null;
    if ($wrapper === null) throw ValueException::invalid_key($key);
    return $wrapper;
  }

  /** @param string|int|null $key */
  function select($key=null): Wrapper {
    $wrapper = $this->_getWrapper($key);
    $this->context->selectedKey = $key;
    return $wrapper;
  }

  /**
   * @param AssocWrapperContext $context
   * @param AssocWrapper $wrapper
   */
  static function _analyze(WrapperContext $context, Wrapper $wrapper, ?array $params): int {
    if ($params["ensure_array"] ?? $context->ensureArray) {
      $valueKey = $context->valueKey;
      $array = $context->input->get($valueKey);
      if ($array === null) $context->input->set([], $valueKey);
    }

    if ($params["ensure_assoc"] ?? $context->ensureAssoc) {
      $context->input->ensureAssoc($context->schema->getKeys());
    }

    $what = ScalarWrapper::_analyze($context, $wrapper, $params);
    if (!$context->result->valid) return $what;

    $result = $context->consolidatedResult;
    $result->setValid();
    foreach ($context->keyWrappers as $key => $keyWrapper) {
      $keyWrapper->analyze($params);
      if ($keyWrapper->isValid()) continue;
      $what = ref_analyze::INVALID;
      #XXX pour $prefix, utiliser si possible la description ou une autre valeur
      # "user-friendly". possibilité de sélectionner la valeur à utiliser avec
      # $params?
      $prefix = $key;
      if (!$keyWrapper->isPresent()) {
        $result->addMissingMessage($keyWrapper, $prefix);
      } elseif (!$keyWrapper->isAvailable()) {
        $result->addUnavailableMessage($keyWrapper, $prefix);
      } elseif ($keyWrapper->isNull()) {
        $result->addNullMessage($keyWrapper, $prefix);
      } else {
        $result->addInvalidMessage($keyWrapper, $prefix);
      }
    }

    return $what;
  }

  /**
   * @param AssocWrapperContext $context
   * @param AssocWrapper $wrapper
   */
  static function _normalize(WrapperContext $context, Wrapper $wrapper, ?array $params): bool {
    $schema = $context->schema;
    $keys = $schema->getKeys();

    $defaults = [];
    $missings = null;
    foreach ($keys as $key) {
      $type = $wrapper->getType($key);
      $default = $schema->getSchema($key)->default;
      if ($default === null) $default = $type->getNullValue();
      $defaults[$key] = $default;
      $missing = $type->getMissingValue($valid);
      if ($valid) $missings[$key] = $missing;
    }
    if ($params["ensure_keys"] ?? $context->ensureKeys) {
      $context->input->ensureKeys($defaults, $missings, $params);
    } else {
      $context->input->deleteMissings($missings, $params);
    }

    if ($params["ensure_order"] ?? $context->ensureOrder) {
      $context->input->ensureOrder($keys, $params);
    }

    $modified = ScalarWrapper::_normalize($context, $wrapper, $params);
    foreach ($context->keyWrappers as $keyWrapper) {
      if ($keyWrapper->normalize($params)) $modified = true;
    }
    return $modified;
  }

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

  function getResult($key=false): Result {
    if ($key === null) return $this->getConsolidatedResult();
    if ($key === false) $key = $this->context->selectedKey;
    return $this->_getWrapper($key)->getResult();
  }

  function getType($key=false): IType {
    if ($key === false) $key = $this->context->selectedKey;
    return $this->_getWrapper($key)->getType();
  }

  function get($default=null, $key=false) {
    $context = $this->context;
    if (!$context->arrayWrapper->isAvailable()) return $default;
    if ($key === false) $key = $context->selectedKey;
    return $this->_getWrapper($key)->get($default);
  }

  function set($value, ?array $params=null, $key=false): Wrapper {
    $context = $this->context;
    if ($key === false) $key = $context->selectedKey;
    $this->_getWrapper($key)->set($value);
    return $this;
  }

  function unset(?array $params=null, $key=false): Wrapper {
    $context = $this->context;
    if ($key === false) $key = $context->selectedKey;
    $this->_getWrapper($key)->unset();
    return $this;
  }

  function format($format=null, $key=false): string {
    $context = $this->context;
    if ($key === false) $key = $context->selectedKey;
    return $this->_getWrapper($key)->format($format);
  }
}
