/****************************************************************************
 * Copyright (C) 2009-2015 EPAM Systems
 * 
 * This file is part of Indigo toolkit.
 * 
 * This file may be distributed and/or modified under the terms of the
 * GNU General Public License version 3 as published by the Free Software
 * Foundation and appearing in the file LICENSE.GPL included in the
 * packaging of this file.
 * 
 * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 ***************************************************************************/

#include "oracle/bingo_oracle.h"

#include "base_cpp/scanner.h"
#include "base_cpp/output.h"

#include "oracle/ora_wrap.h"
#include "oracle/ora_logger.h"
#include "oracle/bingo_profiling.h"
#include "oracle/bingo_oracle_context.h"

#include "molecule/sdf_loader.h"
#include "molecule/rdf_loader.h"
#include "gzip/gzip_output.h"
#include "gzip/gzip_scanner.h"
#include "base_cpp/string_pool.h"
#include "base_cpp/auto_ptr.h"
#include "base_cpp/profiling.h"

ORAEXT void oraLogPrint (OCIExtProcContext *ctx, char *str)
{
   ORABLOCK_BEGIN
   {
      OracleEnv env(ctx, logger);

      env.dbgPrintf("%s\n", str);
   }
   ORABLOCK_END
}

ORAEXT OCILobLocator *oraLoadFileToCLOB (OCIExtProcContext *ctx, char *filename,
                                         short filename_indicator, short *return_indicator)
{
   OCILobLocator *result = 0;

   ORABLOCK_BEGIN
   {
      *return_indicator = OCI_IND_NULL;

      OracleEnv env(ctx, logger);

      if (filename_indicator == OCI_IND_NULL)
         throw BingoError("Null filename given");

      FileScanner scanner(filename);

      QS_DEF(Array<char>, buf);

      scanner.readAll(buf);

      OracleLOB lob(env);

      lob.createTemporaryCLOB();
      lob.write(0, buf); 
      *return_indicator = OCI_IND_NOTNULL;
      lob.doNotDelete();
      result = lob.get();
   }
   ORABLOCK_END

   return result;
}

ORAEXT OCILobLocator *oraLoadFileToBLOB (OCIExtProcContext *ctx, char *filename,
                                         short filename_indicator, short *return_indicator)
{
   OCILobLocator *result = 0;

   ORABLOCK_BEGIN
   {
      *return_indicator = OCI_IND_NULL;

      OracleEnv env(ctx, logger);

      if (filename_indicator != OCI_IND_NOTNULL)
         throw BingoError("Null filename given");

      FileScanner scanner(filename);

      QS_DEF(Array<char>, buf);

      scanner.readAll(buf);

      OracleLOB lob(env);

      lob.createTemporaryBLOB();
      lob.write(0, buf.ptr(), buf.size()); 
      *return_indicator = OCI_IND_NOTNULL;
      lob.doNotDelete();
      result = lob.get();
   }
   ORABLOCK_END

   return result;
}

ORAEXT OCIString *oraLoadFileToString (OCIExtProcContext *ctx, char *filename,
                                       short filename_indicator, short *return_indicator)
{
   OCIString *result = 0;

   ORABLOCK_BEGIN
   {
      OracleEnv env(ctx, logger);

      *return_indicator = OCI_IND_NULL;

      if (filename_indicator != OCI_IND_NOTNULL)
         throw BingoError("Null filename given");

      FileScanner scanner(filename);
      QS_DEF(Array<char>, buf);

      scanner.readAll(buf);

      env.callOCI(OCIStringAssignText(env.envhp(), env.errhp(), (text *)buf.ptr(), buf.size(), &result));

      *return_indicator = OCI_IND_NOTNULL;
   }
   ORABLOCK_END

   return result;
}


ORAEXT void oraSaveLOBToFile (OCIExtProcContext *ctx,
                              OCILobLocator *lob_locator, short lob_indicator,
                              char *file_name, short filename_indicator)
{
   ORABLOCK_BEGIN
   {
      OracleEnv env(ctx, logger);

      if (lob_indicator == OCI_IND_NULL)
         throw BingoError("Null LOB given");
      if (filename_indicator == OCI_IND_NULL)
         throw BingoError("Null file name given");

      OracleLOB lob(env, lob_locator);

      QS_DEF(Array<char>, buf);

      lob.readAll(buf, false);

      FileOutput output(file_name);

      output.writeArray(buf);
   }
   ORABLOCK_END
}

void _exportSDF (OracleEnv &env, const char *table, const char *clob_col, 
                 const char *other_cols, Output &output)
{
   QS_DEF(StringPool, col_names);
   QS_DEF(Array<char>, col_values);
   QS_DEF(Array<char>, word);
   QS_DEF(Array<char>, lob_value);

   BufferScanner scanner(other_cols);
   int i;
   int max_size = 1024;

   col_names.clear();
   col_values.clear();

   while (1)
   {
      scanner.skipSpace();
      if (scanner.isEOF())
         break;
      word.clear();
      scanner.readWord(word, 0);
      if (word.size() < 2)
         break;
      col_names.add(word.ptr());
   }

   col_values.resize(col_names.end() * max_size);

   OracleStatement statement(env);
   OracleLOB lob(env);

   lob.createTemporaryCLOB();

   statement.append("SELECT %s", clob_col);
   for (i = col_names.begin(); i != col_names.end(); i = col_names.next(i))
      statement.append(", to_char(%s)", col_names.at(i));

   statement.append(" FROM %s", table);
   statement.prepare();

   statement.defineClobByPos(1, lob);
   for (i = col_names.begin(); i != col_names.end(); i = col_names.next(i))
      statement.defineStringByPos(i + 2, col_values.ptr() + i * max_size, max_size);

   if (statement.executeAllowNoData()) do
   {
      lob.readAll(lob_value, false);

      if (lob_value.size() < 4)
         continue;

      // hack to handle molfiles which have newline (and those which have not)
      if (lob_value[lob_value.size() - 1] == '\n')
         lob_value.pop();

      // hack to handle molfiles which have $$$$
      if (strncmp(lob_value.ptr() + lob_value.size() - 4, "$$$$", 4) == 0)
         lob_value.resize(lob_value.size() - 4);
         
      output.writeArray(lob_value);

      for (i = col_names.begin(); i != col_names.end(); i = col_names.next(i))
         output.printf("\n> <%s>\n%s\n", col_names.at(i), col_values.ptr() + i * max_size);
      output.printf("\n$$$$\n");
   } while (statement.fetch());

}

void _parseFieldList (const char *str, StringPool &props, StringPool &columns)
{
   QS_DEF(Array<char>, prop);
   QS_DEF(Array<char>, column);
   BufferScanner scanner(str);

   props.clear();
   columns.clear();
   scanner.skipSpace();

   while (!scanner.isEOF())
   {
      scanner.readWord(prop, " ,");
      scanner.skipSpace();
      scanner.readWord(column, " ,");
      scanner.skipSpace();

      props.add(prop.ptr());
      columns.add(column.ptr());
      
      if (scanner.isEOF())
         break;
      
      if (scanner.readChar() != ',')
         throw BingoError("_parseFieldList(): comma expected");
      scanner.skipSpace();
   }
}

void _importSDF (OracleEnv &env, const char *table, const char *clob_col, 
                 const char *other_cols, const char *file_name)
{
   FileScanner scanner(file_name);
   int i, nwritten = 0;
   QS_DEF(Array<char>, word);
   QS_DEF(StringPool, props);
   QS_DEF(StringPool, columns);

   env.dbgPrintfTS("importing into table %s\n", table);

   SdfLoader loader(scanner);

   _parseFieldList(other_cols, props, columns);
 
   while (!loader.isEOF())
   {
      profTimerStart(tread, "import.read_next");
      loader.readNext();
      profTimerStop(tread);

      OracleStatement statement(env);
      OracleLOB lob(env);

      lob.createTemporaryCLOB();
      lob.write(0, loader.data);

      statement.append("INSERT INTO %s(%s", table, clob_col);

      for (i = columns.begin(); i != columns.end(); i = columns.next(i))
         statement.append(", %s", columns.at(i));

      statement.append(") VALUES(:clobdata");

      for (i = columns.begin(); i != columns.end(); i = columns.next(i))
      {
         if (loader.properties.at2(props.at(i)) == 0)
            statement.append(", NULL");
         else
            statement.append(",:%s", columns.at(i));
      }

      statement.append(")");
      statement.prepare();

      statement.bindClobByName(":clobdata", lob);

      for (i = columns.begin(); i != columns.end(); i = columns.next(i))
      {
         if (loader.properties.at2(props.at(i)) == 0)
            continue;
         
         ArrayOutput out(word);

         out.printf(":%s", columns.at(i));
         out.writeChar(0);

         Array<char> &val = loader.properties.at(props.at(i));

         statement.bindStringByName(word.ptr(), val.ptr(), val.size());
      }

      profTimerStart(tinsert, "import.sql_insert");
      statement.execute();
      profTimerStop(tinsert);

      nwritten++;
      if (nwritten % 1000 == 0)
      {
         env.dbgPrintfTS("imported %d items, commiting\n", nwritten);
         OracleStatement::executeSingle(env, "COMMIT");
      }
   }
   if (nwritten % 1000 != 0)
   {
      env.dbgPrintfTS("imported %d items, commiting\n", nwritten);
      OracleStatement::executeSingle(env, "COMMIT");
   }
}

void _importSMILES (OracleEnv &env, const char *table, const char *smiles_col,
                    const char *id_col, const char *file_name)
{
   FileScanner fscanner(file_name);
   AutoPtr<GZipScanner> gzscanner;
   Scanner *scanner;

   int nwritten = 0;
   QS_DEF(Array<char>, id);
   QS_DEF(Array<char>, str);

   env.dbgPrintfTS("importing into table %s\n", table);

   // detect if input is gzipped
   byte magic[2];
   int pos = fscanner.tell();

   fscanner.readCharsFix(2, (char *)magic);
   fscanner.seek(pos, SEEK_SET);

   if (magic[0] == 0x1f && magic[1] == 0x8b)
   {
      gzscanner.reset(new GZipScanner(fscanner));
      scanner = gzscanner.get();
   }
   else
      scanner = &fscanner;

   while (!scanner->isEOF())
   {
      id.clear();
      scanner->readLine(str, false);
      BufferScanner strscan(str);

      strscan.skipSpace();
      while (!strscan.isEOF() && !isspace(strscan.readChar()))
         ;
      strscan.skipSpace();
      if (strscan.lookNext() == '|')
      {
         strscan.readChar();
         while (!strscan.isEOF() && strscan.readChar() != '|')
            ;
         strscan.skipSpace();
      }

      if (!strscan.isEOF() && id_col != 0)
         strscan.readLine(id, true);

      OracleStatement statement(env);

      statement.append("INSERT INTO %s(%s", table, smiles_col);

      if (id_col != 0)
         statement.append(", %s", id_col);

      statement.append(") VALUES(:smiles");

      if (id_col != 0)
      {
         if (id.size() > 1)
            statement.append(", :id");
         else
            statement.append(", NULL");
      }
      statement.append(")");
      statement.prepare();

      str.push(0);
      statement.bindStringByName(":smiles", str.ptr(), str.size());
      if (id.size() > 1)
         statement.bindStringByName(":id", id.ptr(), id.size());

      statement.execute();
      nwritten++;
      if (nwritten % 1000 == 0)
      {
         env.dbgPrintfTS("imported %d items, commiting\n", nwritten);
         OracleStatement::executeSingle(env, "COMMIT");
      }
   }
   if (nwritten % 1000 != 0)
   {
      env.dbgPrintfTS("imported %d items, commiting\n", nwritten);
      OracleStatement::executeSingle(env, "COMMIT");
   }
}

void _importRDF (OracleEnv &env, const char *table, const char *clob_col,
                 const char *other_cols, const char *file_name)
{
   FileScanner scanner(file_name);
   int i, nwritten = 0;
   QS_DEF(Array<char>, word);
   QS_DEF(StringPool, props);
   QS_DEF(StringPool, columns);

   env.dbgPrintfTS("importing into table %s\n", table);

   _parseFieldList(other_cols, props, columns);

   RdfLoader loader(scanner);

   while (!loader.isEOF())
   {
      loader.readNext();

      OracleStatement statement(env);
      OracleLOB lob(env);

      lob.createTemporaryCLOB();
      lob.write(0, loader.data);

      statement.append("INSERT INTO %s(%s", table, clob_col);

      for (i = columns.begin(); i != columns.end(); i = columns.next(i))
         statement.append(", %s", columns.at(i));

      statement.append(") VALUES(:clobdata");

      for (i = columns.begin(); i != columns.end(); i = columns.next(i))
      {
         if (loader.properties.at2(props.at(i)) == 0)
            statement.append(", NULL");
         else
            statement.append(",:%s", columns.at(i));
      }

      statement.append(")");
      statement.prepare();

      statement.bindClobByName(":clobdata", lob);

      for (i = columns.begin(); i != columns.end(); i = columns.next(i))
      {
         if (loader.properties.at2(props.at(i)) == 0)
            continue;

         ArrayOutput out(word);

         out.printf(":%s", columns.at(i));
         out.writeChar(0);

         Array<char> &val = loader.properties.at(props.at(i));

         statement.bindStringByName(word.ptr(), val.ptr(), val.size());
      }

      statement.execute();
      nwritten++;
      if (nwritten % 1000 == 0)
      {
         env.dbgPrintfTS("imported %d items, commiting\n", nwritten);
         OracleStatement::executeSingle(env, "COMMIT");
      }
   }
   if (nwritten % 1000 != 0)
   {
      env.dbgPrintfTS("imported %d items, commiting\n", nwritten);
      OracleStatement::executeSingle(env, "COMMIT");
   }
}


ORAEXT void oraExportSDF (OCIExtProcContext *ctx, 
                          const char *table_name, short table_name_ind,
                          const char *clob_col_name, short clob_col_ind,
                          const char *other_col_names, short other_col_ind,
                          const char *file_name, short file_name_ind)
{
   logger.initIfClosed(log_filename);

   ORABLOCK_BEGIN
   {
      OracleEnv env(ctx, logger);

      if (table_name_ind != OCI_IND_NOTNULL)
         throw BingoError("null table name");

      if (clob_col_ind != OCI_IND_NOTNULL)
         throw BingoError("null CLOB column name");

      if (file_name_ind != OCI_IND_NOTNULL)
         throw BingoError("null file name");

      if (other_col_ind != OCI_IND_NOTNULL)
         other_col_names = 0;

      FileOutput output(file_name);

      _exportSDF(env, table_name, clob_col_name, other_col_names, output);
   }
   ORABLOCK_END
}

ORAEXT void oraExportSDFZip (OCIExtProcContext *ctx,
                             const char *table_name, short table_name_ind,
                             const char *clob_col_name, short clob_col_ind,
                             const char *other_col_names, short other_col_ind,
                             const char *file_name, short file_name_ind)
{
   logger.initIfClosed(log_filename);

   ORABLOCK_BEGIN
   {
      OracleEnv env(ctx, logger);

      if (table_name_ind != OCI_IND_NOTNULL)
         throw BingoError("null table name");

      if (clob_col_ind != OCI_IND_NOTNULL)
         throw BingoError("null CLOB column name");

      if (file_name_ind != OCI_IND_NOTNULL)
         throw BingoError("null file name");

      if (other_col_ind != OCI_IND_NOTNULL)
         other_col_names = 0;

      FileOutput output(file_name);
      GZipOutput gzoutput(output, 9);

      _exportSDF(env, table_name, clob_col_name, other_col_names, gzoutput);
   }
   ORABLOCK_END
}


ORAEXT void oraImportSDF (OCIExtProcContext *ctx,
                          const char *table_name, short table_name_ind,
                          const char *clob_col_name, short clob_col_ind,
                          const char *other_col_names, short other_col_ind,
                          const char *file_name, short file_name_ind)
{
   logger.initIfClosed(log_filename);

   ORABLOCK_BEGIN
   {
      profTimersReset();
      profTimerStart(ttotal, "total");

      OracleEnv env(ctx, logger);

      if (table_name_ind != OCI_IND_NOTNULL)
         throw BingoError("null table name");

      if (clob_col_ind != OCI_IND_NOTNULL)
         throw BingoError("null table name");

      if (clob_col_ind != OCI_IND_NOTNULL)
         throw BingoError("null file name");

      if (other_col_ind != OCI_IND_NOTNULL)
         other_col_names = 0;

      _importSDF(env, table_name, clob_col_name, other_col_names, file_name);

      profTimerStop(ttotal);
      bingoProfilingPrintStatistics(false);
   }
   ORABLOCK_END
}

ORAEXT void oraImportRDF (OCIExtProcContext *ctx,
                          const char *table_name, short table_name_ind,
                          const char *clob_col_name, short clob_col_ind,
                          const char *other_col_names, short other_col_ind,
                          const char *file_name, short file_name_ind)
{
   logger.initIfClosed(log_filename);

   ORABLOCK_BEGIN
   {
      OracleEnv env(ctx, logger);

      if (table_name_ind != OCI_IND_NOTNULL)
         throw BingoError("null table name");

      if (clob_col_ind != OCI_IND_NOTNULL)
         throw BingoError("null table name");

      if (clob_col_ind != OCI_IND_NOTNULL)
         throw BingoError("null file name");

      if (other_col_ind != OCI_IND_NOTNULL)
         other_col_names = 0;

      _importRDF(env, table_name, clob_col_name, other_col_names, file_name);
   }
   ORABLOCK_END
}

ORAEXT void oraImportSMILES (OCIExtProcContext *ctx,
                             const char *table_name, short table_name_ind,
                             const char *smiles_col_name, short smiles_col_ind,
                             const char *id_col_name, short id_col_ind,
                             const char *file_name, short file_name_ind)
{
   logger.initIfClosed(log_filename);

   ORABLOCK_BEGIN
   {
      OracleEnv env(ctx, logger);

      if (table_name_ind != OCI_IND_NOTNULL)
         throw BingoError("null table name");

      if (smiles_col_ind != OCI_IND_NOTNULL)
         throw BingoError("null SMILES column name");

      if (file_name_ind != OCI_IND_NOTNULL)
         throw BingoError("null file name");

      if (id_col_ind != OCI_IND_NOTNULL)
         id_col_name = 0;

      _importSMILES(env, table_name, smiles_col_name, id_col_name, file_name);
   }
   ORABLOCK_END
}


ORAEXT OCIString *oraGetVersion (OCIExtProcContext *ctx, short *return_indicator)
{
   OCIString *result = 0;

   ORABLOCK_BEGIN
   {
      OracleEnv env(ctx, logger);

      // env.callOCI(OCIStringAssignText(env.envhp(), env.errhp(), (const oratext *)bingo_version_string,
      //   strlen(bingo_version_string), &result));
      env.callOCI(OCIStringAssignText(env.envhp(), env.errhp(), (const oratext *)BINGO_VERSION,
        strlen(BINGO_VERSION), &result));


      *return_indicator = OCI_IND_NOTNULL;
   }
   ORABLOCK_END

   return result;
}

ORAEXT void oraBingoZip (OCIExtProcContext *ctx,
                         OCILobLocator *source_locator, short source_indicator,
                         OCILobLocator *dest_locator, short dest_indicator)
{
   ORABLOCK_BEGIN
   {
      OracleEnv env(ctx, logger);

      if (source_indicator == OCI_IND_NULL)
         throw BingoError("null source given");
      if (dest_indicator == OCI_IND_NULL)
         throw BingoError("null destination given");

      OracleLOB source_lob(env, source_locator);
      
      QS_DEF(Array<char>, source);
      QS_DEF(Array<char>, dest);

      source_lob.readAll(source, false);

      {
         ArrayOutput output(dest);
         GZipOutput gzoutput(output, 9);

         gzoutput.write(source.ptr(), source.size());
         gzoutput.flush();
      }

      OracleLOB result_lob(env, dest_locator);

      result_lob.write(0, dest);
      result_lob.trim(dest.size());
   }
   ORABLOCK_END
}

ORAEXT void oraBingoUnzip (OCIExtProcContext *ctx,
                         OCILobLocator *source_locator, short source_indicator,
                         OCILobLocator *dest_locator, short dest_indicator)
{
   ORABLOCK_BEGIN
   {
      OracleEnv env(ctx, logger);

      if (source_indicator == OCI_IND_NULL)
         throw BingoError("null source given");
      if (dest_indicator == OCI_IND_NULL)
         throw BingoError("null destination given");

      OracleLOB source_lob(env, source_locator);

      QS_DEF(Array<char>, source);
      QS_DEF(Array<char>, dest);

      source_lob.readAll(source, false);

      {
         BufferScanner scanner(source);
         GZipScanner gzscanner(scanner);

         gzscanner.readAll(dest);
      }

      OracleLOB result_lob(env, dest_locator);

      result_lob.write(0, dest);
      result_lob.trim(dest.size());
   }
   ORABLOCK_END
}

ORAEXT OCIString *oraBingoGetName (OCIExtProcContext *ctx,
                              OCILobLocator *source_locator, short source_indicator,
                              short *return_indicator)
{
   OCIString *result = NULL;

   ORABLOCK_BEGIN
   {
      OracleEnv env(ctx, logger);

      if (source_indicator == OCI_IND_NULL)
         throw BingoError("null source given");
      
      QS_DEF(Array<char>, source);
      QS_DEF(Array<char>, name);
      OracleLOB source_lob(env, source_locator);
      
      source_lob.readAll(source, false);

      BufferScanner scanner(source);
      bingoGetName(scanner, name);

      if (name.size() < 1)
      {
         // This is needed for Oracle 9. Returning NULL drops the extproc.
         OCIStringAssignText(env.envhp(), env.errhp(), (text *)"nil", 3, &result);
         *return_indicator = OCI_IND_NULL;
      }
      else
      {
         name.push(0);
         OCIStringAssignText(env.envhp(), env.errhp(), (text *)name.ptr(), strlen(name.ptr()), &result);
         *return_indicator = OCI_IND_NOTNULL;
      }
   }
   ORABLOCK_END
   
   return result;
}
