using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Data.SqlClient;
using System.Resources;
using System.Reflection;

namespace indigo
{
   public abstract class BingoIndexData
   {
      public BingoIndexID id;
      public string id_column;
      public string data_column;
      public string bingo_schema;

      public BingoFingerprints fingerprints;
      public BingoStorage storage;

      public enum IndexType { Molecule, Reaction }

      public bool keep_cache;
      public bool locked = false;

      public BingoIndexData (BingoIndexID id, string id_column, string data_column, string bingo_schema)
      {
         this.id = id;
         this.id_column = id_column;
         this.data_column = data_column;
         this.bingo_schema = bingo_schema;

         keep_cache = false;

         fingerprints = new BingoFingerprints(this);
         storage = new BingoStorage(this);
      }

      private static BingoIndexData _extractIndexData (SqlConnection conn, string bingo_schema, 
         BingoIndexID id, bool throw_if_not_exists)
      {
         BingoIndexData data = null;

         using (SqlCommand cmd = new SqlCommand(String.Format(
            "SELECT id_column, data_column, type FROM {0} WHERE obj_id = '{1}'",
            _contextTable(bingo_schema), id.table_id), conn))
         {
            using (SqlDataReader reader = cmd.ExecuteReader())
            {
               if (!reader.Read())
               {
                  if (throw_if_not_exists)
                     throw new Exception("Cannot find Bingo index for table with id=" + id.table_id);
                  else
                     return null;
               }

               string id_column = Convert.ToString(reader[0]);
               string data_column = Convert.ToString(reader[1]);
               string type = Convert.ToString(reader[2]);

               if (type.ToLower().Equals("molecule"))
                  data = new MangoIndexData(id, id_column, data_column, bingo_schema);
               if (type.ToLower().Equals("reaction"))
                  data = new RingoIndexData(id, id_column, data_column, bingo_schema);

               if (data == null)
                  throw new Exception("unknown type: " + type);

            }
         }

         data.keep_cache =
            (BingoConfig.getInt(conn, bingo_schema, "KEEP_CACHE", id.table_id) != 0);

         return data;
      }

      public abstract IndexType getIndexType ();

      private static string _contextTable (string bingo_schema)
      {
         return "[" + bingo_schema + "].CONTEXT";
      }

      public class BingoIndexDataRefs
      {
         public BingoIndexData index_data;
         public List<int> session_ids;
      }

      private static object index_data_list_lock = new Object();
      private static ArrayList index_data_list = new ArrayList();

      public static BingoIndexData CreateIndexData (int spid, SqlConnection conn, string bingo_schema,
                      string table, string id_column, string data_column, bool reaction)
      {
         lock (index_data_list_lock)
         {
            BingoIndexID id = new BingoIndexID(conn, table);
            _DropIndexData(conn, bingo_schema, id, false);

            BingoIndexData data;

            if (reaction)
               data = new RingoIndexData(id, id_column, data_column, bingo_schema);
            else
               data = new MangoIndexData(id, id_column, data_column, bingo_schema);

            BingoSqlUtils.ExecNonQuery(conn,
               "INSERT INTO {0} VALUES({1}, {2}, '{3}', '{4}', '{5}', '{6}')",
               _contextTable(bingo_schema), id.table_id, id.database_id, id.FullTableName(conn),
               id_column, data_column,
               reaction ? "reaction" : "molecule");

            data.CreateTables(conn);
            _AddIndexDataToList(spid, data);

            BingoLog.logMessage("Bingo index for table {0} has been initialized", id.FullTableName(conn));

            return data;
         }
      }

      public static void DropIndexData (SqlConnection conn, string bingo_schema, string table, bool throw_if_not_exists)
      {
         BingoIndexID id = new BingoIndexID(conn, table);
         DropIndexData(conn, bingo_schema, id, throw_if_not_exists);
      }

      public static void DropIndexData (SqlConnection conn, string bingo_schema, BingoIndexID id, bool throw_if_not_exists)
      {
         lock (index_data_list_lock)
         {
            _DropIndexData(conn, bingo_schema, id, throw_if_not_exists);
         }
      }

      private static void _DropIndexData (SqlConnection conn, string bingo_schema, 
         BingoIndexID id, bool throw_if_not_exists)
      {
         BingoIndexData data = _extractIndexData(conn, bingo_schema, id, throw_if_not_exists);
         if (data != null)
         {
            data.DropTables(conn);
            data.DropTriggers(conn);
         }

         BingoSqlUtils.ExecNonQueryNoThrow(conn, "DELETE FROM {0} WHERE obj_id = '{1}'",
            _contextTable(bingo_schema), id.table_id);

         for (int i = index_data_list.Count - 1; i >= 0; i--)
         {
            BingoIndexDataRefs refs = (BingoIndexDataRefs)index_data_list[i];
            if (refs.index_data.id.Equals(id))
            {
               index_data_list.RemoveAt(i);
               BingoLog.logMessage("Sessions for table {0} have been released", id.InformationName(conn));
            }
         }
         if (data != null)
            BingoLog.logMessage("Bingo index for table {0} has been dropped", id.InformationName(conn));
      }

      public static BingoIndexData GetIndexData (SqlConnection conn, string bingo_schema, 
         int table_id, int database_id, int spid)
      {
         BingoIndexID id = new BingoIndexID(table_id, database_id);
         return GetIndexDataById(conn, bingo_schema, id, spid);
      }

      public static BingoIndexData GetIndexData (SqlConnection conn, string bingo_schema, 
         string table, int spid)
      {
         BingoIndexID id = new BingoIndexID(conn, table);
         return GetIndexDataById(conn, bingo_schema, id, spid);
      }
      private static BingoIndexData GetIndexDataById (SqlConnection conn, string bingo_schema, 
         BingoIndexID id, int spid)
      {
         lock (index_data_list_lock)
         {
            foreach (BingoIndexDataRefs index_data_refs in index_data_list)
            {
               if (index_data_refs.index_data.id.Equals(id))
               {
                  if (!index_data_refs.session_ids.Contains(spid))
                  {
                     BingoLog.logMessage("Existing BingoIndexData added for spid={0} table={1}",
                        spid, id.FullTableName(conn));
                     index_data_refs.session_ids.Add(spid);
                  }
                  return index_data_refs.index_data;
               }
            }

            BingoLog.logMessage("Extracting new BingoIndexData for spid={0} table={1}",
               spid, id.FullTableName(conn));

            BingoIndexData data = _extractIndexData(conn, bingo_schema, id, true);
            _AddIndexDataToList(spid, data);

            return data;
         }
      }

      private static void _AddIndexDataToList (int spid, BingoIndexData data)
      {
         BingoIndexDataRefs new_refs = new BingoIndexDataRefs();

         new_refs.index_data = data;
         new_refs.session_ids = new List<int>();
         new_refs.session_ids.Add(spid);

         index_data_list.Add(new_refs);
      }

      class TableWithId
      {
         public string table_name { get; set; }
         public int id { get; set; }
      };

      public static void DropAllIndices (SqlConnection conn, string bingo_schema, bool only_invalid)
      {
         List<TableWithId> tables = new List<TableWithId>();
         using (SqlCommand cmd = new SqlCommand(String.Format(
            "SELECT full_table_name, obj_id FROM {0}",
            _contextTable(bingo_schema)), conn))
         {
            cmd.CommandTimeout = 3600;
            using (SqlDataReader reader = cmd.ExecuteReader())
            {
               while (reader.Read())
               {
                  string full_table_name = Convert.ToString(reader[0]);
                  int table_id = Convert.ToInt32(reader[1]);
                  tables.Add(new TableWithId() { table_name = full_table_name, id = table_id } );
               }
            }
         }

         foreach (TableWithId table in tables)
         {
            if (only_invalid)
            {
               object ret = BingoSqlUtils.ExecObjQuery(conn, "SELECT OBJECT_NAME({0})", table.id);
               if (ret != null)
                  continue;
            }
            DropIndexData(conn, bingo_schema, table.table_name, false);
         }
      }

      public static void FlushInAllSessions (int spid, bool check_spid)
      {
         lock (index_data_list_lock)
         {
            SqlConnection ctx_conn = null;
            try
            {
               for (int i = index_data_list.Count - 1; i >= 0; i--)
               {
                  BingoIndexDataRefs refs = (BingoIndexDataRefs)index_data_list[i];

                  if (check_spid && !refs.session_ids.Contains(spid))
                     continue;

                  if (!refs.index_data.needFlush())
                     continue;

                  if (ctx_conn == null)
                  {
                     ctx_conn = new SqlConnection("context connection=true");
                     ctx_conn.Open();
                  }
                  refs.index_data.flush(ctx_conn);
               }
            }
            finally
            {
               if (ctx_conn != null)
                  ctx_conn.Close();
            }
         }
      }

      public static void OnSessionClose (int spid)
      {
         FlushInAllSessions(spid, true);
         lock (index_data_list_lock)  
         {
            for (int i = index_data_list.Count - 1; i >= 0; i--)
            {
               BingoIndexDataRefs refs = (BingoIndexDataRefs)index_data_list[i];

               refs.session_ids.Remove(spid);
               if (refs.session_ids.Count < 1 && !refs.index_data.keep_cache)
               {
                  index_data_list.RemoveAt(i);
                  BingoLog.logMessage("Session for table {0} has been released", 
                     refs.index_data.id.InformationName());
               }
            }
         }
      }

      public virtual void CreateTables (SqlConnection conn)
      {
         storage.createTables(conn);
         fingerprints.createTables(conn);
      }

      public virtual void DropTables (SqlConnection conn)
      {
         storage.dropTables(conn);
         fingerprints.dropTables(conn);
      }

      public virtual bool needFlush()
      {
         if (storage.needFlush() || fingerprints.needFlush())
            return true;
         return false;
      }

      public virtual void flush (SqlConnection conn)
      {
         storage.flush(conn);
         fingerprints.flush(conn);
      }

      public virtual void prepareForDeleteRecord (SqlConnection conn)
      {
      }

      public void CreateTriggers (SqlConnection conn)
      {
         string cur_db_name = null;
         try
         {
            cur_db_name = BingoSqlUtils.ExecStringQuery(conn, "SELECT DB_NAME()");
            BingoSqlUtils.ExecNonQueryNoThrow(conn, "USE {0}", id.DatabaseName(conn));

            string bingo_db_schema = cur_db_name + "." + bingo_schema;

            string full_name = id.FullTableName(conn);
            object[] trigger_params = new object[] { null, 
               full_name, id_column, data_column, bingo_db_schema, id.table_id, id.database_id };

            trigger_params[0] = GetTriggerName("Insert", conn, cur_db_name);
            string insert_trigger = String.Format(resource.OnInsertTrigger, trigger_params);
            BingoSqlUtils.ExecNonQuery(conn, "{0}", insert_trigger);

            trigger_params[0] = GetTriggerName("Delete", conn, cur_db_name);
            string delete_trigger = String.Format(resource.OnDeleteTrigger, trigger_params);
            BingoSqlUtils.ExecNonQuery(conn, "{0}", delete_trigger);

            trigger_params[0] = GetTriggerName("Update", conn, cur_db_name);
            string update_trigger = String.Format(resource.OnUpdateTrigger, trigger_params);
            BingoSqlUtils.ExecNonQuery(conn, "{0}", update_trigger);
         }
         finally
         {
            if (cur_db_name != null)
               BingoSqlUtils.ExecNonQueryNoThrow(conn, "USE {0}", cur_db_name);
         }
      }

      public void DropTriggers (SqlConnection conn)
      {
         string cur_db_name = null;
         try
         {
            cur_db_name = BingoSqlUtils.ExecStringQuery(conn, "SELECT DB_NAME()");
            BingoSqlUtils.ExecNonQueryNoThrow(conn, "USE {0}", id.DatabaseName(conn));
            BingoSqlUtils.ExecNonQueryNoThrow(conn, "DROP TRIGGER {0}", GetTriggerName("Insert", conn, cur_db_name));
            BingoSqlUtils.ExecNonQueryNoThrow(conn, "DROP TRIGGER {0}", GetTriggerName("Delete", conn, cur_db_name));
            BingoSqlUtils.ExecNonQueryNoThrow(conn, "DROP TRIGGER {0}", GetTriggerName("Update", conn, cur_db_name));
         }
         finally
         {
            if (cur_db_name != null)
               BingoSqlUtils.ExecNonQueryNoThrow(conn, "USE {0}", cur_db_name);
         }
      }

      public virtual void deleteRecordById (int id, SqlConnection conn)
      {
         int? storage_id = getStorageIdById(conn, id);
         if (!storage_id.HasValue)
            // Such molecule wasn't added to the molecule
            // index because it might be invalid
            return;
         storage.deleteRecord(storage_id.Value, conn);
         BingoSqlUtils.ExecNonQuery(conn, "DELETE from {0} where id={1}",
            shadowTable, id);
      }

      public virtual void createIndices (SqlConnection conn)
      {
      }

      private string GetTriggerName (string operation, SqlConnection conn, string bingo_db)
      {
         return String.Format("{0}.[bingo_{1}_{2}_{3}]",
            id.SchemaName(conn), bingo_db, id.table_id, operation);
      }

      public string fingerprintsTable
      {
         get { return "[" + bingo_schema + "].fingerprints_" + id.table_id; }
      }

      public string fingerprintBitsTable
      {
         get { return "[" + bingo_schema + "].fingerprint_bits_" + id.table_id; }
      }

      public string storageTable
      {
         get { return "[" + bingo_schema + "].storage_" + id.table_id; }
      }

      public string shadowTable
      {
         get { return "[" + bingo_schema + "].shadow_" + id.table_id; }
      }

      public void syncContextParameters (SqlConnection conn, string bingo_schema)
      {
         fingerprints.syncContextParameters(getIndexType() == IndexType.Reaction);

         keep_cache =
            (BingoConfig.getInt(conn, bingo_schema, "KEEP_CACHE", id.table_id) != 0);
      }

      public void setKeepCache (SqlConnection conn, string bingo_schema, bool keep)
      {
         if (keep_cache != keep)
         {
            keep_cache = keep;
            BingoConfig.setInt(conn, bingo_schema, "KEEP_CACHE", id.table_id, keep ? 1 : 0);

            BingoLog.logMessage("SetKeepCache has changed for {0} table. New value is {1}", 
               id.FullTableName(conn), keep);
         }
      }

      public int? getStorageIdById (SqlConnection conn, int id)
      {
         return BingoSqlUtils.ExecIntQuery(conn, "SELECT storage_id from {0} where id={1}",
               shadowTable, id);
      }
   }
}
