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

#include <Elementary.h>
#include "elm_priv.h"

// This class store the size information in a compressed array and unpack it
// when necessary. It does maintain a cache of up to 3 uncompressed slot.
// That cache could get dropped when the application is entering the 'pause'
// state.

#define EFL_UI_EXACT_MODEL_CONTENT 1024
#define EFL_UI_EXACT_MODEL_CONTENT_LENGTH (EFL_UI_EXACT_MODEL_CONTENT * sizeof (unsigned int))

// For now only vertical logic is implemented. Horizontal list and grid are not supported.

typedef struct _Efl_Ui_Exact_Model_Data Efl_Ui_Exact_Model_Data;
struct _Efl_Ui_Exact_Model_Data
{
   Efl_Ui_Exact_Model_Data *parent;

   struct {
      Eina_List *width;
      Eina_List *height;
   } compressed;

   struct {
      unsigned int width;
      unsigned int height;
   } total_size;

   struct {
      unsigned int *width;
      unsigned int *height;
      unsigned int start_offset;
      unsigned short usage;
      Eina_Bool defined : 1;
      struct {
         Eina_Bool width : 1;
         Eina_Bool height : 1;
      } decompressed;
   } slot[3];
};

static Efl_Object *
_efl_ui_exact_model_efl_object_constructor(Eo *obj, Efl_Ui_Exact_Model_Data *pd)
{
   Eo *parent = efl_parent_get(obj);

   if (parent && efl_isa(parent, EFL_UI_EXACT_MODEL_CLASS))
     pd->parent = efl_data_scope_get(efl_parent_get(obj), EFL_UI_EXACT_MODEL_CLASS);

   return efl_constructor(efl_super(obj, EFL_UI_EXACT_MODEL_CLASS));
}

static Eina_List *
_efl_ui_exact_model_slot_compress(unsigned int index, Eina_List *compressed, unsigned int *buffer)
{
   unsigned int list_index = index / EFL_UI_EXACT_MODEL_CONTENT;
   static Eina_Binbuf *z = NULL;
   Eina_Binbuf *cbuf;
   Eina_Binbuf *tbuf;
   Eina_List *l = NULL;
   unsigned int i;

   l = eina_list_nth_list(compressed, list_index);

   tbuf = eina_binbuf_manage_new((unsigned char *) buffer, EFL_UI_EXACT_MODEL_CONTENT_LENGTH, EINA_TRUE);
   if (!tbuf) return compressed;

   cbuf = emile_compress(tbuf, EMILE_LZ4, EMILE_COMPRESSOR_FAST);
   eina_binbuf_free(tbuf);
   if (!cbuf) return compressed;

   // Make sure the list has all the buffer up to the needed one filled with valid data
   if (list_index)
     {
        // Create the compressed zero buffer once.
        if (!z)
          {
             unsigned char *zmem;

             zmem = calloc(EFL_UI_EXACT_MODEL_CONTENT, sizeof (unsigned int));
             if (!zmem)
               {
                  if (cbuf) eina_binbuf_free(cbuf);
                  return compressed;
               }

             tbuf = eina_binbuf_manage_new(zmem, EFL_UI_EXACT_MODEL_CONTENT_LENGTH, EINA_TRUE);
             if (!tbuf)
               {
                  if (cbuf) eina_binbuf_free(cbuf);
                  if (zmem) free(zmem);
                  return compressed;
               }

             z = emile_compress(tbuf, EMILE_LZ4, EMILE_COMPRESSOR_FAST);

             eina_binbuf_free(tbuf);
             free(zmem);
          }

        // Fill the list all the way to the needed index with buffer full of zero
        for (i = 0; i < list_index; i++)
          {
             compressed = eina_list_append(compressed, z);
          }
        l = eina_list_last(compressed);
     }

   // Replace older buffer by newer buffer
   tbuf = eina_list_data_get(l);
   compressed = eina_list_prepend_relative(compressed, l, cbuf);
   compressed = eina_list_remove_list(compressed, l);
   if (tbuf != z) eina_binbuf_free(tbuf);

   return compressed;
}

static unsigned int *
_efl_ui_exact_model_buffer_expand(unsigned int list_index, unsigned int *buffer, Eina_List *list)
{
   Eina_Binbuf *tmp;
   Eina_List *l = NULL;

   if (!buffer) buffer = malloc(EFL_UI_EXACT_MODEL_CONTENT_LENGTH);

   l = eina_list_nth_list(list, list_index);

   // Check if the data is in the list
   if (!l)
     {
        // Not found -> everything is assumed to be zero
        memset(buffer, 0, EFL_UI_EXACT_MODEL_CONTENT_LENGTH);
        return buffer;
     }

   // Found -> expand in buffer
   tmp = eina_binbuf_manage_new((unsigned char*) buffer, EFL_UI_EXACT_MODEL_CONTENT_LENGTH, EINA_TRUE);
   emile_expand(eina_list_data_get(l), tmp, EMILE_LZ4);
   eina_binbuf_free(tmp);

   return buffer;
}

static unsigned char
_efl_ui_exact_model_slot_find(Efl_Ui_Exact_Model_Data *pd, unsigned int index,
                              Eina_Bool width_get, Eina_Bool height_get)
{
   unsigned char lookup;
   unsigned char found = EINA_C_ARRAY_LENGTH(pd->parent->slot);

   for (lookup = 0; lookup < EINA_C_ARRAY_LENGTH(pd->parent->slot); lookup++)
     {
        // Check if the slot has valid content
        if (!pd->parent->slot[lookup].defined)
          continue;
        if (pd->parent->slot[lookup].start_offset <= index &&
            index < pd->parent->slot[lookup].start_offset + EFL_UI_EXACT_MODEL_CONTENT)
          found = lookup;
        // Reduce usage to find unused slot.
        if (pd->parent->slot[lookup].usage > 0)
          pd->parent->slot[lookup].usage--;
     }

   // Do we need to find a new slot?
   if (found == EINA_C_ARRAY_LENGTH(pd->parent->slot))
     {
        found = 0;
        for (lookup = 0; lookup < EINA_C_ARRAY_LENGTH(pd->parent->slot); lookup++)
          {
             if (!pd->parent->slot[lookup].defined)
               {
                  // Found an empty slot, let's use that.
                  found = lookup;
                  break;
               }
             if (pd->parent->slot[lookup].usage < pd->parent->slot[found].usage)
               found = lookup;
          }

        // Commit change back to the stored buffer list
        if (pd->parent->slot[found].defined &&
            (pd->parent->slot[found].width ||
             pd->parent->slot[found].height))
          {
             if (pd->parent->slot[found].width &&
                 pd->parent->slot[found].decompressed.width)
               pd->parent->compressed.width = _efl_ui_exact_model_slot_compress(index,
                                                                                pd->parent->compressed.width,
                                                                                pd->parent->slot[found].width);
             if (pd->parent->slot[found].height &&
                 pd->parent->slot[found].decompressed.height)
               pd->parent->compressed.height = _efl_ui_exact_model_slot_compress(index,
                                                                                 pd->parent->compressed.height,
                                                                                 pd->parent->slot[found].height);
          }

        pd->parent->slot[found].defined = EINA_TRUE;
        pd->parent->slot[found].decompressed.width = EINA_FALSE;
        pd->parent->slot[found].decompressed.height = EINA_FALSE;
        pd->parent->slot[found].start_offset = index / EFL_UI_EXACT_MODEL_CONTENT;
     }

   // Increase usage of the returned slot for now
   pd->parent->slot[found].usage++;

   // Unpack the data if requested
   if (width_get && !pd->parent->slot[found].decompressed.width)
     {
        pd->parent->slot[found].width = _efl_ui_exact_model_buffer_expand(pd->parent->slot[found].start_offset,
                                                                          pd->parent->slot[found].width,
                                                                          pd->parent->compressed.width);
        pd->parent->slot[found].decompressed.width = EINA_TRUE;
     }
   if (height_get && !pd->parent->slot[found].decompressed.height)
     {
        pd->parent->slot[found].height = _efl_ui_exact_model_buffer_expand(pd->parent->slot[found].start_offset,
                                                                          pd->parent->slot[found].height,
                                                                          pd->parent->compressed.height);
        pd->parent->slot[found].decompressed.height = EINA_TRUE;
     }

   return found;
}

static Eina_Future *
_efl_ui_exact_model_efl_model_property_set(Eo *obj, Efl_Ui_Exact_Model_Data *pd,
                                           const char *property, Eina_Value *value)
{
   if (pd->parent)
    {
       if (eina_streq(property, _efl_model_property_selfw))
         {
            unsigned int index;
            unsigned char found;

            index = efl_composite_model_index_get(obj);
            found = _efl_ui_exact_model_slot_find(pd, index, EINA_TRUE, EINA_FALSE);
            if (!eina_value_uint_convert(value, &pd->parent->slot[found].width[index % EFL_UI_EXACT_MODEL_CONTENT]))
              return efl_loop_future_rejected(obj, EFL_MODEL_ERROR_INCORRECT_VALUE);
            // We succeeded so let's update the max total size width (As we only handle vertical list case at the moment)
            if (pd->parent->total_size.width < pd->parent->slot[found].width[index % EFL_UI_EXACT_MODEL_CONTENT])
              pd->parent->total_size.width = pd->parent->slot[found].width[index % EFL_UI_EXACT_MODEL_CONTENT];
            return efl_loop_future_resolved(obj, eina_value_uint_init(pd->parent->slot[found].width[index % EFL_UI_EXACT_MODEL_CONTENT]));
         }
       if (eina_streq(property, _efl_model_property_selfh))
         {
            unsigned int old_value;
            unsigned int index;
            unsigned char found;

            index = efl_composite_model_index_get(obj);
            found = _efl_ui_exact_model_slot_find(pd, index, EINA_FALSE, EINA_TRUE);
            old_value = pd->parent->slot[found].height[index % EFL_UI_EXACT_MODEL_CONTENT];
            if (!eina_value_uint_convert(value, &pd->parent->slot[found].height[index % EFL_UI_EXACT_MODEL_CONTENT]))
              return efl_loop_future_rejected(obj, EFL_MODEL_ERROR_INCORRECT_VALUE);
            // We succeeded so let's update the total size
            pd->parent->total_size.height += pd->parent->slot[found].height[index % EFL_UI_EXACT_MODEL_CONTENT] - old_value;
            return efl_loop_future_resolved(obj, eina_value_uint_init(pd->parent->slot[found].height[index % EFL_UI_EXACT_MODEL_CONTENT]));
         }
       // The following property are calculated by the model and so READ_ONLY
       if (eina_streq(property, _efl_model_property_totalh))
         {
            return efl_loop_future_rejected(obj, EFL_MODEL_ERROR_READ_ONLY);
         }
       if (eina_streq(property, _efl_model_property_totalw))
         {
            return efl_loop_future_rejected(obj, EFL_MODEL_ERROR_READ_ONLY);
         }
    }

   if (eina_streq(property, _efl_model_property_itemw))
     {
        // The exact model can not guess a general item size if asked
        // and should refuse to remember anything like that.
        return efl_loop_future_rejected(obj, EFL_MODEL_ERROR_READ_ONLY);
     }
   if (eina_streq(property, _efl_model_property_itemh))
     {
        // The exact model can not guess a general item size if asked
        // and should refuse to remember anything like that.
        return efl_loop_future_rejected(obj, EFL_MODEL_ERROR_READ_ONLY);
     }

   return efl_model_property_set(efl_super(obj, EFL_UI_EXACT_MODEL_CLASS), property, value);
}

static Eina_Value *
_efl_ui_exact_model_efl_model_property_get(const Eo *obj, Efl_Ui_Exact_Model_Data *pd,
                                           const char *property)
{
   if (pd->parent)
     {
        if (eina_streq(property, _efl_model_property_selfw))
          {
             unsigned int index;
             unsigned char found;

             index = efl_composite_model_index_get(obj);
             found = _efl_ui_exact_model_slot_find(pd, index, EINA_TRUE, EINA_FALSE);
             return eina_value_uint_new(pd->parent->slot[found].width[index % EFL_UI_EXACT_MODEL_CONTENT]);
          }
        if (eina_streq(property, _efl_model_property_selfh))
          {
             unsigned int index;
             unsigned char found;

             index = efl_composite_model_index_get(obj);
             found = _efl_ui_exact_model_slot_find(pd, index, EINA_FALSE, EINA_TRUE);
             return eina_value_uint_new(pd->parent->slot[found].height[index % EFL_UI_EXACT_MODEL_CONTENT]);
          }
     }
   if (eina_streq(property, _efl_model_property_totalh))
     {
        return eina_value_uint_new(pd->total_size.height);
     }
   if (eina_streq(property, _efl_model_property_totalw))
     {
        return eina_value_uint_new(pd->total_size.width);
     }
   if (eina_streq(property, _efl_model_property_itemw))
     {
        // The exact model can not guess a general item size if asked.
        return eina_value_error_new(EAGAIN);
     }
   if (eina_streq(property, _efl_model_property_itemh))
     {
        // The exact model can not guess a general item size if asked.
        return eina_value_error_new(EAGAIN);
     }
   return efl_model_property_get(efl_super(obj, EFL_UI_EXACT_MODEL_CLASS), property);
}

#include "efl_ui_exact_model.eo.c"
