<?php
/**
 * 数据库核心类
 *
 * @author     呼吸二氧化碳 <jonwang@myqee.com>
 * @category   MyQEE
 * @package    Module
 * @subpackage Database
 * @copyright  Copyright (c) 2008-2016 myqee.com
 * @license    http://www.myqee.com/license.html
 */
class Module_Database extends Database_QueryBuilder
{
    /**
     * MySQL驱动类型
     *
     * @var string
     */
    const TYPE_MYSQL = 'MySQL';

    /**
     * MySQLI驱动类型
     *
     * @var string
     */
    const TYPE_MYSQLI = 'MySQLI';

    /**
     * Mongo驱动类型
     *
     * @var string
     */
    const TYPE_MONGO = 'Mongo';

    /**
     * SQLite驱动类型
     *
     * @var string
     */
    const TYPE_SQLITE = 'SQLite';

    /**
     * Postgre驱动类型
     *
     * @var string
     */
    const TYPE_POSTGRE = 'Postgre';

    /**
     * PDO驱动类型
     *
     * @var string
     */
    const TYPE_PDO = 'PDO';

    /**
     * 默认配置名
     *
     * @var string
     */
    const DEFAULT_CONFIG_NAME = 'default';

    /**
     * @var array Database instances
     */
    protected static $instances = array();

    /**
     * 当前配置
     *
     * @var array
     */
    protected $config;

    /**
     * 当前驱动
     *
     * @var Database_Driver_MySQLI
     */
    protected $driver;

    /**
     * 数据库连接对象
     *
     * @var mysqli
     */
    protected $_connection;

    /**
     * 是否自动使用主数据库
     *
     * @var boolean
     */
    protected $is_auto_use_master = false;

    /**
     * 记录慢查询
     *
     *     array
     *     (
     *         //    执行时的时间    耗时(单位毫秒)   查询语句
     *         array(1351691389,   1200          ,'select * from test;'),
     *     )
     *
     * @var array
     */
    protected static $slow_querys = array();

    /**
     * 返回数据库实例化对象
     *
     * 支持 `Database::instance('mysqli://root:123456@127.0.0.1/myqee/');` 的方式
     *
     * @param string $config_name 默认值为 Database::DEFAULT_CONFIG_NAME
     * @return Database
     */
    public static function instance($config_name = null)
    {
        if (null===$config_name)
        {
            $config_name = Database::DEFAULT_CONFIG_NAME;
        }

        if (is_string($config_name))
        {
            $i_name = $config_name;
        }
        else
        {
            $i_name = '.config_'.md5(serialize($config_name));
        }

        if (!isset(Database::$instances[$i_name]))
        {
            Database::$instances[$i_name] = new Database($config_name);
        }

        return Database::$instances[$i_name];
    }

    /**
     * new Database('default');
     *
     * 支持 `new Database('mysqli://root:123456@127.0.0.1/myqee/');` 的方式
     *
     * @param string $config_name 默认值为 `Database::DEFAULT_CONFIG_NAME`
     * @return  void
     */
    public function __construct($config_name = null)
    {
        if (null===$config_name)
        {
            $config_name = Database::DEFAULT_CONFIG_NAME;
        }

        if (is_array($config_name))
        {
            $this->config = $config_name;
        }
        elseif (false!==strpos($config_name,'://'))
        {
            list($type) = explode('://', $config_name);

            $this->config = array
            (
                'type'         => $type,
                'connection'   => $config_name,
                'table_prefix' => '',
                'charset'      => 'utf8',
                'caching'      => false,
                'profiling'    => true,
            );
        }
        else
        {
            $this->config = Core::config('database.' . $config_name);
        }

        $this->config['charset'] = strtoupper($this->config['charset']);

        if (!isset($this->config['auto_change_charset']))
        {
            $this->config['auto_change_charset'] = false;
        }
        if ($this->config['auto_change_charset'])
        {
            if (isset($this->config['data_charset']))
            {
                $this->config['data_charset'] = strtoupper($this->config['data_charset']);
            }
            else
            {
                $this->config['data_charset'] = $this->config['charset'];
            }
        }

        $driver = $this->config['type'];
        if (!$driver)
        {
            $driver = 'MySQL';
        }
        $driver = 'Database_Driver_' . $driver;

        if (!class_exists($driver, true))
        {
            throw new Exception('Database Driver:' . $driver . ' not found.');
        }

        if (!isset($this->config['connection']))
        {
            throw new Exception('Database connection not set.');
        }

        if (is_string($this->config['connection']))
        {
            $this->config['connection'] = Database::parse_dsn($this->config['connection']);
        }

        # 当前驱动
        $this->driver = new $driver($this->config);

        parent::__construct();

        # 增加自动关闭连接列队
        Core::add_close_connect_class('Database');
    }

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

    /**
     * 获取驱动引擎对象
     *
     * @return Database_Driver_MySQLI
     */
    public function driver()
    {
        return $this->driver;
    }

    /**
     * 获取当前配置数组
     *
     * @return array
     */
    public function config()
    {
        return $this->config;
    }

    /**
     * 关闭连接
     */
    public function close_connect()
    {
        if ($this->driver)
        {
            $this->driver->close_connect();
        }
    }

    /**
     * 安全的执行SQL模板查询
     *
     * 需要先通过 `$this->prepare($statement)` 设置SQL后执行
     * 如果当前驱动是PDO，则使用PDO相同的方式处理，
     *
     *      // 用法1，替换掉相同关键字的部分
     *      $rp = array
     *      (
     *          ':id'     => $_GET['id'],
     *          ':status' => $_GET['status'],
     *      );
     *      $rs = $db->prepare("SELECT * FROM `my_table` WHERE id = :id AND status = :status")->execute($rp);
     *
     *      // 用法2，按顺序替换掉语句中?的部分
     *      $rp = array
     *      (
     *          $_GET['id'],
     *          $_GET['status'],
     *      );
     *      $rs = $db->prepare("SELECT * FROM `my_table` WHERE id = ? AND status = ?")->execute($rp);
     *
     * @param array $input_parameters
     * @param bool $as_object
     * @param null $use_master
     * @return Database_Driver_MySQLI_Result
     */
    public function execute(array $input_parameters = array(), $as_object = false, $use_master = null)
    {
        if (!$this->_statement)
        {
            throw new Exception(__('not found statement, you need run `$db->prepare($statement)` before execute.'));
        }

        if (null === $use_master && true === $this->is_auto_use_master)
        {
            $use_master = true;
        }

        $time = $this->_start_slow_query();

        $rs = $this->driver->execute($this->_statement, $input_parameters, $as_object, $use_master);

        if (false!==$time)$this->_record_slow_query($time);

        return $rs;
    }

    /**
     * 执行SQL查询
     *
     * [!!] 此方法的SQL语句将不进行SQL注入过滤，执行语句请慎重，建议通过数据库的QueryBuilder来构造SQL语句，如果此方法无法满足要求也只用 `$db->execute()` 方法
     *
     *      $db->query('select * from my_table where id = 10');
     *
     * @param string $sql
     * @param boolean $as_object 返回对象名称 默认false，即返回数组
     * @param boolean $use_master 是否使用主数据库，不设置则自动判断,对更新的SQL无效
     * @return Database_Driver_MySQLI_Result
     */
    public function query($sql, $as_object = false, $use_master = null)
    {
        if (null === $use_master && true === $this->is_auto_use_master)
        {
            $use_master = true;
        }

        $time = $this->_start_slow_query();

        $rs = $this->driver->query($sql, $as_object, $use_master);

        if (false!==$time)$this->_record_slow_query($time);

        return $rs;
    }

    /**
     * 返回当前表前缀
     *
     * @return  string
     */
    public function table_prefix()
    {
        return $this->config['table_prefix'];
    }

    /**
     * 解析为SQL语句
     *
     * @see QueryBuilder::compile()
     * @param string $type select,insert,update,delect,replace
     * @param boolean $use_master 当$type=select此参数有效，设置true则使用主数据库，设置false则使用从数据库，不设置则使用默认
     * @return  string
     */
    public function compile($type = 'select', $use_master = null)
    {
        if ($type === 'select' && null === $use_master && true === $this->is_auto_use_master)
        {
            $use_master = true;
        }
        # 先连接数据库，因为在compile时需要用到mysql_real_escape_string,mysqli_real_escape_string方法
        $this->driver->connect($use_master);

        # 获取查询SQL
        $sql = $this->driver->compile($this->_builder, $type);

        # 重置QueryBuilder
        $this->reset();

        return $sql;
    }

    /**
     * 获取数据
     *
     * @param boolean $as_object 返回对象名称 默认false，即返回数组
     * @param boolean $use_master 是否使用主数据库，不设置则自动判断
     * @return Database_Driver_MySQLI_Result
     */
    public function get($as_object = false, $use_master = null)
    {
        return $this->query($this->compile('select', $use_master), $as_object, $use_master);
    }

    /**
     * 获取一条数据
     *
     * @param boolean $as_object 返回对象名称 默认false，即返回数组
     * @param boolean $use_master 是否使用主数据库，不设置则自动判断
     * @return array|object|stdClass
     */
    public function get_single($as_object = false, $use_master = null)
    {
        return $this->get($as_object, $use_master)->current();
    }

    /**
     * 最后查询的SQL语句
     *
     * @return string
     */
    public function last_query()
    {
        return $this->driver->last_query();
    }

    /**
     * 更新数据
     *
     * @param string $table
     * @param array $value
     * @param array $where
     * @return int 作用的行数
     */
    public function update($table = null, $value = null, $where = null)
    {
        if ($table)
        {
            $this->table($table);
        }
        if ($value)
        {
            $this->set($value);
        }
        if ($where)
        {
            $this->where($where);
        }
        $sql = $this->compile('update', true);

        return $this->query($sql, false, true);
    }

    /**
     * 插入数据
     *
     * @param string $table
     * @param array $value
     * @param Database_Result
     * @return array(插入ID,作用行数)
     */
    public function insert($table = null, $value = null)
    {
        if ($table)
        {
            $this->table($table);
        }
        if ($value)
        {
            $this->columns(array_keys($value));
            $this->values($value);
        }
        $sql = $this->compile('insert', true);

        return $this->query($sql, false, true);
    }

    /**
     * 删除数据
     *
     * @param string $table 表名称
     * @param array $where 条件
     * @return integer 操作行数
     */
    public function delete($table = null, $where = null)
    {
        if ($table)
        {
            $this->table($table);
        }
        if ($where)
        {
            $this->where($where);
        }
        $sql = $this->compile('delete', true);

        return $this->query($sql, false, true);
    }

    /**
     * 统计指定条件的数量
     *
     * @param mixed $table table name string or array(query, alias)
     * @param array $where Where条件
     * @return  integer
     */
    public function total($table = null, $where = null)
    {
        return $this->count_records($table, $where);
    }

    /**
     * 统计指定条件的数量
     *
     * @param mixed $table table name string or array(query, alias)
     * @param array $where Where条件
     * @return  integer
     */
    public function count_records($table = null, $where = null)
    {
        if ($table)
        {
            $this->from($table);
        }
        if ($where)
        {
            $this->where($where);
        }

        // 记录当前builder信息
        $builder = $this->_builder;

        $this->select($this->expr_value('COUNT(1) AS `total_row_count`'));

        $count = (int)$this->query($this->compile('select'), false)->get('total_row_count');

        // 将之前获取的builder信息放倒_builder_bak上，以便可使用->recovery_last_builder()方法恢复前一个builder
        $this->_builder_bak = $builder;

        return $count;
    }

    /**
     * 替换数据 REPLACE INTO
     *
     * `$update_on_duplicate_mode` 参数只对MySQL有效
     *  默认 `false` 使用传统的 REPLACE INTO 语句执行
     *  当设置成 `true` 后，系统将用 `INSERT INTO .... ON DUPLICATE KEY UPDATE ... ` 的语句方式执行，而不是直接REPLACE INTO语句
     *  详情见 [https://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html] 页面
     *
     *
     * @param string $table
     * @param array $value
     * @param array $where
     * @param array $insert_on_duplicate_key_update_mode 只有MySQL支持
     * @param Database_Result
     */
    public function merge($table = null, $value = null, $where = null, $insert_on_duplicate_key_update_mode = false)
    {
        return $this->replace($table, $value, $where, $insert_on_duplicate_key_update_mode);
    }

    /**
     * 替换数据 REPLACE INTO
     *
     * `$update_on_duplicate_mode` 参数只对MySQL有效
     *  默认 `false` 使用传统的 REPLACE INTO 语句执行
     *  当设置成 `true` 后，系统将用 `INSERT INTO .... ON DUPLICATE KEY UPDATE ... ` 的语句方式执行，而不是直接REPLACE INTO语句
     *  详情见 [https://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html] 页面
     *
     * @param string $table
     * @param array $value
     * @param array $where
     * @param array $insert_on_duplicate_key_update_mode 只有MySQL支持
     * @param Database_Result
     */
    public function replace($table = null, $value = null, $where = null, $insert_on_duplicate_key_update_mode = false)
    {
        if ($table)
        {
            $this->table($table);
        }
        if ($value)
        {
            $this->columns(array_keys($value));
            $this->values($value);
        }
        if ($where)
        {
            $this->where($where);
        }

        $sql = $this->compile($insert_on_duplicate_key_update_mode ? 'insert_update' : 'replace', true);

        return $this->query($sql, false, true);
    }

    /**
     * 获取事务对象
     *
     * @return Database_Transaction 事务对象
     */
    public function transaction()
    {
        return $this->driver->transaction();
    }

    /**
     * 返回当前事务处理的类名称
     *
     * 不支持则返回 false
     *
     * @return string|false
     */
    public function transaction_class_name()
    {
        return $this->driver->transaction_class_name();
    }

    /**
     * 设置是否一直在主数据库上查询
     *
     * 这样设置后，select会一直停留在主数据库上，直到$this->auto_use_master(false)后才会自动判断
     * @param boolean $auto_use_master
     * @return Database
     */
    public function auto_use_master($auto_use_master = true)
    {
        $this->is_auto_use_master = (boolean)$auto_use_master;

        return $this;
    }

    /**
     * 是否一直用主数据库查询
     *
     * @return boolean
     */
    public function is_auto_use_master()
    {
        return $this->is_auto_use_master;
    }

    /**
     * 创建一个数据库
     *
     * @param string $database
     * @param string $charset 编码，不传则使用数据库连接配置相同到编码
     * @param string $collate 整理格式
     * @return boolean
     * @throws Exception
     */
    public function create_database($database, $charset = null, $collate=null)
    {
        if (method_exists($this->driver, 'create_database'))
        {
            return $this->driver->create_database($database, $charset, $collate);
        }
        else
        {
            return false;
        }
    }

    /**
     * 返回是否支持对象数据
     *
     * 通常传统的数据库是不支持直接存储对象数据的，而MongoDB是支持的
     *
     * @return bool
     */
    public function is_support_object_value()
    {
        $this->driver()->is_support_object_value();
    }

    /**
     * 开启记录慢查询
     *
     * 返回当前时间，如果系统设置关闭记录慢查询，则返回 false
     *
     * @return bool|mixed
     */
    protected function _start_slow_query()
    {
        if (Database::_get_slow_query_setting_time() > 0)
        {
            return microtime(1);
        }
        else
        {
            return false;
        }
    }

    /**
     * 保存慢查询日志
     *
     * @param float $start_time 由 `$this->_start_slow_query()` 返回的值
     */
    protected function _record_slow_query($start_time)
    {
        if (!$start_time)return;

        $end_time = microtime(1);
        $use_time = 1000 * ($end_time - $start_time);

        if (($min_time = Database::_get_slow_query_setting_time()) && $use_time > $min_time)
        {
            // 记录慢查询
            Database::$slow_querys[] = array
            (
                $start_time,
                $use_time,
                $this->driver->last_query(),
            );
        }
    }

    /**
     * 解析DSN路径格式
     *
     * @param  string $dsn DSN string
     * @return array
     */
    public static function parse_dsn($dsn)
    {

        $db = array
        (
            'type'       => false,
            'username'   => false,
            'password'   => false,
            'hostname'   => false,
            'port'       => false,
            'persistent' => false,
            'database'   => false,
        );

        // Get the protocol and arguments
        list ($db['type'], $connection) = explode('://', $dsn, 2);

        if ($connection[0] === '/')
        {
            // Strip leading slash
            $db['database'] = substr($connection, 1);
        }
        else
        {
            $connection = parse_url('http://' . $connection);

            if (isset($connection['user']))
            {
                $db['username'] = $connection['user'];
            }

            if (isset($connection['pass']))
            {
                $db['password'] = $connection['pass'];
            }

            if (isset($connection['port']))
            {
                $db['port'] = $connection['port'];
            }

            if (isset($connection['host']))
            {
                if ($connection['host'] === 'unix(')
                {
                    list ($db['persistent'], $connection['path']) = explode(')', $connection['path'], 2);
                }
                else
                {
                    $db['hostname'] = $connection['host'];
                }
            }

            if (isset($connection['path']) && $connection['path'])
            {
                // Strip leading slash
                $db['database'] = trim(trim(substr($connection['path'], 1) ,'/'));
            }
        }

        return $db;
    }

    /**
     * 关闭全部数据库链接
     */
    public static function close_all_connect()
    {
        if (!Database::$instances || !is_array(Database::$instances)) return;

        foreach (Database::$instances as $database)
        {
            if ($database instanceof Database)
            {
                $database->close_connect();
            }
        }

        // 执行保存慢查询方法
        Database::save_slow_query();
    }

    /**
     * 记录慢查询
     *
     * @return boolean
     */
    protected static function save_slow_query()
    {
        if (!Database::$slow_querys)return true;

        $queries = array();
        foreach (Database::$slow_querys as $item)
        {
            $queries[] = array
            (
                'from' => $item[0],
                'to'   => $item[1],
                'use'  => $item[1] - $item[0],
                'sql'  => $item[2],
            );
        }

        $data = array
        (
            'url'       => $_SERVER["SCRIPT_URI"] .('' !== $_SERVER["QUERY_STRING"] ? '?'.$_SERVER["QUERY_STRING"] : ''),
            'method'    => HttpIO::METHOD,
            'time'      => TIME,
            'ip'        => HttpIO::IP,
            'page_time' => microtime(1) - START_TIME,
            'post'      => HttpIO::POST(),
            'queries'   => $queries,
        );

        // 写入LOG
        return Core::log('database.slow_query', $data, LOG_WARNING);
    }

    protected static function _get_slow_query_setting_time()
    {
        static $slow_query_m_time = null;

        if (null === $slow_query_m_time)
        {
            if (IS_CLI)
            {
                $slow_query_m_time = false;
            }
            else
            {
                $slow_query_m_time = (int)Core::config('core.slow_query_mtime');
            }
        }

        return $slow_query_m_time;
    }
}
