module dlib.alloc;

import dlib.aliases;
import dlib.math;
import dlib.platform;
import dlib.util;

import core.stdc.string;

static if(NativeTarget)
{
	public import core.memory : malloc = pureMalloc, realloc = pureRealloc, free = pureFree;
}

static Scratch g_scratch;
static usize   g_scratch_index;

const DEFAULT_ALIGNMENT = (void *).sizeof * 2;

struct Scratch
{
	Arena arena;
	bool  init;
}

struct ArenaPool
{
	u8*        mem;
	usize      pos;
	usize      length;
	ArenaPool* next;
}

struct Arena
{
	ArenaPool* first, last;
	usize      def_size;
}

struct TempArena
{
	Arena*     arena;
	usize      start_pos;
	ArenaPool* start_pool;
}

/*
extern(C)
{
	void gc_setProxy(void* p);
	void gc_clrProxy();
}
*/

@nogc:

version(WebAssembly)
{

pragma(LDC_intrinsic, "llvm.wasm.memory.grow.i32")
extern(C) usize grow(usize index, usize size) nothrow @nogc;

pragma(LDC_intrinsic, "llvm.wasm.memory.size.i32")
extern(C) usize size(usize index) nothrow @nogc;


extern(C) u32
WasmGrow(u32 size) nothrow @nogc
{
	return grow(0, size);
}

extern(C) u32
WasmGrow2(u32 index, u32 size)
{
	return grow(0, size);
}

extern(C) u32
WasmSize() nothrow @nogc
{
	return size(0);
}

}

T*
MAlloc(T)()
{
	void* mem = MemAlloc(T.sizeof);
	return cast(T*)mem;
}

T[]
MAlloc(T)(usize count)
{
	void* mem = MemAlloc(T.sizeof * count);
	return (cast(T*)mem)[0 .. count];
}

void
MFree(T)(T* ptr)
{
	MemFree(ptr, T.sizeof);
}

void
MFree(T)(T[] slice)
{
	MemFree(slice.ptr, cast(usize)slice.length * T.sizeof);
}

T*
Alloc(T)()
{
	void* mem = malloc(T.sizeof);
	memset(mem, 0, T.sizeof);
	return (cast(T*)mem);
}

T[]
Alloc(T)(usize count)
{
	void* mem = malloc(T.sizeof * count);
	memset(mem, 0, T.sizeof * count);
	return (cast(T*)mem)[0 .. count];
}

T[]
Alloc(T)(T[] target)
{
	T[] arr = Alloc!(T)(target.length);
	arr[]   = target[];
	return arr;
}

string
Alloc(string target)
{
	u8[] str = Alloc!(u8)(target.length);
	str[]    = cast(u8[])target[];
	return ConvToStr(str);
}

T[]
Alloc(T)(T[] target, usize start, usize len)
{
	T[] arr = Alloc!(T)(len);
	arr[0 .. $] = target[start .. start+len];
	return arr;
}

string
Alloc(string target, usize start, usize len)
{
	u8[] str    = Alloc!(u8)(len);
	str[0 .. $] = cast(u8[])target[start .. start+len];
	return ConvToStr(str);
}

T[]
Alloc(T)(usize count, T set)
{
	T[] arr = Alloc!(T)(count);
	arr[] = set;
	return arr;
}

T[]
Realloc(T)(T[] arr, usize count)
{
	static if(NativeTarget)
	{
		void* mem = realloc(arr.ptr, T.sizeof * count);
	}
	else
	{
		void* mem = alloc(T.sizeof*count);
		memcpy(mem, arr.ptr, T.sizeof*arr.length);
		Free(arr);
	}

	return (cast(T*)mem)[0 .. count];
}

void
Free(T)(T[] arr)
{
	free(arr.ptr);
}

void 
Free(T)(T* ptr)
{
	free(ptr);
}

Arena 
CreateArena(usize size)
{
	Arena arena = { 
		def_size: size,
	};

	AddArenaPool(&arena, size);

	return arena;
}

TempArena
BeginTempArena(Arena* arena)
{
	TempArena t = {
		arena: arena,
	};

	auto n = arena.first;
	for(;;)
	{
		if(n.next == null)
		{
			t.start_pool = n;
			t.start_pos  = n.pos;
			break;
		}

		n = n.next;
	}

	return t;
}

void
End(TempArena* t)
{
	bool resetting = false;
	for(auto n = t.arena.first; n != null; n = n.next)
	{
		if(t.start_pool == n)
		{
			n.pos = t.start_pos;
			resetting = true;
		}
		else if(resetting)
		{
			n.pos = 0;
		}
	}
}

T[]
Alloc(T)(TempArena* t, T[] target)
{
	return Alloc!(T)(t.arena, target);
}

T[]
Alloc(T)(TempArena* t, usize count, T set)
{
	return Alloc!(T)(t.arena, count, set);
}

T[]
Alloc(T)(TempArena* t, usize count)
{
	return Alloc!(T)(t.arena, count);
}

T[]
Alloc(T)(TempArena* t, T[] target)
{
	return Alloc!(T)(t.arena, target);
}

T[]
Alloc(T)(TempArena* t, T[] target, usize start, usize len)
{
	return Alloc!(T)(t.arena, target, start, len);
}

T*
Alloc(T)(TempArena* t)
{
	return Alloc!(T)(t.arena);
}

void
AddArenaPool(Arena* arena, usize size)
{
	u8* mem = Alloc!(u8)(size + ArenaPool.sizeof).ptr;

	ArenaPool* node = cast(ArenaPool*)mem;

	node.mem = (cast(u8*)mem) + ArenaPool.sizeof;
	node.pos = 0;
	node.length = size;

	assert(node.mem != null, "Unable to allocate memory for arena");

	SLLPushFront(arena, node, null);
}

T[]
Alloc(T)(Arena* arena, usize count)
{
	void* mem = AllocAlign(arena, T.sizeof * count, DEFAULT_ALIGNMENT);
	memset(mem, 0, T.sizeof * count);
	return (cast(T*)mem)[0 .. cast(usize)count];
}

T[]
Alloc(T)(Arena* arena, T[] target)
{
	T[] arr = Alloc!(T)(arena, target.length);
	arr[]   = target[];
	return arr;
}

string
Alloc(Arena* arena, string target)
{
	u8[] str = Alloc!(u8)(arena, target.length);
	str[]    = cast(u8[])target[];
	return ConvToStr(str);
}

T[]
Alloc(T)(Arena* arena, T[] target, usize start, usize len)
{
	T[] arr     = Alloc!(T)(arena, len);
	arr[0 .. $] = target[start .. start+len];
	return arr;
}

string
Alloc(Arena* arena, string target, usize start, usize len)
{
	u8[] str    = Alloc!(u8)(arena, len);
	str[0 .. $] = cast(u8[])target[start .. start+len];
	return ConvToStr(str);
}

T[]
Alloc(T)(Arena* arena, usize count, T set)
{
	T[] arr = Alloc!(T)(arena, count);
	arr[]   = set;
	return arr;
}

T* 
Alloc(T)(Arena* arena)
{
	void* mem = AllocAlign(arena, T.sizeof, DEFAULT_ALIGNMENT);
	memset(mem, 0, T.sizeof);
	return cast(T*)mem;
};

void* 
AllocAlign(Arena* arena, usize size, usize alignment)
{
	void* ptr = null;

	usize pool_alloc_size = size;
	if(pool_alloc_size > arena.def_size)
	{
		pool_alloc_size += arena.def_size;
	}

	usize mem_pos, current, offset;
	ArenaPool* node = arena.first;
	while(true)
	{
		if(node == null)
		{
			AddArenaPool(arena, Max(pool_alloc_size, arena.def_size));
			node = arena.first;
		}

		mem_pos = cast(usize)node.mem;
		current = mem_pos + node.pos;
		offset  = AlignPow2(current, alignment) - mem_pos;

		if(offset+size <= node.length)
		{
			break;
		}

		node = node.next;
	}
	
	ptr      = &node.mem[offset];
	node.pos = offset+size;

	return ptr;
};

void
Reset(Arena* arena)
{
	ArenaPool* node = arena.first;
	while(node != null)
	{
		node.pos = 0;
		node = node.next;
	}
}

void
Free(Arena* arena)
{
	ArenaPool* node = arena.first;
	ArenaPool* next;
	while(node != null)
	{
		next = node.next;
		Free(node);
		node = next;
	}
}

void
ResetScratch(usize size)
{
	if(!g_scratch.init)
	{
		g_scratch.arena = CreateArena(size);
		g_scratch.init = true;
	}

	Reset(&g_scratch.arena);
}

T*
ScratchAlloc(T)()
{
	return Alloc!(T)(&g_scratch.arena);
}

T[]
ScratchAlloc(T)(usize count)
{
	return Alloc!(T)(&g_scratch.arena, count);
}

T[]
ScratchAlloc(T)(T[] target)
{
	T[] arr = ScratchAlloc!(T)(target.length);
	arr[] = target[];
	return arr;
}

string
ScratchAlloc(string target)
{
	u8[] str = ScratchAlloc!(u8)(target.length);
	str[]    = cast(u8[])target[];
	return ConvToStr(str);
}

T[]
ScratchAlloc(T)(T[] target, usize start, usize len)
{
	T[] arr     = ScratchAlloc!(T)(len);
	arr[0 .. $] = target[start .. start+len];
	return arr;
}

string
ScratchAlloc(string target, usize start, usize len)
{
	u8[] str    = ScratchAlloc!(u8)(len);
	str[0 .. $] = cast(u8[])target[start .. start+len];
	return ConvToStr(str);
}

T[]
ScratchAlloc(T)(usize count, T set)
{
	T[] arr = ScratchAlloc!(T)(count);
	arr[] = set;
	return arr;
}

version(DLIB_TEST)
{
	void WriteToArray(T)(T[] arr, T factor)
	{
		foreach(i; 0 .. arr.length)
		{
			arr[i] = i*factor;
		}
	}

	void AssertArrayValues(T)(T[] arr, T factor)
	{
		foreach(i; 0 .. arr.length)
		{
			assert(arr[i] == i*factor);
		}
	}

	void DLibTestAlloc()
	{
		{
			u64[5] arr0 = [1, 2, 3, 4, 5];

			u64[] copy = Alloc!(u64)(arr0);

			assert(arr0 == copy);

			u64[] arr1 = Alloc!(u64)(64);

			WriteToArray(arr1, 5);

			assert(arr0 == copy);
			AssertArrayValues(arr1,  5);
		}

		{
			Arena a = CreateArena(64);

			u64[] arr0 = Alloc!(u64)(&a, 128);
			u64[] arr1 = Alloc!(u64)(&a, 256);

			WriteToArray(arr0, 2);
			WriteToArray(arr1, 3);
			WriteToArray(arr0, 2);

			AssertArrayValues(arr0, 2);
			AssertArrayValues(arr1, 3);

			Reset(&a);

			arr0 = Alloc!(u64)(&a, 256);
			arr1 = Alloc!(u64)(&a, 128);

			WriteToArray(arr0, 4);
			WriteToArray(arr1, 5);
			WriteToArray(arr0, 4);

			AssertArrayValues(arr0, 4);
			AssertArrayValues(arr1, 5);
		}
	}

	unittest
	{
		DLibTestAlloc();
	}
}

version(WebAssembly) nothrow @nogc:

struct MemBlock
{
	void*     addr;
	MemBlock* next;
	usize     size;
}

struct Heap
{
	MemBlock* free;
	MemBlock* used;
	MemBlock* fresh;
	usize     top;
}

extern extern(C) u8 __heap_base;
extern extern(C) u8 __data_end;

__gshared Heap* heap;
__gshared void* heap_limit;
__gshared usize heap_split_thresh;
__gshared usize heap_alignment;
__gshared usize heap_max_blocks;
__gshared usize current_pages;

enum bool MALLOC_COMPACT = true;
enum bool MALLOC_SPLIT   = true;

usize WasmPageSize(usize x) => x*1024*64;

/**
 * If compaction is enabled, inserts block
 * into free list, sorted by addr.
 * If disabled, add block has new head of
 * the free list.
 */
void 
InsertBlock(MemBlock *block) 
{
	static if(MALLOC_COMPACT)
	{
		MemBlock *ptr  = heap.free;
		MemBlock *prev = null;
		while(ptr != null) 
		{
			if(cast(usize)block.addr <= cast(usize)ptr.addr) 
			{
				break;
			}
			prev = ptr;
			ptr  = ptr.next;
		}

		if(prev != null) 
		{
			prev.next = block;
		} 
		else 
		{
			heap.free = block;
		}
		block.next = ptr;
	}
	else
	{
		block.next = heap.free;
		heap.free  = block;
	}
}

void 
ReleaseBlocks(MemBlock *scan, MemBlock *to) 
{
	MemBlock *scan_next;
	while(scan != to) 
	{
		scan_next  = scan.next;
		scan.next  = heap.fresh;
		heap.fresh = scan;
		scan.addr  = null;
		scan.size  = 0;
		scan       = scan_next;
	}
}

void 
Compact() 
{
	MemBlock *ptr = heap.free;
	MemBlock *prev;
	MemBlock *scan;
	while(ptr != null) 
	{
		prev = ptr;
		scan = ptr.next;

		while(scan != null && cast(usize)prev.addr + prev.size == cast(usize)scan.addr) 
		{
			prev = scan;
			scan = scan.next;
		}

		if(prev != ptr) 
		{
			usize new_size = cast(usize)prev.addr - cast(usize)ptr.addr + prev.size;
			ptr.size   = new_size;
			MemBlock *next = prev.next;
			// make merged blocks available
			ReleaseBlocks(ptr.next, prev.next);
			// relink
			ptr.next = next;
		}

		ptr = ptr.next;
	}
}

void 
MallocInit(const usize heap_blocks, const usize split_thresh, const usize alignment) 
{
	current_pages = WasmSize();
	void* limit   = cast(void *)WasmPageSize(current_pages);

	heap              = cast(Heap *)&__heap_base;
	heap_limit        = limit;
	heap_split_thresh = split_thresh;
	heap_alignment    = alignment;
	heap_max_blocks   = heap_blocks;

	heap.free   = null;
	heap.used   = null;
	heap.fresh  = cast(MemBlock*)(heap + 1);
	heap.top    = cast(usize)(heap.fresh + heap_blocks);

	MemBlock *block = heap.fresh;
	usize     i     = heap_max_blocks - 1;
	while(i--) 
	{
		block.next = block + 1;
		block++;
	}

	block.next = null;
}

void 
free(void *ptr) 
{
	MemBlock *block = heap.used;
	MemBlock *prev  = null;
	while(block != null) 
	{
		if(ptr == block.addr) 
		{
			if(prev) 
			{
				prev.next = block.next;
			} 
			else 
			{
				heap.used = block.next;
			}
			InsertBlock(block);

			static if(MALLOC_COMPACT)
			{
				Compact();
			}
		}
		prev  = block;
		block = block.next;
	}
}

usize
PtrInfo(void *ptr)
{
	usize result = 0;

	MemBlock *block = heap.used;
	while(block != null)
	{
		if(ptr == block.addr)
		{
			result = block.size;
			break;
		}
	}

	return result;
}

MemBlock*
AllocBlock(usize num) 
{
	MemBlock *ptr  = heap.free;
	MemBlock *prev = null;
	usize top  = heap.top;
	num         = (num + heap_alignment - 1) & -heap_alignment;
	while(ptr != null) 
	{
		const i32 is_top = (cast(usize)ptr.addr + ptr.size >= top) && (cast(usize)ptr.addr + num <= cast(usize)heap_limit);
		if(is_top || ptr.size >= num) 
		{
			if(prev != null) 
			{
				prev.next = ptr.next;
			} 
			else 
			{
				heap.free = ptr.next;
			}
			ptr.next  = heap.used;
			heap.used = ptr;

			static if(!MALLOC_SPLIT)
			{
				if(is_top) 
				{
					ptr.size = num;
					heap.top = cast(usize)ptr.addr + num;
					static if(MALLOC_COMPACT)
					{
						Compact();
					}
				}
			}
			else
			{
				if(is_top) 
				{
					ptr.size = num;
					heap.top = cast(usize)ptr.addr + num;
				} 
				else if(heap.fresh != null) 
				{
					usize excess = ptr.size - num;
					if(excess >= heap_split_thresh) 
					{
						ptr.size        = num;
						MemBlock *split = heap.fresh;
						heap.fresh      = split.next;
						split.addr      = cast(void *)(cast(usize)ptr.addr + num);
						split.size      = excess;
						InsertBlock(split);
						static if(MALLOC_COMPACT)
						{
							Compact();
						}
					}
				}
			}

			return ptr;
		}

		prev = ptr;
		ptr  = ptr.next;
	}

	// no matching free blocks
	// see if any other blocks available
	usize new_top = top + num;
	if(heap.fresh != null && new_top <= cast(usize)heap_limit) 
	{
		ptr         = heap.fresh;
		heap.fresh = ptr.next;
		ptr.addr   = cast(void *)top;
		ptr.next   = heap.used;
		ptr.size   = num;
		heap.used  = ptr;
		heap.top   = new_top;

		return ptr;
	}

	return null;
}

usize
BytesToPages(usize count)
{
	usize pages = count/(1024*64);
	return pages > 0 ? pages : 1;
}

void*
malloc(usize num)
{
	MemBlock *block = AllocBlock(num);

	if(block == null)
	{
		WasmGrow(BytesToPages(num));
		usize new_pages = WasmSize();
		usize page_diff = new_pages - current_pages;
		if(page_diff > 0)
		{
			current_pages = new_pages;
			heap_limit    = cast(void*)WasmPageSize(current_pages);

			block = AllocBlock(num);
		}
	}

	return block != null ? block.addr : null;
}

void 
MemClear(void *ptr, usize num) 
{
	usize *ptrw = cast(usize*)ptr;
	usize numw  = (num & -usize.sizeof) / usize.sizeof;
	while(numw--) 
	{
		*ptrw++ = 0;
	}
	num     &= (usize.sizeof - 1);
	u8* ptrb = cast(u8*)ptrw;
	while(num--) 
	{
		*ptrb++ = 0;
	}
}

void*
calloc(usize num, usize size) 
{
	num *= size;
	MemBlock *block = AllocBlock(num);

	{
		MemClear(block.addr, num);
		return block.addr;
	}

	return null;
}

void*
realloc(void *ptr, usize size)
{
	void  *new_ptr  = malloc(size);
	usize prev_size = PtrInfo(ptr);

	memcpy(new_ptr, ptr, prev_size);

	free(ptr);

	return new_ptr;
}

usize 
CountBlocks(MemBlock *ptr) 
{
	usize num = 0;
	while(ptr != null) 
	{
		num++;
		ptr = ptr.next;
	}
	return num;
}

usize MallocFreeCount() 
{
	return CountBlocks(heap.free);
}

usize MallocUsedCount() 
{
	return CountBlocks(heap.used);
}

usize MallocMallocFreshCount() 
{
	return CountBlocks(heap.fresh);
}

bool MallocCheck() 
{
	return heap_max_blocks == MallocFreeCount() + MallocUsedCount() + MallocMallocFreshCount();
}

