<?php

/**
 * @package   bdk/debug
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2025 Brad Kent
 * @since     2.3
 */

namespace bdk\Debug\Collector\Pdo;

use bdk\Debug\Collector\Pdo as DebugCollectorPdo;
use bdk\Debug\Collector\StatementInfo;
use PDO as PdoBase;   // PDO conflicts with namespace
use PDOException;
use PDOStatement;

/**
 * Debuggable PDOStatement
 */
class Statement extends PDOStatement
{
    /** @var DebugCollectorPdo */
    protected $pdo;

    /** @var array<string,mixed> */
    protected $boundParameters = array();

    /** @var array<string,int> */
    protected $boundParameterTypes = array();

    /**
     * Constructor.
     *
     * @param DebugCollectorPdo $pdo \bdk\Debug\Collector\Pdo instance
     */
    protected function __construct(DebugCollectorPdo $pdo)
    {
        $this->pdo = $pdo;
    }

    /**
     * Bind a column to a PHP variable
     *
     * @param mixed $column     Number of the column (1-indexed) or name of the column in the result set
     * @param mixed $param      Name of the PHP variable to which the column will be bound.
     * @param int   $type       [optional] Data type of the parameter, specified by the PDO::PARAM_* constants.
     * @param int   $maxLen     [optional] A hint for pre-allocation.
     * @param mixed $driverData [optional] Optional parameter(s) for the driver.
     *
     * @return bool
     * @link   http://php.net/manual/en/pdostatement.bindcolumn.php
     */
    #[\ReturnTypeWillChange]
    public function bindColumn($column, &$param, $type = null, $maxLen = null, $driverData = null)
    {
        $this->boundParameters[$column] = $param;
        $this->boundParameterTypes[$column] = $type;
        $args = \array_merge([$column, &$param], \array_slice(\func_get_args(), 2));
        return \call_user_func_array(['PDOStatement', 'bindColumn'], $args);
    }

    /**
     * Binds a parameter to the specified variable name
     *
     * @param mixed $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 int   $dataType      [optional] Explicit data type for the parameter using the PDO::PARAM_* constants.
     * @param int   $length        [optional] Length of the data type. To indicate that a parameter is an OUT
     *                               parameter from a stored procedure, you must explicitly set the length.
     * @param mixed $driverOptions [optional]
     *
     * @return bool
     * @link   http://php.net/manual/en/pdostatement.bindparam.php
     */
    #[\ReturnTypeWillChange]
    public function bindParam($parameter, &$variable, $dataType = PdoBase::PARAM_STR, $length = null, $driverOptions = null)
    {
        $this->boundParameters[$parameter] = $variable;
        $this->boundParameterTypes[$parameter] = $dataType;
        $args = \array_merge([$parameter, &$variable], \array_slice(\func_get_args(), 2));
        return \call_user_func_array(['PDOStatement', 'bindParam'], $args);
    }

    /**
     * Binds a value to a parameter
     *
     * @param mixed $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 $value     The value to bind to the parameter.
     * @param int   $dataType  [optional] Explicit data type for the parameter using the PDO::PARAM_* constants.
     *
     * @return bool
     * @link   http://php.net/manual/en/pdostatement.bindvalue.php
     */
    #[\ReturnTypeWillChange]
    public function bindValue($parameter, $value, $dataType = PdoBase::PARAM_STR)
    {
        $this->boundParameters[$parameter] = $value;
        $this->boundParameterTypes[$parameter] = $dataType;
        return \call_user_func_array(['PDOStatement', 'bindValue'], \func_get_args());
    }

    /**
     * Executes a prepared statement
     *
     * @param array $inputParameters [optional] An array of values with as many elements as there
     *   are bound parameters in the SQL statement being executed. All values are treated as
     *   PDO::PARAM_STR.
     *
     * @return bool
     * @link   http://php.net/manual/en/pdostatement.execute.php
     * @throws PDOException
     */
    #[\ReturnTypeWillChange]
    public function execute($inputParameters = null)
    {
        $info = new StatementInfo(
            $this->queryString,
            $this->mergeParams($inputParameters),
            $this->boundParameterTypes
        );
        $isExceptionMode = $this->pdo->getAttribute(PdoBase::ATTR_ERRMODE) === PdoBase::ERRMODE_EXCEPTION;

        $exception = null;
        $result = false;
        try {
            $result = parent::execute($inputParameters);
            if (!$isExceptionMode && $result === false) {
                $error = $this->errorInfo();
                $exception = new PDOException($error[2], (int) $error[0]);
            }
        } catch (PDOException $e) {
            $exception = $e;
        }

        $info->end($exception, $this->rowCount());
        $this->pdo->addStatementInfo($info);

        if ($isExceptionMode && $exception) {
            throw $exception;
        }
        return $result;
    }

    /**
     * Combine execute's inputParameters with already bound parameters for statementInfo
     *
     * @param array<string,mixed> $inputParameters parameters passed to execute
     *
     * @return array<string,mixed>
     */
    private function mergeParams($inputParameters)
    {
        return \is_array($inputParameters)
            ? \array_merge($this->boundParameters, $inputParameters)
            : $this->boundParameters;
    }
}
