<?php
namespace Bitrix\Main\DB;

abstract class DbConnection
{
	protected $connectionResource;
	protected $isConnected = false;

	/**
	 * @var SqlHelper
	 */
	protected $sqlHelper;

	/**
	 * @var \Bitrix\Main\Diag\SqlTracker
	 */
	protected $sqlTracker;
	protected $trackSql = false;

	protected $version;
	protected $versionExpress;

	protected $dbHost;
	protected $dbName;
	protected $dbLogin;
	protected $dbPassword;
	protected $dbInitCommand = 0;
	protected $dbOptions = 0;

	protected $tableColumnsCache = array();

	const PERSISTENT = 1;
	const DEFERRED = 2;

	public function __construct($dbHost, $dbName, $dbLogin, $dbPassword, $dbInitCommand, $dbOptions = -1)
	{
		//if (!is_string($dbHost) || $dbHost == "")
		//	throw new ConnectionException("Empty database host name");
		if (!is_string($dbName) || $dbName == "")
			throw new \Bitrix\Main\Config\ConfigurationException("Empty database name");
		if (!is_string($dbLogin) || $dbLogin == "")
			throw new \Bitrix\Main\Config\ConfigurationException("Empty database user login");

		$this->dbHost = $dbHost;
		$this->dbName = $dbName;
		$this->dbLogin = $dbLogin;
		$this->dbPassword = $dbPassword;
		$this->dbInitCommand = $dbInitCommand;

		$this->dbOptions = intval($dbOptions);
		if ($this->dbOptions < 0)
			$this->dbOptions = self::PERSISTENT | self::DEFERRED;

		$this->isConnected = false;
	}


	/**********************************************************
	 * SqlHelper
	 **********************************************************/

	/**
	 * @return SqlHelper
	 */
	public function getSqlHelper()
	{
		if ($this->sqlHelper == null)
			$this->sqlHelper = $this->createSqlHelper();

		return $this->sqlHelper;
	}

	/**
	 * @return SqlHelper
	 */
	abstract protected function createSqlHelper();


	/***********************************************************
	 * Connection and disconnection
	 ***********************************************************/

	public function connect()
	{
		if ($this->isConnected)
			return;
		if (($this->dbOptions & self::DEFERRED) != 0)
			return;

		$this->connectInternal();
	}

	public function disconnect()
	{
		if (!$this->isConnected)
			return;
		if (($this->dbOptions & self::PERSISTENT) != 0)
			return;

		$this->disconnectInternal();
	}

	abstract protected function connectInternal();
	abstract protected function disconnectInternal();

	public function getConnectionResource()
	{
		$this->connectInternal();
		return $this->connectionResource;
	}


	/*********************************************************
	 * Query
	 *********************************************************/

	/**
	 * @param $sql
	 * @param array|null $arBinds
	 * @param $offset
	 * @param $limit
	 * @param \Bitrix\Main\Diag\SqlTrackerQuery|null $trackerQuery
	 * @return \mysqli_result
	 * @throws SqlException
	 */
	abstract protected function queryInternal($sql, array $arBinds, $offset, $limit, \Bitrix\Main\Diag\SqlTrackerQuery $trackerQuery = null);

	/**
	 * @param $result
	 * @param \Bitrix\Main\Diag\SqlTrackerQuery $trackerQuery
	 * @return DbResult
	 */
	abstract protected function createDbResult($result, \Bitrix\Main\Diag\SqlTrackerQuery $trackerQuery = null);

	/**
	 * Makes query
	 * query($sql)
	 * query($sql, $limit)
	 * query($sql, $offset, $limit)
	 * query($sql, $arBinds)
	 * query($sql, $arBinds, $limit)
	 * query($sql, $arBinds, $offset, $limit)
	 *
	 * @param string $sql Sql query
	 * @param array $arBinds Array of binds
	 * @param int $offset Offset
	 * @param int $limit Limit
	 * @return DbResult | null
	 */
	public function query($sql)
	{
		list($sql, $arBinds, $offset, $limit) = self::parseQueryFunctionArgs(func_get_args());

		$trackerQuery = null;
		if ($this->trackSql)
			$trackerQuery = $this->sqlTracker->getNewTrackerQuery();

		$result = $this->queryInternal($sql, $arBinds, $offset, $limit, $trackerQuery);

		return $this->createDbResult($result, $trackerQuery);
	}

	public function queryScalar($sql, array $arBinds = null)
	{
		$trackerQuery = null;
		if ($this->trackSql)
			$trackerQuery = $this->sqlTracker->getNewTrackerQuery();

		$result = $this->queryInternal($sql, $arBinds, 0, 1, $trackerQuery);
		$dbResult = $this->createDbResult($result, $trackerQuery);

		$return = null;
		if ($ar = $dbResult->fetch())
			$return = array_shift($ar);

		$result->close();

		return $return;
	}

	public function queryExecute($sql, array $arBinds = null)
	{
		$trackerQuery = null;
		if ($this->trackSql)
			$trackerQuery = $this->sqlTracker->getNewTrackerQuery();

		return $this->queryInternal($sql, $arBinds, 0, 0, $trackerQuery);
	}

	protected static function parseQueryFunctionArgs($args)
	{
		/*
		 * query($sql)
		 * query($sql, $limit)
		 * query($sql, $offset, $limit)
		 * query($sql, $arBinds)
		 * query($sql, $arBinds, $limit)
		 * query($sql, $arBinds, $offset, $limit)
		 */
		$numArgs = count($args);
		if ($numArgs < 1)
			throw new \Bitrix\Main\ArgumentNullException("Empty sql statement", "sql");

		$arBinds = array();
		$offset = 0;
		$limit = 0;

		if ($numArgs == 1)
		{
			$sql = $args[0];
		}
		elseif ($numArgs == 2)
		{
			if (is_array($args[1]))
				list($sql, $arBinds) = $args;
			else
				list($sql, $limit) = $args;
		}
		elseif ($numArgs == 3)
		{
			if (is_array($args[1]))
				list($sql, $arBinds, $limit) = $args;
			else
				list($sql, $offset, $limit) = $args;
		}
		else
		{
			list($sql, $arBinds, $offset, $limit) = $args;
		}

		return array($sql, $arBinds, $offset, $limit);
	}

	abstract public function getInsertedId($name = "");

	public function executeSqlBatch($sqlBatch, $stopOnError = false)
	{
		$delimiter = $this->getSqlHelper()->getQueryDelimiter();

		$sqlBatch = trim($sqlBatch);

		$arSqlBatch = array();
		$sql = "";

		do
		{
			if (preg_match("%^(.*?)(['\"`#]|--|".$delimiter.")%is", $sqlBatch, $match))
			{
				//Found string start
				if ($match[2] == "\"" || $match[2] == "'" || $match[2] == "`")
				{
					$sqlBatch = substr($sqlBatch, strlen($match[0]));
					$sql .= $match[0];
					//find a qoute not preceeded by \
					if (preg_match("%^(.*?)(?<!\\\\)".$match[2]."%s", $sqlBatch, $string_match))
					{
						$sqlBatch = substr($sqlBatch, strlen($string_match[0]));
						$sql .= $string_match[0];
					}
					else
					{
						//String falled beyong end of file
						$sql .= $sqlBatch;
						$sqlBatch = "";
					}
				}
				//Comment found
				elseif ($match[2] == "#" || $match[2] == "--")
				{
					//Take that was before comment as part of sql
					$sqlBatch = substr($sqlBatch, strlen($match[1]));
					$sql .= $match[1];
					//And cut the rest
					$p = strpos($sqlBatch, "\n");
					if ($p === false)
					{
						$p1 = strpos($sqlBatch, "\r");
						if ($p1 === false)
							$sqlBatch = "";
						elseif ($p < $p1)
							$sqlBatch = substr($sqlBatch, $p);
						else
							$sqlBatch = substr($sqlBatch, $p1);
					}
					else
						$sqlBatch = substr($sqlBatch, $p);
				}
				//Delimiter!
				else
				{
					//Take that was before delimiter as part of sql
					$sqlBatch = substr($sqlBatch, strlen($match[0]));
					$sql .= $match[1];
					//Delimiter must be followed by whitespace
					if (preg_match("%^[\n\r\t ]%", $sqlBatch))
					{
						$sql = trim($sql);
						if (!empty($sql))
						{
							$arSqlBatch[] = str_replace("\r\n", "\n", $sql);
							$sql = "";
						}
					}
					//It was not delimiter!
					elseif (!empty($sqlBatch))
					{
						$sql .= $match[2];
					}
				}
			}
			else //End of file is our delimiter
			{
				$sql .= $sqlBatch;
				$sqlBatch = "";
			}
		}
		while (!empty($sqlBatch));

		$sql = trim($sql);
		if (!empty($sql))
			$arSqlBatch[] = str_replace("\r\n", "\n", $sql);

		$result = array();
		foreach ($arSqlBatch as $sql)
		{
			try
			{
				$this->queryExecute($sql);
			}
			catch (SqlException $ex)
			{
				$result[] = $ex->getMessage();
				if ($stopOnError)
					return $result[0];
			}
		}

		return $result;
	}


	/*********************************************************
	 * DDL
	 *********************************************************/

	abstract public function isTableExists($tableName);
	abstract public function isIndexExists($tableName, array $arColumns);
	abstract public function getIndexName($tableName, array $arColumns, $strict = false);
	abstract public function getTableFields($tableName);


	/*********************************************************
	 * Transaction
	 *********************************************************/

	abstract public function startTransaction();
	abstract public function commitTransaction();
	abstract public function rollbackTransaction();


	/*********************************************************
	 * Tracker
	 *********************************************************/

	public function startTracker($reset = false)
	{
		if ($this->sqlTracker == null)
			$this->sqlTracker = new \Bitrix\Main\Diag\SqlTracker();
		if ($reset)
			$this->sqlTracker->reset();

		$this->trackSql = true;
	}

	public function stopTracker()
	{
		$this->trackSql = false;
	}

	public function getTracker()
	{
		return $this->sqlTracker;
	}


	/*********************************************************
	 * Type, version, cache, etc.
	 *********************************************************/

	abstract public function getType();
	abstract public function getVersion();
	abstract protected function getErrorMessage();

	public function clearCaches()
	{
		$this->tableColumnsCache = array();
	}
}
