<?php
namespace nur\data\types;

use Exception;
use nur\A;
use nur\b\IllegalAccessException;
use nur\b\params\IParametrable;
use nur\b\ValueException;
use nur\prop;
use nur\str;

/**
 * Class AbstractType: implémentation partagée
 */
abstract class AbstractType implements IType, IParametrable {
  use _Tparametrable1;

  /**
   * @var bool si une valeur chaine est fournie, doit-elle être trimée avant
   * analyse?
   */
  const TRIM = true;

  /** @var bool|null la chaine vide est-elle autorisée? */
  const ALLOW_EMPTY = true;

  /** @var bool la valeur false est-elle autorisée? */
  const ALLOW_FALSE = true;

  /** @var bool la valeur null est-elle autorisée? */
  const ALLOW_NULL = true;

  /** @var bool si une chaine est fournie, doit-elle correspondre entièrement? */
  const PARSE_ALL = true;

  /** @var array messages standards */
  const MESSAGES = [
    "empty" => "cette valeur ne doit pas être vide",
    "false" => "cette valeur ne doit pas être false",
    "null" => "cette valeur ne doit pas être null",
    "invalid" => "{value_desc}: cette valeur est invalide",
  ];

  #############################################################################

  const PARAMETRABLE_PARAMS_SCHEMA = [
    "trim" => ["bool", null, "faut-il trimmer la valeur chaine avant analyse"],
    "allow_empty" => ["?bool", null, "la chaine vide est-elle autorisée?"],
    "allow_false" => ["bool", null, "la valeur false est-elle autorisée?"],
    "allow_null" => ["bool", null, "la valeur null est-elle autorisée?"],
    "parse_all" => ["bool", null, "la chaine fournie doit-elle correspondre entièrement?"],
    "messages" => ["array", null, "messages à retourner en cas d'erreur"],
  ];

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

  function isTrim(): bool {
    return $this->ppTrim;
  }

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

  function isAllowEmpty(): bool {
    $allowEmpty = $this->ppAllowEmpty;
    if ($allowEmpty !== null) return $allowEmpty;
    else return $this->ppAllowNull;
  }

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

  function isAllowFalse(): bool {
    return $this->ppAllowFalse;
  }

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

  function isAllowNull(): bool {
    return $this->ppAllowNull;
  }

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

  /** @var array messages à retourner en cas d'erreur */
  protected $ppMessages;

  function setMessages(array $messages): void {
    $this->ppMessages = $messages;
  }

  #############################################################################

  function __construct(?array $params=null) {
    A::replace_n($params, "trim", static::TRIM);
    A::replace_n($params, "allow_empty", static::ALLOW_EMPTY);
    A::replace_n($params, "allow_false", static::ALLOW_FALSE);
    A::replace_n($params, "allow_null", static::ALLOW_NULL);
    A::replace_n($params, "parse_all", static::PARSE_ALL);
    $this->initParametrableParams($params);
  }

  abstract function getClass(): string;

  function getPhpType(bool $allowNullable=true): ?string {
    $phpType = $this->getClass();
    if ($phpType === "mixed") return null;
    if ($this->ppAllowNull && $allowNullable) $phpType = "?$phpType";
    return $phpType;
  }

  function isInstance($value, bool $strict=false): bool {
    if ($value === null) return $this->ppAllowNull;
    $class = $this->getClass();
    switch ($class) {
    case "mixed": return true;
    case "bool": return is_bool($value);
    case "int": return is_int($value) || (!$strict && is_float($value));
    case "float": return is_float($value) || (!$strict && is_int($value));
    case "string": return is_string($value);
    case "array": return is_array($value);
    case "iterable": return is_iterable($value);
    case "resource": return is_resource($value);
    default: return $value instanceof $class;
    }
  }

  function getUndefValue() {
    return false;
  }

  function isUndef($value, $key=null): bool {
    if ($key !== null) {
      if (!is_array($value)) return $key !== 0;
      if (!array_key_exists($key, $value)) return true;
      $value = $value[$key];
    }
    return $value === false;
  }

  function is2States(): bool {
    return false;
  }

  function get2States(): array {
    throw IllegalAccessException::not_implemented();
  }

  function is3States(): bool {
    return false;
  }

  function get3States(): array {
    throw IllegalAccessException::not_implemented();
  }

  function canFormat(): bool {
    return false;
  }

  function format($value): string {
    throw IllegalAccessException::not_implemented();
  }

  function canParse(): bool {
    return false;
  }

  function parse(string &$input) {
    throw IllegalAccessException::not_implemented();
  }

  #############################################################################

  protected static function _set_error(array &$result, string $error_code, ?array $messages): void {
    $error = A::get($messages, $error_code);
    if ($error === null) $error = A::get(static::MESSAGES, $error_code);
    if ($error === null) $error = $error_code;
    foreach ($result as $key => $value) {
      switch ($key) {
      case "value":
      case "orig":
        $value = var_export($value, true);
        break;
      case "value_desc":
        if ($value === null) $value = $result["value"];
        $value = var_export($value, true);
        break;
      case "orig_desc":
        if ($value === null) $value = $result["orig"];
        $value = var_export($value, true);
        break;
      case "exception":
        $value = null;
        break;
      }
      $error = str_replace("{".$key."}", $value, $error);
    }
    $result["error"] = $error;
  }

  protected static function result_invalid(?array &$result, string $error_code, $orig, $orig_desc, $parsed, $remains, ?array $messages, ?Exception $exception=null) {
    $result = [
      "valid" => false,
      "value" => $orig,
      "value_desc" => $orig_desc,
      "error_code" => $error_code,
      "error" => null,
      "exception" => $exception,
      "orig" => $orig,
      "orig_desc" => $orig_desc,
      "parsed" => $parsed,
      "remains" => $remains,
    ];
    self::_set_error($result, $error_code, $messages);
    return $orig;
  }

  protected static function result_valid(?array &$result, $value, $orig, $parsed=false, $remains=false) {
    $result = [
      "valid" => true,
      "value" => $value,
      "value_desc" => null,
      "error_code" => null,
      "error" => false,
      "exception" => null,
      "orig" => $orig,
      "orig_desc" => null,
      "parsed" => $parsed,
      "remains" => $remains,
    ];
    return $value;
  }

  /** fonction de support: trimmer la valeur */
  function verifixTrim(string $value): string {
    return trim($value);
  }

  /**
   * fonction de support: corriger la valeur "" le cas échéant
   *
   * par défaut, remplacer "" par null si null est autorisé
   */
  function verifixReplaceEmpty(&$value): void {
    if ($this->ppAllowNull) $value = null;
  }

  /**
   * fonction de support: corriger la valeur false le cas échéant
   *
   * par défaut, remplacer false par null si false n'est pas autorisé.
   */
  function verifixReplaceFalse(&$value): void {
    if (!$this->ppAllowFalse) $value = null;
  }

  /** fonction de support: corriger la valeur null le cas échéant */
  function verifixReplaceNull(&$value): void {
  }

  /**
   * fonction de support qui implémente la vérification des valeurs non chaine
   */
  protected function verifixNoParse(&$value, ?array &$result): bool {
    $orig = $value;
    if (is_string($value)) {
      if ($this->ppTrim) $value = $this->verifixTrim($value);
      if ($value === "") $this->verifixReplaceEmpty($value);
    }
    if ($value === false) $this->verifixReplaceFalse($value);
    if ($value === null) $this->verifixReplaceNull($value);

    if ($value === null) {
      if ($this->ppAllowNull) {
        self::result_valid($result, $value, $orig);
      } else {
        $value = self::result_invalid($result, "null", $orig, null, false, false, $this->ppMessages, null);
      }
      return true;
    }
    if ($value === false) {
      if ($this->ppAllowFalse) {
        self::result_valid($result, $value, $orig);
      } else {
        $value = self::result_invalid($result, "false", $orig, null, false, false, $this->ppMessages, null);
      }
      return true;
    }
    if ($value === "" && !$this->ppAllowEmpty) {
      $value = self::result_invalid($result, "empty", $orig, null, false, "", $this->ppMessages, null);
      return true;
    }
    return false;
  }

  protected abstract function _verifix(&$value, array &$result=null): void;

  function verifix(&$value, array &$result=null, bool $throw=false): bool {
    $this->_verifix($value, $result);
    $valid = $result["valid"];
    if (!$valid && $throw) {
      $exception = $result["exception"];
      if ($exception !== null) throw $exception;
      else throw new ValueException($result["error"]);
    }
    return $valid;
  }

  function with($value) {
    $this->verifix($value, $result, true);
    return $value;
  }

  #############################################################################

  function getGetterName(string $name): string {
    return prop::get_getter_name($name);
  }

  function getSetterName(string $name): string {
    return prop::get_setter_name($name);
  }

  function getDeleterName(string $name): string {
    return prop::get_deletter_name($name);
  }

  function getClassConstName(string $name): string {
    return strtoupper($name);
  }

  function getObjectPropertyName(string $name): string {
    return str::us2camel($name);
  }

  function getArrayKeyName(string $name): string {
    return $name;
  }
}
