<?php

namespace engine;

use BackedEnum;
use mysqli;
use mysqli_result;
use mysqli_sql_exception;

class MySQL
{
    const string DATE_FORMAT = 'Y-m-d';
    const string DATETIME_FORMAT = 'Y-m-d H:i:s';

    protected ?mysqli $link = null;

    public function __construct(
        protected string $host,
        protected string $user,
        protected string $password,
        protected string $database) {
    }

    protected function prepareQuery(string $sql, ...$args): string {
        global $config;
        if (!empty($args)) {
            $mark_count = substr_count($sql, '?');
            $positions = [];
            $last_pos = -1;
            for ($i = 0; $i < $mark_count; $i++) {
                $last_pos = strpos($sql, '?', $last_pos + 1);
                $positions[] = $last_pos;
            }
            for ($i = $mark_count - 1; $i >= 0; $i--) {
                $arg_val = $args[$i];
                if (is_null($arg_val)) {
                    $v = 'NULL';
                } elseif ($arg_val instanceof BackedEnum) {
                    $v = '\''.$this->escape($arg_val->value).'\'';
                } else {
                    $v = '\''.$this->escape($arg_val).'\'';
                }
                $sql = substr_replace($sql, $v, $positions[$i], 1);
            }
        }
        if (!empty($config['mysql']['log']))
            logDebug(__METHOD__.': ', $sql);
        return $sql;
    }

    public function insert(string $table, array $fields) {
        return $this->performInsert('INSERT', $table, $fields);
    }

    public function replace(string $table, array $fields) {
        return $this->performInsert('REPLACE', $table, $fields);
    }

    protected function performInsert(string $command, string $table, array $fields) {
        $names = [];
        $values = [];
        $count = 0;
        foreach ($fields as $k => $v) {
            $names[] = $k;
            if (is_bool($v))
                $v = (int)$v;
            $values[] = $v;
            $count++;
        }

        $sql = "{$command} INTO `{$table}` (`".implode('`, `', $names)."`) VALUES (".implode(', ', array_fill(0, $count, '?')).")";
        array_unshift($values, $sql);

        return $this->query(...$values);
    }

    public function update(string $table, array $rows, ...$cond) {
        $fields = [];
        $args = [];
        foreach ($rows as $row_name => $row_value) {
            $fields[] = "`{$row_name}`=?";
            $args[] = $row_value;
        }
        $sql = "UPDATE `$table` SET ".implode(', ', $fields);
        if (!empty($cond)) {
            $sql .= " WHERE ".$cond[0];
            if (count($cond) > 1)
                $args = array_merge($args, array_slice($cond, 1));
        }
        return $this->query($sql, ...$args);
    }

    public function multipleInsert(string $table, array $rows) {
        list($names, $values) = $this->getMultipleInsertValues($rows);
        $sql = "INSERT INTO `{$table}` (`".implode('`, `', $names)."`) VALUES ".$values;
        return $this->query($sql);
    }

    public function multipleReplace(string $table, array $rows) {
        list($names, $values) = $this->getMultipleInsertValues($rows);
        $sql = "REPLACE INTO `{$table}` (`".implode('`, `', $names)."`) VALUES ".$values;
        return $this->query($sql);
    }

    protected function getMultipleInsertValues(array $rows): array {
        $names = [];
        $sql_rows = [];
        foreach ($rows as $i => $fields) {
            $row_values = [];
            foreach ($fields as $field_name => $field_val) {
                if ($i == 0) {
                    $names[] = $field_name;
                }
                $row_values[] = $this->escape($field_val);
            }
            $sql_rows[] = "('".implode("', '", $row_values)."')";
        }
        return [$names, implode(', ', $sql_rows)];
    }

    public function __destruct() {
        if ($this->link)
            $this->link->close();
    }

    public function connect(): bool {
        $this->link = new mysqli();
        $result = $this->link->real_connect($this->host, $this->user, $this->password, $this->database);
        if ($result)
            $this->link->set_charset('utf8mb4');
        return !!$result;
    }

    public function query(string $sql, ...$args): mysqli_result|bool {
        $sql = $this->prepareQuery($sql, ...$args);
        $q = false;
        try {
            $q = $this->link->query($sql);
            if (!$q)
                logError(__METHOD__.': '.$this->link->error."\n$sql\n".logging\Util::backtraceAsString(1));
        } catch (mysqli_sql_exception $e) {
            logError(__METHOD__.': '.$e->getMessage()."\n$sql\n".logging\Util::backtraceAsString(1));
        }
        return $q;
    }

    public function error() {
        return $this->link?->error;
    }

    public function fetch($q): ?array {
        $row = $q->fetch_assoc();
        if (!$row) {
            $q->free();
            return null;
        }
        return $row;
    }

    public function fetchAll($q): ?array {
        if (!$q)
            return null;
        $list = [];
        while ($f = $q->fetch_assoc()) {
            $list[] = $f;
        }
        $q->free();
        return $list;
    }

    public function fetchRow($q): ?array {
        return $q?->fetch_row();
    }

    public function result($q, $field = 0) {
        return $q?->fetch_row()[$field];
    }

    public function insertId(): int {
        return $this->link->insert_id;
    }

    public function numRows($q): ?int {
        return $q?->num_rows;
    }

    public function affectedRows(): ?int {
        return $this->link?->affected_rows;
    }

    public function foundRows(): int {
        return (int)$this->fetch($this->query("SELECT FOUND_ROWS() AS `count`"))['count'];
    }

    public function escape(string $s): string {
        return $this->link->real_escape_string($s);
    }
}
