/****************************************************************************
 * 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/ora_wrap.h"

#include "base_cpp/output.h"
#include "oracle/mango_shadow_fetch.h"
#include "core/mango_matchers.h"

#include "oracle/bingo_oracle_context.h"
#include "oracle/mango_fetch_context.h"
#include "base_cpp/profiling.h"

IMPL_ERROR(MangoShadowFetch, "mango shadow fetch");

MangoShadowFetch::MangoShadowFetch (MangoFetchContext &context) :
_context(context)
{
   _total_count = -1;
   _table_name.push(0);

   ArrayOutput output(_table_name);
   output.printf(context.context().shadow_table.getName());
   output.writeChar(0);

   ArrayOutput output2(_components_table_name);
   output2.printf(context.context().shadow_table.getComponentsName());
   output2.writeChar(0);

   _executed = false;
   _fetch_type = 0;
   _processed_rows = 0;
   _end = false;

   _rowid.ptr()[0] = 0;
}

MangoShadowFetch::~MangoShadowFetch ()
{
}

int MangoShadowFetch::getTotalCount (OracleEnv &env)
{
   if (_total_count < 0)
   {
      if (!OracleStatement::executeSingleInt(_total_count, env,
         "SELECT COUNT(*) FROM %s", _table_name.ptr()))
         throw Error("getTotalCount() failed");
   }

   return _total_count;
}

bool MangoShadowFetch::getLastRowid (OraRowidText &id)
{
   if (_rowid.ptr()[0] == 0)
      return false;
   memcpy(&id, &_rowid, sizeof(_rowid));
   return true;
}

int MangoShadowFetch::countOracleBlocks (OracleEnv &env)
{
   int res;

   env.dbgPrintf("countOracleBlocks\n");

   if (!OracleStatement::executeSingleInt(res, env, "select blocks from user_tables where "
              "table_name = upper('%s')", _table_name.ptr()))
      return 0;

   return res;
}

int MangoShadowFetch::getIOCost (OracleEnv &env, float selectivity)
{
   return (int)(countOracleBlocks(env) * selectivity);
}

bool MangoShadowFetch::end ()
{
   return _end;
}

float MangoShadowFetch::calcSelectivity (OracleEnv &env, int total_count)
{
   int nrows_select_total;

   if (_counting_select.size() > 1)
   {
      _counting_select.push(0);
      OracleStatement statement(env);

      statement.append("%s", _counting_select.ptr());
      statement.prepare();
      statement.defineIntByPos(1, &nrows_select_total);
      if (_fetch_type == _MASS)
      {
         statement.bindFloatByName(":mass_min", &_context.mass.bottom);
         statement.bindFloatByName(":mass_max", &_context.mass.top);
      }
      else if (_fetch_type == _TAUTOMER)
      {
         const char *gross = _context.tautomer.getQueryGross();
         statement.bindStringByName(":gross", gross, strlen(gross) + 1);
      }
      if (!statement.executeAllowNoData())
         throw Error("selectivity: cannot count rows");
   }
   else
      nrows_select_total = total_count;

   if (_processed_rows == 0)
   {
      if (nrows_select_total == 0)
         return 0;
      throw Error("selectivity: no processed rows");
   }
   
   return (float)nrows_select_total * matched.size() / (total_count * _processed_rows);
}


void MangoShadowFetch::prepareNonSubstructure (OracleEnv &env)
{
   env.dbgPrintf("preparing shadow table for non-substructure match\n");

   _fetch_type = _NON_SUBSTRUCTURE;
   _need_xyz = _context.substructure.needCoords();

   _env.reset(new OracleEnv(env.ctx(), env.logger()));
   _statement.reset(new OracleStatement(_env.ref()));

   if (_need_xyz)
   {
      _lob_cmf.reset(new OracleLOB(_env.ref()));
      _lob_xyz.reset(new OracleLOB(_env.ref()));
      _statement->append("SELECT mol_rowid, cmf, xyz FROM %s", _table_name.ptr());
      _statement->prepare();
      _statement->defineStringByPos(1, _rowid.ptr(), sizeof(_rowid));
      _statement->defineBlobByPos(2, _lob_cmf.ref());
      _statement->defineBlobByPos(3, _lob_xyz.ref());
   }
   else
   {
      _lob_cmf.reset(new OracleLOB(_env.ref()));
      _statement->append("SELECT mol_rowid, cmf FROM %s", _table_name.ptr());
      _statement->prepare();
      _statement->defineStringByPos(1, _rowid.ptr(), sizeof(_rowid));
      _statement->defineBlobByPos(2, _lob_cmf.ref());
   }

   _counting_select.clear();
}

void MangoShadowFetch::prepareNonTautomerSubstructure (OracleEnv &env)
{
   env.dbgPrintf("preparing shadow table for non-tautomer-substructure match\n");
   _fetch_type = _NON_TAUTOMER_SUBSTRUCTURE;

   _env.reset(new OracleEnv(env.ctx(), env.logger()));
   _statement.reset(new OracleStatement(_env.ref()));
   _lob_cmf.reset(new OracleLOB(_env.ref()));

   _statement->append("SELECT mol_rowid, cmf FROM %s", _table_name.ptr());
   _statement->prepare();
   _statement->defineStringByPos(1, _rowid.ptr(), sizeof(_rowid));
   _statement->defineBlobByPos(2, _lob_cmf.ref());

   _counting_select.clear();
}

void MangoShadowFetch::prepareTautomer (OracleEnv &env, int right_part)
{
   if (right_part == 1)
      env.dbgPrintf("preparing shadow table for tautomer match\n");
   else 
      env.dbgPrintf("preparing shadow table for non-tautomer match\n");

   _fetch_type = _TAUTOMER;
   _right_part = right_part;

   _env.reset(new OracleEnv(env.ctx(), env.logger()));
   _statement.reset(new OracleStatement(_env.ref()));
   _lob_cmf.reset(new OracleLOB(_env.ref()));

   _statement->append("SELECT mol_rowid, cmf FROM %s", _table_name.ptr());

   if (right_part == 1)
      _statement->append(" WHERE gross = :gross OR gross LIKE :grossh");

   _statement->prepare();
   _statement->defineStringByPos(1, _rowid.ptr(), sizeof(_rowid));
   _statement->defineBlobByPos(2, _lob_cmf.ref());

   if (right_part == 1)
   {
      ArrayOutput output(_counting_select);
      
      output.printf("SELECT COUNT(*) FROM %s WHERE gross = :gross", _table_name.ptr());
   }
   else
      _counting_select.clear();
}

void MangoShadowFetch::prepareExact (OracleEnv &env, int right_part)
{
   const MangoExact & instance = _context.exact;

   if (right_part == 1)
      env.dbgPrintf("preparing shadow table for exact\n");
   else 
      env.dbgPrintf("preparing shadow table for non-exact\n");

   _fetch_type = _EXACT;
   _right_part = right_part;
   _need_xyz = instance.needCoords();

   _env.reset(new OracleEnv(env.ctx(), env.logger()));
   _statement.reset(new OracleStatement(_env.ref()));
   _lob_cmf.reset(new OracleLOB(_env.ref()));

   _statement->append("SELECT sh.mol_rowid, sh.cmf");
   if (_need_xyz)
      _statement->append(", sh.xyz", _table_name.ptr());
   _statement->append(" FROM %s sh", _table_name.ptr());

   QS_DEF(Array<char>, table_copies);
   QS_DEF(Array<char>, where_clause);
   _prepareExactQueryStrings(table_copies, where_clause);

   _statement->append(table_copies.ptr());
   _statement->append(where_clause.ptr());

   _statement->prepare();
   _statement->defineStringByPos(1, _rowid.ptr(), sizeof(_rowid));
   _statement->defineBlobByPos(2, _lob_cmf.ref());
   
   if (_need_xyz)
   {
      _lob_xyz.reset(new OracleLOB(_env.ref()));
      _statement->defineBlobByPos(3, _lob_xyz.ref());
   }

   ArrayOutput output_cnt(_counting_select);
   output_cnt.printf("SELECT COUNT(*) FROM %s sh", _table_name.ptr());
   output_cnt.printf("%s", table_copies.ptr());
   output_cnt.printf("%s", where_clause.ptr());
}

void MangoShadowFetch::_prepareExactQueryStrings (Array<char> &table_copies, 
                                                      Array<char> &where_clause)
{
   const MangoExact & instance = _context.exact;
   const MangoExact::Hash &hash = instance.getQueryHash();

   ArrayOutput output_tables(table_copies);
   if (_right_part == 1)
   {
      for (int i = 0; i < hash.size(); i++)
         output_tables.printf(", %s t%d", _components_table_name.ptr(), i);
   }
   output_tables.writeChar(0);

   // Create complex WHERE clause
   ArrayOutput output(where_clause);
   if (_right_part == 1)
   {
      bool where_was_added = false;
      if (hash.size() > 0)
      {
         
         output.printf(" WHERE "); 
         where_was_added = true;
         // molecule ids must be same
         output.printf("sh.mol_rowid = t0.mol_rowid and ");
         for (int i = 1; i < hash.size(); i++)
            output.printf("t%d.mol_rowid = t%d.mol_rowid and ", i - 1, i);
         // query components must match target components
         for (int i = 0; i < hash.size(); i++)
            output.printf("t%d.hash = '%08X' and ", i, hash[i].hash);
         // components count mast must target components count
         const char *rel;
         if (instance.needComponentMatching())
            rel = ">=";
         else
            rel = "=";

         for (int i = 0; i < hash.size(); i++)
         {
            if (i != 0)
               output.printf("and ");
            output.printf("t%d.count %s %d ", i, rel, hash[i].count);
         }
      }
      if (!instance.needComponentMatching())
      {
         if (!where_was_added)
            output.printf(" WHERE "); 
         else
            output.printf("and ");

         // There must be no other components in target
         int query_fragments_count = 0;
         for (int i = 0; i < hash.size(); i++)
            query_fragments_count += hash[i].count;
         output.printf("sh.fragments = %d", query_fragments_count);
      }
   }

   output.writeChar(0);
}


void MangoShadowFetch::prepareGross (OracleEnv &env, int right_part)
{
   MangoGross & instance = _context.gross;

   env.dbgPrintf("preparing shadow table for gross formula match\n");

   _fetch_type = _GROSS;
   _right_part = right_part;
   _env.reset(new OracleEnv(env.ctx(), env.logger()));
   _statement.reset(new OracleStatement(_env.ref()));
   _statement->append("SELECT mol_rowid, gross FROM %s ", _table_name.ptr());
   if (*instance.getConditions() != 0 && right_part == 1)
      _statement->append("WHERE %s", instance.getConditions());
   _statement->prepare();
   _statement->defineStringByPos(1, _rowid.ptr(), sizeof(_rowid));
   _statement->defineStringByPos(2, _gross, sizeof(_gross));

   ArrayOutput output(_counting_select);
   output.printf("SELECT COUNT(*) FROM %s WHERE %s", _table_name.ptr(),
                                                     instance.getConditions());
}

void MangoShadowFetch::prepareMass (OracleEnv &env)
{
   env.dbgPrintf("preparing shadow table for molecular mass match\n");

   QS_DEF(Array<char>, where);

   {
      ArrayOutput where_out(where);
      where_out.printf("");
      where_out.writeChar(0);
   }

   _fetch_type = _MASS;
   _env.reset(new OracleEnv(env.ctx(), env.logger()));
   _statement.reset(new OracleStatement(_env.ref()));

   _statement->append("SELECT mol_rowid FROM %s WHERE mass >= :mass_min AND mass <= :mass_max",
                      _table_name.ptr());

   _statement->prepare();
   _statement->defineStringByPos(1, _rowid.ptr(), sizeof(_rowid));

   ArrayOutput output(_counting_select);
   output.printf("SELECT COUNT(*) FROM %s WHERE WHERE mass >= :mass_min AND mass <= :mass_max",
           _table_name.ptr());
}

void MangoShadowFetch::fetch (OracleEnv &env, int maxrows)
{
   matched.clear();

   if (_statement.get() == 0)
      return;

   if (maxrows < 1 || _end)
      return;

   env.dbgPrintf("fetching up to %d rows using shadowtable... ", maxrows);

   while (matched.size() < maxrows)
   {
      bool fetch_res;

      if (_fetch_type == _MASS)
      {
         _statement->bindFloatByName(":mass_min", &_context.mass.bottom);
         _statement->bindFloatByName(":mass_max", &_context.mass.top);
      }
      else if (_fetch_type == _TAUTOMER && _right_part == 1)
      {
         const char *gross = _context.tautomer.getQueryGross();
         _statement->bindStringByName(":gross", gross, strlen(gross) + 1);
         QS_DEF(Array<char>, grossh);
         grossh.readString(gross, false);
         grossh.appendString(" H%", true);
         _statement->bindStringByName(":grossh", grossh.ptr(), grossh.size());
      }
      
      if (!_executed)
      {
         fetch_res = _statement->executeAllowNoData();
         _executed = true;
      }
      else
         fetch_res = _statement->fetch();

      if (!fetch_res)
      {
         _end = true;
         break;
      }

      bool have_match = false;

      if (_fetch_type == _NON_SUBSTRUCTURE)
      {
         MangoSubstructure &instance = _context.substructure;
         QS_DEF(Array<char>, cmf);

         _lob_cmf->readAll(cmf, false);
         
         if (_need_xyz)
         {
            if (_statement->gotNull(3)) // xyz == NULL?
               have_match = true;
            else
            {
               QS_DEF(Array<char>, xyz);

               _lob_xyz->readAll(xyz, false);
               if (!instance.matchBinary(cmf, &xyz))
                  have_match = true;
            }
         }
         else if (!instance.matchBinary(cmf, 0))
            have_match = true;
      }
      else if (_fetch_type == _NON_TAUTOMER_SUBSTRUCTURE)
      {
         MangoTautomer &instance = _context.tautomer;
         QS_DEF(Array<char>, cmf);

         _lob_cmf->readAll(cmf, false);

         if (!instance.matchBinary(cmf))
            have_match = true;
      }
      else if (_fetch_type == _TAUTOMER)
      {
         MangoTautomer &instance = _context.tautomer;
         QS_DEF(Array<char>, cmf);

         _lob_cmf->readAll(cmf, false);

         if (instance.matchBinary(cmf) == (_right_part == 1))
            have_match = true;
      }
      else if (_fetch_type == _EXACT)
      {
         MangoExact &instance = _context.exact;
         QS_DEF(Array<char>, cmf);

         profTimerStart(tlobread, "exact.lobread");
         _lob_cmf->readAll(cmf, false);
         profTimerStop(tlobread);

         if (_need_xyz)
         {
            if (_statement->gotNull(3)) // xyz == NULL?
               have_match = (_right_part == 0);
            else
            {
               QS_DEF(Array<char>, xyz);

               profTimerStart(txyzlobread, "exact.xyzlobread");
               _lob_xyz->readAll(xyz, false);
               profTimerStop(txyzlobread);

               profTimerStart(tmatch, "exact.match");
               if (instance.matchBinary(cmf, &xyz) == (_right_part == 1))
                  have_match = true;
            }
         }
         else
         {
            profTimerStart(tmatch, "exact.match");
            if (instance.matchBinary(cmf, 0) == (_right_part == 1))
               have_match = true;
         }
      }
      else if (_fetch_type == _GROSS)
      {
         MangoGross &instance = _context.gross;

         if (instance.checkGross(_gross) == (_right_part == 1))
            have_match = true;
      }
      else if (_fetch_type == _MASS)
      {
         have_match = true;
      }
      else
         throw Error("unexpected fetch type %d", _fetch_type);

      if (have_match)
         matched.add(_rowid);
      _processed_rows++;
   } 

   env.dbgPrintf("fetched %d\n", matched.size());

   return;
}

