// ::Allocator::Globals::

FLAlloc FL_ALLOC = {0};
Allocator ALLOC = {0};
read_only FLNode FL_NIL_NODE = {0};
read_only FreeList FL_NIL_LIST = {0};

constexpr usize FL_GLOBAL_SIZE = MB(32);
static b32 FL_GLOBAL_INIT = false;

// ::Allocator::Util::Header::

static inline usize CalcPaddingWithHeader(uintptr ptr, uintptr alignment, usize header_size)
{
	Assert(IsPow2(alignment), "Alignment provided to CalcPaddingWithHeader is not a power of two");

	uintptr padding = CalcPadding(ptr, alignment);
	uintptr needed_space = (uintptr)header_size;

	if (padding < needed_space)
	{
		needed_space -= padding;

		if ((needed_space & (alignment-1)) != 0)
			padding += alignment * (1 + (needed_space/alignment));
		else
			padding += alignment * (needed_space/alignment);
	}

	return (usize)padding;
}

static inline usize CalcPadding(uintptr ptr, uintptr alignment)
{
	Assert(IsPow2(alignment), "CalcPadding failure: IsPow2 failed");

	uintptr padding = 0;
	uintptr modulo = ptr & (alignment-1);
	if (modulo != 0)
		padding = alignment - modulo;

	return (usize)padding;
}

// ::Allocator::Arena::Start::

static Arena *ArenaInit(rawptr buffer, usize size)
{
	Arena *arena = (Arena *)buffer;
	buffer = PtrAdd(buffer, ARENA_HEADER_SIZE);

	arena->buffer = buffer;
	arena->length = size;
	arena->pos = 0;

	return arena;
}

static Arena *ArenaCreate(usize size)
{
	u8 *mem = pMemAllocZeroed(size);
	return ArenaInit(mem, size);
}

static Arena *ArenaCreateDebug(usize size, u32 init_line_no)
{
	u8 *mem = pMemAllocZeroed(size);
	return ArenaInitDebug(mem, size, init_line_no);
}

// TODO: investigate overflows when out of memory because something bad is going on
static rawptr ArenaAllocAlign(Arena *arena, usize size, usize align)
{
	rawptr ptr = NULL;

	uintptr curr_ptr = (uintptr)arena->buffer + (uintptr)arena->pos;
	uintptr offset = AlignPow2(curr_ptr, align);
	offset -= (uintptr)arena->buffer;

	if (offset+size <= arena->length)
	{
		ptr = &arena->buffer[offset];
		arena->pos = offset+size;
	}
	else
	{
		Printfln("Out of memory: %d", arena->init_line_no);
		Assert(0, "Memory Failure");
	}

	return ptr;
}

static rawptr ArenaAlloc(Arena *arena, usize size)
{
	return ArenaAllocAlign(arena, size, DEFAULT_ALIGNMENT);
}

static void ArenaFree(Arena *arena)
{
	arena->pos = 0;
}

static void ArenaFreeZeroed(Arena *arena)
{
	MemZero(arena->buffer, arena->pos);
	ArenaFree(arena);
}

static void DeallocArena(Arena *arena)
{
	pMemFree(arena, arena->length);
}

static Arena * ArenaInitDebug(rawptr buffer, usize size, u32 init_line_no)
{
	Arena *arena = ArenaInit(buffer, size);
	arena->init_line_no = init_line_no;
	return arena;
}

// ::Allocator::Arena::End::



// ::Allocator::GlobalAlloc::Start::

static void InitAllocator(usize init_size)
{
	ALLOC.grow_size = init_size;
	ALLOC.buffer = pMemAllocZeroed(init_size);
	ALLOC.size = init_size;
	ALLOC.free_size = init_size;

	ALLOC.hash_table = FLMemAlloc(sizeof(HashTable));
	HashTableInit(ALLOC.hash_table, 32);

	ALLOC.tree = FLMemAlloc(sizeof(RBTree));
	RBTreeInit(ALLOC.tree);

	RBTreeInsert(ALLOC.tree, init_size, ALLOC.buffer);
}

static void DeinitAlloc()
{
	pMemFree(ALLOC.buffer, ALLOC.size);
}

static rawptr Alloc(usize size)
{
	return AllocAlign(size, DEFAULT_ALIGNMENT);
}

static rawptr AllocAlign(usize size, usize alignment)
{
	if (size == 0) return NULL;

	RBNode *node = P_RB_NIL;
	rawptr mem = NULL;
	if (!RBTreeSearchNearest(ALLOC.tree, size + alignment, &node))
	{
		AllocGrow(size);
		RBTreeSearchNearest(ALLOC.tree, size + alignment, &node);
	}

	u64 alloc_size = node->key;
	rawptr free_alloc = node->bucket.last->data;
	RBTreeDelete(ALLOC.tree, alloc_size, free_alloc);

	usize padding = CalcPadding(uintptr(free_alloc), alignment);
	uintptr new_addr = uintptr(free_alloc) + size + padding;
	RBTreeInsert(ALLOC.tree, alloc_size - size - padding, rawptr(new_addr));

	HashTablePushRawptrU64(ALLOC.hash_table, free_alloc, size + padding);

	return free_alloc;
}

// TODO: finish allocator
// need an idea
static void Free(rawptr ptr)
{
	if (ptr == NULL) return;

	
}

static void AllocGrow(usize size)
{
	usize grow_size = size < ALLOC.grow_size ? ALLOC.grow_size : ALLOC.grow_size + size;
	pMemRealloc(ALLOC.buffer, ALLOC.size, ALLOC.size + grow_size);

	RBTreeInsert(ALLOC.tree, grow_size, ALLOC.buffer + ALLOC.size); // TODO: check this if things fuck up it could be wrong

	ALLOC.size += grow_size;
	ALLOC.free_size += grow_size;
}

// ::Allocator::GlobalAlloc::End::



// ::Allocator::FreeList::Start::

static void GlobalFreeListInit(usize size)
{
	FreeListInit(&FL_ALLOC, size);
	FL_GLOBAL_INIT = true;
}

static rawptr FLMemAlloc(usize size)
{
	if (!FL_GLOBAL_INIT)
	{
		GlobalFreeListInit(FL_GLOBAL_SIZE);
	}

	return FreeListAlloc(&FL_ALLOC, size);
}

static rawptr FLMemAllocZeroed(usize size)
{
	rawptr ptr = FLMemAlloc(size);
	MemZero(ptr, size);
	return ptr;
}

static rawptr FLMemRealloc(rawptr old_ptr, usize size)
{
	return FreeListRealloc(&FL_ALLOC, old_ptr, size);
}

static void FLMemFree(rawptr ptr)
{
	FreeListFree(&FL_ALLOC, ptr);
}

/*
 * Should be async safe given no other thread frees a pointer in the middle of a realloc
 */
static rawptr FreeListRealloc(FLAlloc *alloc, rawptr old_ptr, usize size)
{
	rawptr ptr = FreeListAlloc(alloc, size);
	usize old_size = FreeListPtrSize(alloc, old_ptr);

	MemCpy(ptr, old_ptr, old_size);

	FreeListFree(alloc, old_ptr);

	return ptr;
}

static usize FreeListPtrSize(FLAlloc *alloc, rawptr ptr)
{
	if (ptr == NULL) return 0;

	usize size = 0;

	TicketMutLock(&alloc->mut);

	FreeList *list = _FreeListFindList(alloc, ptr);
	if (list != NULL)
	{
		FLAllocHeader *header = cast(FLAllocHeader *, u8ptr(ptr) - sizeof(FLAllocHeader));
		size = header->size + header->padding;
	}

	TicketMutUnlock(&alloc->mut);

	return size;
}

static void FreeListInit(FLAlloc *alloc, usize size)
{
	alloc->list_head = pMemAllocZeroed(sizeof(FreeList));
	alloc->nil = &FL_NIL_NODE;
	alloc->grow_size = size;

	_FreeListInit(&alloc->list_head, size);

	FreeListFreeAll(alloc);
}

static void _FreeListInit(FreeList **alloc, usize size)
{
	*alloc = (FreeList *)pMemAllocZeroed(size);
	(*alloc)->data = (rawptr)(((uintptr)*alloc) + ((uintptr)sizeof(FreeList)));
	(*alloc)->size = size;
	(*alloc)->used = sizeof(FreeList);
	(*alloc)->next = &FL_NIL_LIST;
}

static void FreeListFreeAll(FLAlloc *alloc)
{
	TicketMutLock(&alloc->mut);

	FreeList *current = alloc->list_head->next;
	FreeList *next = &FL_NIL_LIST;
	while (current != &FL_NIL_LIST)
	{
		next = current->next;
		pMemFree(current, current->size);
		current = next;
	}

	current = alloc->list_head;

	FLNode *node = (FLNode *)current->data;
	node->size = current->size;
	node->next = &FL_NIL_NODE;

	current->head = node;
	current->used = sizeof(FreeList);

	TicketMutUnlock(&alloc->mut);
}

static FLNode *FreeListSearch(FreeList *alloc, usize size, usize alignment, usize *out_padding, FLNode **prev_node)
{
	FLNode *node = alloc->head;
	FLNode *prev = &FL_NIL_NODE;

	usize padding = 0;

	while (node != &FL_NIL_NODE)
	{
		padding = CalcPaddingWithHeader((uintptr)node, (uintptr)alignment, sizeof(FLNode));
		usize required_size = size + padding;
		if (node->size >= required_size)
			break;

		prev = node;
		node = node->next;
	}

	if (out_padding) 
		*out_padding = padding;

	if (prev_node)
		*prev_node = prev;

	return node;
}

/*
 *	NOT SAFE TO CALL OUTSIDE OF FreeListAlloc
 */
static FreeList *FreeListGrow(FLAlloc *alloc, usize alloc_size)
{
	usize grow_size = alloc->grow_size;
	if (alloc_size > grow_size)
		grow_size += alloc_size;

	FreeList *list = NULL;

	_FreeListInit(&list, grow_size);

	FLNode *node = (FLNode *)list->data;
	node->size = list->size;
	node->next = alloc->nil;

	list->head = node;

	FreeList *current = alloc->list_head;
	while (current != &FL_NIL_LIST)
	{
		if (current->next == &FL_NIL_LIST)
		{
			current->next = list;
			break;
		}

		current = current->next;
	}

	return list;
}

static rawptr _FreeListAllocAlign(FreeList *alloc, usize size, usize alignment)
{
	if (size == 0) return NULL;

	usize padding = 0;
	FLNode *prev_node = &FL_NIL_NODE;

	FLAllocHeader *header;

	if (size < sizeof(FLNode))
		size = sizeof(FLNode);

	if (alignment < 8)
		alignment = 8;

	FLNode *node = FreeListSearch(alloc, size, alignment, &padding, &prev_node);

	if (node == &FL_NIL_NODE)
		Assert(0, "FreeListAllocAlign failed to allocate, oom");
	
	usize alignment_padding = padding - sizeof(FLAllocHeader);
	usize required_space = size + padding;
	usize remaining = node->size - required_space;

	if (remaining > 0)
	{
		FLNode *new_node = (FLNode *)(((c8 *)node) + required_space);
		new_node->size = remaining;
		FreeListInsert(&alloc->head, node, new_node);
	}

	FreeListRemove(&alloc->head, prev_node, node);

	header = (FLAllocHeader *)(((c8 *)node) + alignment_padding);
	header->size = required_space;
	header->padding = alignment_padding;

	alloc->used += required_space;

	return (rawptr)(((c8 *)header) + sizeof(FLAllocHeader));
}

static rawptr FreeListAlloc(FLAlloc *alloc, usize size)
{
	return FreeListAllocAlign(alloc, size, DEFAULT_ALIGNMENT);
}

static rawptr FreeListAllocAlign(FLAlloc *alloc, usize size, usize alignment)
{
	TicketMutLock(&alloc->mut);

	FreeList *fl = &FL_NIL_LIST;
	FreeList *current = alloc->list_head;
	while (current != &FL_NIL_LIST)
	{
		usize remaining = current->size - current->used;
		if (size < remaining)
		{
			fl = current;
			break;
		}

		current = current->next;
	}

	if (fl == &FL_NIL_LIST)
	{
		fl = FreeListGrow(alloc, size);
	}

	rawptr ptr = _FreeListAllocAlign(fl, size, alignment);

	TicketMutUnlock(&alloc->mut);

	return ptr;
}

static FreeList *_FreeListFindList(FLAlloc *alloc, rawptr ptr)
{
	FreeList *list = NULL;
	FreeList *current = alloc->list_head;
	while (current != &FL_NIL_LIST)
	{
		uintptr ptr_addr = uintptr(ptr);
		uintptr start = uintptr(current->data);
		uintptr end = uintptr(current->data) + uintptr(current->size);
		if (ptr_addr >= start && ptr_addr < end)
		{
			list = current;
			break;
		}
	}

	return list;
}

static void _FreeListFree(FreeList *alloc, rawptr ptr)
{
	FLAllocHeader *header = cast(FLAllocHeader *, u8ptr(ptr) - sizeof(FLAllocHeader));
	FLNode *free_node = cast(FLNode *,header);
	free_node->size = header->size + header->padding;
	free_node->next = &FL_NIL_NODE;

	FLNode *prev_node = &FL_NIL_NODE;
	FLNode *node = alloc->head;
	while (node != &FL_NIL_NODE)
	{
		if (ptr < node)
		{
			FreeListInsert(&alloc->head, prev_node, free_node);
			break;
		}

		prev_node = node;
		node = node->next;
	}

	alloc->used -= free_node->size;

	FreeListCoalescence(alloc, prev_node, free_node);
}

static void FreeListFree(FLAlloc *alloc, rawptr ptr)
{
	if (ptr == NULL) return;

	TicketMutLock(&alloc->mut);

	FreeList *list = _FreeListFindList(alloc, ptr);
	if (list != NULL)
	{
		_FreeListFree(list, ptr);
	}

	TicketMutUnlock(&alloc->mut);
}

static void FreeListCoalescence(FreeList *alloc, FLNode *prev_node, FLNode *free_node)
{
	if (free_node->next != &FL_NIL_NODE && (rawptr)(((c8 *)free_node) + free_node->size) == free_node->next)
	{
		free_node->size += free_node->next->size;
		FreeListRemove(&alloc->head, free_node, free_node->next);
	}

	if (prev_node->next != &FL_NIL_NODE && (rawptr)(((c8 *)prev_node) + prev_node->size) == free_node)
	{
		prev_node->size += free_node->next->size;
		FreeListRemove(&alloc->head, prev_node, free_node);
	}
}

static void FreeListRemove(FLNode **head, FLNode *prev_node, FLNode *del_node)
{
	if (prev_node == &FL_NIL_NODE)
		*head = del_node->next;
	else
		prev_node->next = del_node->next;
}

static void FreeListInsert(FLNode **head, FLNode *prev_node, FLNode *new_node)
{
	if (prev_node == &FL_NIL_NODE)
	{
		if (*head != &FL_NIL_NODE)
		{
			new_node->next = *head;
			*head = new_node;
		}
		else
			*head = new_node;
	}
	else
	{
		new_node->next = prev_node->next;
		prev_node->next = new_node;
	}
}

// ::Allocator::FreeList::End::
