<?php
namespace Zeedhi\Framework\DBAL\Driver\OCI8;

use Doctrine\DBAL\Driver\OCI8\Exception\Error;
use Doctrine\DBAL\Driver\OCI8\Exception\UnknownParameterIndex;
use Doctrine\DBAL\Driver\OCI8\ExecutionMode;
use Doctrine\DBAL\Driver\OCI8\Result;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;
use Zeedhi\Framework\DependencyInjection\InstanceManager;
//use Doctrine\DBAL\Driver\Result as ResultInterface;
//use Doctrine\DBAL\Driver\Statement as StatementInterface;
//use Doctrine\DBAL\ParameterType;
//use Doctrine\Deprecations\Deprecation;

use function func_num_args;
use function is_int;
use function oci_bind_by_name;
use function oci_execute;
use function oci_new_descriptor;

use const OCI_B_BIN;
use const OCI_B_BLOB;
use const OCI_COMMIT_ON_SUCCESS;
use const OCI_D_LOB;
use const OCI_NO_AUTO_COMMIT;
use const OCI_TEMP_BLOB;
use const SQLT_CHR;

class OCI8Statement  implements StatementInterface {

    /** @var resource */
    private $connection;

    /** @var resource */
    private $statement;

    /** @var array<int,string> */
    private array $parameterMap;

    private ExecutionMode $executionMode;

    /**
     * @internal The statement can be only instantiated by its driver connection.
     *
     * @param resource          $connection
     * @param resource          $statement
     * @param array<int,string> $parameterMap
     */
    public function __construct($connection, $statement, array $parameterMap, ExecutionMode $executionMode)
    {
        $this->connection    = $connection;
        $this->statement     = $statement;
        $this->parameterMap  = $parameterMap;
        $this->executionMode = $executionMode;
    }

    /**
     * {@inheritDoc}
     */
    public function bindValue($param, $value, $type = ParameterType::STRING): bool
    {
        if (func_num_args() < 3) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/pull/5558',
                'Not passing $type to Statement::bindValue() is deprecated.'
                . ' Pass the type corresponding to the parameter being bound.',
            );
        }

        return $this->bindParam($param, $value, $type);
    }

//    /**
//     * {@inheritDoc}
//     *
//     * @deprecated Use {@see bindValue()} instead.
//     */
//    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool
//    {
//        Deprecation::trigger(
//            'doctrine/dbal',
//            'https://github.com/doctrine/dbal/pull/5563',
//            '%s is deprecated. Use bindValue() instead.',
//            __METHOD__,
//        );
//
//        if (func_num_args() < 3) {
//            Deprecation::trigger(
//                'doctrine/dbal',
//                'https://github.com/doctrine/dbal/pull/5558',
//                'Not passing $type to Statement::bindParam() is deprecated.'
//                . ' Pass the type corresponding to the parameter being bound.',
//            );
//        }
//
//        if (is_int($param)) {
//            if (! isset($this->parameterMap[$param])) {
//                throw UnknownParameterIndex::new($param);
//            }
//
//            $param = $this->parameterMap[$param];
//        }
//
//        if ($type === ParameterType::LARGE_OBJECT) {
//            if ($variable !== null) {
//                $lob = oci_new_descriptor($this->connection, OCI_D_LOB);
//                $lob->writeTemporary($variable, OCI_TEMP_BLOB);
//
//                $variable =& $lob;
//            } else {
//                $type = ParameterType::STRING;
//            }
//        }
//
//        return oci_bind_by_name(
//            $this->statement,
//            $param,
//            $variable,
//            $length ?? -1,
//            $this->convertParameterType($type),
//        );
//    }

    /**
     * Converts DBAL parameter type to oci8 parameter type
     */
    private function convertParameterType(int $type): int
    {
        switch ($type) {
            case ParameterType::BINARY:
                return OCI_B_BIN;

            case ParameterType::LARGE_OBJECT:
                return OCI_B_BLOB;

            default:
                return SQLT_CHR;
        }
    }

    /**
     * {@inheritDoc}
     */
    public function execute($params = null): ResultInterface
    {
        if ($params !== null) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/pull/5556',
                'Passing $params to Statement::execute() is deprecated. Bind parameters using'
                . ' Statement::bindParam() or Statement::bindValue() instead.',
            );

            foreach ($params as $key => $val) {
                if (is_int($key)) {
                    $this->bindValue($key + 1, $val, ParameterType::STRING);
                } else {
                    $this->bindValue($key, $val, ParameterType::STRING);
                }
            }
        }

        if ($this->executionMode->isAutoCommitEnabled()) {
            $mode = OCI_COMMIT_ON_SUCCESS;
        } else {
            $mode = OCI_NO_AUTO_COMMIT;
        }

        $ret = @oci_execute($this->statement, $mode);
        if (! $ret) {
            throw Error::new($this->statement);
        }

        return new \Zeedhi\Framework\DBAL\Driver\OCI8\OCI8Result($this->statement);
    }

// customizção zeedhi
    protected $fieldTypes = null;
    protected $fieldNames = null;
    protected $fieldIdByName = null;
    protected $thousandsSeparator = ',';
    protected $decimalSeparator = '.';
    protected $setSeparator = true;

    protected function loadResultSetMetaData() {
        if ($this->fieldTypes === null) {
            $this->fieldNames = $this->fieldIdByName = $this->fieldTypes = array();
            $ociFieldNum = oci_num_fields($this->statement);
            for ($i = 1; $i <= $ociFieldNum; $i++) {
                $fieldName = oci_field_name($this->statement, $i);
                $this->fieldTypes[$i] = oci_field_type($this->statement, $i);
                $this->fieldNames[$i] = $fieldName;
                $this->fieldIdByName[$fieldName] = $i;
            }
        }
    }

    public function normalizeFloatString($value) {
        // Remove thousands delimiter char and replace decimal dots.
        return str_replace($this->decimalSeparator, ".", str_replace($this->thousandsSeparator, "", $value));
    }

    protected function normalizeValueByType($value, $type) {
        if ($type === 'NUMBER' && $value !== null) {
            $value = floatval($this->normalizeFloatString($value));
        }

        return $value;
    }

    protected function setnormalizeFloat() {
        // This is know due to OracleSessionInit.
        try {
            if ($this->setSeparator) {
                /** @var OracleSessionInit $sessionInit */
                $sessionInit = InstanceManager::getInstance()->getService('crudSessionInit');
                $defaultSessionVars = $sessionInit->getDefaultSessionVars();
                $nlsNumericCharacters = $defaultSessionVars['NLS_NUMERIC_CHARACTERS'];
                $this->decimalSeparator = $nlsNumericCharacters[0];
                $this->thousandsSeparator = $nlsNumericCharacters[1];
                $this->setSeparator = false;
            }
        }catch (\Exception $e){
            $this->decimalSeparator = '.';
            $this->thousandsSeparator = ',';
        }
    }

    protected function inferTypes($result, $fetchMode) {
        $this->setnormalizeFloat();
        $this->loadResultSetMetaData();
        switch ($fetchMode ?: $this->_defaultFetchMode) {
            case \PDO::FETCH_BOTH:
                foreach($result as $key => $value) {
                    $fieldId = !is_numeric($key) ? $this->fieldIdByName[$key] : $key+1;
                    $result[$key] = $this->normalizeValueByType($value, $this->fieldTypes[$fieldId]);
                }
                break;
            case \PDO::FETCH_ASSOC:
                foreach($result as $key => $value) {
                    $fieldId = $this->fieldIdByName[$key];
                    $result[$key] = $this->normalizeValueByType($value, $this->fieldTypes[$fieldId]);
                }
                break;
            case \PDO::FETCH_NUM:
            case \PDO::FETCH_COLUMN:

                if(!is_array($result)){
                    return [$result];
                }

                foreach($result as $key => $value) {
                    $result[$key] = $this->normalizeValueByType($value, $this->fieldTypes[$key+1]);
                }
                break;
        }

        return $result;
    }

//    public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0) {
//        $result = parent::fetch($fetchMode, $cursorOrientation, $cursorOffset);
//        return $result ? $this->inferTypes($result, $fetchMode) : $result;
//    }

    protected function normalizeFetchAllResultSet($fetchMode, $rows) {
        $fetchMode = $fetchMode ?: $this->_defaultFetchMode;
        //The OCI_BOTH mode is normalized by fetch method, even if called by fetch all.
        if (self::$fetchModeMap[$fetchMode] !== OCI_BOTH) {
            foreach ($rows as &$row) {
                $row = $this->inferTypes($row, $fetchMode);
            }
        }

        return $rows;
    }

//    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) {
//        $rows = parent::fetchAll($fetchMode, $fetchArgument, $ctorArgs);
//        return $this->normalizeFetchAllResultSet($fetchMode, $rows);
//    }

    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null) {
        $column = isset($this->parameterMap[$column]) ? $this->parameterMap[$column] : $column;

        if ($type == \PDO::PARAM_LOB) {
            $lob = oci_new_descriptor($this->statement, OCI_D_LOB);
            $lob->writeTemporary($variable, OCI_TEMP_BLOB);

            return oci_bind_by_name($this->statement, $column, $lob, -1, OCI_B_BLOB);
        } elseif ($type === SQLT_LBI){
            return oci_bind_by_name($this->statement, $column, $variable, strlen($variable), $type);
        } elseif ($length !== null) {
            return oci_bind_by_name($this->statement, $column, $variable, $length);
        }

        return oci_bind_by_name($this->statement, $column, $variable);
    }
}
