#include <stdint.h>
#include <assert.h>
#include <stdlib.h>
#include "defs.h"
#include "bpalloc.h"

#if USING_VALGRIND
#include <valgrind/memcheck.h>
#endif

#define CHUNK_SIZE 4096
#define PADDING 8

typedef struct BPAllocChunk BPAllocChunk;
struct BPAllocChunk {
	BPAllocChunk *prev;
	char body[];
}; 

struct xBPAlloc {
	int flags;
	void *userp;
	void *(*fn_malloc)(void *userp, int size);
	void  (*fn_free  )(void *userp, void *addr);
	int minsize, size, used;
	BPAllocChunk *tail;
};

enum {
	FG_STATIC = 1,
};

static void *default_fn_malloc(void *userp, int   size);
static void  default_fn_free  (void *userp, void *addr);

BPAlloc *BPAlloc_Init(int chunk_size)
{
	return BPAlloc_Init2(-1, chunk_size, NULL, NULL, NULL);
}

BPAlloc *BPAlloc_Init2(int first_size, int chunk_size, 
						void *userp,
						void *(*fn_malloc)(void *userp, int size), 
						void  (*fn_free  )(void *userp, void *addr))
{
	if(chunk_size < 0)
		chunk_size = CHUNK_SIZE;

	if(first_size < 0)
		first_size = chunk_size;

	if(fn_malloc == NULL)
		{
			userp = NULL;
			fn_malloc = default_fn_malloc;
			fn_free   = default_fn_free;
		}

	void *temp = fn_malloc(userp, sizeof(BPAlloc) + sizeof(BPAllocChunk) + first_size + PADDING);

	if(temp == NULL)
		return NULL;

	BPAlloc *alloc = temp;

	BPAllocChunk *chunk = (BPAllocChunk*) (alloc + 1);
	chunk->prev = NULL;

	alloc->flags 	 = 0; 
	alloc->used 	 = 0;
	alloc->size 	 = first_size;
	alloc->tail 	 = chunk;
	alloc->minsize   = chunk_size;
	alloc->userp     = userp;
	alloc->fn_malloc = fn_malloc;
	alloc->fn_free 	 = fn_free;

#if USING_VALGRIND
	VALGRIND_CREATE_MEMPOOL(alloc, PADDING, 0);
#endif

	return alloc;
}

BPAlloc *BPAlloc_Init3(void *mem, int mem_size, int chunk_size, 
						void *userp,
						void *(*fn_malloc)(void *userp, int size), 
						void  (*fn_free  )(void *userp, void *addr))
{
	assert(mem != NULL);
	assert(mem_size >= 0);

	if(chunk_size < 0)
		chunk_size = CHUNK_SIZE;

	if(fn_malloc == NULL)
		{
			userp = NULL;
			fn_malloc = default_fn_malloc;
			fn_free   = default_fn_free;
		}

	int required = sizeof(BPAlloc) 
				 + sizeof(BPAllocChunk) 
				 + PADDING;

	if(mem_size < required)
		// Not enough memory was provided.
		return NULL;

	BPAlloc *alloc = mem;

	BPAllocChunk *chunk = (BPAllocChunk*) (alloc + 1);
	chunk->prev = NULL;

	alloc->flags	 = FG_STATIC;
	alloc->used 	 = 0;
	alloc->size 	 = mem_size - sizeof(BPAlloc) - sizeof(BPAllocChunk);
	alloc->tail 	 = chunk;
	alloc->minsize   = chunk_size;
	alloc->userp     = userp;
	alloc->fn_malloc = fn_malloc;
	alloc->fn_free 	 = fn_free;

#if USING_VALGRIND
	VALGRIND_CREATE_MEMPOOL(alloc, PADDING, 0);
#endif

	return alloc;
}

void BPAlloc_Free(BPAlloc *alloc)
{
	assert(alloc != NULL);

#if USING_VALGRIND
	VALGRIND_DESTROY_MEMPOOL(alloc);
#endif

	BPAllocChunk *chunk = alloc->tail;

	while(chunk->prev)
		{
			BPAllocChunk *prev = chunk->prev;

			if(alloc->fn_free)
				alloc->fn_free(alloc->userp, chunk);

			chunk = prev;
		}

	if(!(alloc->flags & FG_STATIC))
		if(alloc->fn_free)
			alloc->fn_free(alloc->userp, alloc);
}

void *BPAlloc_Malloc(BPAlloc *alloc, int req_size)
{
	assert(alloc != NULL);
	assert(req_size >= 0);

	alloc->used += PADDING;

	if(alloc->used & 7)
		alloc->used = (alloc->used & ~7) + 8;

	if(alloc->used + req_size > alloc->size)
		{
			// If the chunk size is lower than the
			// requested size, then set the chunk
			// size to the requested size.
			int chunk_size = MAX(alloc->minsize, req_size + PADDING);

			assert(alloc->fn_malloc != NULL);
			BPAllocChunk *chunk = alloc->fn_malloc(alloc->userp, sizeof(BPAllocChunk) + chunk_size);
		
			if(chunk == NULL)
				return NULL;

			chunk->prev = alloc->tail;
			alloc->tail = chunk;
			alloc->size = chunk_size;
			alloc->used = PADDING;

			if(alloc->used & 7)
				alloc->used = (alloc->used & ~7) + 8;
		}

	void *addr = alloc->tail->body + alloc->used;

	assert(((intptr_t) addr) % 8 == 0);
	
	alloc->used += req_size;

#if USING_VALGRIND
	VALGRIND_MEMPOOL_ALLOC(alloc, addr, req_size);
//	VALGRIND_MAKE_MEM_NOACCESS((char*) addr - PADDING, PADDING);
//	VALGRIND_MAKE_MEM_NOACCESS((char*) addr + req_size, PADDING);
#endif

	return addr;
}

static void *default_fn_malloc(void *userp, int size)
{
	assert(userp == NULL);
	assert(size >= 0);
	
	return malloc(size);
}

static void default_fn_free(void *userp, void *addr)
{
	assert(userp == NULL);

	free(addr);
}
