<?php
namespace nur\sery\wip\schema\_scalar;

use nur\sery\ref\schema\ref_analyze;
use nur\sery\ValueException;
use nur\sery\wip\schema\input\Input;
use nur\sery\wip\schema\types;
use nur\sery\wip\schema\types\IType;
use nur\sery\wip\schema\Value;

class ScalarValue extends Value {
  function __construct(ScalarSchema $schema, &$dest=null, $destKey=null, bool $defaultVerifix=true, ?bool $defaultThrow=null) {
    if ($dest !== null && $defaultThrow = null) {
      # Si $dest est null, ne pas lancer d'exception, parce qu'on considère que
      # c'est une initialisation sans conséquences
      $defaultThrow = true;
    }
    $this->schema = $schema;
    $this->defaultVerifix = $defaultVerifix;
    $this->defaultThrow = $defaultThrow !== null? $defaultThrow: false;
    $this->result = new ScalarResult();
    $this->reset($dest, $destKey);
    $this->defaultThrow = $defaultThrow !== null? $defaultThrow: true;
  }

  function isScalar(?ScalarValue &$scalar=null): bool { $scalar = $this; return true; }

  /** @var ScalarSchema schéma de cette valeur */
  protected $schema;

  /** @var Input source et destination de la valeur */
  protected $input;

  /** @var string|int|null clé de la valeur dans le tableau destination */
  protected $destKey;

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

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

  /** @var IType type de la valeur après analyse */
  protected $type;

  /** @var ?ScalarResult résultat de l'analyse de la valeur */
  protected $result;

  function reset(&$dest, $destKey=null, ?bool $verifix=null): Value {
    if ($dest instanceof Input) $input = $dest;
    else $input = new Input($dest);
    $this->input = $input;
    $this->destKey = $destKey;
    $this->type = null;
    $this->_analyze();
    if ($verifix == null) $verifix = $this->defaultVerifix;
    if ($verifix) $this->verifix();
    return $this;
  }

  function getKeys(): array {
    return [null];
  }

  function getValue($key=null): ScalarValue {
    if ($key === null) return $this;
    throw ValueException::invalid_key($key);
  }

  /** analyser la valeur et résoudre son type */
  function _analyze(): int {
    $schema = $this->schema;
    $input = $this->input;
    $destKey = $this->destKey;
    $result = $this->result;
    $result->reset();
    if (!$input->isPresent($destKey)) return $result->setMissing($schema);
    $haveType = false;
    $types = [];
    $type = $firstType = null;
    $haveValue = false;
    $value = null;
    # d'abord chercher un type pour lequel c'est une valeur normalisée
    foreach ($schema->type as $name) {
      $type = types::get($name);
      if ($firstType === null) $firstType = $type;
      $types[] = $type;
      if ($type->isAvailable($input, $destKey)) {
        if (!$haveValue) {
          $value = $input->get($destKey);
          $haveValue = true;
        }
        if ($type->isValid($value, $normalized) && $normalized) {
          $haveType = true;
          $this->type = $type;
          break;
        }
      }
    }
    if (!$haveType) {
      # ensuite chercher un type pour lequel la valeur est valide
      foreach ($types as $type) {
        if ($type->isAvailable($input, $destKey) && $type->isValid($value)) {
          $haveType = true;
          $this->type = $type;
          break;
        }
      }
    }
    # sinon prendre le premier type
    if (!$haveType) $type = $this->type = $firstType;
    if (!$type->isAvailable($input, $destKey)) return $result->setUnavailable($schema);
    $value = $input->get($destKey);
    if ($type->isNull($value)) return $result->setNull($schema);
    if ($type->isValid($value, $normalized)) {
      if ($normalized) return $result->setNormalized();
      else return $result->setValid();
    }
    if (is_string($value)) return ref_analyze::STRING;
    else return $result->setInvalid($value, $schema);
  }

  function verifix(?bool $throw=null): bool {
    if ($throw === null) $throw = $this->defaultThrow;
    $destKey = $this->destKey;
    $verifix = false;
    $result =& $this->result;
    $modified = false;
    if ($result->resultAvailable) {
      if ($result->null) {
        # forcer la valeur null, parce que la valeur actuelle est peut-être une
        # valeur assimilée à null
        $this->input->set(null, $destKey);
      } elseif ($result->valid && !$result->normalized) {
        # normaliser la valeur
        $verifix = true;
      }
    } else {
      $verifix = true;
    }
    if ($verifix) {
      $value = $this->input->get($destKey);
      $modified = $this->type->verifix($value, $result, $this->schema);
      if ($result->valid) $this->input->set($value, $destKey);
    }
    if (!$result->valid) $result->throw($throw);
    return $modified;
  }

  function getResult(): ScalarResult {
    return $this->result;
  }

  function isPresent(): bool {
    return $this->result->present;
  }

  function getType(): IType {
    return $this->type;
  }

  function isAvailable(): bool {
    return $this->result->available;
  }

  function isValid(): bool {
    return $this->result->valid;
  }

  function isNormalized(): bool {
    return $this->result->normalized;
  }

  function get($default=null) {
    if ($this->result->available) return $this->input->get($this->destKey);
    else return $default;
  }

  function set($value, ?bool $verifix=null): ScalarValue {
    $this->input->set($value, $this->destKey);
    $this->_analyze();
    if ($verifix === null) $verifix = $this->defaultVerifix;
    if ($verifix) $this->verifix();
    return $this;
  }

  function unset(?bool $verifix=null): ScalarValue {
    $this->input->unset($this->destKey);
    $this->_analyze();
    if ($verifix === null) $verifix = $this->defaultVerifix;
    if ($verifix) $this->verifix();
    return $this;
  }

  function format($format=null): string {
    return $this->type->format($this->input->get($this->destKey), $format);
  }
}
