/**
 * @file mem.c  Memory management with reference counting
 *
 * Copyright (C) 2010 Creytiv.com
 */
#include <re_types.h> 
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_PTHREAD
#include <pthread.h>
#endif
#include <re_list.h>
#include <re_fmt.h>
#include <re_mbuf.h>
#include <re_mem.h>


#define DEBUG_MODULE "mem"
#define DEBUG_LEVEL 5
#include <re_dbg.h>


#ifndef RELEASE
#define MEM_DEBUG 0  /**< Enable memory debugging */
#endif


/** Defines a reference-counting memory object */
struct mem {
	uint32_t nrefs;     /**< Number of references  */
	mem_destroy_h *dh;  /**< Destroy handler       */
#if MEM_DEBUG
	struct le le;       /**< Linked list element   */
	uint32_t magic;     /**< Magic number          */
	size_t size;        /**< Size of memory object */
#endif
};

#if MEM_DEBUG
/* Memory debugging */
static struct list meml = LIST_INIT;
static const uint32_t mem_magic = 0xe7fb9ac4;
static ssize_t threshold = -1;  /**< Memory threshold, disabled by default */

static struct memstat memstat = {
	0,0,0,0,~0,0
};

#ifdef HAVE_PTHREAD

static pthread_mutex_t mem_mutex = PTHREAD_MUTEX_INITIALIZER;

static inline void mem_lock(void)
{
	pthread_mutex_lock(&mem_mutex);
}


static inline void mem_unlock(void)
{
	pthread_mutex_unlock(&mem_mutex);
}

#else

#define mem_lock()    /**< Stub */
#define mem_unlock()  /**< Stub */

#endif

/** Update statistics for mem_zalloc() */
#define STAT_ALLOC(m, size) \
	mem_lock(); \
	memstat.bytes_cur += (size); \
	memstat.bytes_peak = max(memstat.bytes_cur, memstat.bytes_peak); \
	++memstat.blocks_cur; \
	memstat.blocks_peak = max(memstat.blocks_cur, memstat.blocks_peak); \
	memstat.size_min = min(memstat.size_min, size);	\
	memstat.size_max = max(memstat.size_max, size);	\
	mem_unlock(); \
	(m)->size = (size); \
	(m)->magic = mem_magic;

/** Update statistics for mem_realloc() */
#define STAT_REALLOC(m, size) \
	mem_lock(); \
	memstat.bytes_cur += ((size) - (m)->size); \
	memstat.bytes_peak = max(memstat.bytes_cur, memstat.bytes_peak); \
	memstat.size_min = min(memstat.size_min, size);	\
	memstat.size_max = max(memstat.size_max, size); \
	mem_unlock(); \
	(m)->size = (size)

/** Update statistics for mem_deref() */
#define STAT_DEREF(m) \
	mem_lock(); \
	memstat.bytes_cur -= (m)->size; \
	--memstat.blocks_cur; \
	mem_unlock(); \
	memset((m), 0xb5, sizeof(struct mem) + (m)->size)

/** Check magic number in memory object */
#define MAGIC_CHECK(m) \
	if (mem_magic != (m)->magic) { \
		DEBUG_WARNING("%s: magic check failed 0x%08x (%p)\n", \
			      __FUNCTION__, (m)->magic, (m)+1);	      \
		BREAKPOINT;					      \
	}
#else
#define STAT_ALLOC(m, size)
#define STAT_REALLOC(m, size)
#define STAT_DEREF(m)
#define MAGIC_CHECK(m)
#endif


/**
 * Allocate a new reference-counted memory object
 *
 * @param size Size of memory object
 * @param dh   Optional destructor, called when destroyed
 *
 * @return Pointer to allocated object
 */
void *mem_alloc(size_t size, mem_destroy_h *dh)
{
	struct mem *m;

#if MEM_DEBUG
	mem_lock();
	if (-1 != threshold && (memstat.blocks_cur >= (size_t)threshold)) {
		mem_unlock();
		return NULL;
	}
	mem_unlock();
#endif

	m = malloc(sizeof(*m) + size);
	if (!m)
		return NULL;

#if MEM_DEBUG
	memset(&m->le, 0, sizeof(struct le));
	mem_lock();
	list_append(&meml, &m->le, m);
	mem_unlock();
#endif

	m->nrefs = 1;
	m->dh    = dh;

	STAT_ALLOC(m, size);

	return (void *)(m + 1);
}


/**
 * Allocate a new reference-counted memory object. Memory is zeroed.
 *
 * @param size Size of memory object
 * @param dh   Optional destructor, called when destroyed
 *
 * @return Pointer to allocated object
 */
void *mem_zalloc(size_t size, mem_destroy_h *dh)
{
	void *p;

	p = mem_alloc(size, dh);
	if (!p)
		return NULL;

	memset(p, 0, size);

	return p;
}


/**
 * Re-allocate a reference-counted memory object
 *
 * @param data Memory object
 * @param size New size of memory object
 *
 * @return New pointer to allocated object
 *
 * @note Realloc NULL pointer is not supported
 */
void *mem_realloc(void *data, size_t size)
{
	struct mem *m, *m2;

	if (!data)
		return NULL;

	m = ((struct mem *)data) - 1;

	MAGIC_CHECK(m);

#if MEM_DEBUG
	mem_lock();

	/* Simulate OOM */
	if (-1 != threshold && size > m->size) {
		if (memstat.blocks_cur >= (size_t)threshold) {
			mem_unlock();
			return NULL;
		}
	}

	list_unlink(&m->le);
	mem_unlock();
#endif

	m2 = realloc(m, sizeof(*m2) + size);

#if MEM_DEBUG
	mem_lock();
	list_append(&meml, m2 ? &m2->le : &m->le, m2 ? m2 : m);
	mem_unlock();
#endif

	if (!m2) {
		return NULL;
	}

	STAT_REALLOC(m2, size);

	return (void *)(m2 + 1);
}


/**
 * Reference a reference-counted memory object
 *
 * @param data Memory object
 *
 * @return Memory object (same as data)
 */
void *mem_ref(void *data)
{
	struct mem *m;

	if (!data)
		return NULL;

	m = ((struct mem *)data) - 1;

	MAGIC_CHECK(m);

	++m->nrefs;

	return data;
}


/**
 * Dereference a reference-counted memory object. When the reference count
 * is zero, the destroy handler will be called (if present) and the memory
 * will be freed
 *
 * @param data Memory object
 *
 * @return Always NULL
 */
void *mem_deref(void *data)
{
	struct mem *m;

	if (!data)
		return NULL;

	m = ((struct mem *)data) - 1;

	MAGIC_CHECK(m);

	if (--m->nrefs > 0)
		return NULL;

	if (m->dh)
		m->dh(data);

	if (m->nrefs > 0)
		return NULL;

#if MEM_DEBUG
	mem_lock();
	list_unlink(&m->le);
	mem_unlock();
#endif

	STAT_DEREF(m);

	free(m);

	return NULL;
}


/**
 * Get number of references to a reference-counted memory object
 *
 * @param data Memory object
 *
 * @return Number of references
 */
uint32_t mem_nrefs(const void *data)
{
	struct mem *m;

	if (!data)
		return 0;

	m = ((struct mem *)data) - 1;

	MAGIC_CHECK(m);

	return m->nrefs;
}


#if MEM_DEBUG
static bool debug_handler(struct le *le, void *arg)
{
	struct mem *m = le->data;
	const uint8_t *p = (const uint8_t *)(m + 1);
	size_t i;

	(void)arg;

	(void)re_printf("  %p: nrefs=%-2u", p, m->nrefs);

	(void)re_printf(" size=%-7u", m->size);

	(void)re_printf(" [");

	for (i=0; i<16; i++) {
		if (i >= m->size)
			(void)re_printf("   ");
		else
			(void)re_printf("%02x ", p[i]);
	}

	(void)re_printf("] [");

	for (i=0; i<16; i++) {
		if (i >= m->size)
			(void)re_printf(" ");
		else
			(void)re_printf("%c",
					 isprint(p[i]) ? p[i] : '.');
	}

	(void)re_printf("]");

	MAGIC_CHECK(m);

	(void)re_printf("\n");

	return false;
}
#endif


/**
 * Debug all allocated memory objects
 */
void mem_debug(void)
{
#if MEM_DEBUG
	uint32_t n;

	mem_lock();
	n = list_count(&meml);
	mem_unlock();

	if (!n)
		return;

	DEBUG_WARNING("Memory leaks (%u):\n", n);

	mem_lock();
	(void)list_apply(&meml, true, debug_handler, NULL);
	mem_unlock();
#endif
}


/**
 * Set the memory allocation threshold. This is only used for debugging
 * and out-of-memory simulation
 *
 * @param n Threshold value
 */
void mem_threshold_set(ssize_t n)
{
#if MEM_DEBUG
	mem_lock();
	threshold = n;
	mem_unlock();
#else
	(void)n;
#endif
}


/**
 * Print memory status
 *
 * @param pf     Print handler for debug output
 * @param unused Unused parameter
 *
 * @return 0 if success, otherwise errorcode
 */
int mem_status(struct re_printf *pf, void *unused)
{
#if MEM_DEBUG
	struct memstat stat;
	uint32_t c;
	int err = 0;

	(void)unused;

	mem_lock();
	memcpy(&stat, &memstat, sizeof(stat));
	c = list_count(&meml);
	mem_unlock();

	err |= re_hprintf(pf, "Memory status: (%u bytes overhead pr block)\n",
			  sizeof(struct mem));
	err |= re_hprintf(pf, " Cur:  %u blocks, %u bytes (total %u bytes)\n",
			  stat.blocks_cur, stat.bytes_cur,
			  stat.bytes_cur
			  +(stat.blocks_cur*sizeof(struct mem)));
	err |= re_hprintf(pf, " Peak: %u blocks, %u bytes (total %u bytes)\n",
			  stat.blocks_peak, stat.bytes_peak,
			  stat.bytes_peak
			  +(stat.blocks_peak*sizeof(struct mem)));
	err |= re_hprintf(pf, " Block size: min=%u, max=%u\n",
			  stat.size_min, stat.size_max);
	err |= re_hprintf(pf, " Total %u blocks allocated\n", c);

	return err;
#else
	(void)pf;
	(void)unused;
	return 0;
#endif
}


/**
 * Get memory statistics
 *
 * @param mstat Returned memory statistics
 *
 * @return 0 if success, otherwise errorcode
 */
int mem_get_stat(struct memstat *mstat)
{
	if (!mstat)
		return EINVAL;
#if MEM_DEBUG
	mem_lock();
	memcpy(mstat, &memstat, sizeof(*mstat));
	mem_unlock();
	return 0;
#else
	return ENOSYS;
#endif
}
