<?php

namespace Velis\Db;

use ArrayObject;
use Velis\App;

/**
 * Snowflake database adapter based on ODBC module functions
 *
 * @package Velis\Db
 * @author Olek Procki <olo@velis.pl>
 */
class SnowflakeOdbc extends Snowflake
{
    /**
     * Connection handle
     * @var resource|false
     */
    private $_connection;

    /**
     * {@inheritDoc}
     */
    public function connect(array &$profilerInfo = null): self
    {
        if ($this->_debug && $profilerInfo === null) {
            $profilerInfo = [];
            $profilerInfo['query']     = 'CONNECT';
            $profilerInfo['caller']    = $this->_getCaller();
            $profilerInfo['func']      = __FUNCTION__;
            $profilerInfo['startTime'] = $this->getCurrentLifeTime();
            $profilerInfo['server']    = $this->_currentHost;

            $innerCall = false;
        } else {
            $innerCall = true;
        }

        if (!$this->_connectionParams) {
            $this->_connectionParams = App::$config->snowflake;
        }

        $dsn = "Driver="      . App::$config->snowflake->driver;
        $dsn .= ";Server="    . App::$config->snowflake->server;
        $dsn .= ";Account="   . App::$config->snowflake->account;
        $dsn .= ";Port="      . App::$config->snowflake->port;
        $dsn .= ";Schema="    . App::$config->snowflake->schema;
        $dsn .= ";Warehouse=" . App::$config->snowflake->warehouse;
        $dsn .= ";Database="  . App::$config->snowflake->database;


        $this->_connection = odbc_connect(
            $dsn,
            App::$config->snowflake->user,
            App::$config->snowflake->password
        );
        if (!$this->_connection) {
            throw new Exception(odbc_errormsg(), odbc_error(), $this->_currentHost);
        }

        $this->_currentHost = App::$config->snowflake->database . '/' . App::$config->snowflake->warehouse;

        if (App::$config->hasTimezoneSupport() && App::$user && App::$user->getTimezone()) {
            odbc_exec($this->_connection, "ALTER SESSION SET TIMEZONE='" . App::$user->getTimezone() . "';");
        }

        if ($this->_debug) {
            $profilerInfo['info'] = "Connected to "
                . $this->_connectionParams->database
                . "@" . $this->_connectionParams->server;
            if (!$innerCall && $this->shouldLogToProfiler()) {
                $this->_profiler->addRow($profilerInfo);
            }
        }

        return $this;
    }

    public function disconnect(): self
    {
        $this->_connection = null;
        odbc_close($this->_connection);

        return $this;
    }

    public function isConnected(): bool
    {
        return $this->_connection != null;
    }

    /**
     * {@inheritDoc}
     * @return resource|int
     */
    protected function _exec(string $sql, $bindParams = null, array &$profilerInfo = null)
    {
        if ($this->_debug) {
            $profilerInfo['query']  = $sql;
            $profilerInfo['params'] = $bindParams;
            $profilerInfo['server'] = $this->_currentHost;
        }

        $this->checkConnection($profilerInfo);

        $params = $bindParams instanceof ArrayObject ? $bindParams->getArrayCopy() : $bindParams;

        if ($this->_debug) {
            $profilerInfo['params'] = $params;
        }

        $this->_lastExecutionTime = 0;
        $startTime = microtime(true);

        if (count($params)) {
            $this->translateParams($sql, $params);
            $stmt = odbc_prepare($this->_connection, $sql);
            if ($stmt === false) {
                throw new Exception(odbc_errormsg(), odbc_error(), $this->_currentHost, $sql, $params);
            }

            if ($this->_duplicatedQueryCheck) {
                $this->_registerQuery($sql, $params);
            }

            if (!odbc_execute($stmt, $params)) {
                throw new Exception(odbc_errormsg(), odbc_error(), $this->_currentHost, $sql, $params);
            }

            $this->_affectedRows = odbc_num_rows($stmt);
        } else {
            $stmt = $this->_query($sql);

            return odbc_num_rows($stmt);
        }

        $this->_lastExecutionTime = microtime(true) - $startTime;

        if ($this->_debug) {
            $profilerInfo['execTime'] = $this->_lastExecutionTime;
        }

        return $stmt;
    }

    /**
     * Runs queries (without binding params)
     *
     * @param string $sql
     * @param array<string, mixed> $profilerInfo
     * @return resource
     * @throws Exception
     */
    protected function _query(string $sql, array &$profilerInfo = null)
    {
        if ($this->_debug) {
            $profilerInfo['query'] = $sql;
        }

        $this->_lastExecutionTime = 0;
        $startTime = microtime(true);

        $stmt = odbc_exec($this->_connection, $sql);

        if ($stmt === false) {
            throw new Exception(odbc_errormsg(), odbc_error(), $this->_currentHost, $sql, null);
        }

        if ($this->_duplicatedQueryCheck) {
            $this->_registerQuery($sql);
        }

        $this->_lastExecutionTime = microtime(true) - $startTime;

        if ($this->_debug) {
            $profilerInfo['execTime'] = $this->_lastExecutionTime;
        }

        return $stmt;
    }

    /**
     * {@inheritDoc}
     */
    public function getAll($sql, $bindParams = null, $assocKey = null): array
    {
        if ($this->_debug) {
            $profilerInfo = [
                'caller' => $this->_getCaller(),
                'startTime' => $this->getCurrentLifeTime(),
                'func' => __FUNCTION__,
                'server' => $this->_currentHost,
            ];
        }
        $fetchStartTime = microtime(true);

        $stmt = $this->get($sql, $bindParams, $profilerInfo);
        $result = [];

        while ($row = odbc_fetch_array($stmt)) {
            if ($assocKey != null) {
                $result[$row[$assocKey]] = $row;
            } else {
                $result[] = $row;
            }
        }

        if ($this->shouldLogToProfiler()) {
            $profilerInfo['fetchTime'] = microtime(true) - $fetchStartTime;
            $this->_profiler->addRow($profilerInfo);
        }

        return $result;
    }

    /**
     * {@inheritDoc}
     */
    public function getRow(string $sql, $bindParams = null): array|false
    {
        if ($this->_debug) {
            $profilerInfo = [
                'caller'    => $this->_getCaller(),
                'startTime' => $this->getCurrentLifeTime(),
                'func'      => __FUNCTION__,
                'server'    => $this->_currentHost
            ];
        }

        $fetchStartTime = microtime(true);

        $result = odbc_fetch_array(
            $this->get($sql, $bindParams, $profilerInfo)
        );

        if ($this->shouldLogToProfiler()) {
            $profilerInfo['fetchTime'] = microtime(true) - $fetchStartTime;
            $this->_profiler->addRow($profilerInfo);
        }

        return $result;
    }

    /**
     * {@inheritDoc}
     */
    public function startTrans(&$profilerInfo = null): bool
    {
        if ($this->_debug) {
            $profilerInfo = [];
            $profilerInfo['query']     = 'BEGIN TRANSACTION';
            $profilerInfo['caller']    = $this->_getCaller();
            $profilerInfo['func']      = __FUNCTION__;
            $profilerInfo['startTime'] = $this->getCurrentLifeTime();
            $profilerInfo['server']    = $this->_currentHost;
        }

        $this->checkConnection($profilerInfo);

        if (!$this->_transactionStarted) {
            $this->_transactionStarted = true;
            odbc_autocommit($this->_connection, false);
            if ($this->shouldLogToProfiler()) {
                $this->_profiler->addRow($profilerInfo);
            }
            return true;
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public function commit(): self
    {
        if ($this->_transactionStarted) {
            try {
                if (count($this->_transactionTriggers)) {
                    $triggers = $this->_transactionTriggers;
                    $this->_transactionTriggers = [];

                    foreach ($triggers as $callback) {
                        call_user_func($callback);
                    }
                }

                odbc_commit($this->_connection);
                odbc_autocommit($this->_connection, true);
            } catch (\Exception $e) {
                odbc_rollback($this->_connection);
                odbc_autocommit($this->_connection, true);
                throw $e;
            }
            $this->_transactionStarted = false;
        }

        return $this;
    }


    /**
     * {@inheritDoc}
     */
    public function rollback(): self
    {
        if ($this->_transactionStarted) {
            odbc_rollback($this->_connection);
            odbc_autocommit($this->_connection, true);

            $this->_transactionTriggers = [];
            $this->_transactionStarted  = false;
        }

        return $this;
    }
}
