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

use nulib\php\func;
use nulib\ref\schema\ref_analyze;
use nulib\ValueException;
use nur\sery\wip\schema\AnalyzerContext;
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\Wrapper;

class ScalarWrapper extends Wrapper {
  function __construct(ScalarSchema $schema, &$value=null, $valueKey=null, ?array $params=null) {
    $verifix = $params["verifix"] ?? true;
    $throw = $params["throw"] ?? null;
    if ($value !== null && $throw === null) {
      # Si $value est null, ne pas lancer d'exception, parce qu'on considère que
      # c'est une initialisation sans conséquences
      $throw = true;
    }
    $this->verifix = $verifix;
    $this->throw = $throw ?? false;
    $this->schema = $schema;
    $this->result = new ScalarResult();
    $this->reset($value, $valueKey);
    $this->throw = $throw ?? true;
  }

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

  protected bool $verifix;

  protected bool $throw;

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

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

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

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

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

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

  function reset(&$value, $valueKey=null, ?bool $verifix=null): Wrapper {
    if ($value instanceof Input) $input = $value;
    else $input = $this->newInput($value);
    $this->input = $input;
    $this->valueKey = $valueKey;
    $this->type = null;
    $this->analyze();
    if ($verifix ?? $this->verifix) $this->verifix();
    return $this;
  }

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

  /** @param string|int|null $key */
  function select($key): ScalarWrapper {
    if ($key !== null) throw ValueException::invalid_key($key);
    return $this;
  }

  /** analyser la valeur et résoudre son type */
  protected function analyze0(AnalyzerContext $context): int {
    /** @var ScalarSchema $schema */
    $schema = $context->schema;
    $input = $context->input;
    $valueKey = $context->valueKey;
    /** @var ScalarResult $result */
    $result = $context->result;

    $default = $schema->default;
    if (!$input->isPresent($valueKey)) {
      if ($default !== null) {
        $input->set($default, $valueKey);
        return $result->setNormalized();
      } else {
        return $result->setMissing($schema);
      }
    }

    $schemaTypes = $schema->type;
    if ($schemaTypes instanceof IType) {
      $type = $schemaTypes;
    } else {
      # type union
      $haveType = false;
      $types = [];
      $type = $firstType = null;
      $value = null;
      # d'abord chercher un type pour lequel c'est une valeur normalisée
      $index = 0;
      $haveValue = false;
      foreach ($schemaTypes as $key => $name) {
        if ($key === $index) {
          $index++;
          $args = null;
        } else {
          $args = $name;
          $name = $key;
        }
        $type = types::get($schema->nullable, $name, $args, $this->schema->getDefinition());
        if ($firstType === null) $firstType = $type;
        $types[] = $type;
        if ($type->isAvailable($input, $valueKey)) {
          if (!$haveValue) {
            $value = $input->get($valueKey);
            $haveValue = true;
          }
          if ($type->isValid($value, $normalized) && $normalized) {
            $haveType = true;
            break;
          }
        }
      }
      # ensuite chercher un type pour lequel la valeur est valide
      if (!$haveType) {
        foreach ($types as $type) {
          if ($type->isAvailable($input, $valueKey) && $type->isValid($value)) {
            $haveType = true;
            break;
          }
        }
      }
      # sinon prendre le premier type
      if (!$haveType) {
        $type = $firstType;
      }
    }
    $context->type = $this->type = $type;

    if (!$type->isAvailable($input, $valueKey)) {
      if ($default !== null) {
        $input->set($default, $valueKey);
        return $result->setNormalized();
      } else {
        return $result->setUnavailable($schema);
      }
    }

    $value = $input->get($valueKey);
    $context->origValue = $context->value = $value;
    if ($type->isNull($value)) {
      return $result->setNull($schema);
    } elseif (is_string($value)) {
      return ref_analyze::STRING;
    } elseif ($type->isValid($value, $normalized)) {
      if ($normalized) return $result->setNormalized();
      else return $result->setValid();
    } else {
      return $result->setInvalid($value, $schema);
    }
  }

  protected function analyze(): int {
    $schema = $this->schema;
    $input = $this->input;
    $valueKey = $this->valueKey;
    $result = $this->result;
    $result->reset();
    $context = new AnalyzerContext($schema, $this, $input, $valueKey, $result);

    /** @var func $analyzerFunc */
    $analyzerFunc = $schema->analyzerFunc;
    if ($analyzerFunc !== null) $what = $analyzerFunc->invoke([$context]);
    else $what = $this->analyze0($context);
    if ($what !== ref_analyze::STRING) return $what;

    $value = $context->value;
    try {
      /** @var func $extractorFunc */
      $extractorFunc = $schema->extractorFunc;
      if ($extractorFunc !== null) $extracted = $extractorFunc->invoke([$value, $context]);
      else $extracted = $context->type->extract($value);
      $context->value = $extracted;
    } catch (ValueException $e) {
      return $result->setInvalid($context->origValue, $schema, $e);
    }
    if ($context->type->isNull($extracted)) return $result->setNull($schema);

    try {
      /** @var func $parserFunc */
      $parserFunc = $schema->parserFunc;
      if ($parserFunc !== null) $parsed = $parserFunc->invoke([$extracted, $context]);
      else $parsed = $context->type->parse($extracted);
      $context->value = $parsed;
    } catch (ValueException $e) {
      return $result->setInvalid($context->origValue, $schema, $e);
    }

    $normalized = $parsed === $context->origValue;
    if ($normalized) {
      $input->set($parsed, $valueKey);
      return $result->setNormalized();
    } else {
      $input->set($extracted, $valueKey);
      return $result->setValid($parsed);
    }
  }

  function verifix(?bool $throw=null): bool {
    $result = $this->result;
    $valueKey = $this->valueKey;
    $verifix = false;
    $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, $valueKey);
      } elseif ($result->valid && !$result->normalized) {
        $normalizedValue = $result->normalizedValue;
        if ($normalizedValue !== null) {
          # la valeur normalisée est disponible
          $this->input->set($normalizedValue);
          $result->normalizedValue = null;
          $modified = true;
        } else {
          # normaliser la valeur
          $verifix = true;
        }
      }
    } else {
      $verifix = true;
    }

    if ($verifix) {
      $value = $this->input->get($valueKey);
      $schema = $this->schema;
      /** @var func $normalizerFunc */
      $normalizerFunc = $schema->normalizerFunc;
      if ($normalizerFunc !== null) {
        $context = new AnalyzerContext($schema, $this, $this->input, $valueKey, $result);
        $orig = $value;
        $value = $normalizerFunc->invoke([$orig, $context]);
        $modified = $value !== $orig;
      } else {
        $modified = $this->type->verifix($value, $result, $this->schema);
      }
      if ($result->valid) $this->input->set($value, $valueKey);
    }
    if (!$result->valid) $result->throw($throw ?? $this->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->valueKey);
    else return $default;
  }

  function set($value, ?bool $verifix=null): ScalarWrapper {
    $this->input->set($value, $this->valueKey);
    $this->analyze();
    if ($verifix ?? $this->verifix) $this->verifix();
    return $this;
  }

  function unset(?bool $verifix=null): ScalarWrapper {
    $this->input->unset($this->valueKey);
    $this->analyze();
    if ($verifix ?? $this->verifix) $this->verifix();
    return $this;
  }

  function format($format=null): string {
    $value = $this->input->get($this->valueKey);
    /** @var func $formatterFunc */
    $formatterFunc = $this->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 ??= $this->schema->format;
      return $formatterFunc->invoke([$value, $format]);
    } else {
      # on assume que le type a été initialisé avec le format de la définition
      # le cas échéant
      return $this->type->format($value, $format);
    }
  }
}
