<?php
namespace nulib\php\iter;

use ArrayAccess;
use Iterator;
use IteratorAggregate;
use nulib\cl;
use nulib\php\func;
use nulib\php\iter;
use Traversable;

/**
 * Class Cursor: parcours des lignes itérable
 *
 * XXX si on spécifie $cols ou $colsFunc, il y a une possibilité que les clés de
 * $row ne soient pas dans le bon ordre, ou que les clés de $cols ne soient pas
 * présentes dans $row. ajouter les paramètres ensure_keys et order_keys
 *
 * @property-read array|null $value alias pour $row
 * @property-read iterable|null $rows la source des lignes
 */
class Cursor implements Iterator, ArrayAccess {
  const PARAMS_SCHEMA = [
    "rows" => ["?iterable"],
    "rows_func" => ["?callable"],
    "filter" => ["?array"],
    "filter_func" => ["?callable"],
    "map" => ["?array"],
    "map_func" => ["?callable"],
    "cols" => ["?array"],
    "cols_func" => ["?callable"],
  ];

  function __construct(?iterable $rows=null, ?array $params=null) {
    if ($rows !== null) $params["rows"] = $rows;

    $rows = $params["rows"] ?? null;
    $rowsGenerator = null;
    $rowsFunc = $params["rows_func"] ?? null;
    if ($rowsFunc !== null) {
      if ($rowsFunc instanceof Traversable) {
        $rowsGenerator = $rowsFunc;
        $rowsFunc = null;
      } else {
        $rowsFunc = func::with($rowsFunc, [$rows, $this]);
      }
    } elseif ($rows instanceof Traversable) {
      $rowsGenerator = $rows;
    } else {
      $rowsFunc = func::with(function() use ($rows) {
        return $rows;
      });
    }
    $this->rowsGenerator = $rowsGenerator;
    $this->rowsFunc = $rowsFunc;

    $filter = $params["filter"] ?? null;
    $filterFunc = $params["filter_func"] ?? null;
    if ($filterFunc !== null) $this->setFilterFunc($filterFunc);
    elseif ($filter !== null) $this->setFilter($filter);

    $map = $params["map"] ?? null;
    $mapFunc = $params["map_func"] ?? null;
    if ($mapFunc !== null) $this->setMapFunc($mapFunc);
    elseif ($map !== null) $this->setMap($map);

    $this->cols = $params["cols"] ?? null;
    $this->setColsFunc($params["cols_func"] ?? null);
  }

  /** un générateur de lignes */
  private ?Traversable $rowsGenerator;

  /**
   * une fonction de signature <code>function(mixed $rows, Cursor): ?iterable</code>
   */
  private ?func $rowsFunc;

  /**
   * une fonction de signature <code>function(?array $row, Cursor): bool</code>
   */
  private ?func $filterFunc = null;

  function setFilter(array $filter): self {
    $this->filterFunc = func::with(function(?array $row) use ($filter) {
      return cl::filter($row, $filter);
    });
    return $this;
  }

  function setFilterFunc(?callable $func): self {
    if ($func === null) $this->filterFunc = null;
    else $this->filterFunc = func::with($func)->bind($this);
    return $this;
  }

  /**
   * une fonction de signature <code>function(?array $row, Cursor): ?array</code>
   */
  private ?func $mapFunc = null;

  function setMap(array $map): self {
    $this->mapFunc = func::with(function(?array $row) use ($map) {
      return cl::map($row, $map);
    });
    return $this;
  }

  function setMapFunc(?callable $func): self {
    if ($func === null) $this->mapFunc = null;
    else $this->mapFunc = func::with($func)->bind($this);
    return $this;
  }

  /**
   * une fonction de signature <code>function(?array $row, Cursor): ?array</code>
   */
  private ?func $colsFunc = null;

  function setColsFunc(?callable $func): self {
    $this->cols = null;
    if ($func === null) $this->colsFunc = null;
    else $this->colsFunc = func::with($func)->bind($this);
    return $this;
  }

  /** @var iterable|null source des éléments */
  protected ?iterable $rows;

  /** @var array|null listes des colonnes de chaque enregistrement */
  public ?array $cols;

  /**
   * @var int index de l'enregistrement (en ne comptant pas les éléments filtrés)
   */
  public int $index;

  /**
   * @var int index original de l'enregistrement (en tenant compte des éléments
   * filtrés)
   */
  public int $origIndex;

  /** @var string|int clé de l'enregistrement */
  public $key;

  /** @var mixed élément original récupéré depuis la source */
  public $raw;

  /**
   * @var array|null enregistrement après conversion en tableau et application
   * du mapping
   */
  public ?array $row;

  function __get($name) {
    if ($name === "value") return $this->row;
    elseif ($name == "rows") return $this->rows;
    trigger_error("Undefined property $name");
    return null;
  }

  function offsetExists($offset): bool {
    return cl::has($this->row, $offset);
  }

  function offsetGet($offset) {
    return cl::get($this->row, $offset);
  }

  function offsetSet($offset, $value): void {
    cl::set($this->row, $offset, $value);
  }

  function offsetUnset($offset): void {
    cl::del($this->row, $offset);
  }

  /**
   * données de session: cela permet de maintenir certaines informations pendant
   * le parcours des données
   */
  protected ?array $data;

  /** @param string|int $key */
  function has($key): bool {
    return cl::has($this->data, $key);
  }

  /** @param string|int $key */
  function get($key) {
    return cl::get($this->data, $key);
  }

  /** @param string|int $key */
  function set($key, $value): void {
    $this->data[$key] = $value;
  }

  /** @param string|int $key */
  function del($key) {
    $orig = cl::get($this->data, $key);
    unset($this->data[$key]);
    return $orig;
  }

  protected function convertToRow($raw): ?array {
    return cl::withn($raw);
  }

  protected function filterFunc(?array $row): bool {
    return false;
  }

  protected function mapFunc(?array $row): ?array {
    return $this->row;
  }

  protected function colsFunc(?array $row): ?array {
    return $this->row !== null? array_keys($this->row): null;
  }

  #############################################################################
  # Iterator

  function rewind() {
    $this->cols = null;
    $this->index = 0;
    $this->origIndex = 0;
    $this->key = null;
    $this->raw = null;
    $this->row = null;
    $this->data = null;
    if ($this->rowsGenerator !== null) {
      $rows = $this->rowsGenerator;
      if ($rows instanceof IteratorAggregate) $rows = $rows->getIterator();
      $rows->rewind();
      $this->rows = $rows;
    } else {
      $this->rows = $this->rowsFunc->invoke();
    }
  }

  function valid(): bool {
    $colsFunc = $this->colsFunc;
    $filterFunc = $this->filterFunc;
    $mapFunc = $this->mapFunc;
    while ($valid = iter::valid($this->rows)) {
      $this->raw = iter::current($this->rows, $this->key);
      # conversion en enregistrement
      $this->key ??= $this->origIndex;
      $this->row = $this->convertToRow($this->raw);
      # filtrage
      if ($filterFunc === null) $filtered = $this->filterFunc($this->row);
      else $filtered = $filterFunc->invoke([$this->row, $this]);
      if ($filtered) {
        iter::next($this->rows);
        $this->origIndex++;
      } else {
        # l'enregistrement n'as pas été filtré: faire le mapping
        if ($mapFunc === null) $this->row = $this->mapFunc($this->row);
        else $this->row = $mapFunc->invoke([$this->row, $this]);
        # calculer la liste des colonnes le cas échéant
        if ($this->cols === null) {
          if ($colsFunc === null) $this->cols = $this->colsFunc($this->row);
          else $this->cols = $colsFunc->invoke([$this->row, $this]);
        }
        break;
      }
    }
    if (!$valid) {
      iter::close($this->rows);
      $this->rows = null;
      $this->cols = null;
      $this->index = -1;
      $this->origIndex = -1;
      $this->key = null;
      $this->raw = null;
      $this->row = null;
      # ne pas toucher à data, l'utilisateur peut vouloir continuer à consulter
      # les valeurs
    }
    return $valid;
  }

  function current(): ?array {
    return $this->row;
  }

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

  function next() {
    iter::next($this->rows);
    $this->index++;
    $this->origIndex++;
  }

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

  function each(callable $func): void {
    $func = func::with($func);
    $this->rewind();
    while ($this->valid()) {
      $func->invoke([$this]);
      $this->next();
    }
  }
}
