<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2007 - 2016 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flDbResult Class
 *
 * Base class to hold results from a database query.
 *
 * @package fusionLib
 */
abstract class flDbResult {
	/**
	 * The rows.
	 *
	 * @var array
	 */
	protected $rows = null;

	/**
	 * Return the number of rows in the results.
	 *
	 * @return int The number of rows.
	 */
	abstract function numRows();

	/**
	 * Seek to a new position in the results.
	 *
	 * @param int The 0 based row number to move to.
	 * @return bool True if worked else false.
	 */
	abstract function seek($row);

	/**
	 * Fetch the next row from the results.
	 *
	 * @return array An associative array of columns or false/null if no more data.
	 */
	abstract function fetch();
}

/**
 * flDb Class
 *
 * Base class for database interface allowing different databases to be
 * accessed in the same way as well as providing functions to simplify use.
 *
 * @package fusionLib
 */
abstract class flDb {
	/**
	 * The number of queries.
	 *
	 * @var int
	 */
	protected $queryCount = 0;

	/**
	 * The last query string.
	 *
	 * @var string
	 */
	protected $lastQuery = '';

	/**
	 * Create an instance of the appropriate database driver object and connect
	 * to the database.
	 *
	 * The database names are given in DSN format, driver://username:password@host:port/database
	 *
	 * Where:
	 * 	port is optional
	 * 	driver is one of mysql, mysqli, sqlite, sqlite3
	 *
	 * @param string $master The DSN for the master database.
	 * @param string $slave The DSN for the slave database if any or null, if the
	 * driver doesn't support master/slave then this parameter is ignored. The driver is ignored
	 * as it is assumed to be the same as the master.
	 * @return flDb The instance of the database driver object.
	 * @throws flDbException
	 */
	static function connect($master, $slave = null) {

		static $validDrivers = array('mysqli' => 'flDbMySQLi', 'mysql' => 'flDbMySQL', 'sqlite' => 'flDbSQLite', 'sqlite3' => 'flDbSQLite3');

		$m = parse_url($master);
		if($m === false) {
			$x = preg_split('#://#', $master, 2);
			$m['scheme'] = $x[0];
			$m['path'] = $x[1];
		}

		if(!isset($validDrivers[$m['scheme']]))
			throw new flDbException("Unknown database driver type '$m[scheme]'.");

		// If array of slaves given choose 1 at random
		if(is_array($slave))
			$slave = $slave[array_rand($slave)];

		return new $validDrivers[$m['scheme']]($m, $slave);
	}

	/**
	 * Get the number of queries.
	 *
	 * @return int The number of queries.
	 */
	function getQueryCount() {
		return $this->queryCount;
	}

	/**
	 * Escape a value.
	 *
	 * @param mixed $value The value to escape.
	 * @return mixed The escaped value.
	 */
	function escape($value) {
		if(is_string($value))
			$value = "'" . $this->_escape($value) . "'";
		else if(is_bool($value))
			$value = $value ? 1 : 0;
		elseif(is_null($value))
			$value = 'NULL';
		return $value;
	}

	/**
	 * Perform a query on the database.
	 *
	 * ? in the query string is replaced with the escaped quoted parameter
	 *
	 * @param string $query The query string.
	 * @param array $params The array of parameters to use or 1st argument
	 * @return flDbResult The result object or boolean result.
	 * @throws flDbException
	 */
	function query($query, $params = array()) {

		// If parameters given rather than array then get args
		if(!is_array($params)) $params = array_slice(func_get_args(), 1);

		// Break string to parts & check count of parts is 1 more than params
		$parts = explode('?', $query);
		if(count($parts) != count($params) + 1)
			throw new flDbException("Mismatch in the number of query parameters and the number of markers in query.");

		// Compile query
		$query = $parts[0];
		$i = 0;
		foreach($params as $p) {
			$query .= $this->escape($p);
			$query .= $parts[++$i];
		}

		$this->queryCount++;
		$this->lastQuery = $query;
		return $this->_query($query);
	}

	/**
	 * Fetch an array of all rows.
	 *
	 * @param string $query The query string.
	 * @param array $params The array of parameters to use or 1st argument
	 * @return array Array of rows of false on error.
	 */
	function fetchAll($query, $params = array()) {
		if(!is_array($params)) $params = array_slice(func_get_args(), 1);
		if($r = $this->query($query, $params)) {
			$d = array();
			while($row = $r->fetch())
				$d[] = $row;
			return $d;
		}
		else
			return false;
	}

	/**
	 * Perform a query and return the 1st row.
	 *
	 * ? in the query string is replaced with the escaped quoted parameter
	 *
	 * @param string $query The query string.
	 * @param array $params The array of parameters to use or 1st argument
	 * @return array The row or false.
	 */
	function fetchRow($query, $params = array()) {
		if(!is_array($params)) $params = array_slice(func_get_args(), 1);
		$r = $this->query($query, $params);
		return $r ? $r->fetch() : false;
	}

	/**
	 * Perform a query and return the first element of the 1st row.
	 *
	 * ? in the query string is replaced with the escaped quoted parameter
	 *
	 * @param string $query The query string.
	 * @param array $params The array of parameters to use or 1st argument
	 * @return mixed The item or false.
	 */
	function fetchOne($query, $params = array()) {
		if(!is_array($params)) $params = array_slice(func_get_args(), 1);
		$r = $this->query($query, $params);
		if($r !== false) {
			if($d = $r->fetch())
				$r = reset($d);
			else
				$r = false;
		}
		return $r;
	}

	/**
	 * Optimize or repair tables within the database.
	 *
	 * @param bool $repair true to perform a repair or false to optimize.
	 * @return array Results for each table.
	 */
	function maintain($repair = false) {
		$result = array();
		$tables = $this->fetchTables();
		foreach($tables as $t)
			$result[$t] = $this->_maintain($repair, $t);
		return $result;
	}

	/**
	 * Insert the given value pairs of key values into the table.
	 *
	 * @param string $table The table to insert the values into.
	 * @param array $values The array of key value pairs of data to insert.
	 * @return bool true if worked; else false.
	 */
	function insert($table, $values) {
		return $this->query("INSERT INTO $table (" . join(',', array_keys($values)) . ') VALUES (' . substr(str_repeat('?,', count($values)), 0, -1) . ')', $values);
	}

	/**
	 * Replace or insert the given key value pairs in the table.
	 *
	 * @param string $table The table to replace/insert the values into.
	 * @param array $values The array of key value pairs of data to insert.
	 * @return bool true if worked; else false.
	 */
	function replace($table, $values) {
		return $this->query("REPLACE INTO $table (" . join(',', array_keys($values)) . ') VALUES (' . substr(str_repeat('?,', count($values)), 0, -1) . ')', $values);
	}

	/**
	 * Update a row in the table.
	 *
	 * @param string $table The table to update.
	 * @param array $values The array of key/value pairs of data to update.
	 * @param string $where The WHERE clause for selecting the row.
	 * @param array $params The array of parameters to use or 1st argument for use in the WHERE clause.
	 * @return bool true if worked; else false.
	 */
	function update($table, $values, $where, $params) {
		if(!count($values)) return false;
		if(!is_array($params)) $params = array_slice(func_get_args(), 3);
		return $this->query("UPDATE $table SET " . join('=?, ', array_keys($values)) . '=? WHERE ' . $where, array_merge($values, $params));
	}

	/**
	 * Delete a table from the database.
	 *
	 * @param string $table The table to delete.
	 * @param string $where The where clause.
	 * @param array $params Array of parameters to use in the where clause or 1st argument.
	 * @return bool true if deleted; else false.
	 */
	function delete($table, $where, $params) {
		if(!is_array($params)) $params = array_slice(func_get_args(), 2);
		return $this->query("DELETE FROM $table WHERE $where", $params);
	}

	/**
	 * Get the last query string.
	 *
	 * @return string The last query string.
	 */
	function lastQuery() {
		return $this->lastQuery;
	}

	/**
	 * Escape a value in the driver.
	 *
	 * @param mixed $value The value to escape.
	 * @return mixed The escaped value.
	 */
	abstract function _escape($value);

	/**
	 * Perform the SQL query in the driver.
	 *
	 * @param string $query The query string.
	 * @return flDbResult The result object or boolean result.
	 */
	abstract function _query($query);

	/**
	 * Return the id generated by the last insert operation.
	 *
	 * @return int The id.
	 */
	abstract function insertID();

	/**
	 * Get the number of rows affected by the previous operation.
	 *
	 * @return int The number of rows affected.
	 */
	abstract function affectedRows();

	/**
	 * Get the list of tables in the database.
	 *
	 * @return array The list of tables in the database.
	 */
	abstract function fetchTables();

	/**
	 * Get the names of all fields in a table.
	 *
	 * @param string $table The table to query.
	 * @return array An array holding the names of all fields.
	 */
	abstract function fetchFields($table);

	/**
	 * Optimize or repair tables within the database.
	 *
	 * @param bool $repair true to perform a repair or false to optimize.
	 * @param string $table The name of the table to operate on.
	 * @return string The result.
	 */
	abstract function _maintain($repair, $table);

	/**
	 * The error for the previous operation.
	 *
	 * @return string The error string.
	 */
	abstract function errorString();

	/**
	 * The database server version.
	 *
	 * @return string The version string.
	 */
	abstract function version();
}
