<?php
namespace nur\b\coll;

use AppendIterator;
use ArrayIterator;
use EmptyIterator;
use Exception;
use Iterator;
use NoRewindIterator;
use nur\b\io\EOFException;

/**
 * Class AbstractIterator: implémentation de base d'un itérateur
 *
 * les classes dérivées *doivent* implémenter les méthodes key() et current()
 * qui peuvent simplement être implémentée comme des méthodes déléguées de
 * respectivement {@link _key()} et {@link _current()}.
 *
 * Ce mode opératoire permet de créer ces méthodes avec une signature appropriée,
 * permettant à un IDE de découvrir le type des données qui sont accédées
 */
abstract class AbstractIterator implements Iterator {
  const AUTO_REWIND = false;

  private $setup = false;
  private $valid = false;
  private $toredown = true;

  private $index = 0;
  protected $key;
  protected $item = null;

  /**
   * initialiser les ressources nécessaires à l'itération.
   * les exceptions lancées par cette méthode sont ignorées.
   */
  protected function _setup() {}

  /**
   * retourner le prochain élément. retourner false ou lancer l'exception
   * EOFException pour indiquer que plus aucun élément n'est disponible
   *
   * le cas échéant, initialiser $key
   *
   * @throws EOFException
   */
  abstract protected function _next(&$key);

  /**
   * libérer les ressources allouées.
   * les exceptions lancées par cette méthode sont ignorées.
   */
  protected function _teardown() {}

  /**
   * lancer un traitement avant de commencer l'itération.
   *
   * cette méthode est appelée après _setup() et l'objet est garanti d'être dans
   * un état valide. elle est prévue pour être surchargée par l'utilisateur
   */
  protected function beforeIter() {}

  /**
   * modifier un élément avant de le retourner.
   *
   * cette méthode est prévue pour être surchargée par l'utilisateur.
   */
  protected function cook(&$item) {}

  function _key() {
    return $this->key;
  }

  function _current() {
    return $this->item;
  }

  function next() {
    if ($this->toredown) return;
    try {
      $item = $this->_next($key);
    } catch (EOFException $e) {
      $item = false;
    }
    $this->valid = false;
    if ($item !== false) {
      $this->cook($item);
      $this->item = $item;
      if ($key !== null) {
        $this->key = $key;
      } else {
        $this->index++;
        $this->key = $this->index;
      }
      $this->valid = true;
    } else {
      try {
        $this->_teardown();
      } catch (Exception $e) {
      }
      $this->toredown = true;
    }
  }

  function rewind() {
    if ($this->setup) {
      if (!$this->toredown) {
        try {
          $this->_teardown();
        } catch (Exception $e) {
        }
      }
      $this->setup = false;
      $this->valid = false;
      $this->toredown = true;
      $this->index = 0;
      $this->key = null;
      $this->item = null;
    }
  }

  function valid() {
    if (!$this->setup) {
      try {
        $this->_setup();
      } catch (Exception $e) {
      }
      $this->setup = true;
      $this->toredown = false;
      $this->beforeIter();
      $this->next();
    }
    return $this->valid;
  }

  /**
   * retourner la première valeur de la liste ou $default si aucun élément n'est
   * trouvé.
   *
   * si static::AUTO_REWIND est true, appeler rewind() à la fin pour s'assurer
   * que l'itérateur est fermé correctement.
   */
  function get($default=null) {
    # obtenir le premier élément de la liste
    $this->rewind();
    $value = $this->valid()? $this->current(): $default;
    if (static::AUTO_REWIND) $this->rewind();
    return $value;
  }

  /**
   * retourner la première valeur de la liste, ou $default si aucun élément
   * n'est trouvé.
   *
   * si $rewind est true, appeler rewind() à la fin pour s'assurer que
   * l'itérateur est fermé correctement.
   *
   * retourner un tableau [$value, $have_next, $it_nexts]
   * - $have_next vaut true s'il y a encore des données qui suivent
   * - si $rewind==false, $it_nexts est un itérateur qui permet d'accéder aux
   *   données suivantes
   */
  function one($default=null, ?bool $rewind=null): array {
    if ($rewind === null) $rewind = static::AUTO_REWIND;

    $value = $default;
    $have_next = false;
    $it_nexts = new EmptyIterator();

    $this->rewind();
    if ($this->valid()) {
      $value = $this->current();
      $this->next();
      $have_next = $this->valid();
      if ($have_next) {
        $next = $this->current();
        $this->next();
        if (!$rewind) {
          $it_nexts = new AppendIterator();
          $it_nexts->append(new ArrayIterator([$next]));
          $it_nexts->append(new NoRewindIterator($this));
        }
      }
    }
    if ($rewind) $this->rewind();

    return [$value, $have_next, $it_nexts];
  }
}