#include <apos/vmem.h>
#include <apos/mem_nodes.h>
#include <vmem.h>

#define mark_region_used(r) ((r) = 1)
#define mark_region_unused(r) ((r) = 0)
#define is_region_used(r) (r)

static size_t __uvmem_size = 0;

/* pretty major slowdown when we get to some really massive numbers, not
 * entirely sure why. Will need to check up on this at some point, have I
 * somehow managed to come up with a _very_ bad situation for my sp_trees?
 *
 * EDIT: apparently, yeah. Max depth of 106 with a million entries, interesting.
 * I guess since in this scenario all sizes are 1, and I just shove everything
 * to the right? Maybe?
 *
 * EDIT upon EDIT: yeah, when taking the start position of the region into
 * account we get a much more sensible max depth of 39 for 5 million entries.
 * Seems I have found a weakness in sp_trees :D
 *
 * Duplicate entries don't work well with any trees, I think. Good to know,
 * maybe not even anything with sp_trees but more a weakness of binary trees in
 * general?
 */
static struct sp_mem *sp_free_insert_region(struct sp_reg_root *r, struct sp_mem *m)
{
	struct sp_node *n = sp_root(r->free_regions), *p = NULL;
	size_t start = m->start;
	size_t size = m->end - m->start;
	enum sp_dir d = LEFT;

	m->sp_n = (struct sp_node){0};

	while(n){
		struct sp_mem *t = mem_container(n);
		size_t nsize = t->end - t->start;
		p = n;

		if(size < nsize){
			n = sp_left(n);
			d = LEFT;
		}

		else if(size > nsize) {
			n = sp_right(n);
			d = RIGHT;
		}

		else if (start < t->start){
			n = sp_left(n);
			d = LEFT;
		}

		else {
			n = sp_right(n);
			d = RIGHT;
		}
	}

	if(sp_root(r->free_regions))
		sp_insert(&sp_root(r->free_regions), p, &m->sp_n, d);
	else
		sp_root(r->free_regions) = &m->sp_n;

	return m;
}

static struct sp_mem *sp_used_insert_region(struct sp_reg_root *r, struct sp_mem *m)
{
	struct sp_node *n = sp_root(r->used_regions), *p = NULL;
	vm_t start = m->start;
	enum sp_dir d = LEFT;

	m->sp_n = (struct sp_node){0};

	while(n){
		struct sp_mem *t = mem_container(n);

		p = n;

		if(start < t->start){
			n = sp_left(n);
			d = LEFT;
		}

		else {
			/* we should never encounter a situation where start =
			 * t->start */
			n = sp_right(n);
			d = RIGHT;
		}
	}

	if(sp_root(r->used_regions))
		sp_insert(&sp_root(r->used_regions), p, &m->sp_n, d);
	else
		sp_root(r->used_regions) = &m->sp_n;

	return m;
}

int sp_mem_init(struct sp_reg_root *r, vm_t start, size_t arena_size)
{
	/* convert bytes to pages */
	start = __page(start);
	arena_size = __page(arena_size);
	struct sp_mem *m = get_mem_node();
	m->start = start;
	m->end = start + arena_size;
	sp_free_insert_region(r, m);

	return 0;
}

static void __sp_mem_destroy(struct sp_node *n)
{
	if(!n)
		return;

	__sp_mem_destroy(sp_left(n));
	__sp_mem_destroy(sp_right(n));

	struct sp_mem *m = mem_container(n);
	free_mem_node(m);
}

void sp_mem_destroy(struct sp_reg_root *r)
{
	__sp_mem_destroy(sp_root(r->free_regions));
	__sp_mem_destroy(sp_root(r->used_regions));
}

/* interestingly this is now the main bottleneck :D
 *
 * eh, it's not a massive thing I guess, maybe the code could be a bit quicker
 * but I mean 10 000 000 memory allocations in 20 s is good enough for now
 * */
static struct sp_mem *sp_used_find(struct sp_reg_root *r, vm_t start)
{
	struct sp_node *n = sp_root(r->used_regions);
	while(n){
		struct sp_mem *t = mem_container(n);
		if(start == t->start)
			return t;

		if(start < t->start)
			n = sp_left(n);
		else
			n = sp_right(n);
	}

	return 0;
}

static struct sp_mem *sp_mem_create_region(vm_t start, vm_t end,
		struct sp_mem *prev, struct sp_mem *next)
{
	struct sp_mem *m = get_mem_node();
	m->start = start;
	m->end = end;
	m->prev = prev;
	m->next = next;
	return m;
}

/* TODO: should probably check if this actually works :D seems to do, but that's
 * just from really quick checking */
static size_t po_align(size_t s)
{
	for(size_t o = __mm_max_order; o > 0; --o){
		if(s >= __o_size(o))
			return __o_size(o);
	}

	return 0;
}

static struct sp_mem *sp_find_used_closest(struct sp_reg_root *r, vm_t start)
{
	struct sp_mem *closest = 0;
	size_t md = (size_t)(-1);
	struct sp_node *n = sp_root(r->used_regions);
	while(n){
		struct sp_mem *t = mem_container(n);
		size_t d = ABS((ssize_t)start - (ssize_t)t->start);

		if(d == 0) /* exact match */
			return t;

		if(d < md){ /* closest so far */
			closest = t;
			md = d;
		}

		if(start < t->start)
			n = sp_left(n);
		else
			n = sp_right(n);
	}

	return closest;
}

/* should probably document this a bit better but in short, look for the "best"
 * free block, meaning one that is hopefully aligned so as to allow us to later
 * map it to higher order pages. If no block is found such that that is
 * possible, also keep track of the smallest block that we found that the region
 * still fits in, unaligned. If none of these criteria are met, a NULL is
 * returned. Note that this does not check *all* possible memory blocks, only
 * going up in increasing size so as to save time. */
static struct sp_mem *sp_find_free_best(struct sp_reg_root *r, size_t size, size_t *align)
{
	*align = 0;
	size_t offset = __page(po_align(__addr(size)));
	struct sp_mem *quick_best = 0;
	struct sp_node *n = sp_root(r->free_regions);
	while(n){
		struct sp_mem *t = mem_container(n);
		vm_t start = align_up(t->start, offset);

		size_t qsize = t->end - t->start;
		size_t bsize = t->end - start;

		if(!quick_best && size <= qsize)
			quick_best = t;

		if(size <= bsize){
			*align = start - t->start;
			return t;
		}

		n = sp_right(n);
	}

	return quick_best;
}

static size_t sp_use_region(struct sp_reg_root *r, struct sp_mem *m,
		size_t pages, size_t align)
{
	sp_remove(&sp_root(r->free_regions), &m->sp_n);

	vm_t pre_start = m->start;
	vm_t pre_end = pre_start + align;
	
	vm_t start = pre_end;
	vm_t end = start + pages;

	vm_t post_start = end;
	vm_t post_end = m->end;

	if(pre_start != pre_end){
		struct sp_mem *n = sp_mem_create_region(pre_start, pre_end, m->prev, m);
		m->prev = n;
		if(n->prev)
			n->prev->next = n;

		sp_free_insert_region(r, n);
	}

	if(post_start != post_end){
		struct sp_mem *n = sp_mem_create_region(post_start, post_end, m, m->next);
		m->next = n;
		if(n->next)
			n->next->prev = n;

		sp_free_insert_region(r, n);
	}

	m->end = end;
	m->start = start;
	mark_region_used(m->flags);
	sp_used_insert_region(r, m);
	return __addr(start);
}

/* apparently Linux doesn't necessarily give a shit about mmap hints, so I'll
 * just ignore them for now. Note that alloc_region should only be used when
 * mmap is called with MAP_ANON, all other situations should be handled in some
 * fs server */
vm_t alloc_region(struct sp_reg_root *r,
		size_t size, size_t *actual_size)
{
	*actual_size = align_up(size, BASE_PAGE_SIZE);
	size_t pages = __page(*actual_size);

	/* find best fitting, alignment etc. */
	size_t align = 0;
	struct sp_mem *m = sp_find_free_best(r, pages, &align);
	if(!m)
		return 0;

	return sp_use_region(r, m, pages, align);
}


vm_t alloc_fixed_region(struct sp_reg_root *r,
		vm_t start, size_t size, size_t *actual_size)
{
	*actual_size = align_up(size, BASE_PAGE_SIZE);
	size_t pages = __page(*actual_size);
	start = __page(start);

	struct sp_mem *m = sp_find_used_closest(r, start);
	if(!m)
		return 0;

	/* locate actual region where start is between the region start and end */
	while(!((m->start <= start) && (start <= m->end))){
		if(start < m->start)
			m = m->next;
		else
			m = m->prev;
	}

	/* if region is already in use, forget it */
	if(is_region_used(m->flags))
		return 0;

	/* region is too small */
	if(start + pages > m->end)
		return 0;

	/* actually start marking region used */
	return sp_use_region(r, m, pages, start - m->start);
}

static void __sp_try_coalesce_prev(struct sp_reg_root *r, struct sp_mem *m)
{
	while(m){
		if(!m || is_region_used(m->flags))
			return;

		struct sp_mem *p = m->prev;
		if(!p || is_region_used(p->flags))
			return;

		m->start = p->start;
		m->prev = p->prev;

		if(m->prev)
			m->prev->next = m;

		sp_remove(&sp_root(r->free_regions), &p->sp_n);
		free_mem_node(p);

		m = m->prev;
	}
}

static void __sp_try_coalesce_next(struct sp_reg_root *r, struct sp_mem *m)
{
	while(m){
		if(!m || is_region_used(m->flags))
			return;

		struct sp_mem *n = m->next;
		if(!n || is_region_used(n->flags))
			return;

		m->end = n->end;
		m->next = n->next;

		if(m->next)
			m->next->prev = m;

		sp_remove(&sp_root(r->free_regions), &n->sp_n);
		free_mem_node(n);

		m = m->next;
	}
}

static void sp_mem_try_coalesce(struct sp_reg_root *r, struct sp_mem *m)
{
	__sp_try_coalesce_prev(r, m);
	__sp_try_coalesce_next(r, m);
}

void free_region(struct sp_reg_root *r, vm_t start)
{
	/* addr not aligned to page boundary, corrupted or incorrect pointer */
	if(start != __addr(__page(start)))
		return;

	struct sp_mem *m = sp_used_find(r, __page(start));
	if(!m)
		return;

	sp_remove(&sp_root(r->used_regions), &m->sp_n);
	mark_region_unused(m->flags);

	sp_mem_try_coalesce(r, m);
	sp_free_insert_region(r, m);
}

void set_uvmem_size(size_t s)
{
	__uvmem_size = s;
}

size_t uvmem_size()
{
	return __uvmem_size;
}

/* assuming start is chosen to start on an aligned border, this should choose
 * the 'optimal' fit for the mapping.
 *
 * NOTE: not actually optimal, this doesn't bother to go through possible
 * permutations etc. which would be slow and I don't want to implement it.
 */
vm_t map_fill_region(struct vm_branch_t *b, vm_t start, size_t bytes, uint8_t flags)
{
	pm_t offset = 0;
	pm_t runner = __page(start);
	size_t pages = __pages(bytes);
	enum mm_order_t top = __mm_max_order;

	/* actual start might not be the same as the user specified start */
	start = __addr(runner);

	for(; pages; top--){
		size_t o_size = __o_size(top);
		size_t o_pages = __pages(o_size);

		/* NULL does pass this check, so technically all NULL pages are
		 * aligned, but they're caught in the while expr so this should
		 * work even if someone tries to map NULL */
		if(!aligned(runner, o_pages))
			continue;

		while(pages >= o_pages){
			offset = alloc_page(top, offset);
			if(!offset)
				break;

			map_vmem(b, offset, __addr(runner), flags, top);
			pages -= o_pages;
			runner += o_pages;
		}
	}

	return start;
}

vm_t alloc_uvmem(struct tcb *t, size_t s, uint8_t flags)
{
	vm_t v = alloc_region(&t->sp_r, s, &s);
	return map_fill_region(t->b_r, v, s, flags);
}

void free_uvmem(struct tcb *t, vm_t a)
{
	free_region(&t->sp_r, a);
}
