<?php
class Database {
    private $mysqli;
    public $ErrorInfo;
    private static $_options;
    public static $_instance;
    protected static $current_instance;
    protected static $current_server_instance;
    protected static $current_query_key;
    public static $queries = array();
    public static $query_times = array();
    protected static $current_slave_server;
    protected static $servers = array();
    protected static $slave_servers = array();
    protected static $instance = array();
    protected $reset_section_cache = False;
    public static $no_of_cached_queries = 0;
    public static $read_replica_number;
    public static $no_of_queries = 0;
    public function __construct($options = null) {
        self::dbConnect();
        $ObjRewrite = Registry::getInstance('Rewrite');
        if ($ObjRewrite->_section === $ObjRewrite->_manageURLBase)
            $this->reset_section_cache = True;
        $this->objCache             = new Cache(CACHE_DRIVER);
        $ObjConfig                  = Registry::getInstance('Config');
        self::$servers              = $ObjConfig->db_servers;
        self::$slave_servers        = $ObjConfig->db_slave_servers;
        $this->read_replica_number  = (count(self::$slave_servers) > 0) ? rand(1, count(self::$slave_servers)) : 0;
        self::$current_slave_server = rand(1, count(self::$slave_servers));
    }
    public static function getInstance($server = 'master') {
        $total_servers = count(self::$servers);
        if ($server == 'master' || $total_servers == 1) {
            $id_server = 0;
        } else {
            $id_server = self::$current_slave_server;
        }
        if (!isset(self::$instance[$id_server])) {
            self::$instance[$id_server] = new mysqli(self::$servers[$id_server]['DB_HOST'], self::$servers[$id_server]['DB_USER'], self::$servers[$id_server]['DB_PASSWORD'], self::$servers[$id_server]['DB_NAME']);
            self::$instance[$id_server]->set_charset("utf8");
        }
        if (DEVELOPER_MODE == TRUE) {
            self::setInstanceServer(self::$servers[$id_server]);
        }
        return self::$instance[$id_server];
    }
    private static function setInstanceServer($current_instance) {
        self::$current_instance = $current_instance;
    }
    private function getInstanceServer() {
        return "DB_HOST : " . self::$current_instance['DB_HOST'] . "\nDB_USER : " . self::$current_instance['DB_USER'] . "\nDB_NAME : " . self::$current_instance['DB_NAME'] . "
			";
    }
    public function dbConnect() {
        $ObjConfig    = Registry::getInstance('Config');
        $this->mysqli = new mysqli($ObjConfig->db_host, $ObjConfig->db_user, $ObjConfig->db_password, $ObjConfig->db_name);
        if (!$this->mysqli) {
            $this->ErrorInfo = mysqli_connect_error();
            return FALSE;
        } else {
            $this->mysqli->set_charset("utf8");
            return TRUE;
        }
    }
    public function dbClose() {
        $this->mysqli->close();
    }
    public function setQuery($query, $aggregate = true) {
        $this->switchInstance();
        return $this->runQuery($query, $aggregate);
    }
    public function setQuerySlave($query, $aggregate = true) {
        if ($this->reset_section_cache)
            return $this->setQuery($query, $aggregate);
        $this->switchInstance();
        $this->runQuery($query, $aggregate);
        if (count(self::$servers) > 1) {
            $this->switchInstance('slave');
            $this->runQuery($query, $aggregate);
        }
        return;
    }
    public function numberOfRecords($query) {
        $rowCount  = 0;
        $resultSet = $this->runQuery($query);
        if ($resultSet) {
            $rowCount = $resultSet->num_rows;
            $resultSet->free();
        }
        return $rowCount;
    }
    public function readValues($query, $resultType = MYSQLI_BOTH) {
        $this->switchInstance();
        $resultData = array();
        $resultSet  = $this->runQuery($query);
        if ($resultSet) {
            $rowCount = $resultSet->num_rows;
            for ($i = 0; $i < $rowCount; $i++) {
                $resultData[$i] = $resultSet->fetch_array($resultType);
            }
            $resultSet->free();
        }
        return $resultData;
    }
    public function readValue($query, $resultType = MYSQLI_BOTH) {
        $this->switchInstance();
        $resultData = array();
        $resultSet  = $this->runQuery($query);
        if ($resultSet) {
            $resultData[0] = $resultSet->fetch_array($resultType);
            $resultSet->free();
            return $resultData[0];
        }
        return $resultData;
    }
    public function readValuesSlave($query, $resultType = MYSQLI_BOTH) { 
        if ($this->reset_section_cache)
            return $this->readValues($query, $resultType);
        $this->switchInstance('slave');
        self::$current_query_key = $query_key = md5($query);
        if (!$resultData = $this->objCache->get($query_key)) {
            if (is_array($resultData)) {
                self::$no_of_cached_queries++;
                return array();
            } else {
                $resultData = array();
                $resultSet  = $this->runQuery($query);
                if ($resultSet) {
                    $rowCount = $resultSet->num_rows;
                    for ($i = 0; $i < $rowCount; $i++) {
                        $resultData[$i] = $resultSet->fetch_array($resultType);
                    }
                    $resultSet->free();
                }
                $this->objCache->set($query_key, $resultData, CACHE_TTL_QUERIES);
            }
        } else {
            self::$no_of_cached_queries++;
        }
        return $resultData;
    }
    public function readValueSlave($query, $resultType = MYSQLI_BOTH, $cache = "") {
        if ($this->reset_section_cache)
            return $this->readValue($query, $resultType);
        $this->switchInstance('slave');
        self::$current_query_key = $query_key = md5($query);
        $resultData              = $this->objCache->get($query_key);
        if ($cache == "no-cache" || $resultData === FALSE) {
            $resultData = array();
            $resultSet  = $this->runQuery($query);
            if ($resultSet) { 
                $resultData = $resultSet->fetch_array($resultType);
                $resultSet->free();
            }
            if ($cache != 'no-cache') {
                $this->objCache->set($query_key, $resultData, CACHE_TTL_QUERIES);
            }
        } else {
            self::$no_of_cached_queries++;
        }
        return $resultData;
    }
    public function readValuesObject($query, $resultType = MYSQLI_BOTH) {
        $this->switchInstance();
        $resultData = array();
        $resultSet  = $this->runQuery($query);
        if ($resultSet) {
            return $resultSet;
        }
        return $resultData;
    }
    public function getInsertId() {
        return $this->mysqli->insert_id;
    }
    public function readField($query) {
        $this->switchInstance();
        $resultData = array();
        $resultSet  = $this->runQuery($query);
        if ($resultSet) {
            $resultData = $resultSet->fetch_array();
            $resultSet->free();
        }
        return $resultData;
    }
    public function getRecordCount($rs) {
        $resultCount = 0;
        if ($rs && $rs->num_rows > 0) {
            $resultCount = $rs->num_rows;
        }
        return $resultCount;
    }
    public function getRecordMaxCount() {
        if (self::$current_server_instance == 'master') {
            $execMaxLimit = $this->readValue("SELECT FOUND_ROWS()");
        } else {
            $query_key = self::$current_query_key . "_rows";
            if (!$execMaxLimit = $this->objCache->get($query_key)) {
                $execMaxLimit = $this->readValueSlave("SELECT FOUND_ROWS()", MYSQLI_BOTH, 'no-cache');
                $this->objCache->set($query_key, $execMaxLimit, CACHE_TTL_QUERIES);
            }
        }
        return isset($execMaxLimit[0]) ? $execMaxLimit[0] : 0;
    }
    public function checkExist($tableName, $fieldName, $fieldValue, $customQuery = '') {
        $query = "
				SELECT
					COUNT(*) AS cnt
				FROM
					" . DB_PREFIX . $tableName . "
				WHERE
					$fieldName='" . $this->escape($fieldValue) . "'";
        if ($customQuery != "") {
            $query .= " 
					AND " . $customQuery;
        }
        $resultData = $this->readValue($query);
        if (isset($resultData['cnt']) && $resultData['cnt'] > 0) {
            return "YES";
        } else {
            return "NO";
        }
    }
    public function escape($valueString) {
        return $this->mysqli->real_escape_string(trim(stripslashes($valueString)));
    }
    public function showError($errorInfo, $query) {
        if (DEVELOPER_MODE == TRUE && $errorInfo) {
            echo ' <br>
			<div style="background-color:#f2201e;position:fixed;z-index:100000;"> 
			MYSQL ERROR : ' . $errorInfo . ' 
			<br>Query : ' . $query . '
			<br></div><br>';
        }
    }
    public function switchInstance($slave = '') {
        self::$current_server_instance = ($slave != '') ? 'slave' : 'master';
        $this->mysqli                  = $this->getInstance(self::$current_server_instance);
    }
    public function runQuery($query, $aggregate = true) {
        // Start the Query Timer
        $time_start = list($sm, $ss) = explode(' ', microtime());
        if (DEVELOPER_MODE == TRUE) {
            $result = $this->mysqli->query($query);
            if (!$result) {
                $this->ErrorInfo = $this->mysqli->error;
                $this->showError($this->ErrorInfo, $query);
            }
            // Stop and aggregate the query time results
            if ($aggregate) {
                $this->queries[] = $query . $this->get_callstack();
            }
        } else {
            $result = $this->mysqli->query($query);
        }
        self::$no_of_queries++;
        $time_end            = list($em, $es) = explode(' ', microtime());
        $query_times         = ($em + $es) - ($sm + $ss);
        $this->query_times[] = number_format($query_times, 6);
        return $result;
    }
    public function get_callstack() {
        $dt = debug_backtrace();
        $cs = "\n/*\n";
        $cs .= $this->getInstanceServer() . "\n";
        foreach ($dt as $t) {
            if (isset($t['file']) && strpos($t['file'], 'application/core/')) {
                $file = substr($t['file'], strpos($t['file'], 'application/core/') + 17, strlen($t['file']));
                $cs .= $file . ' line ' . $t['line'] . ' function ' . $t['function'] . "()\n";
            }
        }
        $cs .= "*/";
        return $cs;
    }
    //Set Auto Commit
    public function setAutoCommit($mode = true) {
        $this->mysqli->autocommit($mode);
    }
    //Commit Changes
    public function commit() {
        return $this->mysqli->commit();
    }
    //Rollback
    public function rollback() {
        return $this->mysqli->rollback();
    }
    //New FUNCTIONS
    /**
     * Execute Multi query
     */
    public function setMultiQuery($query) {
        $this->switchInstance();
        // Start the Query Timer
        $time_start = list($sm, $ss) = explode(' ', microtime());
        if (DEVELOPER_MODE == TRUE) {
            $result = $this->mysqli->multi_query($query);
            if (!$result) {
                $this->ErrorInfo = $this->mysqli->error;
                $this->showError($this->ErrorInfo, $query);
            }
            $this->queries[] = $query . $this->get_callstack();
        } else {
            $result = $this->mysqli->multi_query($query);
        }
        self::$no_of_queries++;
        // Stop and aggregate the query time results
        $time_end            = list($em, $es) = explode(' ', microtime());
        $query_times         = ($em + $es) - ($sm + $ss);
        $this->query_times[] = number_format($query_times, 6);
        return $result;
    }
    public function getField($query) {
        $this->switchInstance();
        $resultData = array();
        $resultSet  = $this->runQuery($query);
        if ($resultSet) {
            $resultData = $resultSet->fetch_row();
        }
        $resultData[0] = isset($resultData[0]) ? $resultData[0] : '';
        return (isset($resultData) && is_array($resultData)) ? $resultData[0] : NULL;
    }
    public function getFieldSlave($query) {
        if ($this->reset_section_cache)
            return $this->getField($query);
        $this->switchInstance('slave');
        self::$current_query_key = $query_key = md5($query);
        $resultData              = $this->objCache->get($query_key);
        if ($resultData === FALSE) {
            $resultData = array();
            $resultSet  = $this->runQuery($query);
            if ($resultSet) {
                $resultData = $resultSet->fetch_row();
            }
            $this->objCache->set($query_key, $resultData, CACHE_TTL_QUERIES);
        } else {
            self::$no_of_cached_queries++;
        }
        $resultData[0] = isset($resultData[0]) ? $resultData[0] : '';
        return (isset($resultData) && is_array($resultData)) ? $resultData[0] : NULL;
    }
    /**
     * Execute query and format result as set of first column from all rows
     * @return array structured data
     */
    public function getFields($query) {
        $this->switchInstance();
        $resultData = array();
        $resultSet  = $this->runQuery($query);
        if ($resultSet) {
            while ($arr = $resultSet->fetch_array(MYSQLI_NUM)) {
                $resultData[] = $arr[0];
            }
            $resultSet->free();
        }
        return $resultData;
    }
    /**
     * Execute query and format result as set of first column from all rows
     * @return array structured data
     */
    public function getFieldsSlave($query) {
        if ($this->reset_section_cache)
            return $this->getFields($query);
        $this->switchInstance('slave');
        self::$current_query_key = $query_key = md5($query);
        if (!$resultData = $this->objCache->get($query_key)) {
            if (is_array($resultData)) {
                self::$no_of_cached_queries++;
                return array();
            } else {
                $resultData = array();
                $resultSet  = $this->runQuery($query);
                if ($resultSet) {
                    while ($arr = $resultSet->fetch_array(MYSQLI_NUM)) {
                        $resultData[] = $arr[0];
                    }
                    $resultSet->free();
                }
                $this->objCache->set($query_key, $resultData, CACHE_TTL_QUERIES);
            }
        } else {
            self::$no_of_cached_queries++;
        }
        return $resultData;
    }
    /**
     * Execute query and format result as associative array with column names as keys and index as defined field
     * @return array structured data
     */
    public function getHashArray($query, $field) {
        $this->switchInstance();
        $resultData = array();
        $resultSet  = $this->runQuery($query);
        if ($resultSet) {
            while ($arr = $resultSet->fetch_array(MYSQLI_ASSOC)) {
                if (isset($arr[$field])) {
                    $resultData[$arr[$field]] = $arr;
                }
            }
            $resultSet->free();
        }
        return $resultData;
    }
    public function getHashArraySlave($query, $field) {
        if ($this->reset_section_cache)
            return $this->getHashArray($query, $field);
        $this->switchInstance('slave');
        $query_key = md5($query);
        if (!$resultData = $this->objCache->get($query_key)) {
            if (is_array($resultData)) {
                self::$no_of_cached_queries++;
                return array();
            } else {
                $resultData = array();
                $resultSet  = $this->runQuery($query);
                if ($resultSet) {
                    while ($arr = $resultSet->fetch_array(MYSQLI_ASSOC)) {
                        if (isset($arr[$field])) {
                            $resultData[$arr[$field]] = $arr;
                        }
                    }
                    $resultSet->free();
                }
                $this->objCache->set($query_key, $resultData, CACHE_TTL_QUERIES);
            }
        } else {
            self::$no_of_cached_queries++;
        }
        return $resultData;
    }
    /**
     * Execute query and format result as one of: field => array(field2 => value), field => array(field2 => row_data), field => array([n] => row_data)
     *
     * @param string $query unparsed query
     * @param array $params array with 3 elements (field, field2, value)
     * @return array structured data
     */
    public function getHashMultiArray($query, $params) {
        $this->switchInstance();
        @list($field, $field2, $value) = $params;
        $resultData = array();
        $resultSet  = $this->runQuery($query);
        if ($resultSet) {
            while ($arr = $resultSet->fetch_array(MYSQLI_ASSOC)) {
                if (!empty($field2)) {
                    $resultData[$arr[$field]][$arr[$field2]] = !empty($value) ? $arr[$value] : $arr;
                } else {
                    $resultData[$arr[$field]][] = $arr;
                }
            }
            $resultSet->free();
        }
        return $resultData;
    }
    public function getHashMultiArraySlave($query, $params) {
        if ($this->reset_section_cache)
            return $this->getHashMultiArray($query, $params);
        $this->switchInstance('slave');
        $query_key = md5($query);
        if (!$resultData = $this->objCache->get($query_key)) {
            if (is_array($resultData)) {
                self::$no_of_cached_queries++;
                return array();
            } else {
                @list($field, $field2, $value) = $params;
                $resultData = array();
                $resultSet  = $this->runQuery($query);
                if ($resultSet) {
                    while ($arr = $resultSet->fetch_array(MYSQLI_ASSOC)) {
                        if (!empty($field2)) {
                            $resultData[$arr[$field]][$arr[$field2]] = !empty($value) ? $arr[$value] : $arr;
                        } else {
                            $resultData[$arr[$field]][] = $arr;
                        }
                    }
                    $resultSet->free();
                }
                $this->objCache->set($query_key, $resultData, CACHE_TTL_QUERIES);
            }
        } else {
            self::$no_of_cached_queries++;
        }
        return $resultData;
    }
    /**
     * Execute query and format result as key => value array
     *
     * @param string $query unparsed query
     * @param array $params array with 2 elements (key, value)
     * @return array structured data
     */
    public function getHashSingleArray($query, $params) {
        $this->switchInstance();
        @list($key, $value) = $params;
        $resultData = array();
        $resultSet  = $this->runQuery($query);
        if ($resultSet) {
            while ($arr = $resultSet->fetch_array(MYSQLI_ASSOC)) {
                $resultData[$arr[$key]] = $arr[$value];
            }
            $resultSet->free();
        }
        return $resultData;
    }
}
