<?php

namespace Hura8\Database;

use Hura8\Traits\ClassCacheTrait;

final class ConnectDB implements iConnectDB
{
    use ClassCacheTrait;

    private $debug = false;

    private static $instance = [];
    private static $cnn_props = [];

    /* @var $connection \mysqli */
    private $connection;

	private static $traces = [];

    private $db_id = '';

	private function __construct($db_id = 'main', $debug = false)
	{
        // enable database debug
        if($debug) $this->debug = $debug; // (defined('ENABLE_DB_DEBUG') && ENABLE_DB_DEBUG);
        if($db_id) $this->db_id = $db_id;

        if(!sizeof(self::$cnn_props)) {
            self::$cnn_props = self::setConnectionSettings();
        }
	}

    private static function setConnectionSettings() {
        return static::getCache("getConnectionSettings", function (){
            $db_file = CONFIG_DIR.'/db.php';
            if(file_exists($db_file)) {
                return include $db_file;
            }

            return [];
        });
    }

    /**
     * @param string $table
     * @return array
     */
    public function getTableDefaultItemInfo($table) {
	    $column_info = $this->getTableInfo($table);
	    $default_info = [];
	    foreach ($column_info as $field => $info) {
	        $default_info[$field] = $info['COLUMN_DEFAULT'];
        }

	    return $default_info;
    }


    //28-07-2015 get all columns of a table
    public function getTableInfo($table) {

        return self::getCache('getTableInfo-'.$table, function () use ($table) {

            $db_props = $this->getConnectionProps();
            if(!$db_props) {
                return false;
            }

            $database = $db_props['db'] ?? null;
            if(!$database) {
                return false;
            }

            $query = $this->runQuery(
                "SELECT
                         `COLUMN_NAME` ,
                          COLUMN_DEFAULT,
                          ORDINAL_POSITION,
                          COLUMN_DEFAULT,
                          DATA_TYPE,
                          CHARACTER_MAXIMUM_LENGTH,
                          COLUMN_TYPE,
                          COLUMN_KEY,
                          EXTRA
                    FROM INFORMATION_SCHEMA.COLUMNS
                    WHERE `TABLE_SCHEMA` = ? AND`TABLE_NAME` = ? ",
                ['s', 's'], [$database, $table]
            );

            $output = [];
            foreach( $this->fetchAll($query) as $row){
                $output[$row['COLUMN_NAME']] = $row;
            }

            return $output;
        });
    }


	protected function setTrace($query, $start_time, $msg = ''){
        static::$traces[] = [
            "msg"          => $msg,
	        "query"         => $query,
            "start_time"    => $start_time,
            "total_time"    => round($this->getCurrentTime() - $start_time, 5),
        ];
    }


    public static function getTraces() {
	    $query_count = sizeof(static::$traces);
	    $query_time  = array_sum(array_map(function ($item){ return $item['total_time'];}, static::$traces)) ;
	    return [
	        'query_count'   => $query_count,
            'query_time'    => $query_time,
	        'list'          => static::$traces,
        ];
    }


    private function __clone(){
       //
    }


    /**
     * @param string $db_id
     * @param bool $debug
     * @return iConnectDB
     */
    public static function getInstance(string $db_id = '', bool $debug = false) {
        if( ! $db_id ) $db_id = 'main';

        if(!isset(ConnectDB::$instance[$db_id])) {
            ConnectDB::$instance[$db_id] = new self($db_id, $debug);
        }

        return ConnectDB::$instance[$db_id];
    }


    //close all connections
    public static function close() {
        foreach (ConnectDB::$instance as $db_id => $cnn) {
            $cnn->disconnect();
        }
        // reset
        ConnectDB::$instance = [];
    }


    public function isConnected(): bool {
        return ($this->connection instanceof \mysqli);
    }


    private function getConnectionProps() {
        return self::$cnn_props[$this->db_id] ?? null;
    }


    private function connect()
    {
        // already connect
        if($this->connection instanceof \mysqli) {
            return true;
        }

        // connection props not set
        $db_props = $this->getConnectionProps();
        if(!$db_props) {
            return false;
        }

        $host          = $db_props['host'] ?? null;
        $user          = $db_props['user'] ?? null;
        $pass          = $db_props['pass'] ?? null;
        $database      = $db_props['db'] ?? null;
        $db_charset    = $db_props['charset'] ?? 'latin1';

        if(!$host || !$user || !$pass || !$database) {
            return false;
        }

        try {

            //create the object
            $cnn = \mysqli_init();
            $cnn->options(MYSQLI_OPT_CONNECT_TIMEOUT, 5);
            //specify the read timeout
            if (!defined('MYSQLI_OPT_READ_TIMEOUT')) {
                define ('MYSQLI_OPT_READ_TIMEOUT', 11);
            }
            $cnn->options(MYSQLI_OPT_READ_TIMEOUT, 10);

            if($cnn->real_connect($host, $user, $pass)){
                $cnn->select_db($database);
                $cnn->query("SET NAMES ".$db_charset); // UTF8|latin1
                $cnn->query("SET sql_mode=''"); //for old version to work with mysql 5.7
                //mysqli_select_db($this->connection, $database);
                //mysqli_query($this->connection, "SET NAMES UTF8") ;//set utf8 if database using utf-8 encode
                //mysqli_query($this->connection, "SET NAMES latin1") ;//set back to latin1
                //mysqli_query($this->connection, "SET sql_mode=''") ;//for old version to work with mysql 5.7
                $this->connection = $cnn;

                return true;
            }

            //throw new \Exception('Unable to connect');
            return false;

        }catch (\Exception $e) {
            //die($e->getMessage());
            echo $e->getMessage();

            return false;
        }
    }


    public function disconnect()
    {
        if($this->connection instanceof \mysqli) {
            $this->connection->close();
            $this->connection = null;
        }
    }


    public function ping() {
        if($this->connection) {
            $this->connection->ping();
        }
    }


    /**
     * @param string $query
     * @param array $bind_types
     * @param array $bind_values
     * @param bool $get_affected_row_or_id get the affected row or newly insert-id from the query, default return the mysqli_result
     * @return \mysqli_result | false | int
     */
    public function runQuery($query, array $bind_types=[], array $bind_values=[], $get_affected_row_or_id = false)
    {
        $start_time = $this->getCurrentTime();

        // connect on demand
        if(!$this->connect()) {
            if($this->debug) $this->setTrace($query, $start_time, 'runQuery: Connection fails');
            return false;
        }

        $stmt = $this->connection->prepare( $query );
        if(!$stmt) {
            //throw new \Exception($stmt->error);
            //throw new \Exception($this->connection->error);
            if($this->debug) {
                $this->setTrace($query, $start_time, 'runQuery: '.$this->connection->error);
                die("runQuery error: ".$this->connection->error.". Query: ".$query);
            }
            return false;
        }

        if(sizeof($bind_types) && sizeof($bind_types) == sizeof($bind_values)) {
            if(!$stmt->bind_param(join('', $bind_types), ...$bind_values)) {
                if($this->debug) {
                    $this->setTrace($query, $start_time, 'runQuery: bind_param '.$this->connection->error);
                    die('runQuery: bind_param '.$this->connection->error);
                }
                return false;
            }
        }


        if(!$stmt->execute()) {
            if($this->debug) {
                $this->setTrace($query, $start_time, 'runQuery: execute '.$this->connection->error);
                die("runQuery error: ".$this->connection->error.". Query: ".$query);
            }
            return false;
        }

        if($this->debug) $this->setTrace($query, $start_time, '');

        if($get_affected_row_or_id) {
            return $stmt->insert_id ?: $stmt->affected_rows;
        }

        // default
        return $stmt->get_result();
    }


    private function getCurrentTime(){
	    return microtime(true);
    }


    /**
     * @deprecated Unsafe, make sure all variables are properly escaped
     */
    public function multi_query(array $array_query){

        if(!sizeof($array_query)) return false;

        $start_time = $this->getCurrentTime();

        // late connect
        if(!$this->connect()) {
            if($this->debug) $this->setTrace(join("; ", $array_query), $start_time, 'multi_query: Connection fails');
            return false;
        }

        $multi_query = join(";", $array_query);
        $multi_query = str_replace("\n", " ", $multi_query);
        //remove double ; if exist
        $multi_query = preg_replace("/;(\s+)?;/i", ";", $multi_query);

        $set = 0;
        $list = [];
        if(mysqli_multi_query($this->connection, $multi_query)){
            // flush multi_queries, so any query after can run
            do {
                $set ++;
                /* store first result set */
                if ($result = mysqli_store_result($this->connection)) {
                    /*while ($row = mysqli_fetch_assoc($result)) {
                        $list[$set][] = $row;
                    }*/
                    $list[$set] = $this->fetchAll($result);
                    mysqli_free_result($result);
                }
            } while (mysqli_more_results($this->connection) && mysqli_next_result($this->connection));
        }

        if($this->debug) $this->setTrace(join(";", $array_query), $start_time);

        return $list;
    }


    // a simple utitily method which we use very frequently
    public function getItemInfo($table_name, $id_value, $id_field = 'id'){
        return $this->select(
            $table_name,
            [],
            [
                $id_field => ["=", $id_value],
            ],
            '',
            1
        );
    }


    //$table_name = 'table_abc'
    /*$fields = array(
            "field_name_1",
        );*/
    /*$where_condition = array(
        "field_name_1" => ["=", "value_1"],
        "field_name_2" => ["LIKE", "value_1"],
    );*/
    public function select($table_name, $fields =[],  $where_condition=[],  $order_by = '', $limit='1'){

        // connect on demand
        if(!$this->connect()) {
            if($this->debug) $this->setTrace($table_name, 0, 'select: Connection fails');
            return false;
        }

        $start_time = $this->getCurrentTime();

        $check_fields = [];
        if(sizeof($fields) && $fields[0] != '*') $check_fields = $fields;
        $check_fields = array_merge($check_fields, array_keys($where_condition));

        $cols_in_tables = $this->filterColumnInTable($table_name, $check_fields);

        $bind_types = [];
        $bind_values = [];
        $condition_list = [];
        $permitted_operator = [">", ">=", "<", "<=", "!=", "=", "BETWEEN", "LIKE", "IS"]; // https://dev.mysql.com/doc/refman/8.0/en/non-typed-operators.html
        foreach ($where_condition as $field_name => $operator_value) {

            // if any field invalid, stop the query
            if(!in_array($field_name, $cols_in_tables)) {
                echo "Invalid field_name ".$field_name;
                return false;
            };

            // invalidate
            if(!is_array($operator_value) || sizeof($operator_value) != 2) continue;

            $operator = $operator_value[0];
            if(!in_array($operator, $permitted_operator)) continue;

            $value    = $operator_value[1];

            $condition_list[] = " `".$field_name."` ".$operator." ? ";
            $bind_types[] = (is_int($value)) ? 'd' : 's';
            $bind_values[] = $value;
        }

        $select_fields = (sizeof($fields) > 0) ? array_map(function ($field){ return "`".$field."`";}, $fields) : ["*"];
        $order_by_query = ($order_by) ? "ORDER BY ".preg_replace("/[^a-z0-9\s_]/i", "", $order_by) : "";
        $safe_limit = ($limit) ? preg_replace("/[^0-9\,\s]/i", "", $limit) : "1";
        $where_condition = (sizeof($condition_list)) ? " AND " . join(" AND ", $condition_list) : "";

        $query = "SELECT ".join(',', $select_fields)." 
                  FROM `".$table_name."` WHERE 1 ".$where_condition." 
                  ".$order_by_query." 
                  LIMIT ".$safe_limit;

        $stmt = $this->connection->prepare( $query );
        if(!$stmt) return false;

        if(sizeof($bind_types)) {
            $stmt->bind_param(join('', $bind_types), ...$bind_values);
        }
        $stmt->execute();
        $result = $stmt->get_result();

        if($this->debug) $this->setTrace($query, $start_time);

        return ($safe_limit == '1') ? $result->fetch_assoc() : $result->fetch_all();
    }


    protected function filterColumnInTable($table_name, array $list_check_fields) {
        $table_columns = $this->getTableInfo($table_name);

        $safe_fields = [];
        foreach ($list_check_fields as $field_name) {
            // make sure the field exists
            if(!array_key_exists($field_name, $table_columns)) continue;

            $safe_fields[]  = $field_name;
        }

        return $safe_fields;
    }


    //fetch array
    /**
     * @deprecated
     */
    public function fetchArray(\mysqli_result $resource){
        return $resource->fetch_array();
    }

    /**
     * @param \mysqli_result | false $resource
     * @return array|null
     */
    public function fetchAssoc( $resource){
        if(!$resource) {
            //die("Resource failed: code fetchAssoc 122");
            return null;
        }

        return $resource->fetch_assoc();
    }

    /**
     * @param \mysqli_result | false $resource
     * @return array|mixed
     */
    public function fetchAll($resource){

        if(!$resource) return [];

        //some hosting does not enable this function, so this is the work-around
        if( ! function_exists("mysqli_fetch_all")) {
            $item_list = array();
            while ($item = $this->fetchAssoc($resource)) {
                $item_list[] = $item;
            }

            return $item_list;
        }

        return ($resource) ? $resource->fetch_all(MYSQLI_ASSOC) : [];
    }

    //affected rows from last query
    public function get_affected_rows(){
        return $this->connection->affected_rows;
    }


    public function getErrorNo(){
        return mysqli_errno($this->connection);
    }


    public function __destruct() {
        $this->disconnect();
    }
}
