#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <Efl_Core.h>

#include "ecore_internal.h"

typedef struct _Efl_Filter_Model_Mapping Efl_Filter_Model_Mapping;
struct _Efl_Filter_Model_Mapping
{
   EINA_RBTREE;

   unsigned int original;
   unsigned int mapped;

   EINA_REFCOUNT;
};

typedef struct _Efl_Filter_Model_Data Efl_Filter_Model_Data;
struct _Efl_Filter_Model_Data
{
   Efl_Filter_Model_Mapping *self;

   Eina_Rbtree *mapping;

   struct {
      void *data;
      EflFilterModel cb;
      Eina_Free_Cb free_cb;
      unsigned int count;
   } filter;

   unsigned int counted;
   Eina_Bool counting_started : 1;
   Eina_Bool processed : 1;
};

static Eina_Rbtree_Direction
_filter_mapping_cmp_cb(const Eina_Rbtree *left, const Eina_Rbtree *right, void *data EINA_UNUSED)
{
   const Efl_Filter_Model_Mapping *l, *r;

   l = (const Efl_Filter_Model_Mapping *) left;
   r = (const Efl_Filter_Model_Mapping *) right;

   if (l->mapped < r->mapped)
     return EINA_RBTREE_LEFT;
   return EINA_RBTREE_RIGHT;
}

static int
_filter_mapping_looking_cb(const Eina_Rbtree *node, const void *key,
                           int length EINA_UNUSED, void *data EINA_UNUSED)
{
   const Efl_Filter_Model_Mapping *n = (const Efl_Filter_Model_Mapping *) node;
   const unsigned int *k = key;

   return n->mapped - *k;
}

static void
_efl_filter_model_filter_set(Eo *obj EINA_UNUSED, Efl_Filter_Model_Data *pd,
                             void *filter_data, EflFilterModel filter, Eina_Free_Cb filter_free_cb)
{
   if (pd->filter.cb)
     pd->filter.free_cb(pd->filter.data);
   pd->filter.data = filter_data;
   pd->filter.cb = filter;
   pd->filter.free_cb = filter_free_cb;
}

static void
_rbtree_free_cb(Eina_Rbtree *node, void *data EINA_UNUSED)
{
   Efl_Filter_Model_Mapping *m = (Efl_Filter_Model_Mapping*) node;

   EINA_REFCOUNT_UNREF(m)
     free(m);
}

typedef struct _Efl_Filter_Request Efl_Filter_Request;
struct _Efl_Filter_Request
{
   Efl_Filter_Model_Data *pd;
   Efl_Model *parent;
   Efl_Model *child;
   unsigned int index;
};

static Efl_Filter_Model *
_efl_filter_lookup(const Efl_Class *klass,
                   Efl_Model *parent, Efl_Model *view,
                   Efl_Filter_Model_Mapping *mapping)
{
   Efl_Filter_Model *child;
   Efl_Filter_Model_Data *cpd;

   child = _efl_composite_lookup(klass, parent, view, mapping->mapped);
   if (!child) return NULL;

   cpd = efl_data_scope_get(child, EFL_FILTER_MODEL_CLASS);
   cpd->processed = EINA_TRUE;
   cpd->self = mapping;
   EINA_REFCOUNT_REF(mapping);

   return child;
}

static Eina_Value
_efl_filter_model_filter(Eo *o EINA_UNUSED, void *data, const Eina_Value v)
{
   Efl_Filter_Model_Mapping *mapping;
   Efl_Filter_Model *child;
   Efl_Model_Children_Event cevt = { 0 };
   Efl_Filter_Request *r = data;
   Eina_Value ret = v;
   Eina_Bool result = EINA_FALSE;

   if (!eina_value_bool_get(&v, &result)) goto end;
   if (!result) goto end;

   mapping = calloc(1, sizeof (Efl_Filter_Model_Mapping));
   if (!mapping)
     {
        ret = eina_value_bool_init(EINA_FALSE);
        goto end;
     }
   EINA_REFCOUNT_INIT(mapping);

   mapping->original = r->index;
   mapping->mapped = r->pd->filter.count++;

   r->pd->mapping = eina_rbtree_inline_insert(r->pd->mapping, EINA_RBTREE_GET(mapping),
                                              _filter_mapping_cmp_cb, NULL);

   child = _efl_filter_lookup(efl_class_get(r->parent), r->parent, r->child, mapping);
   if (!child) goto end;

   cevt.index = mapping->mapped;
   cevt.child = child;

   efl_event_callback_call(r->parent, EFL_MODEL_EVENT_CHILD_ADDED, &cevt);
   efl_event_callback_call(r->parent, EFL_MODEL_EVENT_CHILDREN_COUNT_CHANGED, NULL);

   efl_unref(cevt.child);

   ret = eina_value_bool_init(EINA_TRUE);

 end:
   return ret;
}

static void
_efl_filter_model_filter_clean(Eo *o EINA_UNUSED, void *data, const Eina_Future *dead_future EINA_UNUSED)
{
   Efl_Filter_Request *r = data;

   efl_unref(r->parent);
   efl_unref(r->child);
   free(r);
}

static Eina_Value
_efl_filter_model_child_fetch(Eo *o EINA_UNUSED, void *data, const Eina_Value v)
{
   Efl_Filter_Request *r = data;
   unsigned int i, len;
   Eina_Future *f;
   Eo *target = NULL;

   // Get the first and only child in the array
   EINA_VALUE_ARRAY_FOREACH(&v, len, i, target)
     break;

   r->child = efl_ref(target);

   f = r->pd->filter.cb(r->pd->filter.data, r->parent, r->child);
   f = efl_future_then(r->parent, f,
                       .success = _efl_filter_model_filter,
                       .success_type = EINA_VALUE_TYPE_BOOL,
                       .free = _efl_filter_model_filter_clean,
                       .data = r);
   return eina_future_as_value(f);
}

static Eina_Value
_efl_filter_model_child_error(Eo *o, void *data, Eina_Error err)
{
   _efl_filter_model_filter_clean(o, data, NULL);
   return eina_value_error_init(err);
}

static void
_efl_filter_model_child_added(void *data, const Efl_Event *event)
{
   Efl_Filter_Model_Data *pd = data;
   Efl_Model_Children_Event *ev = event->info;
   Efl_Model *child = ev->child;
   Efl_Filter_Request *r;
   Eina_Future *f;

   if (child)
     {
        Efl_Filter_Model_Data *cpd = efl_data_scope_get(child, EFL_FILTER_MODEL_CLASS);

        if (cpd->processed) return ;
     }

   r = calloc(1, sizeof (Efl_Filter_Request));
   if (!r) return ;

   r->pd = pd;
   r->index = ev->index;
   r->parent = efl_ref(event->object);

   if (!child)
     {
        f = efl_model_children_slice_get(efl_ui_view_model_get(r->parent), r->index, 1);
        f = efl_future_then(event->object, f,
                            .success = _efl_filter_model_child_fetch,
                            .success_type = EINA_VALUE_TYPE_ARRAY,
                            .error = _efl_filter_model_child_error,
                            .data = r);
        return ;
     }

   r->child = efl_ref(child);

   f = pd->filter.cb(pd->filter.data, r->parent, r->child);
   f = efl_future_then(event->object, f,
                       .success = _efl_filter_model_filter,
                       .success_type = EINA_VALUE_TYPE_BOOL,
                       .free = _efl_filter_model_filter_clean,
                       .data = r);

   efl_event_callback_stop(event->object);
}

static void
_efl_filter_model_child_removed(void *data, const Efl_Event *event)
{
   Efl_Filter_Model_Mapping *mapping;
   Efl_Filter_Model_Data *pd = data;
   Efl_Model_Children_Event *ev = event->info;
   unsigned int removed = ev->index;

   mapping = (void *)eina_rbtree_inline_lookup(pd->mapping,
                                               &removed, sizeof (uint64_t),
                                               _filter_mapping_looking_cb, NULL);
   if (!mapping) return;

   pd->mapping = eina_rbtree_inline_remove(pd->mapping, EINA_RBTREE_GET(mapping),
                                           _filter_mapping_cmp_cb, NULL);

   EINA_REFCOUNT_UNREF(mapping)
     free(mapping);

   // Update the tree for the index to reflect the removed child
   for (removed++; removed < pd->filter.count; removed++)
     {
        mapping = (void *)eina_rbtree_inline_lookup(pd->mapping,
                                                    &removed, sizeof (uint64_t),
                                                    _filter_mapping_looking_cb, NULL);
        if (!mapping) continue;

        pd->mapping = eina_rbtree_inline_remove(pd->mapping, EINA_RBTREE_GET(mapping),
                                                _filter_mapping_cmp_cb, NULL);
        mapping->mapped--;
        pd->mapping = eina_rbtree_inline_insert(pd->mapping, EINA_RBTREE_GET(mapping),
                                                _filter_mapping_cmp_cb, NULL);
     }
   pd->filter.count--;
   pd->counted--;
}

EFL_CALLBACKS_ARRAY_DEFINE(filters_callbacks,
                           { EFL_MODEL_EVENT_CHILD_ADDED, _efl_filter_model_child_added },
                           { EFL_MODEL_EVENT_CHILD_REMOVED, _efl_filter_model_child_removed });

static void
_efl_filter_model_efl_object_destructor(Eo *obj, Efl_Filter_Model_Data *pd)
{
   eina_rbtree_delete(pd->mapping, _rbtree_free_cb, NULL);

   if (pd->self)
     {
        EINA_REFCOUNT_UNREF(pd->self)
          free(pd->self);
     }
   pd->self = NULL;

   efl_destructor(efl_super(obj, EFL_FILTER_MODEL_CLASS));
}

static Eina_Value
_filter_remove_array(Eo *o EINA_UNUSED, void *data, const Eina_Value v)
{
   Efl_Filter_Model_Mapping *mapping = data;
   unsigned int i, len;
   Eo *target = NULL;

   EINA_VALUE_ARRAY_FOREACH(&v, len, i, target)
     break;

   if (efl_isa(target, EFL_FILTER_MODEL_CLASS))
     {
        Efl_Filter_Model_Data *pd = efl_data_scope_get(target, EFL_FILTER_MODEL_CLASS);

        pd->self = mapping;
        EINA_REFCOUNT_REF(pd->self);
     }

   return eina_value_object_init(target);
}

static Eina_Future *
_efl_filter_model_efl_model_children_slice_get(Eo *obj, Efl_Filter_Model_Data *pd,
                                               unsigned int start, unsigned int count)
{
   Efl_Filter_Model_Mapping **mapping = NULL;
   Eina_Future **r = NULL;
   Eina_Future *f;
   unsigned int i;
   Eina_Error err = ENOMEM;

   if ((uint64_t) start + (uint64_t) count > pd->filter.count)
     return efl_loop_future_rejected(obj, EFL_MODEL_ERROR_INCORRECT_VALUE);
   if (count == 0)
     return efl_loop_future_rejected(obj, EFL_MODEL_ERROR_INCORRECT_VALUE);

   r = calloc(1, (count + 1) * sizeof (Eina_Future *));
   if (!r) return efl_loop_future_rejected(obj, ENOMEM);

   mapping = calloc(count, sizeof (Efl_Filter_Model_Mapping *));
   if (!mapping) goto on_error;

   for (i = 0; i < count; i++)
     {
        unsigned int lookup = start + i;

        mapping[i] = (void *)eina_rbtree_inline_lookup(pd->mapping,
                                                       &lookup, sizeof (unsigned int),
                                                       _filter_mapping_looking_cb, NULL);
        if (!mapping[i]) goto on_error;
     }

   for (i = 0; i < count; i++)
     {
        r[i] = efl_model_children_slice_get(efl_super(obj, EFL_FILTER_MODEL_CLASS),
                                            mapping[i]->original, 1);
        r[i] = efl_future_then(obj, r[i], .success_type = EINA_VALUE_TYPE_ARRAY,
                               .success = _filter_remove_array,
                               .data = mapping[i]);
        if (!r[i]) goto on_error;
     }
   r[i] = EINA_FUTURE_SENTINEL;

   f = efl_future_then(obj, eina_future_all_array(r), .success = _efl_future_all_repack);
   free(r);
   free(mapping);

   return f;

 on_error:
   free(mapping);

   if (r)
     for (i = 0; i < count; i ++)
       if (r[i]) eina_future_cancel(r[i]);
   free(r);

   return efl_loop_future_rejected(obj, err);
}

typedef struct _Efl_Filter_Model_Result Efl_Filter_Model_Result;
struct _Efl_Filter_Model_Result
{
   Efl_Filter_Model_Data *pd;
   unsigned int count;
   Efl_Model *targets[1];
};

// This future receive an array of boolean that indicate if a fetched object is to be kept
static Eina_Value
_efl_filter_model_array_result_request(Eo *o EINA_UNUSED, void *data, const Eina_Value v)
{
   Efl_Filter_Model_Result *req = data;
   Efl_Filter_Model_Data *pd = req->pd;
   unsigned int i, len;
   Eina_Value request = EINA_VALUE_EMPTY;
   unsigned int pcount = pd->filter.count;

   EINA_VALUE_ARRAY_FOREACH(&v, len, i, request)
     {
        Efl_Filter_Model_Mapping *mapping;
        Efl_Model_Children_Event cevt = { 0 };
        Eina_Bool b;

        if (eina_value_type_get(&request) != EINA_VALUE_TYPE_BOOL)
          continue ;

        if (!eina_value_bool_get(&request, &b)) continue;
        if (!b) continue;

        mapping = calloc(1, sizeof (Efl_Filter_Model_Mapping));
        if (!mapping) continue;

        EINA_REFCOUNT_INIT(mapping);
        mapping->original = i;
        mapping->mapped = pd->filter.count++;

        // Insert in tree here
        pd->mapping = eina_rbtree_inline_insert(pd->mapping, EINA_RBTREE_GET(mapping),
                                                _filter_mapping_cmp_cb, NULL);

        cevt.index = mapping->mapped;
        cevt.child = _efl_filter_lookup(efl_class_get(o), o, req->targets[i], mapping);
        if (!cevt.child) continue;

        efl_event_callback_call(o, EFL_MODEL_EVENT_CHILD_ADDED, &cevt);
        efl_unref(cevt.child);
     }

   if (pcount != pd->filter.count)
     efl_event_callback_call(o, EFL_MODEL_EVENT_CHILDREN_COUNT_CHANGED, NULL);

   return v;
}

static void
_efl_filter_model_array_result_free(Eo *o EINA_UNUSED, void *data, const Eina_Future *dead_future EINA_UNUSED)
{
   Efl_Filter_Model_Result *req = data;
   unsigned int i;

   for (i = 0; i < req->count; i++)
     efl_unref(req->targets[i]);
   free(req);
}

// This future receive an array of children object
static Eina_Value
_efl_filter_model_array_fetch(Eo *o, void *data, const Eina_Value v)
{
   Efl_Filter_Model_Result *req;
   Efl_Filter_Model_Data *pd = data;
   unsigned int i, len;
   Eo *target = NULL;
   Eina_Future **array = NULL;
   Eina_Future *r;
   Eina_Value res = v;

   if (!eina_value_array_count(&v)) return v;

   array = malloc((eina_value_array_count(&v) + 1) * sizeof (Eina_Future*));
   if (!array) return eina_value_error_init(ENOMEM);

   req = malloc(sizeof (Efl_Filter_Model_Result) +
                sizeof (Eo*) * (eina_value_array_count(&v) - 1));
   if (!req)
     {
        res = eina_value_error_init(ENOMEM);
        goto on_error;
     }

   req->pd = pd;
   req->count = eina_value_array_count(&v);

   EINA_VALUE_ARRAY_FOREACH(&v, len, i, target)
     {
        array[i] = pd->filter.cb(pd->filter.data, o, target);
        req->targets[i] = efl_ref(target);
     }

   array[i] = EINA_FUTURE_SENTINEL;

   r = eina_future_all_array(array);
   r = efl_future_then(o, r, .success_type = EINA_VALUE_TYPE_ARRAY,
                       .success = _efl_filter_model_array_result_request,
                       .free = _efl_filter_model_array_result_free,
                       .data = req);
   res = eina_future_as_value(r);

 on_error:
   free(array);

   return res;
}

static unsigned int
_efl_filter_model_efl_model_children_count_get(const Eo *obj, Efl_Filter_Model_Data *pd)
{
   if (!pd->counting_started && pd->filter.cb)
     {
        pd->counting_started = EINA_TRUE;

        // Start watching for children now
        efl_event_callback_array_add((Eo *)obj, filters_callbacks(), pd);

        // Start counting (which may trigger filter being added asynchronously)
        pd->counted = efl_model_children_count_get(efl_super(obj, EFL_FILTER_MODEL_CLASS));
        if (pd->counted > 0)
          {
             Eina_Future *f;

             f = efl_model_children_slice_get(efl_ui_view_model_get(obj), 0, pd->counted);
             efl_future_then(obj, f, .success_type = EINA_VALUE_TYPE_ARRAY,
                             .success = _efl_filter_model_array_fetch,
                             .data = pd);
          }
     }

   return pd->filter.count;
}

static Eina_Value *
_efl_filter_model_efl_model_property_get(const Eo *obj, Efl_Filter_Model_Data *pd,
                                         const char *property)
{
   if (pd->self && eina_streq(property, EFL_COMPOSITE_MODEL_CHILD_INDEX))
     {
        return eina_value_uint64_new(pd->self->mapped);
     }

   return efl_model_property_get(efl_super(obj, EFL_FILTER_MODEL_CLASS), property);
}

static unsigned int
_efl_filter_model_efl_composite_model_index_get(const Eo *obj, Efl_Filter_Model_Data *pd)
{
   if (pd->self) return pd->self->mapped;
   return efl_composite_model_index_get(efl_super(obj, EFL_FILTER_MODEL_CLASS));
}

#include "efl_filter_model.eo.c"
