<?php namespace ProcessWire;

/**
 * ProcessWire MySQLi Database
 *
 * Serves as a wrapper to PHP's mysqli classes
 * 
 * This file is licensed under the MIT license
 * https://processwire.com/about/license/mit/
 * 
 * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
 * https://processwire.com
 *
 *
 */

/**
 * Database class provides a layer on top of mysqli
 *
 */
class Database extends \mysqli implements WireDatabase {

	/**
	 * Log of all queries performed in this instance
	 *
	 */
	protected $queryLog = array();

	/**
	 * Should WireDatabaseException be thrown on error?
	 *
	 */
	protected $throwExceptions = true;

	/**
	 * @var ProcessWire
	 * 
	 */
	protected $wire;

	/**
	 * @var bool
	 * 
	 */
	protected $debug = false;

	/**
	 * Construct the Database 
	 *
	 * Since this extends MySQL all the MySQL construct params are kept in tact. 
	 * However, you may just supply an object with the following properties if you prefer: 
	 * $o->dbUser, $o->dbPass, $o->dbHost, $o->dbName, $config->dbPort, $config->dbSocket (optional).
	 * This would usually be from a ProcessWire Config ($config) API var, but kept as generic object
	 * in case someone wants to use this class elsewhere. 
	 * 
	 * @param string|Config $host Hostname or object with config properties. 
	 * @param string $user Username
	 * @param string $pass Password
	 * @param string $db Database name
	 * @param int $port Port
	 * @param string $socket Socket
	 * @throws WireDatabaseException
	 * 
	 */
	public function __construct($host = 'localhost', $user = null, $pass = null, $db = null, $port = null, $socket = null) {

		if(is_object($host) && $host->dbHost) {
			$config = $host;
			$this->debug = $config->debug; 
			$host = $config->dbHost; 
			$user = $config->dbUser; 
			$pass = $config->dbPass; 
			$db = $config->dbName; 
			$port = $config->dbPort; 
			$socket = $config->dbSocket ? $config->dbSocket : null;
		} else $config = null;

		@parent::__construct($host, $user, $pass, $db, $port, $socket);
		if(mysqli_connect_error()) throw new WireDatabaseException("DB connect error " . mysqli_connect_errno() . ' - ' . mysqli_connect_error()); 

		if($config) {
			if($config->dbCharset) $this->set_charset($config->dbCharset); 
				else if($config->dbSetNamesUTF8) $this->query("SET NAMES 'utf8'");
		}
	}
	
	/**
	 * Overrides default mysqli query method so that it also records and times queries. 
	 *
	 * @param string $sql SQL Query
	 * @param int $resultmode See http://www.php.net/manual/en/mysqli.query.php
	 * @return mixed Returns FALSE on failure. 
	 * 	For successful SELECT, SHOW, DESCRIBE or EXPLAIN queries mysqli_query() will return a MySQLi_Result object. 
	 *	For other successful queries mysqli_query() will return TRUE.
	 * @throws WireDatabaseException
	 *
	 */
	#[\ReturnTypeWillChange]
	public function query($sql, $resultmode = MYSQLI_STORE_RESULT) {

		static $timerTotalQueryTime = 0;
		static $timerFirstStartTime = 0; 

		if(is_object($sql) && $sql instanceof DatabaseQuery) $sql = $sql->getQuery();

		if($this->debug) {
			$timerKey = Debug::timer();
			if(!$timerFirstStartTime) $timerFirstStartTime = (float) $timerKey; 
		} else $timerKey = null; 

		$result = @parent::query($sql, $resultmode); 

		if($result) {
			if($this->debug) { 
				if(isset($result->num_rows)) $sql .= " [" . $result->num_rows . " rows]";
				if(!is_null($timerKey)) {
					$elapsed = (float) Debug::timer($timerKey); 
					$timerTotalQueryTime += $elapsed; 
					$timerTotalSinceStart = ((float) Debug::timer()) - $timerFirstStartTime; 
					$sql .= " [{$elapsed}s, {$timerTotalQueryTime}s, {$timerTotalSinceStart}s]";
				}
				$this->queryLog($sql); 
			}

		} else if($this->throwExceptions) {
			throw new WireDatabaseException($this->error . ($this->debug ? "\n$sql" : '')); 
		}

		return $result; 
	}
	
	/**
	 * Get an array of all queries that have been executed thus far
	 *
	 * Active in ProcessWire debug mode only
	 *
	 * @param ProcessWire $wire ProcessWire instance, if omitted returns queries for all instances
	 * @return array
	 * @deprecated
	 *
	 */
	static public function getQueryLog(ProcessWire $wire = null) {
		if($wire) {
			return $wire->database->queryLog();
		} else {
			$log = array();
			foreach(ProcessWire::getInstances() as $wire) {
				$log = array_merge($log, $wire->database->queryLog());
			}
		}
		return $log;
	}

	/**
	 * Log a query or return the query log
	 * 
	 * @param string $sql Omit to instead return the query log
	 * @return array|bool Returns query log array when $sql argument is omitted
	 * 
	 */
	public function queryLog($sql = '') {
		if($sql) {
			$this->queryLog[] = $sql;
			return true;
		} else {
			return $this->queryLog;
		}
	}

	/**
	 * Get array of all tables in this database.
	 *
	 * @return array
	 *
	 */
	public function getTables() {
		static $tables = array();

		if(!count($tables)) {
			$result = $this->query("SHOW TABLES"); 			
			while($row = $result->fetch_array()) $tables[] = current($row); 
		} 

		return $tables; 
	}

	/**
	 * Is the given string a database comparison operator?
	 *
	 * @param string $str 1-2 character opreator to test
	 * @return bool 
	 *
	 */
	public function isOperator($str) {
		return in_array($str, array('=', '<', '>', '>=', '<=', '<>', '!=', '&', '~', '|', '^', '<<', '>>'));
	}

	/**
	 * Set whether Exceptions should be thrown on query errors
	 *
	 * @param bool $throwExceptions Default is true
	 *
	 */
	public function setThrowExceptions($throwExceptions = true) {
		$this->throwExceptions = $throwExceptions; 
	}

	/**
	 * Sanitize a table name for _a-zA-Z0-9
	 *
	 * @param string $table
	 * @return string
	 *
	 */
	public function escapeTable($table) {
		$table = (string) trim($table); 
		if(ctype_alnum($table)) return $table; 
		if(ctype_alnum(str_replace('_', '', $table))) return $table;
		return preg_replace('/[^_a-zA-Z0-9]/', '_', $table);
	}

	/**
	 * Sanitize a column name for _a-zA-Z0-9
	 *
	 * @param string $col
	 * @return string
	 *
	 */
	public function escapeCol($col) {
		return $this->escapeTable($col);
	}

	/**
	 * Sanitize a table.column string, where either part is optional
	 *
	 * @param string $str
	 * @return string
	 *
	 */
	public function escapeTableCol($str) {
		if(strpos($str, '.') === false) return $this->escapeTable($str); 
		list($table, $col) = explode('.', $str); 
		return $this->escapeTable($table) . '.' . $this->escapeCol($col);
	}

	/**
	 * Escape a string value, camelCase alias of escape_string()
	 *
	 * @param string $str
	 * @return string
	 *
	 */
	public function escapeStr($str) {
		return $this->escape_string($str); 
	}

	/**
	 * Escape a string value, plus escape characters necessary for a MySQL 'LIKE' phrase
	 *
	 * @param string $like
	 * @return string
	 *
	 */
	public function escapeLike($like) {
		$like = $this->escape_string($like); 
		return addcslashes($like, '%_'); 
	}


}
