<?php
namespace nur\mapper\base;

use IteratorAggregate;
use nur\b\ICloseable;
use nur\b\params\IParametrable;
use nur\b\params\Parametrable;
use nur\b\params\Tparametrable;
use nur\func;
use nur\mapper\base\oobd\IOobdManager;
use nur\mapper\base\oobd\TOobdManager;

/**
 * Class Mapper: un mappeur / filtre de données
 *
 * La méthode {@link mapper()} permet d'associer un élément à zéro, un ou
 * plusieurs autres éléments. il faut:
 * - soit retourner une valeur correspondante. si la valeur retournée est null,
 * alors cela revient à ignorer la valeur en entrée
 * - soit appeler l'une des méthodes {@link mapTo()}. il est possible de mapper
 * vers une valeur, un tableau de valeurs, ou un iterateur/générateur
 *
 * Pour pouvoir être utilisé avec {@link Consumer}, les constructeurs des
 * classes dérivées doivent par convention mentionner $source comme dernier
 * argument.
 *
 * --autogen-properties-and-methods--
 * @method iterable getSource()
 * @method iterable setSource(iterable $value)
 */
abstract class Mapper extends Parametrable implements IteratorAggregate, ICloseable, IParametrable, IOobdManager {
  use Tparametrable, TOobdManager;

  /**
   * @return bool indiquer s'il faut appeler {@link mapper()} une première fois
   * au début de l'itération avec {null => null}. cela permet d'implémenter des
   * méthodes de mapping plus complexes.
   */
  protected function MAP_SOF(): bool {
    return static::MAP_SOF;
  } const MAP_SOF = false;

  /**
   * @return bool indiquer s'il faut appeler {@link mapper()} une dernière fois
   * à la fin de l'itération avec {null => null}. cela permet d'implémenter des
   * méthodes de mapping plus complexes.
   */
  protected function MAP_EOF(): bool {
    return static::MAP_EOF;
  } const MAP_EOF = false;

  /**
   * @param iterable|null $source dans les classes dérivées, cet argument doit
   * par convention être le dernier de la liste
   */
  function __construct(?iterable $source=null) {
    $params = null;
    if ($source !== null) $params["source"] = $source;
    parent::__construct($params);
  }

  /**
   * @var array liste de paramètres pour la configuration générique de cet objet
   */
  const PARAMETRABLE_PARAMS_SCHEMA = [
    "source" => ["iterable", null, "source de données"],
  ];

  /** @var iterable */
  protected $ppSource;

  function hasOvalue(string $name): bool {
    if ($this->_hasOobdValue($name)) {
      return true;
    } elseif ($this->sharedOobdManager !== null
      && $this->sharedOobdManager->hasOvalue($name)) {
      return true;
    } elseif ($this->ppSource instanceof IOobdManager) {
      return $this->ppSource->hasOvalue($name);
    }
    return false;
  }

  function getOvalue(string $name, $default=null) {
    if ($this->_hasOobdValue($name)) {
      return $this->_getOobdValue($name, $default);
    } elseif ($this->sharedOobdManager !== null
      && $this->sharedOobdManager->hasOvalue($name)) {
      return $this->sharedOobdManager->getOvalue($name, $default);
    } elseif ($this->ppSource instanceof IOobdManager) {
      return $this->ppSource->getOvalue($name, $default);
    }
    return null;
  }

  /** @var bool */
  private $setup = false;

  function ensureSetup(): void {
    if (!$this->setup) {
      $this->setup();
      $this->setup = true;
    }
  }

  /**
   * initialiser l'itérateur. cette méthode est appelée avant de lancer
   * l'itération
   */
  protected function setup(): void {
  }

  function getIterator(): iterable {
    $this->ensureSetup();
    $this->sof = false;
    $this->eof = false;
    try {
      $mapFunc = func::_prepare([$this, "mapper"]);
      $outIndex = 0;
      $srcIndex = 0;
      if ($this->MAP_SOF()) {
        $this->sof = true;
        while (true) {
          $this->action = self::USE_MAPPED_VALUE_ACTION;
          $this->mappedIterables = null;
          $mappedValue = func::_call($mapFunc, [null, null]);
          if ($this->action !== self::RETRY_ACTION) break;
        }
        switch ($this->action) {
        case self::USE_MAPPED_VALUE_ACTION:
          if ($mappedValue !== null) {
            yield $outIndex => $mappedValue;
            $outIndex++;
          }
          break;
        case self::USE_MAPPED_ITERABLES_ACTION:
          foreach ($this->mappedIterables as $mappedIterable) {
            $seqIndex = 0;
            foreach ($mappedIterable as $mappedKey => $mappedValue) {
              if ($mappedKey === $seqIndex) {
                # seq
                yield $outIndex => $mappedValue;
                $outIndex++;
                $seqIndex++;
              } else {
                # assoc
                yield $mappedKey => $mappedValue;
              }
            }
          }
          break;
        }
        $this->sof = false;
      }
      if ($this->ppSource !== null) {
        foreach ($this->ppSource as $key => $value) {
          while (true) {
            $this->mappedIterables = null;
            $this->action = self::USE_MAPPED_VALUE_ACTION;
            $mappedValue = func::_call($mapFunc, [$value, $key]);
            if ($this->action !== self::RETRY_ACTION) break;
          }
          switch ($this->action) {
          case self::USE_MAPPED_VALUE_ACTION:
            if ($mappedValue !== null) {
              if ($key === $srcIndex) {
                yield $outIndex => $mappedValue;
                $outIndex++;
              } else {
                yield $key => $mappedValue;
              }
            }
            break;
          case self::USE_MAPPED_ITERABLES_ACTION:
            foreach ($this->mappedIterables as $mappedIterable) {
              $seqIndex = 0;
              foreach ($mappedIterable as $mappedKey => $mappedValue) {
                if ($mappedKey === $seqIndex) {
                  # seq
                  yield $outIndex => $mappedValue;
                  $outIndex++;
                  $seqIndex++;
                } else {
                  # assoc
                  yield $mappedKey => $mappedValue;
                }
              }
            }
            break;
          }
          if ($key === $srcIndex) $srcIndex++;
        }
      }
      if ($this->MAP_EOF()) {
        $this->eof = true;
        while (true) {
          $this->action = self::USE_MAPPED_VALUE_ACTION;
          $this->mappedIterables = null;
          $mappedValue = func::_call($mapFunc, [null, null]);
          if ($this->action !== self::RETRY_ACTION) break;
        }
        switch ($this->action) {
        case self::USE_MAPPED_VALUE_ACTION:
          if ($mappedValue !== null) {
            yield $outIndex => $mappedValue;
          }
          break;
        case self::USE_MAPPED_ITERABLES_ACTION:
          foreach ($this->mappedIterables as $mappedIterable) {
            $seqIndex = 0;
            foreach ($mappedIterable as $mappedKey => $mappedValue) {
              if ($mappedKey === $seqIndex) {
                # seq
                yield $outIndex => $mappedValue;
                $outIndex++;
                $seqIndex++;
              } else {
                # assoc
                yield $mappedKey => $mappedValue;
              }
            }
          }
          break;
        }
      }
    } finally {
      $this->close();
    }
  }

  /** terminer l'itération. cette méthode est appelée à la fin de l'itération */
  protected function teardown(): void {
    if ($this->ppSource instanceof ICloseable) $this->ppSource->close();
  }

  function close(): void {
    if ($this->setup) {
      $this->teardown();
      $this->setup = false;
    }
  }

  /**
   * @var bool indique que l'on est au début de l'itération si et seulement si
   * {@link MAP_SOF()} retourne true
   */
  protected $sof;

  /**
   * @var bool indique que l'on est à la fin de l'itération si et seulement si
   * {@link MAP_EOF()} retourne true
   */
  protected $eof;

  const USE_MAPPED_VALUE_ACTION = 0;
  const USE_MAPPED_ITERABLES_ACTION = 1;
  const RETRY_ACTION = 2;

  /** @var bool action à effectuer au retour de la fonction {@link mapper()} */
  private $action;

  /** @var array valeurs générées par les méthodes {@link mapTo()} */
  private $mappedIterables;

  /**
   * indiquer que la valeur courante doit être mappée vers $values. si $values
   * est null, la valeur courante n'est pas mappée.
   * Cette méthode doit être appelée depuis le corps de {@link mapper()}
   */
  function mapTo(?iterable $values) {
    if ($this->action !== self::USE_MAPPED_ITERABLES_ACTION) {
      $this->action = self::USE_MAPPED_ITERABLES_ACTION;
      $this->mappedIterables = [];
    }
    if ($values !== null) {
      $this->mappedIterables[] = $values;
    }
    return null;
  }

  /**
   * indiquer que la valeur courante doit être mappée vers $value avec
   * éventuellement la clé $key. $value est fournie telle quelle: notamment,
   * cette méthode peut être utilisée pour mapper la valeur courante vers null.
   * Cette méthode doit être appelée depuis le corps de {@link mapper()}
   */
  function mapToValue($value, $key=null) {
    if ($key !== null) return $this->mapTo([$key => $value]);
    else return $this->mapTo([$value]);
  }

  /**
   * indiquer que la valeur courante n'est pas mappée.
   * Cette méthode doit être appelée depuis le corps de {@link mapper()}
   */
  function mapToNone() {
    return $this->mapTo(null);
  }

  /**
   * indiquer que la valeur courante ne peut pas être traitée pour le moment et
   * qu'elle doit de nouveau être présentée à la fonction {@link mapper()} la
   * fois suivante. cela permet d'implémenter des méthodes de mapping plus
   * complexes. attention tout de même à ne pas créer de boucles infinies.
   * Cette méthode doit être appelée depuis le corps de {@link mapper()}
   */
  function retry() {
    $this->action = self::RETRY_ACTION;
    return null;
  }

  /**
   * mapper la valeur courante $item. il faut
   * - soit retourner la valeur mappée. si on retourne null, la valeur n'est pas
   * mappée (il n'y a pas de valeur correspondante, elle est donc ignorée).
   * avec cette façon de faire, les clés originales sont conservées
   * - soit appeler l'une des méthodes {@link mapTo()}, {@link mapToValue()},
   * {@link mapToNone()} pour indiquer la ou les valeurs correspondantes (ainsi
   * que les clés correspondantes le cas échéant). avec cette façon de faire,
   * les clés originales sont perdues
   *
   * Dans les classes dérivées, il est possible de rajouter "$key=null" à la
   * signature de cette fonction pour avoir aussi la clé
   */
  abstract function mapper($item);

  #############################################################################
  const _AUTOGEN_CONSTS = [
    "" => [self::class, "_autogen_consts"],
  ];
  const _AUTOGEN_LITERALS = /*autogen*/[
    [
      \nur\b\params\parametrable_utils::class,
      '\\nur\\b\\params\\parametrable_utils::class',
    ],
    [
      self::PARAMETRABLE_PARAMS_SCHEMA,
      'self::PARAMETRABLE_PARAMS_SCHEMA',
    ],
  ];
  const _AUTOGEN_METHODS = /*autogen*/[
    [
      \nur\b\params\parametrable_utils::class,
      '_autogen_methods_getters',
      self::PARAMETRABLE_PARAMS_SCHEMA,
      null,
    ],
    [
      \nur\b\params\parametrable_utils::class,
      '_autogen_methods_setters',
      self::PARAMETRABLE_PARAMS_SCHEMA,
      null,
    ],
  ];
  const _AUTO_GETTERS = /*autogen*/[
    'getSource' => 'source',
  ];
  const _AUTO_SETTERS = /*autogen*/[
    'setSource' => 'source',
  ];
  #--autogen-dynamic--
}
