/***************************************************************************
                          SIMPLY POWERFUL TOOLKIT (SPTK)
                          CODBCDatabase.cpp  -  description
                             -------------------
    begin                : Fri Oct 03 2003
    copyright            : (C) 2003-2005 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>

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) {}
};

using namespace std;

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 (!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 (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) {
         throw CException(queryError(query),__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);
               buffer = (char *)field->getBuffer();
               char *offset = buffer + readSize - 1;
               readSize = dataLength - readSize + 1;
               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 "";
}
