<?php
namespace fox;

use Exception;

/**
 *
 * Class fox\sql
 * @desc MySQL DB adapter
 *
 * @copyright MX STAR LLC 2021
 * @version 4.0.0
 * @author Pavel Dmitriev
 * @license GPLv3
 *        
 */
class sql
{

    var $mysqli;

    var $stmt;

    var $bind_names;

    var $ctr;

    var $sqlQueryString;

    var $sqlQueryStringL;

    var $param_type;

    var $queryType;

    var bool $paramClosed = false;

    protected bool $connected = false;

    protected ?string $server = null;

    protected ?string $db = null;

    protected ?string $user = null;

    protected ?string $passwd = null;

    public const ERR_DUPLICATE = 1062;

    public const ERR_NOT_NULL_COLUMN = 1048;

    public const ERR_NO_DEFAULT = 1364;

    public static function getConnection(string $idx = "core", $server = null, $db = null, $user = null, $passwd = null): sql
    {
        global $sqlConnArray;
        if (gettype($sqlConnArray) == 'array' && array_key_exists($idx, $sqlConnArray) && ($sqlConnArray[$idx] instanceof sql)) {
            return $sqlConnArray[$idx];
        } else {
            $sqlConnArray[$idx] = new sql($server, $db, $user, $passwd);
            return $sqlConnArray[$idx];
        }
    }

    public static function flushConnections() {
        global $sqlConnArray;
        
        if (gettype($sqlConnArray) == 'array') {
            foreach ($sqlConnArray as $key=>&$conn) {
                $conn->__destruct();
                unset($sqlConnArray[$key]);
            }
        }
        $sqlConnArray=[];
    }
    
    function __destruct() {
        
    }
    
    function __construct($server = null, $db = null, $user = null, $passwd = null)
    {
        if (! isset($server)) {
            $server = config::get("sqlServer");
            $user = config::get("sqlUser");
            $passwd = config::get("sqlPasswd");
            $db = config::get("sqlDB");
        }

        $this->server = $server;
        $this->db = $db;
        $this->user = $user;
        $this->passwd = $passwd;
        $this->connected = false;
    }

    function connect($server = null, $db = null, $user = null, $passwd = null)
    {
        if (! $this->connected) {
            if (isset($server)) {
                $this->server = $server;
                $this->db = $db;
                $this->user = $user;
                $this->passwd = $passwd;
                $this->connected = false;
            }

            $this->mysqli = @mysqli_connect($this->server, $this->user, $this->passwd, $this->db);
            mysqli_set_charset($this->mysqli, "utf8");
            if (! $this->mysqli) { // Если дескриптор равен 0 соединение не установлено
                throw new Exception("SQL Connection to $this->server failed");
                exit();
            }
            $this->connected = true;
        }
        return $this->mysqli;
    }

    function quickExec($sqlQueryString, &$result = null, $hideError = null)
    {
        $this->connect();
        if ($this->mysqli->connect_errno) {
            if (! isset($hideError)) {
                throw new Exception("SQL Connect error");
            }
            ;
            return null;
        }

        $result = $this->mysqli->query($sqlQueryString);

        if (! $result) {
            if (! isset($hideError)) {
                throw new Exception("SQL Error: ." . $this->mysqli->error);
            }
            ;
            return null;
            exit();
        }
        return $result;
    }

    function quickExec1Line($sqlQueryString, &$result = null, $hideError = null)
    {
        $this->connect();
        $result = $this->quickExec($sqlQueryString, $result, $hideError);
        if (mysqli_num_rows($result) == 0) {
            return null;
        }

        $retVal = mysqli_fetch_assoc($result);
        return $retVal;
    }

    // General functions
    function prepare()
    {
        $this->ctr = 0;
        $this->bind_names = null;
        $this->param_type = null;
        $this->stmt = null;
        $this->sqlQueryString = null;
        $this->sqlQueryStringL = null;
        $this->paramClosed = false;
    }

    function prepareUpdate($tableName)
    {
        $this->prepare();
        $this->queryType = "update";
        $this->sqlQueryString = "UPDATE `$tableName` SET";
    }

    function prepareInsert($tableName)
    {
        $this->prepare();
        $this->queryType = "insert";
        $this->sqlQueryString = "INSERT INTO `$tableName` (";
    }

    function execute()
    {
        if (! $this->paramClosed && $this->queryType == "insert") {
            $this->paramClose();
        }
        if (! $this->paramClosed) {
            throw new Exception("Params not closed for " . $this->queryType . "!");
        }

        $this->connect();
        if ($this->ctr > 0) {
            $this->stmt = mysqli_prepare($this->mysqli, $this->sqlQueryString);

            if (! $this->stmt) {
                $err = 'ERR:EXEC 1P' . mysqli_errno($this->mysqli) . ' ' . mysqli_error($this->mysqli);
                throw new Exception($err, mysqli_errno($this->mysqli));
                exit();
            }
            call_user_func_array(array(
                $this->stmt,
                'bind_param'
            ), $this->bind_names);
            if (! (mysqli_stmt_execute($this->stmt))) {
                $err = 'ERR:EXEC 2P' . mysqli_errno($this->mysqli) . ' ' . $this->stmt->error . mysqli_error($this->mysqli);
                $errNo = mysqli_errno($this->mysqli);
                $this->stmt->close();
                throw new Exception($err, $errNo);
                exit();
            }
            return true;
        }
    }

    function quickExecute()
    {
        if (! $this->execute()) {
            if ($this->stmt) {
                $this->stmt->close();
            }
            throw new \Exception('ERR:EXEC 3P' . mysqli_errno($this->mysqli) . '  ' . mysqli_error($this->mysqli), mysqli_errno($this->mysqli));
            exit();
        }
        $this->stmt->close();
        return true;
    }

    function getInsertId()
    {
        return mysqli_insert_id($this->mysqli);
    }

    function paramAdd($sqlParamName, $paramValue, $setNull = false)
    {
        if ($this->queryType == "insert") {
            return $this->paramAddInsert($sqlParamName, $paramValue, $setNull);
        } elseif ($this->queryType == "update") {
            return $this->paramAddUpdate($sqlParamName, $paramValue, $setNull);
        }
    }

    function paramAddInsert($sqlParamName, $paramValue = null, $setNull = null)
    {
        $var = $paramValue;

        if (($var !== null) || $setNull) {
            if ($setNull) {
                $var = null;
            }
            if ($this->ctr != 0) {
                $this->sqlQueryString .= ', ';
                $this->sqlQueryStringL .= ', ';
            }
            $this->sqlQueryString .= "`$sqlParamName` ";
            $this->sqlQueryStringL .= "? ";
            $this->ctr ++;
            if (! isset($this->bind_names)) {
                $x = 'XX';
                $bind_name = 'bind' . $this->ctr;
                $$bind_name = $x;
                $this->bind_names[] = &$$bind_name;
            }
            $bind_name = 'bind' . $this->ctr;
            $$bind_name = $var;
            $this->bind_names[] = &$$bind_name;
            $this->param_type .= 's';
        }
    }

    function paramAddUpdate($sqlParamName, $paramValue = null, $setNull = null)
    {
        if ($setNull) {
            $var = null;
        } elseif (isset($paramValue)) {
            $var = $paramValue;
        }

        if ($var !== null || $setNull) {
            if ($setNull) {
                $var = null;
            }
            if ($this->ctr != 0) {
                $this->sqlQueryString .= ', ';
            }
            $this->sqlQueryString .= "`$sqlParamName`=? ";
            $this->ctr ++;
            if (! isset($this->bind_names)) {
                $x = 'XX';
                $bind_name = 'bind' . $this->ctr;
                $$bind_name = $x;
                $this->bind_names[] = &$$bind_name;
            }
            $bind_name = 'bind' . $this->ctr;
            $$bind_name = $var;
            $this->bind_names[] = &$$bind_name;
            $this->param_type .= 's';
        }
    }

    function paramClose($sqlQueryStringWhere = null)
    {
        $bind_name = 'bind';
        $$bind_name = $this->param_type;
        $this->bind_names[0] = &$$bind_name;

        if ($this->queryType == "insert") {
            $this->sqlQueryString = $this->sqlQueryString . ") VALUES (" . $this->sqlQueryStringL . ")";
        } elseif (isset($sqlQueryStringWhere)) {
            $this->sqlQueryString .= " where " . $sqlQueryStringWhere;
        }
        $this->paramClosed = true;
    }

    function export($tlist)
    {
        $retval = "";
        if (gettype($tlist) == "string") {
            $tlist = array(
                $tlist
            );
        } elseif (gettype($tlist) != "array") {
            throw new Exception("Incorrect type " . gettype($tlist) . " for tables. Expecting 'string' or 'array'");
        }

        foreach ($tlist as $table) {
            $res = $this->quickExec1Line("SHOW CREATE TABLE `$table`");
            if (array_key_exists("Create Table", $res)) {
                $t_create = $res["Create Table"];
                $retval .= "CREATE TABLE IF NOT EXISTS $table (zzz int);\n";
                $columns = preg_split("/[\n\r]/", $t_create);

                // Определяем столбцы
                foreach ($columns as $col) {
                    $matches = [];
                    if (preg_match("/^[ ]*(`(.*)`\ [a-z\(0-9\)]*\ [A-Z _'0-9a-z]*)/", $col, $matches)) {
                        // var_dump($matches);
                        if (preg_match("/AUTO_INCREMENT/", $col)) {
                            $retval .= "ALTER TABLE `$table` ADD COLUMN IF NOT EXISTS " . $matches[1] . " PRIMARY KEY;\n";
                        } else {
                            $retval .= "ALTER TABLE `$table` ADD COLUMN IF NOT EXISTS " . $matches[1] . ";\n";
                        }
                    }
                }
                $retval .= "ALTER TABLE `$table` DROP COLUMN IF EXISTS zzz;\n";

                // Определяем индексы
                foreach ($columns as $col) {
                    $matches = [];
                    if (preg_match("/^[ ]*(([A-Z ]*)KEY\ `(.*)`\ \((.*)\))/", $col, $matches)) {
                        $retval .= "DROP INDEX IF EXISTS`" . $matches[3] . "` ON `$table`;\n";
                        $retval .= "CREATE " . $matches[2] . "INDEX `" . $matches[3] . "` ON `$table` (" . $matches[4] . ");\n";
                    }
                }
            } elseif (array_key_exists("Create View", $res)) {
                $retval .= "DROP VIEW IF EXISTS `" . $table . "`;\n";
                $retval .= $res["Create View"] . ";\n";
            }
        }
        return $retval;
    }

    public static function getMigration($folder, $namespace)
    {
        $res = "";
        foreach (scandir($folder) as $file) {
            $r = [];
            if (! preg_match("/(.*)\.php$/", $file, $r)) {
                continue;
            }
            $mod = $namespace . "\\" . $r[1];
            if (! is_a($mod, dbStoredBase::class, true)) {
                continue;
            }
            if (is_a($mod, noSqlMigration::class, true)) {
                continue;
            }

            $m = new $mod();
            if (! empty($m::$sqlTable)) {
                $res .= "-- $mod\n";
                $res .= (static::getMigrationForClass($m));
            }
        }
        return $res;
    }

    protected static function getMigrationForClass(dbStoredBase $type)
    {
        if (empty($type::$sqlTable)) {
            throw new Exception("Empty SQLTable not allowed here");
        }

        $res = "CREATE TABLE IF NOT EXISTS `" . $type::$sqlTable . "` (zzz int);\n";
        // columns
        foreach ($type->getSqlSchema() as $key => $val) {
            $res .= "ALTER TABLE `" . $type::$sqlTable . "` ADD COLUMN IF NOT EXISTS `" . $key . "` " . $val["type"] . ((array_key_exists("nullable", $val) && $val["nullable"] == false) ? " NOT NULL" : ((array_key_exists("default", $val)) ? (" DEFAULT \"" . $val["default"] . "\"") : " DEFAULT NULL")) . ((array_key_exists("index", $val) && $val["index"] == "AI") ? " AUTO_INCREMENT PRIMARY KEY" : "") . ((array_key_exists("first", $val) && $val["first"] == true) ? " FIRST" : "") . ";\n";
        }

        $res .= "ALTER TABLE `" . $type::$sqlTable . "` DROP COLUMN IF EXISTS zzz;\n";
        foreach ($type->getSqlSchema() as $key => $val) {
            if (empty($val["index"]) || $val["index"] == "AI") {
                continue;
            }
            $res .= "DROP INDEX IF EXISTS `" . $key . "` ON `" . $type::$sqlTable . "`;\n";
            $res .= "CREATE " . (($val["index"] == "INDEX") ? "" : $val["index"] . " ") . "INDEX `" . $key . "` ON `" . $type::$sqlTable . "` (`" . $key . "`);\n";
        }

        return $res;
    }

    function getAffectedRows()
    {
        return $this->mysqli->affected_rows;
    }
    
    public static function doMigration(moduleInfo $module, $classFolder=null) {
        
        if (!$module->singleInstanceOnly) {
            throw new \Exception("Migration of multi-instance modules not allowed");
        }
        
        if (empty($classFolder)) {
            $classFolder=__DIR__."/../../modules/".$module->instanceOf."/".$module->instanceOf;
        }
        
        $sql = new static();
        if ($sql->quickExec1Line("SHOW TABLES LIKE '__SqlSchemaVersion'")) {
            $version = $sql->quickExec1Line("SELECT `version` from `__SqlSchemaVersion` where `module`='".$module->name."'");
            if ($version) { $version=$version["version"];}
        } else {
            $version=null;
        }

        $migration=$sql::getMigration($classFolder, $module->namespace);
        
        if ($version != hash("md5",$migration)) {
            
            print "Module '".$module->name."' DB schema version mismatch $version != ".hash("md5",$migration)."\n";
            print "Updating Module '".$module->name."' DB schema...";
            $sql->quickExec("CREATE TABLE IF NOT EXISTS `__SqlSchemaVersion` (`module` VARCHAR(255), `version` VARCHAR(255))");
            foreach (explode("\n", $migration) as $line) {
                if (!empty($line) && !preg_match("/^--/", $line)) {
                    $sql->quickExec($line);
                }
            }
            
            $sql->quickExec("DELETE FROM `__SqlSchemaVersion` where `module` = '".$module->name."'");
            $sql->quickExec("INSERT INTO `__SqlSchemaVersion` (`module`, `version`) VALUES ('".$module->name."', '".hash("md5",$migration)."')");
            print "OK\n";
            return true;
            
        }
    }
}

?>