<?php # -*- coding: utf-8 mode: php -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8
namespace nur;

use AppendIterator;
use ArrayIterator;
use EmptyIterator;
use Exception;
use Generator;
use Iterator;
use IteratorAggregate;
use NoRewindIterator;
use nur\b\ICloseable;
use nur\b\StopException;
use nur\b\ValueException;
use Traversable;

/**
 * Class iter: gestion des itérateurs
 */
class iter {
  private static final function unexpected_type($object): ValueException {
    return ValueException::unexpected_type("iterable", $object);
  }

  /**
   * fermer "proprement" un itérateur ou un générateur. retourner true en cas de
   * succès, ou false si c'est un générateur et qu'il ne supporte pas l'arrêt
   * avec StopException (la valeur de retour n'est alors pas disponible)
   */
  static function close($it): bool {
    if ($it instanceof ICloseable) {
      $it->close();
      return true;
    } elseif ($it instanceof Generator) {
      try {
        $it->throw(new StopException());
        return true;
      } catch (StopException $e) {
      }
    }
    return false;
  }

  /**
   * retourner la première valeur du tableau, de l'itérateur ou de l'instance
   * de Traversable, ou $default si aucun élément n'est trouvé.
   */
  static final function first($values, $default=null) {
    if ($values instanceof IteratorAggregate) $values = $values->getIterator();
    if ($values instanceof Iterator) {
      try {
        $values->rewind();
        $value = $values->valid()? $values->current(): $default;
      } finally {
        self::close($values);
      }
    } elseif (is_array($values) || $values instanceof Traversable) {
      $value = $default;
      foreach ($values as $value) {
        break;
      }
    } else {
      throw self::unexpected_type($values);
    }
    return $value;
  }

  /**
   * retourner la première clé du tableau, de l'itérateur ou de l'instance
   * de Traversable, ou $default si aucun élément n'est trouvé.
   */
  static final function first_key($values, $default=null) {
    if ($values instanceof IteratorAggregate) $values = $values->getIterator();
    if ($values instanceof Iterator) {
      try {
        $values->rewind();
        $key = $values->valid()? $values->key(): $default;
      } finally {
        self::close($values);
      }
    } elseif (is_array($values) || $values instanceof Traversable) {
      $key = $default;
      foreach ($values as $key => $ignored) {
        break;
      }
    } else {
      throw self::unexpected_type($values);
    }
    return $key;
  }

  /**
   * retourner la première valeur du tableau ou de l'itérateur, ou $default si
   * aucun élément n'est trouvé. Les instances de Traversable ne sont pas supportés.
   *
   * 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
   */
  static final function one($values, $default=null, bool $rewind=false): array {
    if (is_array($values)) $values = new ArrayIterator($values);

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

    if ($values instanceof IteratorAggregate) $values = $values->getIterator();
    if ($values instanceof Iterator) {
      $values->rewind();
      if ($values->valid()) {
        $value = $values->current();
        $values->next();
        $have_next = $values->valid();
        if ($have_next) {
          $next = $values->current();
          $values->next();
          if (!$rewind) {
            $it_nexts = new AppendIterator();
            $it_nexts->append(new ArrayIterator([$next]));
            $it_nexts->append(new NoRewindIterator($values));
          }
        }
      }
      if ($rewind) $values->rewind();
    } else {
      throw ValueException::unexpected_type(Iterator::class, $values);
    }

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

  /**
   * retourner [$first, $second, $all] où:
   * - $first est la première valeur de l'itérateur, ou $default si pas de
   * premier élément
   * - $second est la deuxième valeur de l'itérateur, ou $default si pas de
   * deuxième élément
   * - $all est un iterateur permettant de parcourir *toute* les valeurs, sans
   * devoir rembobiner $iterator
   *
   * cette méthode permet de savoir:
   * - si l'itérateur n'a aucun élément ($first === $default)
   * - si l'itérateur n'a qu'un seul élémement ($second === $default)
   * - sinon il permet de parcourir toutes les valeurs normalement
   *
   * si $rewind est true, appeler rewind() à la fin pour s'assurer que
   * l'itérateur est fermé correctement.
   */
  static final function peek($iterator, $default=null, bool $rewind=false): array {
    if (is_array($iterator)) $iterator = new ArrayIterator($iterator);

    $first = $default;
    $second = $default;
    $all = new EmptyIterator();
    if ($iterator instanceof IteratorAggregate) $iterator = $iterator->getIterator();
    if ($iterator instanceof Iterator) {
      $fsValues = [];
      $iterator->rewind();
      if ($iterator->valid()) {
        $first = $iterator->current();
        $fsValues[$iterator->key()] = $first;
        $iterator->next();
        if ($iterator->valid()) {
          $second = $iterator->current();
          $fsValues[$iterator->key()] = $second;
          $iterator->next();
        }
      }
      if ($rewind) {
        $iterator->rewind();
      } else {
        $all = new AppendIterator();
        $all->append(new ArrayIterator($fsValues));
        $all->append(new NoRewindIterator($iterator));
      }
    } else {
      throw ValueException::unexpected_type(Iterator::class, $iterator);
    }

    return [$first, $second, $all];
  }

  #############################################################################
  # outils pour gérer de façon générique des instances de {@link Iterator} ou
  # des arrays

  /**
   * @param $it ?iterable|array
   * @return bool true si l'itérateur ou le tableau ont pu être réinitialisés
   */
  static function rewind(&$it, ?Exception &$exception=null): bool {
    if ($it instanceof Iterator) {
      try {
        $exception = null;
        $it->rewind();
        return true;
      } catch (Exception $e) {
        $exception = $e;
      }
    } elseif ($it !== null) {
      reset($it);
      return true;
    }
    return false;
  }

  /**
   * @param $it ?iterable|array
   */
  static function valid($it): bool {
    if ($it instanceof Iterator) {
      return $it->valid();
    } elseif ($it !== null) {
      return key($it) !== null;
    } else {
      return false;
    }
  }

  /**
   * @param $it ?iterable|array
   */
    static function current($it, &$key=null) {
    if ($it instanceof Iterator) {
      $key = $it->key();
      return $it->current();
    } elseif ($it !== null) {
      $key = key($it);
      return current($it);
    } else {
      $key = null;
      return null;
    }
  }

  /**
   * @param $it ?iterable|array
   */
  static function next(&$it, ?Exception &$exception=null): void {
    if ($it instanceof Iterator) {
      try {
        $exception = null;
        $it->next();
      } catch (Exception $e) {
        $exception = $e;
      }
    } elseif ($it !== null) {
      next($it);
    }
  }

  /**
   * obtenir la valeur de retour si $it est un générateur terminé, ou null sinon
   */
  static function get_return($it) {
    if ($it instanceof Generator) {
      try {
        return $it->getReturn();
      } catch (Exception $e) {
      }
    }
    return null;
  }
}