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

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

abstract class _base {
  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 &$bindings): 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_bool($cond)) {
          # ignorer les valeurs true et false
        } elseif (is_array($cond)) {
          # condition récursive
          self::parse_conds($cond, $condsql, $bindings);
        } else {
          # condition litérale
          $condsql[] = strval($cond);
        }
        $index++;
      } elseif ($cond === false) {
        ## associatif
        # condition litérale ignorée car condition false
      } elseif ($cond === true) {
        # condition litérale sélectionnée car condition true
        $condsql[] = strval($key);
      } else {
        ## associatif
        # paramètre
        $param0 = preg_replace('/^.+\./', "", $key);
        $i = false;
        if ($bindings !== null && array_key_exists($param0, $bindings)) {
          $i = 2;
          while (array_key_exists("$param0$i", $bindings)) {
            $i++;
          }
        }
        # value ou [operator, value]
        $condprefix = $condsep = $condsuffix = null;
        if (is_array($cond)) {
          $condkey = 0;
          $condkeys = array_keys($cond);
          $op = null;
          if (array_key_exists("op", $cond)) {
            $op = $cond["op"];
          } elseif (array_key_exists($condkey, $condkeys)) {
            $op = $cond[$condkeys[$condkey]];
            $condkey++;
          }
          $op = strtolower($op);
          $condvalues = null;
          switch ($op) {
          case "between":
            # ["between", $upper, $lower]
            $condsep = " and ";
            if (array_key_exists("lower", $cond)) {
              $condvalues[] = $cond["lower"];
            } elseif (array_key_exists($condkey, $condkeys)) {
              $condvalues[] = $cond[$condkeys[$condkey]];
              $condkey++;
            }
            if (array_key_exists("upper", $cond)) {
              $condvalues[] = $cond["upper"];
            } elseif (array_key_exists($condkey, $condkeys)) {
              $condvalues[] = $cond[$condkeys[$condkey]];
              $condkey++;
            }
            break;
          case "in":
            # ["in", $values]
            $condprefix = "(";
            $condsep = ", ";
            $condsuffix = ")";
            $condvalues = null;
            if (array_key_exists("values", $cond)) {
              $condvalues = cl::with($cond["values"]);
            } elseif (array_key_exists($condkey, $condkeys)) {
              $condvalues = cl::with($cond[$condkeys[$condkey]]);
              $condkey++;
            }
            break;
          case "null":
          case "is null":
            $op = "is null";
            break;
          case "not null":
          case "is not null":
            $op = "is not null";
            break;
          default:
            if (array_key_exists("value", $cond)) {
              $condvalues = [$cond["value"]];
            } elseif (array_key_exists($condkey, $condkeys)) {
              $condvalues = [$cond[$condkeys[$condkey]]];
              $condkey++;
            }
          }
        } elseif ($cond !== null) {
          $op = "=";
          $condvalues = [$cond];
        } else {
          $op = "is null";
          $condvalues = null;
        }
        $cond = [$key, $op];
        if ($condvalues !== null) {
          $parts = [];
          foreach ($condvalues as $condvalue) {
            if (is_array($condvalue)) {
              $first = true;
              foreach ($condvalue as $value) {
                if ($first) {
                  $first = false;
                } else {
                  if ($sep === null) $sep = "and";
                  $parts[] = " $sep ";
                  $parts[] = $key;
                  $parts[] = " $op ";
                }
                $param = "$param0$i";
                $parts[] = ":$param";
                $bindings[$param] = $value;
                if ($i === false) $i = 2;
                else $i++;
              }
            } else {
              $param = "$param0$i";
              $parts[] = ":$param";
              $bindings[$param] = $condvalue;
              if ($i === false) $i = 2;
              else $i++;
            }
          }
          $cond[] = $condprefix.implode($condsep, $parts).$condsuffix;
        }
        $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 &$bindings): 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, $bindings);
        } else {
          # paramètre litéral
          $parts[] = strval($part);
        }
        $index++;
      } else {
        ## associatif
        # paramètre
        $param = $param0 = preg_replace('/^.+\./', "", $key);
        if ($bindings !== null && array_key_exists($param0, $bindings)) {
          $i = 2;
          while (array_key_exists("$param0$i", $bindings)) {
            $i++;
          }
          $param = "$param0$i";
        }
        # value
        $value = $part;
        $part = [$key, "="];
        if ($value === null) {
          $part[] = "null";
        } else {
          $part[] = ":$param";
          $bindings[$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");
    }
  }

  abstract protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void;

  function __construct($sql, ?array $bindings=null) {
    static::verifix($sql, $bindings, $meta);
    $this->sql = $sql;
    $this->bindings = $bindings;
    $this->meta = $meta;
  }

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

  function getSql(): string {
    return $this->sql;
  }

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

  function getBindings(): ?array {
    return $this->bindings;
  }

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

  function isInsert(): bool {
    return ($this->meta["isa"] ?? null) === "insert";
  }
}
