/***************************************************************************
                          SIMPLY POWERFUL TOOLKIT (SPTK)
                          CSQLite3Database.cpp  -  description
                             -------------------
    begin                : Fri Oct 28 2005
    copyright            : (C) 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 <sqlite3.h>
#include <sptk3/CSQLite3Database.h>
#include <sptk3/CDatabaseField.h>
#include <sptk3/CQuery.h>
#include <sptk3/CParams.h>
#include <string>
#include <stdio.h>

class CSQLite3Field : public CDatabaseField {
   friend class CSQLite3Database;
public:
   CSQLite3Field(const std::string fieldName,int fieldColumn,int fieldType,CVariantType dataType,int fieldLength,int fieldScale)
   : CDatabaseField(fieldName,fieldColumn,fieldType,dataType,fieldLength,fieldScale) {}
};

using namespace std;

CSQLite3Database::CSQLite3Database(string connectionString)
: CDatabase(connectionString) {
   m_connect = 0;
}

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

void CSQLite3Database::openDatabase(const string newConnectionString) throw(CException) {
   if (!active()) {
      m_inTransaction = false;
      if (newConnectionString.length())
         m_connString = newConnectionString;
      if (sqlite3_open(m_connString.c_str(), &m_connect) != 0) {
         string error = sqlite3_errmsg(m_connect);
         sqlite3_close(m_connect);
         m_connect = 0;
         throw CException(error);
      }
   }
}

void CSQLite3Database::closeDatabase() throw(CException) {
   for (unsigned i = 0; i < m_queryList.size(); i++) {
      try {
         CQuery *query = (CQuery *)m_queryList[i];
         queryFreeStmt(query);
      }
      catch (...) {}
   }
   sqlite3_close(m_connect);
   m_connect = 0L;
}

unsigned long CSQLite3Database::handle() const {
   return (unsigned long)m_connect;
}

bool CSQLite3Database::active() const {
   return m_connect != 0L;
}

void CSQLite3Database::driverBeginTransaction() throw(CException) {
   if (!m_connect) open();

   if (m_inTransaction)
      throw CException("Transaction already started.");

   char *zErrMsg;
   if (sqlite3_exec(m_connect,"BEGIN TRANSACTION", 0, 0, &zErrMsg) != SQLITE_OK)
      throw CException(zErrMsg);

   m_inTransaction = true;
}

void CSQLite3Database::driverEndTransaction(bool commit) throw(CException) {
   if (!m_inTransaction)
      throw CException("Transaction isn't started.");

   string action;
   if (commit)
      action = "COMMIT";
   else  action = "ROLLBACK";

   char *zErrMsg;
   if (sqlite3_exec(m_connect,action.c_str(), 0, 0, &zErrMsg) != SQLITE_OK)
      throw CException(zErrMsg);

   m_inTransaction = false;
}

//-----------------------------------------------------------------------------------------------
static inline bool successful(int ret) {
   return ret==SQLITE_OK;
}

string CSQLite3Database::queryError(const CQuery *query) const {
   return sqlite3_errmsg(m_connect);;
}

// Doesn't actually allocate stmt, but makes sure 
// the previously allocated stmt is released
void CSQLite3Database::queryAllocStmt(CQuery *query) {
   CSQLite3Lock lock(this);

   SQLHSTMT stmt = (SQLHSTMT)query->statement();
   if (stmt) sqlite3_finalize(stmt);
   querySetStmt(query,0L);
}

void CSQLite3Database::queryFreeStmt(CQuery *query) {
   CSQLite3Lock lock(this);

   SQLHSTMT stmt = (SQLHSTMT)query->statement();
   if (stmt) sqlite3_finalize(stmt);
   querySetStmt(query,0L);
   querySetPrepared(query,false);
}

void CSQLite3Database::queryCloseStmt(CQuery *query) {
   CSQLite3Lock lock(this);

   //sqlite3_reset((SQLHSTMT)query->statement());
   SQLHSTMT stmt = (SQLHSTMT)query->statement();
   if (stmt) sqlite3_finalize(stmt);
   querySetStmt(query,0L);
   querySetPrepared(query,false);
}

void CSQLite3Database::queryPrepare(CQuery *query) {
   CSQLite3Lock lock(this);

   SQLHSTMT stmt;
   const char *pzTail;
   if (sqlite3_prepare(m_connect,query->sql().c_str(),
         query->sql().length(),&stmt,&pzTail) != SQLITE_OK) 
   {
      const char *errorMsg = sqlite3_errmsg(m_connect);
      throw CException(errorMsg,__FILE__,__LINE__,query->sql().c_str());
   }

   //sqlite3_clear_bindings(stmt);

   querySetStmt(query,stmt);
   querySetPrepared(query,true);
}

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

void CSQLite3Database::queryExecute(CQuery *query) {
   CSQLite3Lock lock(this);

   if (!query->prepared()) 
      throw CException("Query isn't prepared");
}

int CSQLite3Database::queryColCount(CQuery *query) {
   CSQLite3Lock lock(this);

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

   short count = sqlite3_column_count(stmt);

   return count;
}

void CSQLite3Database::queryBindParameters(CQuery *query) {
   CSQLite3Lock lock(this);

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

   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++) {

         int   rc = -1;
         short paramNumber = short(param->bindIndex(j) + 1);

         if (param->isNull()) 
            rc = sqlite3_bind_null(stmt,paramNumber);
         else
            switch (ptype) {
            case VAR_INT:
               rc = sqlite3_bind_int(stmt,paramNumber,param->getInteger());
               break;
            case VAR_FLOAT:
               rc = sqlite3_bind_double(stmt,paramNumber,param->getFloat());
               break;
            case VAR_STRING:
            case VAR_TEXT:
               rc = sqlite3_bind_text(stmt,paramNumber,param->getString(),param->dataSize(),SQLITE_STATIC);
               break;
            case VAR_BUFFER: 
               rc = sqlite3_bind_blob(stmt,paramNumber,param->getString(),param->dataSize(),SQLITE_STATIC);
               break;
            case VAR_DATE:
            case VAR_DATE_TIME:
               throwException("Date and time types isn't yet supported for SQLite3");
               /*
               {
                  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;
            default:
               throw CException("Unsupported type of parameter "+int2string(paramNumber),__FILE__,__LINE__,query->sql().c_str());
         }
         if (rc != SQLITE_OK) {
            string error = sqlite3_errmsg(m_connect);
            throw CException(error + ", in binding parameter "+int2string(paramNumber),__FILE__,__LINE__,query->sql().c_str());
         }
      }
   }
}

void CSQLite3Database::SQLITEtypeToCType(int sqliteType,CVariantType& dataType) {
   switch(sqliteType) {
      case SQLITE_INTEGER:
         dataType = VAR_INT;
         return;

      case SQLITE_FLOAT:
         dataType = VAR_FLOAT;
         return;

      case SQLITE_TEXT:
         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 SQLITE_BLOB:
         dataType = VAR_BUFFER;
         return;
   }
   dataType = VAR_NONE;
}

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

   if (query->active()) return;

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

   if (!query->prepared()) queryPrepare(query);

   queryBindParameters(query);
   queryExecute(query);

   short count = queryColCount(query);

   query->fields().clear();

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

   if (count < 1) {
      if (sqlite3_step(stmt) != SQLITE_DONE) {
         string error = queryError(query);
         queryCloseStmt(query);
         throw CException(error,__FILE__,__LINE__,query->sql().c_str());
      }
      queryCloseStmt(query);
      return;
   } else {
      querySetActive(query,true);

      // Reading the column attributes
      char  columnName[256];
      long  columnType;
      CVariantType dataType;
      for (short column = 1; column <= count; column++) {
         strncpy(columnName,sqlite3_column_name(stmt,column-1),255); columnName[255] = 0;
         columnType = sqlite3_column_type(stmt,column-1);
         SQLITEtypeToCType(columnType,dataType);
         if (columnName[0] == 0)
            sprintf(columnName,"column%02i",column);

         CSQLite3Field *field = new CSQLite3Field(
               columnName,
               column,
               columnType,
               dataType,0,0);
         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)
      return 0;
   s[0] = ch;
   return p - s;
}

void CSQLite3Database::queryFetch(CQuery *query) {
   if (!query->active())
      throw CException("Dataset isn't open",__FILE__,__LINE__,query->sql().c_str());

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

   CSQLite3Lock lock(this);

   int rc = sqlite3_step(statement);

   switch (rc) {
      case SQLITE_DONE:
         querySetEof(query,true);
         return;
      case SQLITE_ROW:
         break;

      default:
         throw CException(queryError(query),__FILE__,__LINE__,query->sql().c_str());
   }

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

   if (!fieldCount) return;

   CSQLite3Field  *field = 0;
   for (unsigned column = 0; column < fieldCount; column++) {
      try {
         field = (CSQLite3Field *) & (*query)[column];
         short fieldType = (short) field->fieldType();
         if (!fieldType)
            fieldType = sqlite3_column_type(statement,column);

         dataLength = sqlite3_column_bytes(statement,column);

         if (dataLength) {
            switch (fieldType) {

               case SQLITE_INTEGER:
                  field->setInteger(sqlite3_column_int64(statement,column));
                  break;

               case SQLITE_FLOAT:
                  field->setFloat(sqlite3_column_double(statement,column));
                  break;

               case SQLITE_TEXT:
                  field->setString((const char *)sqlite3_column_text(statement,column),dataLength);
                  dataLength = trimField((char *)field->getString(),dataLength);
                  break;

               case SQLITE_BLOB:
                  field->setBuffer(sqlite3_column_blob(statement,column),dataLength);
                  break;

               default:
                  dataLength = 0;
                  break;
            }

            field->dataSize(dataLength);

         } else {
            field->setString("");
            field->setNull();
         } 
      }
      catch (exception& e) {
         throw CException("Can't read field "+field->fieldName()+"\n"+string(e.what()),__FILE__,__LINE__,query->sql().c_str());
      }
   }
}
