<?php
namespace nur\m\base;

use Iterator;
use nur\A;
use nur\m\IQuery;
use nur\m\IRowIncarnation;
use nur\m\IRowIterator;

abstract class Query implements IQuery {
  /**
   * retourner une chaine quotée pour SQL ou "null" si
   * ($value === "" && !$allow_empty)
   */
  static function qv(string $value, bool $allowEmpty=false): string {
    if ($value) return "'".str_replace("'", "''", $value)."'";
    elseif ($allowEmpty) return "''";
    else return "null";
  }

  # nb de caractères minimum pour une recherche approximative
  const LIKE_THRESHOLD = 2;

  static function likev($value, bool $partial=false, ?int $likeThreshold=null) {
    if ($value === false || $value === null) return $value;

    if ($likeThreshold === null) $likeThreshold = static::LIKE_THRESHOLD;
    if ($partial && strlen($value) >= $likeThreshold) {
      // Rajouter un espace qui sera remplacé par %
      if (!preg_match('/[- %]$/', $value)) $value .= " ";
    }
    return preg_replace('/[- ]+/', '%', $value);
  }

  static function build_actual_query(string $query, ?array $bindings) {
    if ($bindings !== null) {
      AbstractConn::fix_sql_with_seq_bindings($query, $bindings);
      foreach ($bindings as $name => $value) {
        if (A::is_seq($value) && count($value) == 0) {
          $value = null;
        }
        if ($value === null) {
          $value = "null";
          $query = str_replace(":$name", $value, $query);
        } elseif (A::is_seq($value)) {
          $values = $value;
          $count = count($values);
          for ($i = $count - 1; $i >= 0; $i--) {
            $value = $values[$i];
            if (is_array($value)) {
              # tableau associatif avec les informations sur le type de la valeur
              #XXX pour le moment, le type est ignoré
              $value = $value["value"];
            }
            if (!is_string($value)) $value = strval($value);
            // chaine
            if (!preg_match('/^[1-9][0-9]*$/', $value)) {
              $value = self::qv($value, true);
            }
            $query = str_replace(":${name}_$i", $value, $query);
          }
        } else {
          if (is_array($value)) {
            # tableau associatif avec les informations sur le type de la valeur
            #XXX pour le moment, le type est ignoré
            $value = $value["value"];
          }
          if (!is_string($value)) $value = strval($value);
          // chaine
          if (!preg_match('/^[1-9][0-9]*$/', $value)) {
            $value = self::qv($value, true);
          }
          $query = str_replace(":$name", $value, $query);
        }
      }
    }
    return str_replace("\n", " ", $query);
  }
  
  const TYPE_SELECT = 1, TYPE_UPDATE = 2, TYPE_INSERT = 3;

  /** @var int type de requête */
  protected $type;

  /** @var string|null la requête SQL effective */
  protected $sql;

  /** @var array|null le filtre permettant de sélectionner les lignes */
  protected $filter;

  /** @var mixed une ligne de données, pour mise à jour ou insertion */
  protected $row;

  /** @var array|null le tableau retour */
  protected $results;

  /** @var IRowIncarnation */
  protected $incarnation;

  abstract protected function newRowIncarnation(): IRowIncarnation;

  abstract protected function newRowIterator(?string $sql, ?array &$bindings=null, ?IRowIncarnation $incarnation=null): IRowIterator;

  function setIncarnation(IRowIncarnation $incarnation): IQuery {
    $this->incarnation = $incarnation;
    return $this;
  }

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

  /** @var string requête SQL de sélection par défaut */
  const SQL_SELECT = null;

  function select($sql=null, ?array $filter=null): IQuery {
    if ($sql === null) $sql = static::SQL_SELECT;
    $this->type = self::TYPE_SELECT;
    $this->sql = $sql;
    $this->filter = $filter;
    $this->row = null;
    $result = null;
    $this->results =& $result;
    return $this;
  }

  /** @var string requête SQL de mise à jour par défaut */
  const SQL_UPDATE = null;

  function update($sql=null, ?array $filter=null, $row=null, ?array &$results=null): IQuery {
    if ($sql === null) $sql = static::SQL_UPDATE;
    $this->type = self::TYPE_UPDATE;
    $this->sql = $sql;
    $this->filter = $filter;
    $this->row = $row;
    $this->results =& $results;
    return $this;
  }

  /** @var string requête SQL d'insertion par défaut */
  const SQL_INSERT = null;

  function insert($sql=null, $row=null, ?array &$results=null): IQuery {
    if ($sql === null) $sql = static::SQL_INSERT;
    $this->type = self::TYPE_INSERT;
    $this->sql = $sql;
    $this->filter = null;
    $this->row = $row;
    $this->results =& $results;
    return $this;
  }

  /** valider les filtres utilisés et retourner le cas échéant une requête sql mise à jour */
  protected function validateFilter(): ?string {
    return null;
  }

  abstract protected function _execute(bool $commit): IRowIterator;

  function execute(bool $commit=false): IRowIterator {
    return $this->_execute($commit);
  }

  function execute2(?array $filter=null, $row=null, ?array &$results=null): IRowIterator {
    if ($filter !== null) $this->filter = $filter;
    if ($row !== null) $this->row = $row;
    if ($results !== null) $this->results =& $results;
    return $this->_execute(false);
  }

  function all(): array { return $this->execute()->all(); }
  function allVals(?string $name=null): array {
    return rows::vals($this->all(), $name);
  }
  function first($default=null) { return $this->execute()->first($default); }
  function firstVal(?string $name=null, $default=null) {
    return rows::val($this->first(), $name, $default);
  }
  function numRows(): int { return $this->execute()->numRows(); }
  function insertId() { return $this->execute()->insertId(); }
  function one($default=null, ?bool $rewind=null): array { return $this->execute()->one($default, $rewind); }
  function peek($default=null, ?bool $rewind=false): array { return $this->execute()->peek($default, $rewind); }

  /** @var Iterator */
  protected $rowIterator;

  protected $firstRow;

  function getRow(?string $sql=null) {
    if ($sql !== null) $this->search(null, $sql);
    return $this->firstRow;
  }

  function search(?array $filter, ?string $sql=null): bool {
    if ($sql !== null) $this->sql = $sql;
    $this->firstRow = null;
    $this->rowIterator = null;
    [$first, $second, $iterator] = $this->execute2($filter)->peek();
    if ($second !== null) {
      $this->rowIterator = $iterator;
    } elseif ($first !== null) {
      $this->firstRow = $first;
    }
    return $this->firstRow !== null;
  }

  function isClosed(): bool {
    $rowIterator = $this->rowIterator;
    if ($rowIterator instanceof IRowIterator) return $rowIterator->isClosed();
    else return true;
  }

  function rewind(): void {
    if ($this->isClosed()) $this->rowIterator = $this->execute();
    $this->rowIterator->rewind();
  }
  function valid(): bool {
    $valid = $this->rowIterator->valid();
    if (!$valid) $this->rowIterator = null;
    return $valid;
  }
  function key() { return $this->rowIterator->key(); }
  function current() { return $this->rowIterator->current(); }
  function next() { $this->rowIterator->next(); }
}
