<?php
/**
 * Part of the Joomla Framework Database Package
 *
 * @copyright  Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

namespace Joomla\Database\Pdo;

use Joomla\Database\FetchMode;
use Joomla\Database\FetchOrientation;
use Joomla\Database\ParameterType;
use Joomla\Database\StatementInterface;

/**
 * PDO Database Statement.
 *
 * @since  2.0.0
 */
class PdoStatement implements StatementInterface
{
	/**
	 * Mapping array for fetch modes.
	 *
	 * @var    array
	 * @since  2.0.0
	 */
	private const FETCH_MODE_MAP = [
		FetchMode::ASSOCIATIVE     => \PDO::FETCH_ASSOC,
		FetchMode::NUMERIC         => \PDO::FETCH_NUM,
		FetchMode::MIXED           => \PDO::FETCH_BOTH,
		FetchMode::STANDARD_OBJECT => \PDO::FETCH_OBJ,
		FetchMode::COLUMN          => \PDO::FETCH_COLUMN,
		FetchMode::CUSTOM_OBJECT   => \PDO::FETCH_CLASS,
	];

	/**
	 * Mapping array for parameter types.
	 *
	 * @var    array
	 * @since  2.0.0
	 */
	private const PARAMETER_TYPE_MAP = [
		ParameterType::BOOLEAN      => \PDO::PARAM_BOOL,
		ParameterType::INTEGER      => \PDO::PARAM_INT,
		ParameterType::LARGE_OBJECT => \PDO::PARAM_LOB,
		ParameterType::NULL         => \PDO::PARAM_NULL,
		ParameterType::STRING       => \PDO::PARAM_STR,
	];

	/**
	 * The decorated PDOStatement object.
	 *
	 * @var    \PDOStatement
	 * @since  2.0.0
	 */
	protected $pdoStatement;

	/**
	 * Statement constructor
	 *
	 * @param   \PDOStatement  $pdoStatement  The decorated PDOStatement object.
	 *
	 * @since   2.0.0
	 */
	public function __construct(\PDOStatement $pdoStatement)
	{
		$this->pdoStatement = $pdoStatement;
	}

	/**
	 * Binds a parameter to the specified variable name.
	 *
	 * @param   string|integer  $parameter      Parameter identifier. For a prepared statement using named placeholders, this will be a parameter
	 *                                          name of the form `:name`. For a prepared statement using question mark placeholders, this will be
	 *                                          the 1-indexed position of the parameter.
	 * @param   mixed           $variable       Name of the PHP variable to bind to the SQL statement parameter.
	 * @param   string          $dataType       Constant corresponding to a SQL datatype, this should be the processed type from the QueryInterface.
	 * @param   integer         $length         The length of the variable. Usually required for OUTPUT parameters.
	 * @param   array           $driverOptions  Optional driver options to be used.
	 *
	 * @return  boolean
	 *
	 * @since   2.0.0
	 */
	public function bindParam($parameter, &$variable, string $dataType = ParameterType::STRING, ?int $length = null, ?array $driverOptions = null)
	{
		$type            = $this->convertParameterType($dataType);
		$extraParameters = array_slice(func_get_args(), 3);

		if (count($extraParameters) !== 0)
		{
			$extraParameters[0] = $extraParameters[0] ?? 0;
		}

		$this->pdoStatement->bindParam($parameter, $variable, $type, ...$extraParameters);

		return true;
	}

	/**
	 * Closes the cursor, enabling the statement to be executed again.
	 *
	 * @return  void
	 *
	 * @since   2.0.0
	 */
	public function closeCursor(): void
	{
		$this->pdoStatement->closeCursor();
	}

	/**
	 * Fetches the SQLSTATE associated with the last operation on the statement handle.
	 *
	 * @return  string
	 *
	 * @since   2.0.0
	 */
	public function errorCode()
	{
		return $this->pdoStatement->errorCode();
	}

	/**
	 * Fetches extended error information associated with the last operation on the statement handle.
	 *
	 * @return  array
	 *
	 * @since   2.0.0
	 */
	public function errorInfo()
	{
		return $this->pdoStatement->errorInfo();
	}

	/**
	 * Executes a prepared statement
	 *
	 * @param   array|null  $parameters  An array of values with as many elements as there are bound parameters in the SQL statement being executed.
	 *
	 * @return  boolean
	 *
	 * @since   2.0.0
	 */
	public function execute(?array $parameters = null)
	{
		return $this->pdoStatement->execute($parameters);
	}

	/**
	 * Fetches the next row from a result set
	 *
	 * @param   integer|null  $fetchStyle         Controls how the next row will be returned to the caller. This value must be one of the
	 *                                            FetchMode constants, defaulting to value of FetchMode::MIXED.
	 * @param   integer       $cursorOrientation  For a StatementInterface object representing a scrollable cursor, this value determines which row
	 *                                            will be returned to the caller. This value must be one of the FetchOrientation constants,
	 *                                            defaulting to FetchOrientation::NEXT.
	 * @param   integer       $cursorOffset       For a StatementInterface object representing a scrollable cursor for which the cursorOrientation
	 *                                            parameter is set to FetchOrientation::ABS, this value specifies the absolute number of the row in
	 *                                            the result set that shall be fetched. For a StatementInterface object representing a scrollable
	 *                                            cursor for which the cursorOrientation parameter is set to FetchOrientation::REL, this value
	 *                                            specifies the row to fetch relative to the cursor position before `fetch()` was called.
	 *
	 * @return  mixed  The return value of this function on success depends on the fetch type. In all cases, boolean false is returned on failure.
	 *
	 * @since   2.0.0
	 */
	public function fetch(?int $fetchStyle = null, int $cursorOrientation = FetchOrientation::NEXT, int $cursorOffset = 0)
	{
		if ($fetchStyle === null)
		{
			return $this->pdoStatement->fetch();
		}

		return $this->pdoStatement->fetch($this->convertFetchMode($fetchStyle), $cursorOrientation, $cursorOffset);
	}

	/**
	 * Returns the number of rows affected by the last SQL statement.
	 *
	 * @return  integer
	 *
	 * @since   2.0.0
	 */
	public function rowCount(): int
	{
		return $this->pdoStatement->rowCount();
	}

	/**
	 * Sets the fetch mode to use while iterating this statement.
	 *
	 * @param   integer  $fetchMode  The fetch mode, must be one of the FetchMode constants.
	 * @param   mixed    ...$args    Optional mode-specific arguments.
	 *
	 * @return  void
	 *
	 * @since   2.0.0
	 */
	public function setFetchMode(int $fetchMode, ...$args): void
	{
		$this->pdoStatement->setFetchMode($this->convertFetchMode($fetchMode), ...$args);
	}

	/**
	 * Converts the database API's fetch mode to a PDO fetch mode
	 *
	 * @param   integer  $mode  Fetch mode to convert
	 *
	 * @return  integer
	 *
	 * @since   2.0.0
	 * @throws  \InvalidArgumentException if the fetch mode is unsupported
	 */
	private function convertFetchMode(int $mode): int
	{
		if (!isset(self::FETCH_MODE_MAP[$mode]))
		{
			throw new \InvalidArgumentException(sprintf('Unsupported fetch mode `%s`', $mode));
		}

		return self::FETCH_MODE_MAP[$mode];
	}

	/**
	 * Converts the database API's parameter type to a PDO parameter type
	 *
	 * @param   string  $type  Parameter type to convert
	 *
	 * @return  integer
	 *
	 * @since   2.0.0
	 * @throws  \InvalidArgumentException if the parameter type is unsupported
	 */
	private function convertParameterType(string $type): int
	{
		if (!isset(self::PARAMETER_TYPE_MAP[$type]))
		{
			throw new \InvalidArgumentException(sprintf('Unsupported parameter type `%s`', $type));
		}

		return self::PARAMETER_TYPE_MAP[$type];
	}
}
