/***************************************************************************
                          SIMPLY POWERFUL TOOLKIT (SPTK)
                          CODBCDatabase.cpp  -  description
                             -------------------
    begin                : Fri Oct 03 2003
    copyright            : (C) 2003-2006 by Alexey Parshin
    email                : alexeyp@gmail.com
 ***************************************************************************/

/***************************************************************************
   This library is free software; you can redistribute it and/or modify it 
   under the terms of the GNU Library General Public License as published by
   the Free Software Foundation; either version 2 of the License, or (at 
   your option) any later version.

   This library is distributed in the hope that it will be useful, but 
   WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library
   General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; if not, write to the Free Software Foundation, 
   Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.

   Please report all bugs and problems to "alexeyp@gmail.com"
 ***************************************************************************/

#include <sptk3/CODBC.h>
#include <sptk3/CODBCDatabase.h>
#include <sptk3/CQuery.h>
#include <sptk3/CParams.h>
#include <sptk3/CDatabaseField.h>
#include <string>
#include <stdio.h>

using namespace std;
using namespace sptk;

namespace sptk {

	class CODBCField : public CDatabaseField {
		friend class CODBCDatabase;
	protected:
		char *getData() { return (char *) &m_data; }
	public:
		CODBCField(const std::string fieldName,int fieldColumn,int fieldType,CVariantType dataType,int fieldLength,int fieldScale)
		: CDatabaseField(fieldName,fieldColumn,fieldType,dataType,fieldLength,fieldScale) {}
	};

}

CODBCDatabase::CODBCDatabase(string connectionString)
: CDatabase(connectionString) {
	m_connect = new CODBCConnection;
}

CODBCDatabase::~CODBCDatabase() {
	try {
		if (m_inTransaction && active())
			rollbackTransaction();
		close();
		while (m_queryList.size()) {
			try {
				CQuery *query = (CQuery *)m_queryList[0];
				query->disconnect();
			}
			catch (...) {
			}
		}
		m_queryList.clear();
		delete m_connect;
	}
	catch (...) {
	}
}

void CODBCDatabase::openDatabase(const string newConnectionString) throw (CException) {
	if (!active()) {
		m_inTransaction = false;
		if (newConnectionString.length())
			m_connString = newConnectionString;

		string finalConnectionString;
		m_connect->connect(m_connString,finalConnectionString,false);
	}
}

void CODBCDatabase::closeDatabase() throw (CException) {
	for (unsigned i = 0; i < m_queryList.size(); i++) {
		try {
			queryFreeStmt(m_queryList[i]);
		}
		catch (...) {}
	}
	m_connect->freeConnect();
}

unsigned long CODBCDatabase::handle() const {
	return (unsigned long)m_connect->handle();
}

bool CODBCDatabase::active() const {
	return m_connect->isConnected();
}

void CODBCDatabase::driverBeginTransaction() throw(CException) {
	if (!m_connect->isConnected())
		open();

	if (m_inTransaction) 
		logAndThrow("CODBCDatabase::driverBeginTransaction","Transaction already started.");

	m_connect->beginTransaction();
	m_inTransaction = true;
}

void CODBCDatabase::driverEndTransaction(bool commit) throw(CException) {
	if (!m_inTransaction) {
		if (commit)
			logAndThrow("CODBCDatabase::driverEndTransaction","Can't commit - transaction isn't started.");
		else
			logAndThrow("CODBCDatabase::driverEndTransaction","Can't rollback - transaction isn't started.");
	}

	if (commit)
		m_connect->commit();
	else m_connect->rollback();

	m_connect->setConnectOption(SQL_AUTOCOMMIT,SQL_AUTOCOMMIT_ON);   
	m_inTransaction = false;
}

//-----------------------------------------------------------------------------------------------
static inline BOOL successful(int ret) {
	return ret==SQL_SUCCESS || ret==SQL_SUCCESS_WITH_INFO;
}

string CODBCDatabase::connectString() const { 
	return m_connect->connectString(); 
}

string CODBCDatabase::queryError(const CQuery *query) const {
	SQLCHAR errorDescription[SQL_MAX_MESSAGE_LENGTH];
	SQLCHAR errorState[SQL_MAX_MESSAGE_LENGTH];

	SWORD       pcnmsg = 0;
	SQLINTEGER  nativeError = 0;
	*errorDescription = 0;
	*errorState = 0;

	int rc = SQLError(SQL_NULL_HENV,(SQLHDBC)handle(),query->statement(),errorState,&nativeError,errorDescription,sizeof(errorDescription),&pcnmsg);
	if (rc != SQL_SUCCESS) {
		rc = SQLError(SQL_NULL_HENV,(SQLHDBC)handle(),0L,errorState,&nativeError,errorDescription,sizeof(errorDescription),&pcnmsg);
		if (rc != SQL_SUCCESS)
			if (!*errorDescription) strcpy((char *)errorDescription,"Unknown error");
	}

	return string(removeDriverIdentification((char *)errorDescription));
}

void CODBCDatabase::queryAllocStmt(CQuery *query) {
	CODBCLock lock(m_connect);

	SQLHSTMT stmt = (SQLHSTMT)query->statement();
	if (stmt != SQL_NULL_HSTMT)
		SQLFreeStmt(stmt,SQL_DROP);

	SQLHDBC hdb = (SQLHDBC)handle();
	int rc = SQLAllocStmt(hdb,&stmt);

	if (rc != SQL_SUCCESS) {
		string error = queryError(query);
		querySetStmt(query,SQL_NULL_HSTMT);
		logAndThrow("CODBCDatabase::queryAllocStmt","Can't allocate statement. ");
	}

	querySetStmt(query,stmt);
}

void CODBCDatabase::queryFreeStmt(CQuery *query) {
	CODBCLock lock(m_connect);

	SQLFreeStmt(query->statement(),SQL_DROP);
	querySetStmt(query,SQL_NULL_HSTMT);
	querySetPrepared(query,false);
}

void CODBCDatabase::queryCloseStmt(CQuery *query) {
	CODBCLock lock(m_connect);

	SQLFreeStmt(query->statement(),SQL_CLOSE);
}

void CODBCDatabase::queryPrepare(CQuery *query) {
	CODBCLock lock(m_connect);

	if (!successful(SQLPrepare(query->statement(),(SQLCHAR *)query->sql().c_str(),SQL_NTS))) 
		query->logAndThrow("CODBCDatabase::queryPrepare",queryError(query));
}

void CODBCDatabase::queryUnprepare(CQuery *query) {
	queryFreeStmt(query);
}

void CODBCDatabase::queryExecute(CQuery *query) {
	CODBCLock lock(m_connect);

	int rc = 0;
	if (query->prepared())
		rc = SQLExecute(query->statement());
	else  rc = SQLExecDirect(query->statement(),(SQLCHAR *)query->sql().c_str(),SQL_NTS);

	if (rc != SQL_SUCCESS) {
		SQLCHAR     state[16];
		SQLINTEGER  nativeError = 0;
		SQLCHAR     text[256];
		SQLSMALLINT textLength = 0;
		int         recno = 1;  // diag record number
		for (;;) {
			int rcd = SQLGetDiagRec(
					SQL_HANDLE_STMT,query->statement(),
					recno,state,&nativeError,
					text,sizeof(text),&textLength);
			if (rcd != SQL_SUCCESS) break;
			recno++;
			query->messages().push_back(removeDriverIdentification((const char *)text));
		}
	}

	if (!successful(rc))
		query->logAndThrow("CODBCDatabase::queryExecute",queryError(query));
}

int CODBCDatabase::queryColCount(CQuery *query) {
	CODBCLock lock(m_connect);

	short count = 0;
	if (!successful(SQLNumResultCols(query->statement(),&count)))
		query->logAndThrow("CODBCDatabase::queryColCount",queryError(query));

	return count;
}

void CODBCDatabase::queryColAttributes(CQuery *query,short column,short descType,long& value) {
	CODBCLock lock(m_connect);

	if (!successful(SQLColAttributes(query->statement(),column,descType,0,0,0,(SQLINTEGER *)&value)))
		query->logAndThrow("CODBCDatabase::queryColAttributes",queryError(query));
}

void CODBCDatabase::queryColAttributes(CQuery *query,short column,short descType,LPSTR buff,int len) {
	short available;
	if (!buff || len <= 0)
		query->logAndThrow("CODBCDatabase::queryColAttributes","Invalid buffer or buffer len");

	CODBCLock lock(m_connect);

	if (!successful(SQLColAttributes(query->statement(),(short)column,descType,buff,(short)len,&available,0)))
		query->logAndThrow("CODBCDatabase::queryColAttributes",queryError(query));
}

void CODBCDatabase::queryBindParameters(CQuery *query) {
	CODBCLock lock(m_connect);

	static SQLINTEGER cbNullValue = SQL_NULL_DATA;

	int         rc;

	for (unsigned i = 0; i < query->paramCount(); i++) {
		CParam *param = &query->param(i);
		CVariantType ptype = param->dataType();
		SQLINTEGER& cblen = param->callbackLength();
		for (unsigned j = 0; j < param->bindCount(); j++) {

			short paramType = 0, sqlType = 0, scale = 0;
			void  *buff = param->dataBuffer();
			long  len = 0;
			short paramNumber = short(param->bindIndex(j) + 1);

			short parameterMode = SQL_PARAM_INPUT;
			switch (ptype) {
				case VAR_INT:
					paramType = SQL_C_SLONG;
					sqlType   = SQL_INTEGER;
					break;
				case VAR_FLOAT:
					paramType = SQL_C_DOUBLE;
					sqlType   = SQL_DOUBLE;
					break;
				case VAR_STRING:
					buff      = (void *)param->getString();
					len       = param->dataSize();
					paramType = SQL_C_CHAR;
					sqlType   = SQL_CHAR;
					break;
				case VAR_TEXT:
					buff      = (void *)param->getString();
					len       = param->dataSize();
					paramType = SQL_C_CHAR;
					sqlType   = SQL_LONGVARCHAR;
					break;
				case VAR_BUFFER: 
					{
						buff      = (void *)param->getString();
						len       = param->dataSize();
						paramType = SQL_C_BINARY;
						sqlType   = SQL_LONGVARBINARY;
						cblen = len;
						rc = SQLBindParameter((SQLHSTMT)query->statement(),paramNumber,parameterMode,SQL_C_BINARY,SQL_LONGVARBINARY,len,scale,buff,SQLINTEGER(len),&cblen);
						continue;
					}
					break;
				case VAR_DATE:
					{
						paramType = SQL_C_TIMESTAMP;
						sqlType   = SQL_TIMESTAMP;
						len = sizeof(TIMESTAMP_STRUCT);
						TIMESTAMP_STRUCT *t = (TIMESTAMP_STRUCT *)param->conversionBuffer();
						CDateTime dt = param->getDateTime();
						buff = t;
						if (dt) {
							dt.decodeDate((short *)&t->year,(short *)&t->month,(short *)&t->day);
							t->hour = t->minute = t->second = 0;
							t->fraction = 0;
						} else {
							paramType = SQL_C_CHAR;
							sqlType   = SQL_CHAR;
							*(char *)buff = 0;
						}
					}
					break;
				case VAR_DATE_TIME:
					{
						paramType = SQL_C_TIMESTAMP;
						sqlType   = SQL_TIMESTAMP;
						len = sizeof(TIMESTAMP_STRUCT);
						TIMESTAMP_STRUCT *t = (TIMESTAMP_STRUCT *)param->conversionBuffer();
						CDateTime dt = param->getDateTime();
						short ms;
						buff = t;
						if (dt) {
							dt.decodeDate((short *)&t->year,(short *)&t->month,(short *)&t->day);
							dt.decodeTime((short *)&t->hour,(short *)&t->minute,(short *)&t->second,&ms);
							t->fraction = 0;
						} else {
							paramType = SQL_C_CHAR;
							sqlType   = SQL_CHAR;
							*(char *)buff = 0;
						}
					}
					break;
				default:
					query->logAndThrow("CODBCDatabase::queryBindParameters","Unknown type of parameter "+int2string(paramNumber));
			}
			if (param->isNull()) {
				len = 0;
				scale = 0;
				rc = SQLBindParameter((SQLHSTMT)query->statement(),paramNumber,parameterMode,paramType,sqlType,len,scale,buff,SQLINTEGER(len),&cbNullValue);
			}
			else rc = SQLBindParameter((SQLHSTMT)query->statement(),paramNumber,parameterMode,paramType,sqlType,len,scale,buff,SQLINTEGER(len),NULL);
			if (rc != 0)
				query->logAndThrow("CODBCDatabase::queryBindParameters","Can't bind parameter "+int2string(paramNumber));
		}
	}
}

void CODBCDatabase::ODBCtypeToCType(int odbcType,long &cType,CVariantType& dataType) {
	switch(odbcType) {
		case SQL_BIGINT:
		case SQL_TINYINT:
		case SQL_SMALLINT:
		case SQL_INTEGER:
			cType = SQL_C_SLONG;
			dataType = VAR_INT;
			return;

		case SQL_NUMERIC:
		case SQL_REAL:
		case SQL_DECIMAL:
		case SQL_DOUBLE:
		case SQL_FLOAT:
			cType = SQL_C_DOUBLE;
			dataType = VAR_FLOAT;
			return;

		case SQL_LONGVARCHAR:
		case SQL_VARCHAR:
		case SQL_CHAR:
			cType = SQL_C_CHAR;
			dataType = VAR_STRING;
			return;

		case SQL_DATE:       // ODBC 2.0 only
		case SQL_TYPE_DATE:  // ODBC 3.0 only
			cType = SQL_C_TIMESTAMP;
			dataType = VAR_DATE;
			return;

		case SQL_TIME:
		case SQL_TIMESTAMP:
		case SQL_TYPE_TIME:
		case SQL_TYPE_TIMESTAMP:
			if (odbcType == SQL_TYPE_DATE) {
				cType = SQL_C_TIMESTAMP;
				dataType = VAR_DATE;
			} else {
				cType = SQL_C_TIMESTAMP;
				dataType = VAR_DATE_TIME;
			}
			return;

		case SQL_BINARY:
		case SQL_LONGVARBINARY:
		case SQL_VARBINARY:
			cType = SQL_C_BINARY;
			dataType = VAR_BUFFER;
			return;


		case SQL_BIT:        
			cType = SQL_C_BIT;
			dataType = VAR_STRING;
			return;
	}
	cType = 0;
	dataType = VAR_NONE;
}

void CODBCDatabase::queryOpen(CQuery *query) {
	if (!active()) open();

	if (query->active()) return;

	if (active() && !query->statement())
		queryAllocStmt(query);

	try {
		queryBindParameters(query);
	}
	catch (exception& e) {
		string error = queryError(query);
		query->logAndThrow("CODBCDatabase::queryOpen",string(e.what())+ "\n" + error);
	}

	if (query->autoPrepare() && !query->prepared()) {
		queryPrepare(query);
		querySetPrepared(query,true);
	}

	queryExecute(query);

	short count = queryColCount(query);

	query->fields().clear();

	if (count < 1) {
		queryCloseStmt(query);
		return;
	} else {
		querySetActive(query,true);

      // Reading the column attributes
		char  columnName[256];
		long  columnType;
		long  columnLength;
		long  columnScale;
		long  cType;
		CVariantType dataType;
		for (short column = 1; column <= count; column++) {
			queryColAttributes(query,column,SQL_COLUMN_NAME,columnName,255);
			queryColAttributes(query,column,SQL_COLUMN_TYPE,columnType);
			queryColAttributes(query,column,SQL_COLUMN_LENGTH,columnLength);
			queryColAttributes(query,column,SQL_COLUMN_SCALE,columnScale);
			ODBCtypeToCType(columnType,cType,dataType);
			if (dataType == VAR_STRING && columnLength > 65535)
				dataType = VAR_TEXT;
			if (columnName[0] == 0)
				sprintf(columnName,"column%02i",column);
			if (columnLength > FETCH_BUFFER_SIZE)
				columnLength = FETCH_BUFFER_SIZE;

			if (dataType == VAR_FLOAT && (columnScale < 0 || columnScale > 20))
				columnScale = 0;

			CField *field = new CODBCField(
					columnName,
					column,
					cType,
					dataType,
					(int)columnLength,
					(int)columnScale);
			query->fields().push_back(field);
		}
	}
	querySetEof(query,false);
	queryFetch(query);
}

static unsigned trimField(char *s,int sz) {
	register char *p = s + sz;
	char ch = s[0];
	s[0] = '!';
	while ( *(--p) == ' ' ) {
	}
	*(++p) = 0;
	if (ch == ' ' && s[1] == 0) {
		s[0] = 0;
		return 0;
	}
	s[0] = ch;
	return p - s;
}

void CODBCDatabase::queryFetch(CQuery *query) {
	if (!query->active())
		query->logAndThrow("CODBCDatabase::queryFetch","Dataset isn't open");

	SQLHSTMT    statement = (SQLHSTMT) query->statement();

	CODBCLock lock(m_connect);

	int rc = SQLFetch(statement);

	if (!successful(rc)) {
		if (rc < 0) {
			string error = queryError(query);
			query->logText(error);
			throw CException(error,__FILE__,__LINE__,query->sql().c_str());
		} else {
			querySetEof(query,rc==SQL_NO_DATA);
			return;
		}
	}

	unsigned    fieldCount = query->fieldCount();
	SQLINTEGER  dataLength;

	if (!fieldCount) return;

	CODBCField  *field = 0;
	for (unsigned column = 0; column < fieldCount; )
		try {
		field = (CODBCField *) & (*query)[column];
		const short fieldType = (short) field->fieldType();
		int         readSize = field->bufferSize();
		char       *buffer = field->getData();

		column++;

		switch (fieldType) {

			case SQL_C_SLONG:
			case SQL_C_DOUBLE:
				rc = SQLGetData(statement,column,fieldType,buffer,0,&dataLength);
				break;

			case SQL_C_TIMESTAMP: {
					TIMESTAMP_STRUCT t;
					rc = SQLGetData(statement,column,fieldType,&t,0,&dataLength);
					if (dataLength > 0) {
						CDateTime dt(t.year,t.month,t.day,t.hour,t.minute,t.second);
						if (field->dataType() == VAR_DATE)
							field->setDate(dt);
						else  field->setDateTime(dt);
					}
					break;
				}

			case SQL_C_BINARY:
			case SQL_C_CHAR:
				buffer = (char *)field->getBuffer();
				rc = SQLGetData(statement,column,fieldType,buffer,readSize,&dataLength);
				if (dataLength > readSize) { // continue to fetch BLOB data
					field->checkSize(dataLength+1);
					buffer = (char *)field->getBuffer();
					char *offset = buffer + readSize - 1;
					readSize = dataLength - readSize + 2;
					rc = SQLGetData(statement,column,fieldType,offset,readSize,NULL);
				}
				break;

			case SQL_BIT:
				rc = SQLGetData(statement,column,fieldType,buffer,1,&dataLength);
				break;

			default:
				dataLength = 0;
				break;
		}

		if (fieldType == SQL_C_CHAR && dataLength > 0) {
			dataLength = trimField(buffer,dataLength);
		}
		if (dataLength <= 0) {
			field->setNull();
		} else {
			field->dataSize(dataLength);
		}
	}
	catch (exception& e) {
		query->logAndThrow("CODBCDatabase::queryFetch","Can't read field "+field->fieldName()+"\n"+string(e.what()));
	}
}

string CODBCDatabase::driverDescription() const {
	if (m_connect) return m_connect->driverDescription();
	else return "";
}

void SP_EXPORT spdb_odbc_presented() {}
