#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#ifdef HAVE_VALGRIND
# include <valgrind.h>
# include <memcheck.h>
#endif

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <inttypes.h>

#ifdef _WIN32
# include <evil_private.h> /* mmap */
#else
# include <sys/mman.h>
#endif

#include "eina_config.h"
#include "eina_private.h"

#define _EINA_INTERNAL_SAFEPOINTER
#include "eina_safepointer.h"
#include "eina_mempool.h"
#include "eina_trash.h"
#include "eina_log.h"
#include "eina_lock.h"

typedef struct _Eina_Memory_Header Eina_Memory_Header;

#ifdef ERR
#undef ERR
#endif
#define ERR(...) EINA_LOG_DOM_ERR(_eina_sp_log_dom, __VA_ARGS__)

#ifdef DBG
#undef DBG
#endif
#define DBG(...) EINA_LOG_DOM_DBG(_eina_sp_log_dom, __VA_ARGS__)

/* Macro used to compose an Eo id */
#define SP_COMPOSE_PARTIAL_ID(MID_TABLE, TABLE)                         \
  ( \
    ((Eina_Sp_Id)(MID_TABLE & EINA_MASK_MID_TABLE_ID) << EINA_SHIFT_MID_TABLE_ID)   |  \
    ((Eina_Sp_Id)(TABLE & EINA_MASK_TABLE_ID) << EINA_SHIFT_TABLE_ID) \
    )

#define SP_COMPOSE_FINAL_ID(PARTIAL_ID, ENTRY, GENERATION)     \
    (PARTIAL_ID                                             |  \
     ((ENTRY & EINA_MASK_ENTRY_ID) << EINA_SHIFT_ENTRY_ID)            |  \
     ((GENERATION & EINA_MASK_GENERATIONS) << EINA_SHIFT_GENERATION))

struct _Eina_Memory_Header
{
   EINA_MAGIC;
   size_t size;
};

EAPI Eina_Memory_Table **_eina_sp_ids_tables[EINA_MAX_MID_TABLE_ID] = { NULL };
EAPI int _eina_sp_log_dom = -1;

/* Spare empty table */
static Eina_Memory_Table *empty_table = NULL;

// We are using a Spinlock even with the amount of syscall we do as it shouldn't
// take that long anyway.
static Eina_Spinlock sl;

#define MEM_PAGE_SIZE 4096
#define SAFEPOINTER_MAGIC 0x7DEADC03

static void *
_eina_safepointer_calloc(int number, size_t size)
{
#ifdef HAVE_MMAP
# ifdef HAVE_VALGRIND
   if (RUNNING_ON_VALGRIND) return calloc(number, size);
   else
# endif
     {
        Eina_Memory_Header *header;
        size_t newsize;

        size = size * number + sizeof (Eina_Memory_Header);
        newsize = ((size / MEM_PAGE_SIZE) +
                   (size % MEM_PAGE_SIZE ? 1 : 0))
          * MEM_PAGE_SIZE;

        header = mmap(NULL, newsize, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANON, -1, 0);
        if (header == MAP_FAILED)
          {
             ERR("mmap of Eina_Safepointer table region failed.");
             return NULL;
          }

        header->size = newsize;
        EINA_MAGIC_SET(header, SAFEPOINTER_MAGIC);

        return (void *)(header + 1);
     }
#else
   return calloc(number, size);
#endif
}

static void
_eina_safepointer_free(void *pointer)
{
#ifdef HAVE_MMAP
# ifdef HAVE_VALGRIND
   if (RUNNING_ON_VALGRIND) free((void *)((uintptr_t) pointer & ~0x3));
   else
# endif
     {
        Eina_Memory_Header *header;

        if (!pointer) return;

        header = (Eina_Memory_Header*)(pointer) - 1;
        if (!EINA_MAGIC_CHECK(header, SAFEPOINTER_MAGIC))
          EINA_MAGIC_FAIL(header, SAFEPOINTER_MAGIC);

        EINA_MAGIC_SET(header, 0);
        munmap(header, header->size);
     }
#else
   free((void *)((uintptr_t) pointer & ~0x3));
#endif
}

#ifdef EINA_DEBUG_MALLOC
static void
_eina_safepointer_protect(void *pointer, Eina_Bool may_not_write)
{
#ifdef HAVE_MMAP
# ifdef HAVE_VALGRIND
   if (RUNNING_ON_VALGRIND) { (void) pointer; }
   else
# endif
     {
        Eina_Memory_Header *header;

        if (!pointer) return;

        header = (Eina_Memory_Header*)(pointer) - 1;
        if (!EINA_MAGIC_CHECK(header, SAFEPOINTER_MAGIC))
          EINA_MAGIC_FAIL(header, SAFEPOINTER_MAGIC);

        mprotect(header, header->size, PROT_READ | ( may_not_write ? 0 : PROT_WRITE));
     }
#else
   (void) pointer;
#endif
}

#define   PROTECT(Ptr) _eina_safepointer_protect(Ptr, EINA_TRUE)
#define UNPROTECT(Ptr) _eina_safepointer_protect(Ptr, EINA_FALSE)

#else

#define   PROTECT(Ptr)
#define UNPROTECT(Ptr)

#endif

static Eina_Memory_Table *
_eina_safepointer_table_new(Eina_Table_Index mid_table_id,
                            Eina_Table_Index table_id)
{
   Eina_Memory_Table *table;

   if (empty_table)
     {
        /* Recycle the available empty table */
        table = empty_table;
        empty_table = NULL;
        UNPROTECT(table);
     }
   else
     {
        table = _eina_safepointer_calloc(1, sizeof (Eina_Memory_Table));
        if (!table)
          {
             ERR("Failed to allocate leaf table at [%i][%i]", mid_table_id, table_id);
             return NULL;
          }
     }

   table->partial_id = SP_COMPOSE_PARTIAL_ID(mid_table_id,
                                             table_id);
   PROTECT(table);
   UNPROTECT(_eina_sp_ids_tables[mid_table_id]);
   _eina_sp_ids_tables[mid_table_id][table_id] = table;
   PROTECT(_eina_sp_ids_tables[mid_table_id]);

   return table;
}

static Eina_Memory_Table *
_eina_safepointer_table_find(void)
{
   Eina_Table_Index mid_table_id;

   for (mid_table_id = 0; mid_table_id < EINA_MAX_MID_TABLE_ID; mid_table_id++)
     {
        Eina_Table_Index table_id;

        if (!_eina_sp_ids_tables[mid_table_id])
          {
             _eina_sp_ids_tables[mid_table_id] = _eina_safepointer_calloc(EINA_MAX_TABLE_ID, sizeof (Eina_Memory_Table*));
          }
        if (!_eina_sp_ids_tables[mid_table_id])
          {
             ERR("Failed to allocate mid table at [%i]", mid_table_id);
             return NULL;
          }

        for (table_id = 0; table_id < EINA_MAX_TABLE_ID; table_id++)
          {
             Eina_Memory_Table *table;

             table = _eina_sp_ids_tables[mid_table_id][table_id];

             if (!table)
               table = _eina_safepointer_table_new(mid_table_id, table_id);

             if (!table) return NULL;

             if (table->trash ||
                 table->start < EINA_MAX_ENTRY_ID)
               return table;
          }
     }

   return NULL;
}

static Eina_Memory_Entry *
_eina_safepointer_entry_find(Eina_Memory_Table *table)
{
   Eina_Memory_Entry *entry = NULL;

   if (table->trash)
     {
        entry = eina_trash_pop(&table->trash);
     }
   else if (table->start < EINA_MAX_ENTRY_ID)
     {
        entry = &(table->entries[table->start]);
        table->start++;
     }
   else
     {
        ERR("Impossible to find an entry in %" PRIxPTR ".", table->partial_id);
     }

   return entry;
}

EAPI const Eina_Safepointer *
eina_safepointer_register(const void *target)
{
   Eina_Memory_Table *table;
   Eina_Memory_Entry *entry = NULL;
   Eina_Sp_Id id = 0;
   unsigned int gen;

   // We silently handle NULL
   if (!target) return NULL;

   eina_spinlock_take(&sl);

   table = _eina_safepointer_table_find();
   if (!table) goto no_table;

   UNPROTECT(table);
   entry = _eina_safepointer_entry_find(table);
   if (!entry) goto on_error;

   entry->ptr = (void*) target;
   entry->active = 1;
   gen = entry->generation + 1;
   entry->generation = (gen == EINA_MAX_GENERATIONS) ? 1 : gen;

   id = SP_COMPOSE_FINAL_ID(table->partial_id,
                            (entry - table->entries),
                            entry->generation);

 on_error:
   PROTECT(table);
 no_table:
   eina_spinlock_release(&sl);

   return (void*) id;
}

EAPI void
eina_safepointer_unregister(const Eina_Safepointer *safe)
{
   Eina_Memory_Table *table;
   Eina_Memory_Entry *entry;
   Eina_Table_Index entry_id;

   // We silently handle NULL
   if (!safe) return ;

   entry = _eina_safepointer_entry_get(safe, &table);
   if (!entry) return ;

   eina_spinlock_take(&sl);

   // In case of a race condition during a double free attempt
   // The entry could have been unactivated since we did found it
   // So check again.
   if (!entry->active) goto on_error;

   UNPROTECT(table);
   entry->active = 0;
   eina_trash_push(&table->trash, entry);
   PROTECT(table);

   entry_id = entry - table->entries;
   if (entry_id == EINA_MAX_ENTRY_ID - 1)
     {
        Eina_Table_Index i;

        for (i = entry_id; i >= 0; i--)
          {
             if (table->entries[i].active)
               break ;
          }

        // No more active entry
        // Could be speed up by tracking the
        // number of allocated entries, but
        // with all the syscall around, not sure
        // it is worth it.
        if (i == -1)
          {
             Eina_Table_Index mid_table_id, table_id;

             mid_table_id = (table->partial_id >> EINA_SHIFT_MID_TABLE_ID) & EINA_MASK_MID_TABLE_ID;
             table_id = (table->partial_id >> EINA_SHIFT_TABLE_ID) & EINA_MASK_TABLE_ID;
             UNPROTECT(_eina_sp_ids_tables[mid_table_id]);
             _eina_sp_ids_tables[mid_table_id][table_id] = NULL;
             PROTECT(_eina_sp_ids_tables[mid_table_id]);
             if (!empty_table)
               empty_table = table;
             else
               _eina_safepointer_free(table);
          }
     }

 on_error:
   eina_spinlock_release(&sl);
}

Eina_Bool
eina_safepointer_init(void)
{
   eina_magic_string_set(SAFEPOINTER_MAGIC, "Safepointer");
   _eina_sp_log_dom = eina_log_domain_register("eina_safepointer",
                                               EINA_LOG_COLOR_DEFAULT);
   if (_eina_sp_log_dom < 0)
     {
        EINA_LOG_ERR("Could not register log domain: eina_safepointer.");
        return EINA_FALSE;
     }

   eina_spinlock_new(&sl);

   DBG("entry[Size, Align] = { %zu, %u }",
       sizeof (Eina_Memory_Entry), eina_mempool_alignof(sizeof (Eina_Memory_Entry)));
   DBG("table[Size, Align] = { %zu, %u }",
       sizeof (Eina_Memory_Table), eina_mempool_alignof(sizeof (Eina_Memory_Table)));

   return EINA_TRUE;
}

Eina_Bool
eina_safepointer_shutdown(void)
{
   eina_spinlock_free(&sl);

   return EINA_TRUE;
}
