<?php
namespace nur\sery\db\sqlite;

use nur\sery\cl;
use nur\sery\str;
use nur\sery\ValueException;
use SQLite3;
use SQLite3Stmt;

class _query {
  static function verifix(&$query, ?array &$params=null): void {
    if (is_array($query)) {
      $prefix = $query[0] ?? null;
      if ($prefix === null) {
        throw new ValueException("requête invalide");
      } elseif (_query_create::isa($prefix)) {
        $query = _query_create::parse($query, $params);
      } elseif (_query_select::isa($prefix)) {
        $query = _query_select::parse($query, $params);
      } elseif (_query_insert::isa($prefix)) {
        $query = _query_insert::parse($query, $params);
      } elseif (_query_update::isa($prefix)) {
        $query = _query_update::parse($query, $params);
      } elseif (_query_delete::isa($prefix)) {
        $query = _query_delete::parse($query, $params);
      } else {
        throw SqliteException::wrap(ValueException::invalid_kind($query, "query"));
      }
    } elseif (!is_string($query)) {
      $query = strval($query);
    }
  }

  protected static function consume(string $pattern, string &$string, ?array &$ms=null): bool {
    if (!preg_match("/^$pattern/i", $string, $ms)) return false;
    $string = substr($string, strlen($ms[0]));
    return true;
  }

  /** fusionner toutes les parties séquentielles d'une requête */
  protected static function merge_seq(array $query): string {
    $index = 0;
    $sql = "";
    foreach ($query as $key => $value) {
      if ($key === $index) {
        $index++;
        if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) {
          $sql .= " ";
        }
        $sql .= $value;
      }
    }
    return $sql;
  }

  protected static function is_sep(&$cond): bool {
    if (!is_string($cond)) return false;
    if (!preg_match('/^\s*(and|or|not)\s*$/i', $cond, $ms)) return false;
    $cond = $ms[1];
    return true;
  }

  static function parse_conds(?array $conds, ?array &$sql, ?array &$params): void {
    if (!$conds) return;
    $sep = null;
    $index = 0;
    $condsql = [];
    foreach ($conds as $key => $cond) {
      if ($key === $index) {
        ## séquentiel
        if ($index === 0 && self::is_sep($cond)) {
          $sep = $cond;
        } elseif (is_array($cond)) {
          # condition récursive
          self::parse_conds($cond, $condsql, $params);
        } else {
          # condition litérale
          $condsql[] = strval($cond);
        }
        $index++;
      } else {
        ## associatif
        # paramètre
        $param = $key;
        if ($params !== null && array_key_exists($param, $params)) {
          $i = 1;
          while (array_key_exists("$key$i", $params)) {
            $i++;
          }
          $param = "$key$i";
        }
        # value ou [operator, value]
        if (is_array($cond)) {
          $op = null;
          $value = null;
          $condkeys = array_keys($cond);
          if (array_key_exists("op", $cond)) $op = $cond["op"];
          if (array_key_exists("value", $cond)) $value = $cond["value"];
          $condkey = 0;
          if ($op === null && array_key_exists($condkey, $condkeys)) {
            $op = $cond[$condkeys[$condkey]];
            $condkey++;
          }
          if ($value === null && array_key_exists($condkey, $condkeys)) {
            $value = $cond[$condkeys[$condkey]];
            $condkey++;
          }
        } else {
          $op = "=";
          $value = $cond;
        }
        $cond = [$key, $op];
        if ($value !== null) {
          $cond[] = ":$param";
          $params[$param] = $value;
        }
        $condsql[] = implode(" ", $cond);
      }
    }
    if ($sep === null) $sep = "and";
    $count = count($condsql);
    if ($count > 1) {
      $sql[] = "(" . implode(" $sep ", $condsql) . ")";
    } elseif ($count == 1) {
      $sql[] = $condsql[0];
    }
  }

  static function parse_set_values(?array $values, ?array &$sql, ?array &$params): void {
    if (!$values) return;
    $index = 0;
    $parts = [];
    foreach ($values as $key => $part) {
      if ($key === $index) {
        ## séquentiel
        if (is_array($part)) {
          # paramètres récursifs
          self::parse_set_values($part, $parts, $params);
        } else {
          # paramètre litéral
          $parts[] = strval($part);
        }
        $index++;
      } else {
        ## associatif
        # paramètre
        $param = $key;
        if ($params !== null && array_key_exists($param, $params)) {
          $i = 1;
          while (array_key_exists("$key$i", $params)) {
            $i++;
          }
          $param = "$key$i";
        }
        # value
        $value = $part;
        $part = [$key, "="];
        if ($value === null) {
          $part[] = "null";
        } else {
          $part[] = ":$param";
          $params[$param] = $value;
        }
        $parts[] = implode(" ", $part);
      }
    }
    $sql = cl::merge($sql, $parts);
  }

  protected static function check_eof(string $tmpsql, string $usersql): void {
    self::consume(';\s*', $tmpsql);
    if ($tmpsql) {
      throw new ValueException("unexpected value at end: $usersql");
    }
  }

  function __construct($sql, ?array $params=null) {
    self::verifix($sql, $params);
    $this->sql = $sql;
    $this->params = $params;
  }

  /** @var string */
  protected $sql;

  /** @var ?array */
  protected $params;

  function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool {
    if ($this->params !== null) {
      /** @var SQLite3Stmt $stmt */
      $stmt = SqliteException::check($db, $db->prepare($this->sql));
      $close = true;
      try {
        foreach ($this->params as $param => $value) {
          SqliteException::check($db, $stmt->bindValue($param, $value));
        }
        $close = false;
        return true;
      } finally {
        if ($close) $stmt->close();
      }
    } else {
      $sql = $this->sql;
      return false;
    }
  }
}
