<?php
namespace nulib\php\coll;

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

/**
 * Class Cursor: parcours des lignes itérable
 *
 * @property-read array|null $value alias pour $row
 * @property-read iterable|null $rows la source des lignes
 */
class Cursor implements Iterator {
  const PARAMS_SCHEMA = [
    "rows" => ["?iterable"],
    "rows_func" => ["?callable"],
    "cols" => ["?array"],
    "cols_func" => ["?callable"],
    "map" => ["?array"],
    "map_func" => ["?callable"],
    "filter" => ["?array"],
    "filter_func" => ["?callable"],
  ];

  /**
   * mapper le tableau source $row selon les règles suivantes illustrées dans
   * l'exemple suivant:
   * si
   *   $map = ["a", "b" => "x", "c" => function() { return "y"; }, "d" => null]
   * alors retourner le tableau
   *   ["a" => $row["a"], "b" => $row["x"], "c" => "y", "d" => null]
   */
  protected static function map_row(array $row, ?array $map): array {
    if ($map === null) return $row;
    $index = 0;
    $mapped = [];
    foreach ($map as $key => $value) {
      if ($key === $index) {
        $index++;
        if ($value === null) $mapped[] = null;
        else $mapped[$value] = cl::get($row, $value);
      } elseif (is_callable($value)) {
        $func = func::with($value);
        $value = cl::get($row, $key);
        $mapped[$key] = $func->invoke([$value, $key, $row]);
      } else {
        if ($value === null) $mapped[$key] = null;
        else $mapped[$key] = cl::get($row, $value);
      }
    }
    return $mapped;
  }

  /**
   * tester si $row satisfait les conditions de $filter
   * - $filter est un scalaire, le transformer en [$filter]
   * - sinon $filter doit être un tableau de scalaires
   *
   * les règles des conditions sont les suivantes:
   * - une valeur séquentielle $key est équivalente à la valeur associative
   * $key => true
   * - une valeur associative $key => bool indique que la clé correspondante ne
   * doit pas (resp. doit) exister selon que bool vaut false (resp. true)
   * - une valeur associative $key => $value indique que la clé correspondante
   * doit exiter avec la valeur spécifiée
   */
  protected static function filter_row(array $row, $filter): bool {
    if ($filter === null) return false;
    if (!is_array($filter)) $filter = [$filter];
    if (!$filter) return false;

    $index = 0;
    foreach ($filter as $key => $value) {
      if ($key === $index) {
        $index++;
        if (!array_key_exists($value, $row)) return false;
      } elseif (is_bool($value)) {
        if ($value) {
          if (!array_key_exists($key, $row)) return false;
        } else {
          if (array_key_exists($key, $row)) return false;
        }
      } else {
        if (!array_key_exists($key, $row)) return false;
        if ($row[$key] !== $value) return false;
      }
    }
    return true;
  }

  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]);
      }
    } elseif ($rows instanceof Traversable) {
      $rowsGenerator = $rows;
    } else {
      $rowsFunc = func::with(function() use ($rows) {
        return $rows;
      });
    }
    $this->rowsGenerator = $rowsGenerator;
    $this->rowsFunc = $rowsFunc;

    $this->cols = $params["cols"] ?? null;
    $colsFunc = $params["cols_func"] ?? null;
    if ($colsFunc !== null) $colsFunc = func::with($colsFunc);
    $this->colsFunc = $colsFunc;

    $map = $params["map"] ?? null;
    $mapFunc = $params["map_func"] ?? null;
    if ($mapFunc !== null) {
      $mapFunc = func::with($mapFunc);
    } elseif ($map !== null) {
      $mapFunc = func::with(function(array $row) use ($map) {
        return self::map_row($row, $map);
      });
    }
    $this->mapFunc = $mapFunc;

    $filter = $params["filter"] ?? null;
    $filterFunc = $params["filter_func"] ?? null;
    if ($filterFunc !== null) {
      $filterFunc = func::with($filterFunc);
    } elseif ($filter !== null) {
      $filterFunc = func::with(function(array $row) use ($filter) {
        return self::filter_row($row, $filter);
      });
    }
    $this->filterFunc = $filterFunc;
  }

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

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

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

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

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

  protected ?iterable $rows;

  public ?array $cols;

  public int $index;

  public int $origIndex;

  public $key;

  public $raw;

  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;
  }

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

  protected function filter(): bool {
    return false;
  }

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

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

  function rewind() {
    $this->cols = null;
    $this->index = 0;
    $this->origIndex = 0;
    $this->key = null;
    $this->raw = null;
    $this->row = 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 {
    $cols = $this->colsFunc;
    $filter = $this->filterFunc;
    $map = $this->mapFunc;
    while ($valid = iter::valid($this->rows)) {
      $this->raw = iter::current($this->rows, $this->key);
      $this->key ??= $this->origIndex;
      $this->row = cl::withn($this->raw);
      if ($filter === null) $filtered = $this->filter();
      else $filtered = $filter->invoke([$this]);
      if (!$filtered) {
        if ($map === null) $this->row = $this->map();
        else $this->row = $map->invoke([$this]);
        if ($this->cols === null) {
          if ($cols === null) $this->cols = $this->cols();
          else $this->cols = $cols->invoke([$this]);
        }
        break;
      } else {
        iter::next($this->rows);
        $this->origIndex++;
      }
    }
    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;
    }
    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();
    }
  }
}
