//
// SessionHandle.cpp
//
// Library: Data/PostgreSQL
// Package: PostgreSQL
// Module:  SessionHandle
//
// Copyright (c) 2015, Applied Informatics Software Engineering GmbH.
// and Contributors.
//
// SPDX-License-Identifier:	BSL-1.0
//


#include "Poco/Data/PostgreSQL/SessionHandle.h"
#include "Poco/Data/PostgreSQL/PostgreSQLException.h"
#include "Poco/Data/PostgreSQL/PostgreSQLTypes.h"
#include "Poco/Data/Session.h"
#include "Poco/NumberFormatter.h"


#define POCO_POSTGRESQL_VERSION_NUMBER ((NDB_VERSION_MAJOR<<16) | (NDB_VERSION_MINOR<<8) | (NDB_VERSION_BUILD&0xFF))


namespace Poco {
namespace Data {
namespace PostgreSQL {


//const std::string SessionHandle::POSTGRESQL_READ_UNCOMMITTED = "READ UNCOMMITTED";
const std::string SessionHandle::POSTGRESQL_READ_COMMITTED  = "READ COMMITTED";
const std::string SessionHandle::POSTGRESQL_REPEATABLE_READ = "REPEATABLE READ";
const std::string SessionHandle::POSTGRESQL_SERIALIZABLE	= "SERIALIZABLE";


SessionHandle::SessionHandle():
	_pConnection(nullptr),
	_inTransaction(false),
	_isAsynchronousCommit(false),
	_tranactionIsolationLevel(Session::TRANSACTION_READ_COMMITTED)
{
}


SessionHandle::~SessionHandle()
{
	try
	{
		disconnect();
	}
	catch (...)
	{
	}
}


bool SessionHandle::isConnected() const
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	return isConnectedNoLock();
}


bool SessionHandle::isConnectedNoLock() const
{
	// DO NOT ACQUIRE THE MUTEX IN PRIVATE METHODS

	if (_pConnection && PQstatus(_pConnection) == CONNECTION_OK)
	{
		return true;
	}
	return false;
}


void SessionHandle::connect(const std::string& aConnectionString)
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	_inTransaction = false;
	_preparedStatementsToBeDeallocated.clear();

	if (isConnectedNoLock())
	{
		throw ConnectionFailedException("Already Connected");
	}
	else if (_pConnection)
	{
		// free bad connection
		PQfinish(_pConnection);
		_pConnection = nullptr;
	}

	_pConnection = PQconnectdb(aConnectionString.c_str());

	if (!isConnectedNoLock())
	{
		std::string msg = std::string("Connection Error: ") + lastErrorNoLock();
		if (_pConnection)
		{
			PQfinish(_pConnection);
			_pConnection = nullptr;
		}
		throw ConnectionFailedException(msg);
	}

	_connectionString = aConnectionString;
}


void SessionHandle::connect(const char* aConnectionString)
{
	connect(std::string(aConnectionString));
}


void SessionHandle::connect(const char* aHost, const char* aUser, const char* aPassword,
	const char* aDatabase, 	unsigned short aPort, unsigned int aConnectionTimeout)
{
	std::string connectionString;

	connectionString.append("host=");
	connectionString.append(aHost);
	connectionString.append(" ");

	connectionString.append("user=");
	connectionString.append(aUser);
	connectionString.append(" ");

	connectionString.append("password=");
	connectionString.append(aPassword);
	connectionString.append(" ");

	connectionString.append("dbname=");
	connectionString.append(aDatabase);
	connectionString.append(" ");

	connectionString.append("port=");
	Poco::NumberFormatter::append(connectionString, aPort);
	connectionString.append(" ");

	connectionString.append("connect_timeout=");
	Poco::NumberFormatter::append(connectionString, aConnectionTimeout);

	connect(connectionString);
}


void SessionHandle::disconnect()
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (_pConnection)
	{
		PQfinish(_pConnection);

		_pConnection = nullptr;

		_connectionString = std::string();
		_inTransaction= false;
		_isAsynchronousCommit = false;
		_tranactionIsolationLevel = Session::TRANSACTION_READ_COMMITTED;
	}
}


// TODO: Figure out what happens if a connection is reset with a pending transaction
bool SessionHandle::reset()
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (_pConnection)
	{
		PQreset(_pConnection);
	}

	if (isConnectedNoLock())
	{
		return true;
	}

	return false;
}


std::string SessionHandle::lastError() const
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		return std::string();
	}

	return lastErrorNoLock();
}


std::string SessionHandle::lastErrorNoLock() const
{
	// DO NOT ACQUIRE THE MUTEX IN PRIVATE METHODS
	std::string lastErrorString (nullptr != _pConnection ? PQerrorMessage(_pConnection) : "not connected");

	return lastErrorString;
}


void SessionHandle::startTransaction()
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	if (_inTransaction)
	{
		return; // NO-OP
	}

	PGresult* pPQResult = PQexec(_pConnection, "BEGIN");

	PQResultClear resultClearer(pPQResult);

	if (PQresultStatus(pPQResult) != PGRES_COMMAND_OK)
	{
		throw StatementException(std::string("BEGIN statement failed:: ") + lastErrorNoLock());
	}

	_inTransaction = true;
}


void SessionHandle::commit()
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	PGresult* pPQResult = PQexec(_pConnection, "COMMIT");

	PQResultClear resultClearer(pPQResult);

	if (PQresultStatus(pPQResult) != PGRES_COMMAND_OK)
	{
		throw StatementException(std::string("COMMIT statement failed:: ") + lastErrorNoLock());
	}

	_inTransaction = false;

	deallocateStoredPreparedStatements();
}


void SessionHandle::rollback()
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	PGresult* pPQResult = PQexec(_pConnection, "ROLLBACK");

	PQResultClear resultClearer(pPQResult);

	if (PQresultStatus(pPQResult) != PGRES_COMMAND_OK)
	{
		throw StatementException(std::string("ROLLBACK statement failed:: ") + lastErrorNoLock());
	}

	_inTransaction = false;

	deallocateStoredPreparedStatements();
}


void SessionHandle::autoCommit(bool val)
{
	// There is no PostgreSQL API call to switch autocommit (unchained) mode off.
	if (isTransaction())
	{
		throw Poco::InvalidAccessException();
	}
}


void SessionHandle::setAsynchronousCommit(bool aShouldAsynchronousCommit)
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	if (aShouldAsynchronousCommit == _isAsynchronousCommit)
	{
		return;
	}

	PGresult* pPQResult = PQexec(_pConnection, aShouldAsynchronousCommit ? "SET SYNCHRONOUS COMMIT TO OFF" : "SET SYNCHRONOUS COMMIT TO ON");

	PQResultClear resultClearer(pPQResult);

	if (PQresultStatus(pPQResult) != PGRES_COMMAND_OK)
	{
		throw StatementException(std::string("SET SYNCHRONUS COMMIT statement failed:: ") + lastErrorNoLock());
	}

	_isAsynchronousCommit = aShouldAsynchronousCommit;
}


void SessionHandle::cancel()
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	PGcancel* ptrPGCancel = PQgetCancel(_pConnection);

	PGCancelFree cancelFreer(ptrPGCancel);

	PQcancel(ptrPGCancel, nullptr, 0); // no error buffer
}


void SessionHandle::setTransactionIsolation(Poco::UInt32 aTI)
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	if (aTI == _tranactionIsolationLevel)
	{
		return;
	}

	if (!hasTransactionIsolation(aTI))
	{
		throw Poco::InvalidArgumentException("setTransactionIsolation()");
	}

	std::string isolationLevel;

	switch (aTI)
	{
	case Session::TRANSACTION_READ_COMMITTED:
		isolationLevel = POSTGRESQL_READ_COMMITTED; break;
	case Session::TRANSACTION_REPEATABLE_READ:
		isolationLevel = POSTGRESQL_REPEATABLE_READ; break;
	case Session::TRANSACTION_SERIALIZABLE:
		isolationLevel = POSTGRESQL_SERIALIZABLE; break;
	}

	PGresult* pPQResult = PQexec(_pConnection, Poco::format("SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL %s", isolationLevel).c_str());

	PQResultClear resultClearer(pPQResult);

	if (PQresultStatus(pPQResult) != PGRES_COMMAND_OK)
	{
		throw StatementException(std::string("set transaction isolation statement failed: ") + lastErrorNoLock());
	}

	_tranactionIsolationLevel = aTI;
}


Poco::UInt32 SessionHandle::transactionIsolation() const
{
	return _tranactionIsolationLevel;
}


bool SessionHandle::hasTransactionIsolation(Poco::UInt32 aTI)
{
	return Session::TRANSACTION_READ_COMMITTED	== aTI
		|| Session::TRANSACTION_REPEATABLE_READ	== aTI
		|| Session::TRANSACTION_SERIALIZABLE	== aTI;
}


void SessionHandle::deallocatePreparedStatement(const std::string& aPreparedStatementToDeAllocate)
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	if (!_inTransaction)
	{
		deallocatePreparedStatementNoLock(aPreparedStatementToDeAllocate);
	}
	else
	{
		try
		{
			_preparedStatementsToBeDeallocated.push_back(aPreparedStatementToDeAllocate);
		}
		catch (std::bad_alloc&)
		{
		}
	}
}


void SessionHandle::deallocatePreparedStatementNoLock(const std::string& aPreparedStatementToDeAllocate)
{
	PGresult* pPQResult = PQexec(_pConnection, (std::string("DEALLOCATE ") + aPreparedStatementToDeAllocate).c_str());

	PQResultClear resultClearer(pPQResult);

	if (PQresultStatus(pPQResult) != PGRES_COMMAND_OK)
	{
		throw StatementException(std::string("DEALLOCATE statement failed: ") + lastErrorNoLock());
	}
}


void SessionHandle::deallocateStoredPreparedStatements()
{
	// DO NOT ACQUIRE THE MUTEX IN PRIVATE METHODS
	while (!_preparedStatementsToBeDeallocated.empty())
	{
		deallocatePreparedStatementNoLock(_preparedStatementsToBeDeallocated.back());

		_preparedStatementsToBeDeallocated.pop_back();
	}
}


int SessionHandle::serverVersion() const
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	return PQserverVersion(_pConnection);
}


int SessionHandle::serverProcessID() const
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	return PQbackendPID(_pConnection);
}


int SessionHandle::protocoVersion() const
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	return PQprotocolVersion(_pConnection);
}


std::string SessionHandle::clientEncoding() const
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	return pg_encoding_to_char(PQclientEncoding(_pConnection));
}


std::string SessionHandle::parameterStatus(const std::string& param) const
{
	Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

	if (!isConnectedNoLock())
	{
		throw NotConnectedException();
	}

	const char* pValue = PQparameterStatus(_pConnection, param.c_str());
	if (pValue)
		return std::string(pValue);
	else
		return std::string();
}


int SessionHandle::libpqVersion() const
{
	return PQlibVersion();
}


SessionParametersMap SessionHandle::setConnectionInfoParameters(PQconninfoOption* pConnInfOpt)
{
	SessionParametersMap sessionParametersMap;

	while (pConnInfOpt->keyword)
	{
		try
		{
			std::string keyword						= pConnInfOpt->keyword  ? pConnInfOpt->keyword  : std::string();
			std::string environmentVariableVersion	= pConnInfOpt->envvar   ? pConnInfOpt->envvar   : std::string();
			std::string compiledVersion				= pConnInfOpt->compiled ? pConnInfOpt->compiled : std::string();
			std::string currentValue				= pConnInfOpt->val  ? pConnInfOpt->val  : std::string();
			std::string dialogLabel					= pConnInfOpt->label? pConnInfOpt->label: std::string();
			std::string dialogDisplayCharacter		= pConnInfOpt->dispchar ? pConnInfOpt->dispchar : std::string();
			int dialogDisplaysize					= pConnInfOpt->dispsize;

			SessionParameters connParams(keyword, environmentVariableVersion, compiledVersion,
				currentValue, dialogLabel, dialogDisplayCharacter, dialogDisplaysize);

			sessionParametersMap.insert(SessionParametersMap::value_type(connParams.keyword(), connParams));
		}
		catch (std::bad_alloc&)
		{
		}

		++pConnInfOpt;
	}

	return sessionParametersMap;
}


SessionParametersMap SessionHandle::connectionDefaultParameters()
{
	PQconninfoOption* ptrConnInfoOptions = PQconndefaults();

	PQConnectionInfoOptionsFree connectionOptionsFreeer(ptrConnInfoOptions);

	return setConnectionInfoParameters(ptrConnInfoOptions);
}


SessionParametersMap SessionHandle::connectionParameters() const
{
	if (!isConnected())
	{
		throw NotConnectedException();
	}

	PQconninfoOption* ptrConnInfoOptions = nullptr;
	{
		Poco::FastMutex::ScopedLock mutexLocker(_sessionMutex);

		ptrConnInfoOptions = PQconninfo(_pConnection);
	}

	PQConnectionInfoOptionsFree connectionOptionsFreeer(ptrConnInfoOptions);

	return setConnectionInfoParameters(ptrConnInfoOptions);
}


} } } // Poco::Data::PostgreSQL
