<?php
namespace nur\data\types;

use Exception;
use nur\A;
use nur\b\ValueException;
use nur\md;
use nur\types;

/**
 * Class CompositeType: squelette d'implémentation d'un type composite
 *
 * un type composite permet de gérer une valeur qui est stockée dans plusieurs
 * champs de types différents
 *
 * Par convention, les classes dérivées sont nommées C{name}Type
 */
abstract class AbstractCompositeType extends AbstractType {
  use _Tparametrable;

  const ALLOW_NULL = true;

  const COMPONENT_SCHEMA = [
    "name" => ["string", null, "nom de la composante", "required" => true],
    "type" => ["mixed", null, "type de la composante", "required" => true],
    "key" => ["?string", null, "nom de la clé de la composante dans l'objet destination"],
    "title" => ["?string", null, "libellé de la composante"],
  ];

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

  /** @var array la définition des composantes */
  const COMPONENTS = [];

  /** @var string le séparateur entre chaque composant utilisé lors du formatage */
  const SEPARATOR = " ";

  /** @var string le pattern qui matche le séparateur entre les composants */
  const SEPARATOR_PATTERN = '/^\s+/';

  private static function itype($type): IType { return $type; }

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

  /** @var array */
  protected $components;

  function setComponents(array $components): self {
    foreach ($components as $key => &$component) {
      md::ensure_schema($component, self::COMPONENT_SCHEMA, $key, false);
      A::replace_n_indirect($component, "key", "name");
      $type = $component["type"];
      if (is_array($type) && count($type) == 1) {
        $type[] = $component;
      }
      $component["type"] = types::get($type);
    }; unset($component);
    $this->components = $components;
    return $this;
  }

  const PARAMETRABLE_PARAMS_SCHEMA = [
    "ckeys" => ["array", null, "clés à utiliser pour les composantes"],
    "separator" => ["string", null, "séparateur pour le format"],
    "separator_pattern" => ["string", null, "séparateur pour l'analyse"],
    "cmessages" => ["array", null, "messages à retourner en cas d'erreur pour les composantes"],
  ];

  function pp_setCkeys(array $ckeys): self {
    if (A::is_seq($ckeys)) {
      $index = 0;
      foreach ($this->components as &$component) {
        $component["key"] = $ckeys[$index++];
      }; unset($component);
    } else {
      foreach ($ckeys as $cname => $ckey) {
        $this->components[$cname]["key"] = $ckey;
      }
    }
    return $this;
  }

  /** @var string */
  protected $ppSeparator;

  /** @var string */
  protected $ppSeparatorPattern;

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

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

  function __construct(?array $params=null) {
    $this->setComponents(static::COMPONENTS);
    $this->ppSeparator = static::SEPARATOR;
    $this->ppSeparatorPattern = static::SEPARATOR_PATTERN;
    parent::__construct($params);
  }

  /**
   * retourner une liste {cname => component} des composantes, qui sont chacune
   * conformes au schéma {@link COMPONENT_SCHEMA}
   */
  function getComponents(): array {
    return $this->components;
  }

  protected function _getComponent(string $cname) {
    return ValueException::check_nn(A::get($this->components, $cname), "$cname: invalid component");
  }

  /** obtenir le type d'une composante en particulier */
  function getCtype(string $cname): IType {
    return $this->_getComponent($cname)["type"];
  }

  protected function _getCvalue(array $component, $value) {
    return $value[$component["key"]];
  }

  protected function _setCvalue(array $component, &$value, $cvalue): void {
    $value[$component["key"]] = $cvalue;
  }

  /** obtenir la valeur d'une composante en particulier */
  function getCvalue(string $cname, $value) {
    return $this->_getCvalue($this->_getComponent($cname), $value);
  }

  /** spécifier la valeur d'une composante en particulier */
  function setCvalue(string $cname, &$value, $cvalue): void {
    $this->_setCvalue($this->_getComponent($cname), $value, $cvalue);
  }

  function getClass(): string {
    return "array";
  }

  function format($value, ?array $params=null): string {
    $parts = [];
    foreach ($this->components as $component) {
      $ctype = self::itype($component["type"]);
      $parts[] = $ctype->format($this->_getCvalue($component, $value));
    }
    return implode(static::SEPARATOR, $parts);
  }

  function parse(string &$input, ?array $params=null) {
    $data = $input;
    $values = [];
    foreach ($this->components as $component) {
      $ckey = $component["key"];
      $ctype = self::itype($component["type"]);
      $isaSimpleType = $ctype instanceof AbstractSimpleType;
      if ($data !== "") {
        $value = $ctype->parse($data);
      } elseif ($isaSimpleType && $ctype->isAllowParseEmpty()) {
        $value = null;
      } elseif (!$isaSimpleType && $ctype->isAllowEmpty()) {
        $value = "";
        $ctype->verifixReplaceEmpty($value);
      } else {
        $value = false;
      }
      if ($value === false) return false;
      if ($isaSimpleType) $ctype->verifixConvert($value);
      $values[$ckey] = $value;
      $data = preg_replace(static::SEPARATOR_PATTERN, "", $data);
    }
    $input = $data;
    return $values;
  }

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

  protected static function _update_error(array &$cresult, string $error_code, array $component, ?array $cmessages): void {
    $cresult["cname"] = $component["name"];
    $cresult["ckey"] = $component["key"];
    if ($cmessages === null) $cmessages = static::CMESSAGES;
    self::_set_error($cresult, $error_code, $cmessages);
  }

  /**
   * fonction de support: convertir le résultat de l'analyse de la chaine dans
   * le type approprié. retourner true si la valeur est valide, false si la
   * valeur est invalide (bien que peut-être syntaxiquement correcte)
   */
  function verifixConvert(&$value): bool {
    return true;
  }

  /**
   * retourner une description de la valeur, utilisable le cas échéant dans les
   * messages d'erreur
   */
  protected function getValueDesc($orig, $parsed, $value): ?string {
    if (is_array($value)) {
      $parts = [];
      foreach ($this->components as $component) {
        $parts[] = A::get($value, $component["key"]);
      }
      return implode("-", $parts);
    }
    return null;
  }

  protected function _verifix(&$value, array &$result=null): void {
    $orig = $value;
    if ($this->verifixNoParse($value, $result)) return;
    if (is_string($value)) {
      try {
        $input = $unparsed = $value;
        $cvalues = $this->parse($input);
        $parsed = substr($unparsed, 0, strlen($unparsed) - strlen($input));
        if (($input === "" || !$this->ppParseAll) && $this->verifixConvert($cvalues)) {
          $value = $cvalues;
          self::result_valid($result, $value, $orig, $parsed, $input);
        } else {
          $orig_desc = $this->getValueDesc($orig, $parsed, $cvalues);
          $value = self::result_invalid($result, "invalid", $orig, $orig_desc, $value, $input, $this->ppMessages, null);
        }
      } catch (Exception $e) {
        $orig_desc = $this->getValueDesc($orig, false, $value);
        $value = self::result_invalid($result, "invalid", $orig, $orig_desc, false, false, $this->ppMessages, $e);
      }
    } else {
      $cresults = [];
      $cvalid = true;
      foreach ($this->components as $component) {
        $ckey = $component["key"];
        $ctype = self::itype($component["type"]);
        $ctype->verifix($value[$ckey], $cresult);
        if (!$cresult["valid"]) {
          $cvalid = false;
          self::_update_error($cresult, $cresult["error_code"], $component, $this->ppCmessages);
        }
        $cresults[$ckey] = $cresult;
      }
      if ($cvalid && $this->verifixConvert($value)) {
        self::result_valid($result, $value, $orig, false, false);
      } else {
        $orig_desc = $this->getValueDesc($orig, false, $value);
        $value = self::result_invalid($result, "invalid", $orig, $orig_desc, false, false, $this->ppMessages, null);
      }
      $result["cresults"] = $cresults;
    }
  }
}
