<?php
namespace nulib\schema\_scalar;

use nulib\php\func;
use nulib\ref\schema\ref_analyze;
use nulib\schema\Result;
use nulib\schema\Schema;
use nulib\schema\types;
use nulib\schema\types\IType;
use nulib\schema\Wrapper;
use nulib\schema\WrapperContext;
use nulib\ValueException;

/**
 * Class ScalarWrapper
 *
 * @method ScalarWrapper reset(&$value, $valueKey=null, ?array $params=null)
 * @method self set($value, ?array $params=null, $key=false)
 * @method self unset(?array $params=null, $key=false)
 */
class ScalarWrapper extends Wrapper {
  function __construct(Schema $schema, &$value=null, $valueKey=null, ?array $params=null, ?WrapperContext $context=null) {
    if ($context === null) $context = new WrapperContext($schema, null, null, $params);
    $context->result = new Result();
    $this->context = $context;

    if ($value !== null) {
      # n'initialiser que si $value n'est pas null
      $this->reset($value, $valueKey);
    } else {
      # il faut au moins que le type soit disponible
      $this->resetContext(false);
    }
  }

  protected WrapperContext $context;

  function getKeys(): array {
    return ScalarSchema::KEYS;
  }

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

  /** analyser la valeur et résoudre son type */
  protected static function _analyze0(WrapperContext $context): int {
    /** @var ScalarSchema $schema */
    $schema = $context->schema;
    $input = $context->input;
    $valueKey = $context->valueKey;
    $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, $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 = $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);
    $missing = $type->getMissingValue($haveMissing);
    if ($haveMissing && $value === $missing) {
      if ($default !== null) {
        $input->set($default, $valueKey);
        return $result->setNormalized();
      } else {
        return $result->setMissing($schema);
      }
    }

    $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);
    }
  }

  /**
   * @param ScalarWrapper $wrapper
   */
  static function _analyze(WrapperContext $context, Wrapper $wrapper, ?array $params): int {
    /** @var ScalarSchema $schema */
    $schema = $context->schema;
    $input = $context->input;
    $valueKey = $context->valueKey;
    $result = $context->result;

    /** @var func $analyzerFunc */
    $analyzerFunc = $schema->analyzerFunc;
    if ($analyzerFunc !== null) $what = $analyzerFunc->invoke([$context, $wrapper]);
    else $what = self::_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, $wrapper]);
      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, $wrapper]);
      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);
    }
  }

  /**
   * @param ScalarWrapper $wrapper
   */
  static function _normalize(WrapperContext $context, Wrapper $wrapper, ?array $params): bool {
    /** @var ScalarSchema $schema */
    $schema = $context->schema;
    $input = $context->input;
    $valueKey = $context->valueKey;
    $result = $context->result;

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

    if ($normalize) {
      $value = $input->get($valueKey);
      /** @var func $normalizerFunc */
      $normalizerFunc = $schema->normalizerFunc;
      if ($normalizerFunc !== null) {
        $orig = $value;
        $value = $normalizerFunc->invoke([$orig, $context, $wrapper]);
        $modified = $value !== $orig;
      } else {
        $modified = $context->type->normalize($value, $result, $schema);
      }
      if ($result->valid) $input->set($value, $valueKey);
    }
    return $modified;
  }
}
