<?php
namespace nur\v\bs3\fo;

use ArrayAccess;
use Countable;
use nur\A;
use nur\b\coll\TArrayTools;
use nur\b\coll\TBaseArray;
use nur\b\coll\TGenericArray;
use nur\b\params\IParametrable;
use nur\b\params\Tparametrable1;
use nur\data\types\IType;
use nur\data\types\md_utils;
use nur\data\types\Metadata;
use nur\F;
use nur\msg;
use nur\v\al;
use nur\v\base\Alerter;
use nur\v\base\ComponentPrintable;
use nur\v\v;
use nur\v\vo;

class Form extends ComponentPrintable implements IParametrable, ArrayAccess, Countable {
  use Tparametrable1, TBaseArray, TGenericArray, TArrayTools;

  /** @return bool est-ce un formulaire de type "horizontal" par opposition à "inline" */
  function HORIZONTAL(): bool {
    return static::HORIZONTAL;
  } const HORIZONTAL = false;
  /** @return ?string classe pour les boutons submit en form horizontal */
  function FGS_CLASS(): ?string { return null; }
  /** @return ?string classe pour les labels en form horizontal */
  function FGL_CLASS(): ?string { return null; }
  /** @return ?string classe pour les contrôle en form horizontal */
  function FGC_CLASS(): ?string { return null; }
  function SHOW_HELP(): bool {
    return static::SHOW_HELP;
  } const SHOW_HELP = true;

  /** @var ?string|?array classes spécifiques à ce formulaire */
  const FORM_CLASS = null;

  /** @var bool faut-il tenir compte du paramètre [autohide_label]? */
  const ALLOW_AUTOHIDE_LABEL = false;

  /** @var ?array schéma des données du formulaire */
  const SCHEMA = null;

  /** @var array[] schéma de la définition des paramètres de formulaire */
  const PARAMS_SCHEMA = [
    "key" => ["string", null, "nom de la clé dans le schéma"],
    "name" => ["?string", null, "nom du paramètre"],
    "default" => [null, null, "valeur par défaut du paramètre"],
    "value" => [null, null, "valeur du paramètre à forcer"],
    "label" => [null, null, "label du contrôle de formulaire"],
    "control" => ["?string", null, "classe de contrôle de formulaire à utiliser"],
  ];

  /** @var ?array paramètres de formulaire à récupérer */
  const PARAMS = null;

  /** @var ?array paramètres à exclure de la liste calculée automatiquement */
  const PARAMS_EXCLUDES = null;

  /**
   * @var ?array paramètres de formulaire à récupérer. cette valeur est mergée
   * avec PARAMS. cela permet d'augmenter le paramétrage calculé automatiquement
   */
  const PARAMS_OVERRIDES = null;

  const INVALID_MSG = null;

  /** @var ?array paramètres du bouton submit automatique */
  const SUBMIT = "Envoyer";

  const CONTROL_PREFIX = "\n";

  const CONTROL_SUFFIX = null;

  const SUBMITTED_KEY = null;

  const CONTROL_CLASSES = [
    "hidden" => ControlHidden::class,
    "fixed" => ControlFixed::class,
    "text" => ControlText::class,
    "number" => ControlNumber::class,
    "date" => ControlDate::class,
    "time" => ControlTime::class,
    "password" => ControlPassword::class,
    "select" => ControlSelect::class,
    "checkbox" => ControlCheckbox::class,
    "radiobutton" => ControlRadiobutton::class,
    "textarea" => ControlTextarea::class,
    "file" => ControlFile::class,
    "submit" => ControlSubmit::class,
    "reset" => ControlReset::class,
  ];

  /** @var int type de contrôle: name, name+value ou name+value+checked */
  const N = 1, NV = 2, NVC = 3;
  const CONTROL_TYPES = [
    ControlHidden::class => [self::NV, null],
    ControlFixed::class => [self::NV, "label"],
    ControlText::class => [self::NV, "label"],
    ControlNumber::class => [self::NV, "label"],
    ControlDate::class => [self::NV, "label"],
    ControlTime::class => [self::NV, "label"],
    ControlPassword::class => [self::NV, "label"],
    ControlSelect::class => [self::NV, "label"],
    ControlCheckbox::class => [self::NVC, "text"],
    ControlRadiobutton::class => [self::NVC, "text"],
    ControlTextarea::class => [self::NV, "label"],
    ControlFile::class => [self::N, "label"],
    ControlSubmit::class => [self::NV, "submit"],
    ControlReset::class => [self::NV, "reset"],
  ];

  const PARAMETRABLE_PARAMS_SCHEMA = [
    "action" => ["?string", null, "url destination du formulaire"],
    "method" => ["?string", "get", "méthode du formulaire (post, get)"],
    "enctype" => ["?string", null, "type d'encodage du formulaire"],
    "upload" => ["bool", null, "ce formulaire est-il utilisé pour uploader des fichiers?"],
    "class" => ["?array", null, "classes CSS du formulaire"],
    "attrs" => ["?array", null, "attributs HTML supplémentaires"],
    "autohide_label" => ["bool", true, "faut-il cacher le label s'il y a le placeholder?"],
    "schema" => ["?array", null, "schéma des données du formulaire"],
    "params" => ["?array", null, "paramètres de formulaire à récupérer"],
    "params_excludes" => ["?array", null, "paramètres à exclure de la liste calculée automatiquement"],
    "params_overrides" => ["?array", null, "paramètres de formulaire à récupérer à merger avec [params]"],
    "alerter" => [Alerter::class, null, "instance utilisée pour afficher un message avant le bouton submit"],
    "invalid_msg" => ["?content", null, "message à afficher si le formulaire est invalide"],
    "submit" => ["?array", null, "paramètres du bouton submit automatique"],
    "submits" => ["?array", null, "boutons submit supplémentaires"],
    "autoadd_submit" => ["bool", true, "Faut-il rajouter le bouton submit automatique?"],
    "control_prefix" => [null, null, "valeur à afficher avant chaque contrôle"],
    "control_suffix" => [null, null, "valeur à afficher après chaque contrôle"],
    "submitted_key" => ["?string", null, "nom d'un champ du formulaire qui, s'il existe, indique que le formulaire a été soumis"],
    "autoload_params" => ["bool", false, "faut-il charger les paramètres de formulaire de suite?"],
    "autoprint" => ["bool", false, "faut-il afficher le formulaire de suite?"],
  ];

  function __construct(?array $params=null) {
    self::set_parametrable_params_defaults($params, [
      "schema" => static::SCHEMA,
      "params_exclude" => static::PARAMS_EXCLUDES,
      "params_overrides" => static::PARAMS_OVERRIDES,
      "params" => static::PARAMS,
      "invalid_msg" => q(static::INVALID_MSG),
      "submit" => A::with(static::SUBMIT),
      "control_prefix" => q(static::CONTROL_PREFIX),
      "control_suffix" => q(static::CONTROL_SUFFIX),
      "submitted_key" => static::SUBMITTED_KEY,
    ]);
    [$params, $attrs] = $this->splitParametrableParams($params);
    if ($attrs) A::merge($params["attrs"], $attrs);
    $this->initParametrableParams($params);
    if ($this->ppAutoloadParams) $this->loadParams();
    if ($this->ppAutoprint) $this->print();
  }

  /** @var ?string */
  protected $ppAction;
  
  /** @var ?string */
  protected $ppMethod;

  /** @var ?string */
  protected $ppEnctype;

  function pp_setUpload(bool $upload=true): void {
    if ($upload) {
      $this->ppMethod = "post";
      $this->ppEnctype = "multipart/form-data";
    }
  }

  /** @var ?array */
  protected $ppClass;

  /** @var ?array */
  protected $ppAttrs;

  /** @var ?bool */
  protected $ppAutohideLabel;

  function isAutohideLabel(): bool {
    return static::ALLOW_AUTOHIDE_LABEL && $this->ppAutohideLabel;
  }

  /** @var ?Metadata */
  protected $md;

  function pp_setSchema(?array $schema): void {
    $md = null;
    if ($schema !== null) $md = new Metadata($schema);
    $this->md = $md;
  }

  private static $params_md;

  protected static function params_md(): Metadata {
    return md_utils::ensure_md(self::$params_md, self::PARAMS_SCHEMA);
  }

  protected static function params_ensure_schema(array &$params, ?Metadata $md=null): void {
    $params_md = self::params_md();
    foreach ($params as $key => &$param) {
      $params_md->ensureSchema($param, $key);
      A::replace_n($param, "name", $key);
      if ($md !== null) {
        $sfield = A::get($md->getSfields(), $key);
        if ($sfield !== null) {
          A::replace_n($param, "default", $sfield["default"]);
        }
      }
      $controlClass = $param["control"];
      if ($controlClass !== null) {
        $controlClass = A::get(static::CONTROL_CLASSES, $controlClass, $controlClass);
        $param["control"] = $controlClass;
      }
    }; unset($param);
  }

  /** @var ?array */
  protected $ppParamsExcludes;

  /** @var ?array */
  protected $ppParamsOverrides;

  /** @var ?array */
  protected $ppParams;

  /** @var bool des valeurs ont-elle été attribuées à des paramètres? */
  protected $paramsValuesSet;

  /** @var array fusion de $ppParams, $ppParamsExcludes et $ppParamsOverrides */
  protected $params;

  /** @var ?Alerter */
  protected $ppAlerter;

  function al(): ?Alerter {
    $al = $this->ppAlerter;
    if ($al === null) $al = al::get();
    return $al;
  }

  /** @var array|string|null */
  protected $ppInvalidMsg;

  /** @var ?array */
  protected $ppSubmit;

  /** @var ?array */
  protected $ppSubmits;

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

  /** @var array contenu à afficher avant chaque élément de formulaire */
  protected $ppControlPrefix;

  function getControlPrefix(): ?array {
    return $this->ppControlPrefix;
  }

  /** @var array contenu à afficher après chaque élément de formulaire */
  protected $ppControlSuffix;

  function getControlSuffix(): ?array {
    return $this->ppControlSuffix;
  }

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

  /** @var ?string */
  protected $ppSubmittedKey;

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

  protected function afterSetParametrableParams(array $modifiedKeys, ?Metadata $md=null): void {
    if (self::was_parametrable_param_modified($modifiedKeys, "submits")
      && !self::was_parametrable_param_modified($modifiedKeys, "autoadd_submit")
    ) {
      $this->ppAutoaddSubmit = false;
    }
    if (self::was_parametrable_param_modified($modifiedKeys, "params", "params_excludes", "params_overrides")) {
      $params = [];
      if ($this->ppParams !== null) {
        $paramsExcludes = $this->ppParamsExcludes;
        $index = 0;
        foreach ($this->ppParams as $key => $param) {
          if ($key === $index) {
            $index++;
            $key = $param;
          }
          if ($paramsExcludes === null || !in_array($key, $paramsExcludes)) {
            $params[$key] = $param;
          }
        }
        self::params_ensure_schema($params, $md);
      }
      $paramsOverrides = $this->ppParamsOverrides;
      if ($paramsOverrides !== null) {
        $oparams = [];
        $index = 0;
        foreach ($paramsOverrides as $key => $param) {
          if ($key === $index) {
            $index++;
            $key = $param;
          }
          $oparams[$key] = $param;
        }
        self::params_ensure_schema($oparams, $md);
        foreach ($oparams as $key => $param) {
          if (!A::has($params, $key)) {
            $params[$key] = $param;
          } else {
            A::merge_nn($params[$key], $param);
          }
        }
      }
      $this->paramsValuesSet = false;
      $this->params = $params;
    }
  }

  #############################################################################
  ## Params

  private function resolveName($key, ?string &$name, &$value=null): bool {
    $params = $this->params;
    if (A::has($params, $key)) {
      $param = $params[$key];
      $name = $param["name"];
      $value = $param["default"];
      return true;
    } else {
      $name = $key;
      $value = null;
      return false;
    }
  }

  private function resolveNameType($key, ?string $name=null): array {
    if ($name === null) $this->resolveName($key, $name);
    $md = $this->md;
    $type = $md !== null? $md->getType($key, false): null;
    return [$name, $type];
  }

  function set($key, $value, ?string $name=null): self {
    $unset = false;
    /** @var IType $type */
    [$name, $type] = $this->resolveNameType($key, $name);
    if ($type !== null) {
      if ($type->isUndef($value)) $unset = true;
      else $type->verifix($value, $this->results[$name]);
    }
    $this->paramsValuesSet = true;
    if ($unset) $this->_del($key);
    else $this->_set($key, $value);
    return $this;
  }

  protected function paramExists(string $name): bool {
    return F::has($name);
  }

  protected function paramGet($key, ?string $name, &$value): bool {
    /** @var IType $type */
    [$name, $type] = $this->resolveNameType($key, $name);
    $value = F::get($name);
    if ($value === null) return false;
    if ($type !== null) {
      if ($type->isTrim()) $value = trim($value);
      if ($value === "") $type->verifixReplaceEmpty($value);
      if ($value === false) $type->verifixReplaceFalse($value);
      if ($value === null) $type->verifixReplaceNull($value);
      if ($type->isUndef($value)) return false;
      elseif ($value === null && !$type->isAllowNull()) return false;
      elseif ($value === false && !$type->isAllowFalse()) return false;
      elseif ($value === "" && !$type->isAllowEmpty()) $value = null;
    }
    return true;
  }

  protected $submitted;

  protected function checkSubmitted(): ?bool {
    $submittedKey = $this->ppSubmittedKey;
    if ($submittedKey !== null) {
      if ($this->paramGet($submittedKey, null, $value)) {
        return true;
      }
    }
    $submitted = null;
    foreach (A::keys($this->params) as $key) {
      if ($this->paramGet($key, null, $value)) {
        if ($submitted === null) $submitted = true;
      } else {
        $submitted = false;
        break;
      }
    }
    return $submitted;
  }

  /** tester si le formulaire a été soumis */
  function isSubmitted(): bool {
    if ($this->submitted === null) {
      $this->submitted = boolval($this->checkSubmitted());
    }
    return $this->submitted;
  }

  function setSubmitted(bool $submitted=true): void {
    $this->submitted = $submitted;
  }

  function loadParams(?array $keys=null): self {
    $md = $this->md;
    if ($this->ppParams === null && $md !== null) {
      $this->ppParams = $md->getKeys();
      $this->afterSetParametrableParams(["params"], $md);
    }
    $params = $this->params;
    if ($keys === null && $params !== null) {
      $keys = array_keys($params);
    }
    $submitted = $this->submitted;
    if ($keys !== null) {
      $submitted = $this->checkSubmitted();
      foreach ($keys as $key) {
        $this->resolveName($key, $name, $value);
        if ($this->paramExists($name)) {
          if ($submitted === null) $submitted = true;
          if ($this->paramGet($key, $name, $value)) {
            $this->set($key, $value, $name);
          } else {
            #XXX?
            #$this->del($key);
          }
        } elseif ($value !== null) {
          $this->set($key, $value, $name);
        } else {
          #XXX
          #$this->del($key);
        }
      }
      if ($submitted === null) $submitted = false;
    }
    $this->paramsValuesSet = true;
    $this->submitted = $submitted;
    return $this;
  }

  function autoloadParams(): self {
    if (!$this->paramsValuesSet) $this->loadParams();
    return $this;
  }

  function resetParams(string ...$keys): void {
    foreach ($keys as $key) {
      $this->_del($key);
    }
  }

  /** retourner $params avec les $key traduits en $name */
  function bp(?array $params=null): array {
    $result = [];
    if ($params !== null) {
      $index = 0;
      foreach ($params as $key => $value) {
        if ($key === $index) {
          $index++;
          $key = $value;
          $value = $this->get($key);
        }
        $this->resolveName($key, $name);
        $result[$name] = $value;
      }
    }
    return $result;
  }

  #############################################################################
  ## Validation

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

  function setInvalid(string $key, ?string $message=null): void {
    if ($message === null) $message = "Cette valeur est invalide";
    $this->resolveName($key, $name);
    $value = A::get($this->data, $key);
    $this->results[$name] = [
      "valid" => false,
      "value" => $value,
      "error" => $message,
      "exception" => null,
      "orig" => $value,
      "parsed" => null,
      "remains" => null,
    ];
  }

  function _getResult(string $name): ?array {
    return A::get($this->results, $name);
  }

  function getFirstInvalid(): ?string {
    if ($this->results !== null) {
      foreach ($this->results as $name => $result) {
        if (!$result["valid"]) return $name;
      }
    }
    return null;
  }

  function isValidForm(): bool {
    # si le formulaire n'a pas été soumis, il ne peut pas être valide
    $valid = false;
    if ($this->isSubmitted() && $this->results !== null) {
      foreach ($this->results as $result) {
        if (!$result["valid"]) return false;
        $valid = true;
      }
    }
    return $valid;
  }

  function isValid(string $key, ?array &$result=null): bool {
    $this->resolveName($key, $name);
    $result = A::get($this->results, $name);
    return $result === null || $result["valid"];
  }

  #############################################################################
  ## Form

  function printAlert(): void {
    $alerter = $this->ppAlerter;
    if ($alerter !== null) $alerter->print();
    $invalidMsg = $this->ppInvalidMsg;
    if ($invalidMsg && $this->isSubmitted() && !$this->isValidForm()) {
      if ($alerter !== null) $alerter->pwarning($invalidMsg);
      else msg::warning($invalidMsg);
    }
  }

  function printStart(): void {
    vo::write($this->getStart());
  }

  function getStart(): array {
    return v::start("form", [
      "action" => $this->ppAction,
      "method" => $this->ppMethod,
      "enctype" => $this->ppEnctype,
      "class" => [static::FORM_CLASS, $this->ppClass],
      $this->ppAttrs,
      $this->form(),
    ]);
  }

  protected function form(): ?array {
    return null;
  }

  function printEnd(): void {
    vo::write($this->getEnd());
  }

  function getEnd(): array {
    return v::end("form");
  }

  #############################################################################
  ## Controls

  /** @var ?array */
  protected $hiddenControls;

  function addHiddenControl($control, ?string $name=null): self {
    A::set($this->hiddenControls, $name, $control);
    return $this;
  }

  /** @var ?array */
  protected $controls;

  function addControl($control, ?string $name=null): self {
    A::set($this->controls, $name, $control);
    return $this;
  }

  protected function buildControl(?array $param, ?string $key=null, ?string $controlClass=null, ?array $params=null): array {
    if ($param === null) $param = A::get($this->params, $key);
    if ($param !== null) {
      [$key, $name, $default, $value, $label, $defaultControlClass,
      ] = A::getdels($param, array_keys(self::PARAMS_SCHEMA));
      if ($controlClass === null && $defaultControlClass !== null) {
        $controlClass = $defaultControlClass;
      }
    } else {
      $name = $key;
      $default = $value = null;
      $label = null;
      if ($controlClass === null) $controlClass = ControlText::class;
    }

    $md = $this->md;
    $type = $md !== null? $md->getType($key, false): null;
    $sfields = $md !== null? $md->getSfields(): null;
    if ($controlClass === null) {
      $haveItems = A::has($param, "items") || A::has($param, "items_func");
      $is2States = $type !== null && $type->is2States();
      if ($haveItems) $controlClass = ControlSelect::class;
      elseif ($is2States) $controlClass = ControlCheckbox::class;
      else $controlClass = ControlText::class;
    }

    [$controlType, $firstKey] = A::get(static::CONTROL_TYPES, $controlClass, self::NV);

    if ($label === null && $md !== null) {
      $label = A::_pget($sfields, [$key, "title"]);
    }
    if ($label === null) $label = $key;
    if ($firstKey !== null) $param[$firstKey] = $label;

    switch ($controlType) {
    case self::NVC:
      $param["name"] = $name;
      if ($value === null) $value = $default;
      if ($value === null) $value = 1;
      $currentValue = $this->get($key);
      if ($type !== null) {
        #XXX la nécessité de formatter la valeur dépend-elle du contrôle utilisé?!
        $value = $type->format($value);
        $currentValue = $type->format($currentValue);
      }
      $param["value"] = $value;
      #XXX en attendant le formattage ci-dessus, forcer la format texte pour que
      # la comparaison puisse se faire
      $param["checked"] = strval($currentValue) === strval($value);
      break;
    case self::NV:
      if ($value === null) $value = $this->get($key, $default);
      if ($type !== null) $value = $type->format($value);
      $param["value"] = $value;
    case self::N:
      $param["name"] = $name;
    }

    if ($params === null) $params = $param;
    else A::update_n($params, $param);
    return [new $controlClass($this, $params), $name];
  }

  private function _prepareControls(): ?array {
    if ($this->params === null) $this->loadParams();
    A::ensure_array($this->controls);
    return $this->params;
  }

  function autoaddControl(string $key): self {
    $params = $this->_prepareControls();
    if ($key == "") {
      # submit
      $this->addSubmit(null, $this->ppSubmit);
    } elseif (A::has($this->ppSubmits, $key)) {
      $submit = $this->ppSubmits[$key];
      A::replace_n($submit, "id", $key);
      $this->addSubmit(null, $submit);
    } else {
      $param = A::get($params, $key);
      if ($param !== null) {
        [$control, $name] = $this->buildControl($param);
        $this->addControl($control, $name);
      }
    }
    return $this;
  }

  function autoaddControls(?bool $autoaddSubmit=null): self {
    $params = $this->_prepareControls();
    if ($params !== null) {
      foreach ($params as $param) {
        [$control, $name] = $this->buildControl($param);
        if (A::has($this->hiddenControls, $name)) continue;
        if (A::has($this->controls, $name)) continue;
        $this->addControl($control, $name);
      }
    }
    if ($autoaddSubmit === null) $autoaddSubmit = $this->ppAutoaddSubmit;
    if ($autoaddSubmit && !A::has($this->controls, "")) {
      $this->addSubmit(null, $this->ppSubmit);
    }
    $submits = $this->ppSubmits;
    if ($submits !== null) {
      foreach ($submits as $key => $submit) {
        if (!A::has($this->controls, $key)) {
          A::replace_n($submit, "id", $key);
          $this->addSubmit(null, $submit);
        }
      }
    }
    return $this;
  }

  function printControls(): void {
    if ($this->controls === null) $this->autoaddControls();
    vo::write($this->hiddenControls);
    vo::write($this->controls);
  }

  protected function fixControlParams(?array &$params): void {
  }
  protected function fixControl(Control $control): Control {
    return $control;
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # section

  /** @var Section */
  protected $section;

  function startSection(?array $params=null): void {
  }
  function endSection(): void {
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # group

  /** @var Group */
  protected $group;

  function startGroup(?array $params=null): void {
  }
  function endGroup(): void {
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # title

  function title($title, ?array $params=null): ControlTitle {
    A::set_nz($params, "title", $title);
    $this->fixControlParams($params);
    return new ControlTitle($this, $params);
  }

  function addTitle($title, ?array $params=null): self {
    $this->addControl($this->title($title, $params));
    return $this;
  }

  function printTitle($title, ?array $params=null): void {
    vo::write($this->title($title, $params));
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # hidden

  function hidden(string $name, $value, ?array $params=null): ControlHidden {
    A::set_nz($params, "name", $name);
    A::set_nz($params, "value", $value);
    return new ControlHidden($this, $params);
  }

  function addHidden(string $name, $value, ?array $params=null): self {
    $this->addHiddenControl($this->hidden($name, $value, $params), $name);
    return $this;
  }

  function autoaddHidden(string $key, ?array $params=null): self {
    $this->addHiddenControl(...$this->buildControl(null, $key, ControlHidden::class, $params));
    return $this;
  }

  function printHidden(string $name, $value, ?array $params=null): void {
    vo::write($this->hidden($name, $value, $params));
  }

  function hiddens(?array $values, ?array $names=null): array {
    if ($values === null) return [];
    if ($names === null) $names = array_keys($values);
    $vs = [];
    foreach ($names as $name) {
      $value = A::get($values, $name);
      $vs[] = $this->hidden($name, $value);
    }
    return $vs;
  }

  function addHiddens(?array $values, ?array $names=null): self {
    if ($values !== null) {
      if ($names === null) $names = array_keys($values);
      foreach ($names as $name) {
        $value = A::get($values, $name);
        $this->addHiddenControl($this->hidden($name, $value), $name);
      }
    }
    return $this;
  }

  function printHiddens(?array $values, ?array $names=null): void {
    vo::write($this->hiddens($values, $names));
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # fixed

  function fixed($label, string $name, $value, ?array $params=null): ControlFixed {
    A::set_nz($params, "label", $label);
    A::set_nz($params, "name", $name);
    A::set_nz($params, "value", $value);
    $this->fixControlParams($params);
    return $this->fixControl(new ControlFixed($this, $params));
  }

  function addFixed($label, string $name, $value, ?array $params=null): self {
    $this->addControl($this->fixed($label, $name, $value, $params), $name);
    return $this;
  }

  function autoaddFixed(string $key, ?array $params=null): self {
    $this->addControl(...$this->buildControl(null, $key, ControlFixed::class, $params));
    return $this;
  }

  function printFixed($label, string $name, $value, ?array $params=null): void {
    vo::write($this->fixed($label, $name, $value, $params));
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # input

  function text($label, string $name, $value, ?array $params=null): ControlText {
    A::set_nz($params, "label", $label);
    A::set_nz($params, "name", $name);
    A::set_nz($params, "value", $value);
    $this->fixControlParams($params);
    return $this->fixControl(new ControlText($this, $params));
  }

  function addText($label, string $name, $value, ?array $params=null): self {
    $this->addControl($this->text($label, $name, $value, $params), $name);
    return $this;
  }

  function autoaddText(string $key, ?array $params=null): self {
    $this->addControl(...$this->buildControl(null, $key, ControlText::class, $params));
    return $this;
  }

  function printText($label, string $name, $value, ?array $params=null): void {
    vo::write($this->text($label, $name, $value, $params));
  }

  function password($label, string $name, $value, ?array $params=null): ControlPassword {
    A::set_nz($params, "label", $label);
    A::set_nz($params, "name", $name);
    A::set_nz($params, "value", $value);
    $this->fixControlParams($params);
    return $this->fixControl(new ControlPassword($this, $params));
  }

  function addPassword($label, string $name, $value, ?array $params=null): self {
    $this->addControl($this->password($label, $name, $value, $params), $name);
    return $this;
  }

  function autoaddPassword(string $key, ?array $params=null): self {
    $this->addControl(...$this->buildControl(null, $key, ControlPassword::class, $params));
    return $this;
  }

  function printPassword($label, string $name, $value, ?array $params=null): void {
    vo::write($this->password($label, $name, $value, $params));
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # select

  function select($label, string $name, $value, ?array $params=null): ControlSelect {
    A::set_nz($params, "label", $label);
    A::set_nz($params, "name", $name);
    A::set_nz($params, "value", $value);
    $this->fixControlParams($params);
    return $this->fixControl(new ControlSelect($this, $params));
  }

  function addSelect($label, string $name, $value, ?array $params=null): self {
    $this->addControl($this->select($label, $name, $value, $params), $name);
    return $this;
  }

  function autoaddSelect(string $key, ?array $params=null): self {
    $this->addControl(...$this->buildControl(null, $key, ControlSelect::class, $params));
    return $this;
  }

  function printSelect($label, string $name, $value, ?array $params=null): void {
    vo::write($this->select($label, $name, $value, $params));
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # checkbox

  function checkbox($text, string $name, $value, ?bool $checked=null, ?array $params=null): ControlCheckbox {
    A::set_nz($params, "text", $text);
    A::set_nz($params, "name", $name);
    A::set_nz($params, "value", $value);
    A::set_nz($params, "checked", $checked);
    $this->fixControlParams($params);
    return $this->fixControl(new ControlCheckbox($this, $params));
  }

  function addCheckbox($text, string $name, $value, ?bool $checked=null, ?array $params=null): self {
    $this->addControl($this->checkbox($text, $name, $value, $checked, $params), $name);
    return $this;
  }

  function autoaddCheckbox(string $key, ?array $params=null): self {
    $this->addControl(...$this->buildControl(null, $key, ControlCheckbox::class, $params));
    return $this;
  }

  function printCheckbox($text, string $name, $value, ?bool $checked=null, ?array $params=null): void {
    vo::write($this->checkbox($text, $name, $value, $checked, $params));
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # radiobutton

  function radiobutton($text, string $name, $value, ?bool $checked=null, ?array $params=null): ControlRadiobutton {
    A::set_nz($params, "text", $text);
    A::set_nz($params, "name", $name);
    A::set_nz($params, "value", $value);
    A::set_nz($params, "checked", $checked);
    $this->fixControlParams($params);
    return $this->fixControl(new ControlRadiobutton($this, $params));
  }

  function addRadiobutton($text, string $name, $value, ?bool $checked=null, ?array $params=null): self {
    $this->addControl($this->radiobutton($text, $name, $value, $checked, $params), $name);
    return $this;
  }

  function autoaddRadiobutton(string $key, ?array $params=null): self {
    $this->addControl(...$this->buildControl(null, $key, ControlRadiobutton::class, $params));
    return $this;
  }

  function printRadiobutton($text, string $name, $value, ?bool $checked=null, ?array $params=null): void {
    vo::write($this->radiobutton($text, $name, $value, $checked, $params));
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # textarea

  function textarea($label, string $name, $value, ?array $params=null): ControlTextarea {
    A::set_nz($params, "label", $label);
    A::set_nz($params, "name", $name);
    A::set_nz($params, "value", $value);
    $this->fixControlParams($params);
    return $this->fixControl(new ControlTextarea($this, $params));
  }

  function addTextarea($label, string $name, $value, ?array $params=null): self {
    $this->addControl($this->textarea($label, $name, $value, $params), $name);
    return $this;
  }

  function autoaddTextarea(string $key, ?array $params=null): self {
    $this->addControl(...$this->buildControl(null, $key, ControlTextarea::class, $params));
    return $this;
  }

  function printTextarea($label, string $name, $value, ?array $params=null): void {
    vo::write($this->textarea($label, $name, $value, $params));
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # file

  function file($label, string $name, ?array $params=null): ControlFile {
    A::set_nz($params, "label", $label);
    A::set_nz($params, "name", $name);
    $this->fixControlParams($params);
    return $this->fixControl(new ControlFile($this, $params));
  }

  function addFile($label, string $name, ?array $params=null): self {
    $this->addControl($this->file($label, $name, $params), $name);
    return $this;
  }

  function autoaddFile(string $key, ?array $params=null): self {
    $this->addControl(...$this->buildControl(null, $key, ControlFile::class, $params));
    return $this;
  }

  function printFile($label, string $name, ?array $params=null): void {
    vo::write($this->file($label, $name, $params));
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # submit

  function submit($submit, ?array $params=null): ControlSubmit {
    A::set_nz($params, "submit", $submit);
    $this->fixControlParams($params);
    return $this->fixControl(new ControlSubmit($this, $params));
  }

  function addSubmit($submit, ?array $params=null): self {
    $name = A::get($params, "id");
    if ($name === null) A::get($params, "name");
    if ($name === null) $name = "";
    $this->addControl($this->submit($submit, $params), $name);
    return $this;
  }

  function autoaddSubmit(string $key, ?array $params=null): self {
    $this->addControl(...$this->buildControl(null, $key, ControlSubmit::class, $params));
    return $this;
  }

  function printSubmit($submit, ?array $params=null): void {
    vo::write($this->submit($submit, $params));
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # reset

  function reset($reset, ?array $params=null): ControlReset {
    A::set_nz($params, "reset", $reset);
    $this->fixControlParams($params);
    return $this->fixControl(new ControlReset($this, $params));
  }

  function addReset($reset, ?array $params=null): self {
    $name = A::get($params, "name", "");
    $this->addControl($this->reset($reset, $params), $name);
    return $this;
  }

  function autoaddReset(string $key, ?array $params=null): self {
    $this->addControl(...$this->buildControl(null, $key, ControlReset::class, $params));
    return $this;
  }

  function printReset($reset, ?array $params=null): void {
    vo::write($this->reset($reset, $params));
  }

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

  function print(): void {
    $this->autoloadParams();
    $this->printAlert();
    $this->printStart();
    $this->printControls();
    $this->printEnd();
  }
}
