#include "e_mod_main.h"

#define HISTORY_VERSION   2

#define SEVEN_DAYS        604800.0

/* old history entries will be removed when threshold is reached */
#define CLEANUP_THRESHOLD 500

#define TIME_FACTOR(_now) (1.0 - (evry_hist->begin / _now)) / 1000000000000000.0

typedef struct _Cleanup_Data Cleanup_Data;

struct _Cleanup_Data
{
   double      time;
   Eina_List  *keys;
   Eina_Bool   normalize;
   const char *plugin;
};

static E_Config_DD *hist_entry_edd = NULL;
static E_Config_DD *hist_item_edd = NULL;
static E_Config_DD *hist_types_edd = NULL;
static E_Config_DD *hist_edd = NULL;

Evry_History *evry_hist = NULL;

void
evry_history_init(void)
{
#undef T
#undef D
   hist_item_edd = E_CONFIG_DD_NEW("History_Item", History_Item);
#define T History_Item
#define D hist_item_edd
   E_CONFIG_VAL(D, T, plugin, STR);
   E_CONFIG_VAL(D, T, context, STR);
   E_CONFIG_VAL(D, T, input, STR);
   E_CONFIG_VAL(D, T, last_used, DOUBLE);
   E_CONFIG_VAL(D, T, usage, DOUBLE);
   E_CONFIG_VAL(D, T, count, INT);
   E_CONFIG_VAL(D, T, transient, INT);
   E_CONFIG_VAL(D, T, data, STR);
#undef T
#undef D
   hist_entry_edd = E_CONFIG_DD_NEW("History_Entry", History_Entry);
#define T History_Entry
#define D hist_entry_edd
   E_CONFIG_LIST(D, T, items, hist_item_edd);
#undef T
#undef D
   hist_types_edd = E_CONFIG_DD_NEW("History_Types", History_Types);
#define T History_Types
#define D hist_types_edd
   E_CONFIG_HASH(D, T, types, hist_entry_edd);
#undef T
#undef D
   hist_edd = E_CONFIG_DD_NEW("History", Evry_History);
#define T Evry_History
#define D hist_edd
   E_CONFIG_VAL(D, T, version, INT);
   E_CONFIG_VAL(D, T, begin, DOUBLE);
   E_CONFIG_HASH(D, T, subjects, hist_types_edd);
#undef T
#undef D
}

static Eina_Bool
_hist_entry_free_cb(const Eina_Hash *hash EINA_UNUSED, const void *key EINA_UNUSED, void *data, void *fdata EINA_UNUSED)
{
   History_Entry *he = data;
   History_Item *hi;

   EINA_LIST_FREE (he->items, hi)
     {
        if (hi->input)
          eina_stringshare_del(hi->input);
        if (hi->plugin)
          eina_stringshare_del(hi->plugin);
        if (hi->context)
          eina_stringshare_del(hi->context);
        if (hi->data)
          eina_stringshare_del(hi->data);
        E_FREE(hi);
     }

   E_FREE(he);

   return EINA_TRUE;
}

static Eina_Bool
_hist_free_cb(const Eina_Hash *hash EINA_UNUSED, const void *key EINA_UNUSED, void *data, void *fdata EINA_UNUSED)
{
   History_Types *ht = data;

   if (ht->types)
     {
        eina_hash_foreach(ht->types, _hist_entry_free_cb, NULL);
        eina_hash_free(ht->types);
     }

   E_FREE(ht);

   return EINA_TRUE;
}

static Eina_Bool
_hist_entry_cleanup_cb(const Eina_Hash *hash EINA_UNUSED, const void *key, void *data, void *fdata)
{
   History_Entry *he = data;
   Cleanup_Data *d = fdata;
   History_Item *hi;
   Eina_List *l, *ll;

   EINA_LIST_FOREACH_SAFE (he->items, l, ll, hi)
     {
        if (hi->last_used < d->time - SEVEN_DAYS)
          {
             hi->count--;
             hi->last_used = d->time - SEVEN_DAYS / 2.0;
          }

        /* item is transient or too old */
        if ((hi->count < 1) || hi->transient)
          {
             if (hi->input)
               eina_stringshare_del(hi->input);
             if (hi->plugin)
               eina_stringshare_del(hi->plugin);
             if (hi->context)
               eina_stringshare_del(hi->context);
             if (hi->data)
               eina_stringshare_del(hi->data);
             E_FREE(hi);

             he->items = eina_list_remove_list(he->items, l);
          }
     }

   if (!he->items)
     {
        E_FREE(he);
        d->keys = eina_list_append(d->keys, key);
     }

   return EINA_TRUE;
}

static Eina_Bool
_hist_cleanup_cb(const Eina_Hash *hash EINA_UNUSED, const void *key, void *data, void *fdata)
{
   History_Types *ht = data;
   Cleanup_Data *d = fdata;

   if (ht->types)
     {
        eina_hash_foreach(ht->types, _hist_entry_cleanup_cb, fdata);

        EINA_LIST_FREE (d->keys, key)
          eina_hash_del_by_key(ht->types, key);
     }

   return EINA_TRUE;
}

void
evry_history_free(void)
{
   evry_hist = e_config_domain_load("module.everything.cache", hist_edd);

   if ((evry_hist) && (evry_hist->subjects) &&
       (eina_hash_population(evry_hist->subjects) > CLEANUP_THRESHOLD))
     {
        Cleanup_Data *d;

        d = E_NEW(Cleanup_Data, 1);
        d->time = ecore_time_unix_get();
        eina_hash_foreach(evry_hist->subjects, _hist_cleanup_cb, d);
        E_FREE(d);
     }

   evry_history_unload();

   E_CONFIG_DD_FREE(hist_item_edd);
   E_CONFIG_DD_FREE(hist_entry_edd);
   E_CONFIG_DD_FREE(hist_types_edd);
   E_CONFIG_DD_FREE(hist_edd);
}

void
evry_history_load(void)
{
   if (evry_hist) return;

   evry_hist = e_config_domain_load("module.everything.cache", hist_edd);

   if (evry_hist && evry_hist->version != HISTORY_VERSION)
     {
        eina_hash_foreach(evry_hist->subjects, _hist_free_cb, NULL);
        eina_hash_free(evry_hist->subjects);

        E_FREE(evry_hist);
        evry_hist = NULL;
     }

   if (!evry_hist)
     {
        evry_hist = E_NEW(Evry_History, 1);
        evry_hist->version = HISTORY_VERSION;
        evry_hist->begin = ecore_time_unix_get() - SEVEN_DAYS;
     }
   if (!evry_hist->subjects)
     evry_hist->subjects = eina_hash_string_superfast_new(NULL);
}

void
evry_history_unload(void)
{
   if (!evry_hist) return;

   e_config_domain_save("module.everything.cache", hist_edd, evry_hist);

   eina_hash_foreach(evry_hist->subjects, _hist_free_cb, NULL);
   eina_hash_free(evry_hist->subjects);

   E_FREE(evry_hist);
   evry_hist = NULL;
}

History_Types *
evry_history_types_get(Evry_Type _type)
{
   History_Types *ht;
   const char *type = evry_type_get(_type);

   if (!evry_hist)
     return NULL;

   if (!type)
     return NULL;

   ht = eina_hash_find(evry_hist->subjects, type);

   if (!ht)
     {
        ht = E_NEW(History_Types, 1);
        eina_hash_add(evry_hist->subjects, type, ht);
     }

   if (!ht->types)
     ht->types = eina_hash_string_superfast_new(NULL);

   return ht;
}

History_Item *
evry_history_item_add(Evry_Item *it, const char *ctxt, const char *input)
{
   History_Entry *he;
   History_Types *ht;
   History_Item *hi = NULL;
   Eina_List *l;
   int rem_ctxt = 1;
   const char *id;

   if (!evry_hist)
     return NULL;

   if (!it)
     return NULL;

   if ((!it->plugin->history) && !(CHECK_TYPE(it, EVRY_TYPE_PLUGIN)))
     return NULL;

   if (it->type == EVRY_TYPE_ACTION)
     {
        GET_ACTION(act, it);
        if (!act->remember_context)
          rem_ctxt = 0;
     }

   if (it->hi)
     {
        /* keep hi when context didn't change */
        if ((!rem_ctxt) || (!it->hi->context && !ctxt) ||
            (it->hi->context && ctxt && !strcmp(it->hi->context, ctxt)))
          hi = it->hi;
     }

   if (!hi)
     {
        id = (it->id ? it->id : it->label);
        ht = evry_history_types_get(it->type);
        if (!ht)
          return NULL;

        he = eina_hash_find(ht->types, id);

        if (!he)
          {
             he = E_NEW(History_Entry, 1);
             eina_hash_add(ht->types, id, he);
          }
        else
          {
             EINA_LIST_FOREACH (he->items, l, hi)
               if ((hi->plugin == it->plugin->name) &&
                   (!rem_ctxt || (ctxt == hi->context)))
                 break;
          }
     }

   if (!hi)
     {
        hi = E_NEW(History_Item, 1);
        hi->plugin = eina_stringshare_ref(it->plugin->name);
        he->items = eina_list_append(he->items, hi);
     }

   if (hi)
     {
        it->hi = hi;

        hi->last_used = ecore_time_unix_get();
        hi->usage /= 4.0;
        hi->usage += TIME_FACTOR(hi->last_used);
        hi->transient = it->plugin->transient;
        hi->count += 1;

        if (ctxt && !hi->context && rem_ctxt)
          {
             hi->context = eina_stringshare_ref(ctxt);
          }

        if (input && hi->input)
          {
             if (strncmp(hi->input, input, strlen(input)))
               {
                  eina_stringshare_del(hi->input);
                  hi->input = eina_stringshare_add(input);
               }
          }
        else if (input)
          {
             hi->input = eina_stringshare_add(input);
          }
     }

   /* reset usage */
   it->usage = 0.0;

   return hi;
}

int
evry_history_item_usage_set(Evry_Item *it, const char *input, const char *ctxt)
{
   History_Entry *he;
   History_Types *ht;
   History_Item *hi = NULL;
   Eina_List *l;
   int rem_ctxt = 1;

   if (evry_conf->history_sort_mode == 3)
     {
        it->usage = -1;
        return 1;
     }
   else
     it->usage = 0.0;

   if ((!it->plugin->history) && !(CHECK_TYPE(it, EVRY_TYPE_PLUGIN)))
     return 0;

   if (it->hi)
     {
        /* keep hi when context didn't change */
        if ((!rem_ctxt) || (!it->hi->context && !ctxt) ||
            (it->hi->context && ctxt && !strcmp(it->hi->context, ctxt)))
          hi = it->hi;
     }

   if (!hi)
     {
        ht = evry_history_types_get(it->type);

        if (!ht)
          return 0;

        if (!(he = eina_hash_find(ht->types, (it->id ? it->id : it->label))))
          return 0;

        if (it->type == EVRY_TYPE_ACTION)
          {
             GET_ACTION(act, it);
             if (!act->remember_context)
               rem_ctxt = 0;
          }

        EINA_LIST_FOREACH (he->items, l, hi)
          {
             if (hi->plugin != it->plugin->name)
               continue;

             if (rem_ctxt && ctxt && (hi->context != ctxt))
               {
                  it->hi = hi;
                  continue;
               }

             it->hi = hi;
             break;
          }
     }

   if (!hi) return 0;

   if (evry_conf->history_sort_mode == 0)
     {
        if (!input || !hi->input)
          {
             it->usage += hi->usage * hi->count;
          }
        else
          {
             /* higher priority for exact matches */
             if (!strncmp(input, hi->input, strlen(input)))
               {
                  it->usage += hi->usage * hi->count;
               }
             if (!strncmp(input, hi->input, strlen(hi->input)))
               {
                  it->usage += hi->usage * hi->count;
               }
          }

        if (ctxt && hi->context && (hi->context == ctxt))
          {
             it->usage += hi->usage * hi->count * 10.0;
          }
     }
   else if (evry_conf->history_sort_mode == 1)
     {
        it->usage = hi->count * (hi->last_used / 10000000000.0);

        if (ctxt && hi->context && (hi->context == ctxt))
          {
             it->usage += hi->usage * hi->count * 10.0;
          }
     }
   else if (evry_conf->history_sort_mode == 2)
     {
        if (hi->last_used > it->usage)
          it->usage = hi->last_used;
     }
   if (it->fuzzy_match > 0)
     it->usage /= (double)it->fuzzy_match;
   else
     it->usage /= 100.0;

   if (it->usage > 0.0)
     return 1;

   it->usage = -1.0;

   return 0;
}

