<?php

//
function debug($var, $is_exit = false, $dir = 'ltr') {
    echo "<div style='margin:10px;padding:5px 10px;direction:{$dir};background:#ccc;border:1px solid #777;white-space:pre-wrap;'>" . print_r($var, true) . "</div>";
    if ($is_exit)
        exit;
}

//
class db_standalone {

    private $dbh = null;
    private $trnsctn_strt = null;
    private $is_die_on_error = true;
    private $err_arr = array();
    private $err = array();

    //
    public function __construct($conn_str = null, $host = null, $user = null, $pass = null, $db_name = null, $char_encod = "utf8") {
        if (is_null($host))
            $host = CMS_SQL_HOST;
        if (is_null($user))
            $user = CMS_SQL_USER;
        if (is_null($pass))
            $pass = CMS_SQL_PASS;
        if (is_null($db_name))
            $db_name = CMS_SQL_DB_NAME;
        //set the PDO error mode to exception, so we can catch the error | we can set it later using setAttribute(..)
        $options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION);
        if (is_null($conn_str))
            $conn_str = "mysql:host={$host};dbname={$db_name};" . (empty($char_encod) ? "" : "charset={$char_encod}");
        try {
            $this->dbh = new PDO($conn_str, $user, $pass, $options);
            if (!empty($char_encod)) {
                $this->dbh->exec("set names {$char_encod}");
                $this->dbh->exec("set CHARACTER SET {$char_encod}");
                //if this is set to true means to always emulate prepare statement even if sql driver support prepare statement
                //if set false then it will use sql driver native prepare statement and will fallback to emulate prepare statement
                //if driver not native support it.. so ATTR_EMULATE_PREPARES is true then prepare parameter replacement will occur
                //in the PHP PDO library while if it is false then will replace parameter in the native sql driver [if there is].
                //if it is true may get syntax errors at prepare-time rather than at execute-time
//                $this->dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
            }
        } catch (PDOException $e) {
            $this->catch_error($e);
        }
    }

    //
    public function set_is_die_on_error($is_die_on_error = true) {
        $this->is_die_on_error = $is_die_on_error;
        return $this;
    }

    public function get_is_die_on_error() {
        return $this->is_die_on_error;
    }

    //
    public function get_error_arr() {
        return $this->err_arr;
    }

    //
    public function catch_error($e, $stmt = null) {
        $err_msg = null;
        $err_arr = array();
        if (empty($stmt)) {
            if (empty($this->dbh)) {
                $err_msg = $e->getMessage();
                $this->err = array('HY000', 'DB-CONCT-999'/* any temp not exist number */, 'Error While Connecting to Db.');
            } else
                $err_arr = $this->err = $this->dbh->errorInfo();
        } else
            $err_arr = $this->err = $stmt->errorInfo();
        if (!empty($err_arr)) {
            $tmp_msg = "{{$err_arr[1]}}: {$err_arr[2]}";
            if (empty($err_arr[1]) && empty($err_arr[2]))
                $tmp_msg = $e->getMessage();
            $err_msg = "[{$err_arr[0]}] " . $tmp_msg;
            $this->err_arr[] = $err_msg;
        }
        //save error in a log file
        $db_err_path = dirname(dirname(__DIR__)) . '/uploads/db_error/';
        if (!file_exists($db_err_path))
            mkdir($db_err_path);
        $err_log = "\r\n\r\n===============================================================================\r\n";
        $url = (empty($_SERVER['REQUEST_URI']) ? (empty($_SERVER['QUERY_STRING']) ? '' : $_SERVER['QUERY_STRING']) : $_SERVER['REQUEST_URI']);
        $err_log .= "User: Db. StandAlone Class  |  Time: " . date('H:i:s') . "  |  URL: {$url}\r\n";
        $err_log .= "\r\n" . print_r($err_arr, true);
        $query = "";
        $params_str = "";
        if (!empty($stmt)) {
            ob_start(); //we use ob start since debugDumpParams auto print to browser
            try {
                $stmt->debugDumpParams();   //here it will return query also in addition to the parameters
                $params_str = ob_get_contents();
                $query = $stmt->queryString;
            } catch (Exception $e) {
                
            }
            ob_end_clean();
        }
        $query_str = (empty($params_str) ? $query : $params_str);
        //$query_str = str_replace(array("\r\n", "\n", "\r"), " ", $query_str);   //remove line break
        $err_log .= "\r\n".$query_str;
        file_put_contents($db_err_path . date('Y-m-d') . '.txt', $err_log, FILE_APPEND);
        //
        if ($this->is_die_on_error) {
            //header("HTTP/1.0 500 Internal Server Error");
            die(json_encode(array('error_info' => 'Database Error: ' . $err_msg)));
        }
    }

    //
    public function get_dbh() {
        return $this->dbh;
    }

    //here $params must send like: array(&$var1, &$var2, ..) then just call 'query_prepare_execute' 
    //change value and call it again as we need
    public function query_prepare_init($query, $parms = null) {
        $this->err = array();
        $stmt = null;
        try {
            $stmt = $this->dbh->prepare($query);
            if (is_array($parms) && count($parms)) {
                $i = 0;
                foreach ($parms as $indx => &$parm)  //if numeric must start with 1
                    $stmt->bindParam((is_numeric($indx) ? ++$i : $indx), $parm, PDO::PARAM_STR);
            }
        } catch (PDOException $e) {
            $this->catch_error($e, $stmt);
        }
        return $stmt;
    }

    public function query_prepare_execute($stmt) {
        try {
            $stmt->execute();
        } catch (PDOException $e) {
            $this->catch_error($e, $stmt);
        }
    }

    //
    public function query_execute($query, $parms = null) {
        $this->err = array();
        $stmt = null;
        try {
            $stmt = $this->dbh->prepare($query);
            if (is_array($parms) && count($parms)) {
                $i = 0;
                foreach ($parms as $indx => $parm)  //if numeric must start with 1
                    $stmt->bindValue((is_numeric($indx) ? ++$i : $indx), $parm, PDO::PARAM_STR);
            }
            $stmt->execute($parms);
        } catch (PDOException $e) {
            $this->catch_error($e, $stmt);
        }
        //multi query example
        //note that emulate prepares must be true :: $this->dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
        //important put the result loop nextRowset inside try catch so if there is an error inside a statement we catch it
//            $stmt = $this->query_execute("SELECT ?;SELECT ?", [1, 2]);
//        try {
//            do {
//                $data = $stmt->fetchAll();
//                var_dump($data);
//            } while ($stmt->nextRowset());    //loop on next query
//        } catch (PDOException $e) {
//            $this->catch_error($e, $stmt);
//        }
        return $stmt;
    }

    //get affected results count for insert, update, delete & for select in some databases may return result number
    //but we must not depend on it.. we must get select count(*)..
    public function query_result_count($stmt) {
        if ($stmt === false)
            return 0;
        return $stmt->rowCount();
    }

    public function error_info() {
        return $this->err;
    }

    public function error_code() {
        return isset($this->err[1]) ? $this->err[1] : null;
    }

    public function error_define() {
        return isset($this->err[2]) ? $this->err[2] : null;
    }

    public function error_message() {
        return isset($this->err[3]) ? $this->err[3] : $this->error_define();
    }

    public function is_error() {
        return $this->error_code() != 0;
    }

    //
    public function last_id() {
        return $this->dbh->lastInsertId();
    }

    //
    public function escape($query, $parm_typ = PDO::PARAM_STR, $remove_quotes = true) {
        //it return string between quotes but we don't need the first/last quote
        $quote_query = $this->dbh->quote($query, $parm_typ);
        if ($remove_quotes)
            $quote_query = mb_substr($quote_query, 1, -1);
        return $quote_query;
    }

    //
    public function transaction_start() {
        //here we not enable nested transaction like savepoint xx, rollback to xx,.. 
        if (isset($this->trnsctn_strt) && $this->trnsctn_strt === true)
            return;
        $this->trnsctn_strt = true;
        $this->dbh->beginTransaction();
    }

    //
    public function transaction_end() {
        if (!isset($this->trnsctn_strt) || $this->trnsctn_strt !== true)
            return;
        $this->err_arr = array();   //empty error array
        $this->trnsctn_strt = false;
        $this->dbh->commit();
    }

    //
    public function transaction_rollback() {
        if (!isset($this->trnsctn_strt) || $this->trnsctn_strt !== true)
            return;
        $this->err_arr = array();   //empty error array
        $this->trnsctn_strt = false;
        $this->dbh->rollBack();
    }

    //
    public function query_fetch($stmt, $fetch_style = PDO::FETCH_ASSOC) {
        return $stmt->fetch($fetch_style);
    }

}
