#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <stdio.h>
#include <stddef.h>
#include <string.h>
#include <unistd.h>

#include "QF/cmem.h"
#include "QF/set.h"
#include "QF/sys.h"

#define SUPER_LINES (sizeof (memsuper_t) / MEM_LINE_SIZE)

static int
test_block (memsuper_t *super)
{
	size_t      size = super->page_size;
	void       *mem = cmemalloc (super, size);
	memblock_t *block;

	if (!mem) {
		fprintf (stderr, "could not allocate %zd byte block\n",
				 super->page_size);
		return 0;
	}
	if ((size_t) mem & super->page_mask) {
		fprintf (stderr, "mem not page aligned: %p %zd\n",
				 mem, super->page_size);
		return 0;
	}
	block = super->memblocks;
	if ((size_t) mem != (size_t) block + super->page_size) {
		fprintf (stderr, "super does not point to mem\n");
		return 0;
	}
	if (block->post_size < size) {
		fprintf (stderr, "block post_size too small: %zd < %zd\n",
				 block->post_size, size);
		return 0;
	}
	if (block->post_size - size >= super->page_size) {
		fprintf (stderr, "block post_size too big: %zd < %zd\n",
				 block->post_size - size, super->page_size);
		return 0;
	}
	memset (mem, 0, size);	// valgrind check
	cmemfree (super, mem);
	if ((size_t) super->memblocks + super->page_size == (size_t) mem) {
		fprintf (stderr, "super still points to mem\n");
		return 0;
	}
	return 1;
}

static int
check_block (memblock_t *block, int line_count, int allocated)
{
	set_t      *visited = set_new ();
	size_t      free_bytes = 0;
	int         ret = 1;
	int         count = 0;

	for (memline_t **l = &block->free_lines; *l; l = &(*l)->block_next) {
		memline_t  *line = *l;
		ptrdiff_t   ind = (byte *) line - (byte *) block;
		ind /= MEM_LINE_SIZE;
		if (ind < 1 || (size_t ) (ind - 1) > block->size / MEM_LINE_SIZE) {
			fprintf (stderr, "line outside of block: %p %p\n",
					 line, block);
			return 0;
		}
		if (set_is_member (visited, ind)) {
			fprintf (stderr, "loop in block free_lines\n");
			return 0;
		}
		count++;
		set_add (visited, ind);
		if (!line->size) {
			fprintf (stderr, "line with size 0\n");
			ret = 0;
		}
		if (line->block_next && line->block_next < line) {
			fprintf (stderr, "line link in wrong direction\n");
			ret = 0;
		}
		if ((size_t) line + line->size == (size_t) line->block_next) {
			fprintf (stderr, "adjacant free line blocks\n");
			ret = 0;
		}
		if (line->block_prev != l) {
			fprintf (stderr, "line block_prev link incorrect\n");
			ret = 0;
		}
		if (line->size > block->size) {
			fprintf (stderr, "line size too large: %zd / %zd\n",
					 line->size, block->size);
			ret = 0;
		}
		if (line->size % MEM_LINE_SIZE) {
			fprintf (stderr, "bad line size: %zd / %zd\n",
					 line->size, line->size % MEM_LINE_SIZE);
			ret = 0;
		}
		if (ret) {
			free_bytes += line->size;
		}
	}
	if (ret) {
		if (free_bytes + block->allocated != block->size) {
			fprintf (stderr, "block space mismatch: s: %zd a: %zd f: %zd\n",
					 block->size, block->allocated, free_bytes);
			ret = 0;
		}
		if (line_count >= 0 && line_count != count) {
			fprintf (stderr, "incorrect number of lines: e: %d g: %d\n",
					 line_count, count);
			ret = 0;
		}
		if (allocated >= 0 && (size_t) allocated != block->allocated) {
			fprintf (stderr, "allocated wrong size: %zd != %d\n",
					 block->allocated, allocated);
		}
	}
	set_delete (visited);
	return ret;
}

static int __attribute__ ((pure))
check_for_loop (memline_t *line, memline_t **stop)
{
	memline_t  *next = line->free_next;

	for (memline_t **l = line->free_prev; l != stop;
		 l = line->free_prev) {
		line = (memline_t *) l;
		if (line == next) {
			return 1;
		}
	}
	return 0;
}

static int
check_bins (memsuper_t *super, int mask)
{
	int ret = 1;
	for (int i = MAX_CACHE_LINES; i-- > 0; ) {
		if (mask >= 0) {
			if (mask & (1 << i)) {
				if (!super->free_lines[i]) {
					fprintf (stderr, "super free_lines[%d] is empty\n", i);
					ret = 0;
				}
			} else {
				if (super->free_lines[i]) {
					fprintf (stderr, "super free_lines[%d] is occupied\n", i);
					ret = 0;
				}
			}
		}
		for (memline_t **l = &super->free_lines[i]; *l; l = &(*l)->free_next) {
			memline_t  *line = *l;
			if (line->free_prev != l) {
				fprintf (stderr, "super free_lines[%d] has bad prev\n", i);
				ret = 0;
				break;
			}
			if (check_for_loop (line, &super->free_lines[i])) {
				fprintf (stderr, "super free_lines[%d] loop detected\n", i);
				ret = 0;
				break;
			}
			if ((MEM_LINE_SIZE << i) > (int) line->size
				|| (MEM_LINE_SIZE << (i + 1)) <= (int) line->size) {
				fprintf (stderr, "line in wrong i: %d %d %zd\n",
						 i, MEM_LINE_SIZE << i, line->size);
				ret = 0;
			}
		}
	}
	return ret;
}

static int
test_line (memsuper_t *super)
{
	memline_t  *line1 = cmemalloc (super, MEM_LINE_SIZE);
	memline_t  *line2 = cmemalloc (super, MEM_LINE_SIZE);
	memline_t  *line3 = cmemalloc (super, MEM_LINE_SIZE);
	memblock_t *block = super->memblocks;

	if (block->next) {
		fprintf (stderr, "too many memblocks\n");
		return 0;
	}
	if (line1 < (memline_t *) (block + 1)
		|| line1 >= (memline_t *) ((byte *) block + super->page_size)) {
		fprintf (stderr, "line1 outside block line pool: %p %p\n",
				 line1, block);
		return 0;
	}
	if (line2 < (memline_t *) (block + 1)
		|| line2 >= (memline_t *) ((byte *) block + super->page_size)) {
		fprintf (stderr, "line2 outside block line pool: %p %p\n",
				 line2, block);
		return 0;
	}
	if (line3 < (memline_t *) (block + 1)
		|| line3 >= (memline_t *) ((byte *) block + super->page_size)) {
		fprintf (stderr, "line3 outside block line pool: %p %p\n",
				 line3, block);
		return 0;
	}
	if (!((size_t) line1 & super->page_mask)) {
		fprintf (stderr, "line1 is page aligned\n");
		return 0;
	}
	if (!((size_t) line2 & super->page_mask)) {
		fprintf (stderr, "line2 is page aligned\n");
		return 0;
	}
	if (!((size_t) line3 & super->page_mask)) {
		fprintf (stderr, "line3 is page aligned\n");
		return 0;
	}
	if (line1 + 1 != line2 || line2 + 1 != line3) {
		fprintf (stderr, "lines not contiguous\n");
		return 0;
	}
	if (line3 + 1 != block->free_lines) {
		fprintf (stderr, "line3 not contiguous with free lines\n");
		return 0;
	}

	if (!check_block (block, 1, (3 + SUPER_LINES) * MEM_LINE_SIZE)) {
		fprintf (stderr, "line block check 1 failed\n");
		return 0;
	}
	if (!check_bins (super, 0x20)) {
		fprintf (stderr, "bin check 1 failed\n");
		return 0;
	}

	cmemfree (super, line2);

	if (!check_block (block, 2, (2 + SUPER_LINES) * MEM_LINE_SIZE)) {
		fprintf (stderr, "line block check 2 failed\n");
		return 0;
	}
	if (!check_bins (super, 0x21)) {
		fprintf (stderr, "bin check 2 failed\n");
		return 0;
	}

	if (block->free_lines != line2) {
		fprintf (stderr, "block free_lines not pointing to line2\n");
		return 0;
	}
	if (super->free_lines[0] != line2) {
		fprintf (stderr, "super free_lines[0] not pointing to line2\n");
		return 0;
	}

	cmemfree (super, line3);
	if (!check_block (block, 1, (1 + SUPER_LINES) * MEM_LINE_SIZE)) {
		fprintf (stderr, "line block check 3 failed\n");
		return 0;
	}
	if (!check_bins (super, 0x20)) {
		fprintf (stderr, "bin check 3 failed\n");
		return 0;
	}

	if (block->free_lines != line2) {
		fprintf (stderr, "free lines not pointing to line2 2\n");
		return 0;
	}

	cmemfree (super, line1);
	if (!check_block (block, 1, (0 + SUPER_LINES) * MEM_LINE_SIZE)) {
		fprintf (stderr, "line block check 4 failed\n");
		return 0;
	}
	if (!check_bins (super, 0x20)) {
		fprintf (stderr, "bin check 4 failed\n");
		return 0;
	}

	if (super->free_lines[5] != line1) {
		fprintf (stderr, "super free_lines[5] not pointing to line1\n");
		return 0;
	}

	line1 = cmemalloc (super, MEM_LINE_SIZE);
	line2 = cmemalloc (super, MEM_LINE_SIZE);
	line3 = cmemalloc (super, MEM_LINE_SIZE);
	block = super->memblocks;

	if (!check_block (block, 1, (3 + SUPER_LINES) * MEM_LINE_SIZE)) {
		fprintf (stderr, "line block check 4 failed\n");
		return 0;
	}
	if (!check_bins (super, 0x20)) {
		fprintf (stderr, "bin check 4 failed\n");
		return 0;
	}

	cmemfree (super, line1);

	if (!check_block (block, 2, (2 + SUPER_LINES) * MEM_LINE_SIZE)) {
		fprintf (stderr, "line block check 5 failed\n");
		return 0;
	}
	if (!check_bins (super, 0x21)) {
		fprintf (stderr, "bin check 5 failed\n");
		return 0;
	}

	cmemfree (super, line2);

	if (!check_block (block, 2, (1 + SUPER_LINES) * MEM_LINE_SIZE)) {
		fprintf (stderr, "line block check 6 failed\n");
		return 0;
	}
	if (!check_bins (super, 0x22)) {
		fprintf (stderr, "bin check 6 failed\n");
		return 0;
	}

	cmemfree (super, line3);

	line1 = cmemalloc (super, MEM_LINE_SIZE);
	line2 = cmemalloc (super, MEM_LINE_SIZE);
	line3 = cmemalloc (super, MEM_LINE_SIZE);
	block = super->memblocks;

	if (!check_block (block, 1, (3 + SUPER_LINES) * MEM_LINE_SIZE)) {
		fprintf (stderr, "line block check 7 failed\n");
		return 0;
	}
	if (!check_bins (super, 0x20)) {
		fprintf (stderr, "bin check 7 failed\n");
		return 0;
	}

	cmemfree (super, line3);

	if (!check_block (block, 1, (2 + SUPER_LINES) * MEM_LINE_SIZE)) {
		fprintf (stderr, "line block check 8 failed\n");
		return 0;
	}
	if (!check_bins (super, 0x20)) {
		fprintf (stderr, "bin check 8 failed\n");
		return 0;
	}

	cmemfree (super, line2);

	if (!check_block (block, 1, (1 + SUPER_LINES) * MEM_LINE_SIZE)) {
		fprintf (stderr, "line block check 9 failed\n");
		return 0;
	}
	if (!check_bins (super, 0x20)) {
		fprintf (stderr, "bin check 9 failed\n");
		return 0;
	}

	cmemfree (super, line1);

	return 1;
}

typedef struct {
	size_t      size;
	int         group_id;
} sline_block_t;

static sline_block_t group_tests[] = {
	{ 2,  4},
	{ 4,  4},
	{ 5,  8},
	{ 3,  4},
	{ 1,  4},
	{ 6,  8},
	{ 9, 16},
	{ 4,  4},
	{ 4,  4},
	{ 7,  8},
	{ 2,  4},
	{ 4,  4},
	{ 8,  8},
	{13, 16},
	{ 3,  4},
	{ 1,  4},
	{ 8,  8},
	{ 8,  8},
	{ 4,  4},
	{ 4,  4},
	{16, 16},
	{32, 32},
	{32, 33},
};
#define num_group_tests (sizeof (group_tests) / sizeof (group_tests[0]))
#define mask(x) (((size_t) (x)) & ~(MEM_LINE_SIZE - 1))
#define pagemask(x,o,s) (((size_t) (x)) & (o (s)->page_mask))

static int
test_sline (memsuper_t *super)
{
	void       *mem[num_group_tests];
	int         ret = 1;

	for (size_t i = 0; i < num_group_tests; i++) {
		mem[i] = cmemalloc (super, group_tests[i].size);
		if (!mem[i]) {
			fprintf (stderr, "mem[%zd] is null\n", i);
			return 0;
		}
		if (!((size_t) mem[i] % MEM_LINE_SIZE)) {
			fprintf (stderr, "mem[%zd] is aligned with chache line\n", i);
			ret = 0;
		}
	}
	for (size_t i = 0; i < num_group_tests; i++) {
		for (size_t j = i + 1; j < num_group_tests; j++) {
			if (mem[i] == mem[j]) {
				fprintf (stderr, "mem[%zd] is dupped with %zd\n", i, j);
				ret = 0;
			}
			if (mask (mem[i]) == mask (mem[j])) {
				if (group_tests[i].group_id != group_tests[j].group_id) {
					fprintf (stderr, "mem[%zd](%d) is grouped with %zd(%d)\n",
							 i, group_tests[i].group_id,
							 j, group_tests[j].group_id);
					ret = 0;
				}
			} else {
				if (group_tests[i].group_id == group_tests[j].group_id) {
					fprintf (stderr,
							 "mem[%zd](%d) is not grouped with %zd(%d)\n",
							 i, group_tests[i].group_id,
							 j, group_tests[j].group_id);
					ret = 0;
				}
			}
			if (pagemask (mem[i], ~, super) != pagemask (mem[j], ~, super)) {
				fprintf (stderr,
						 "mem[%zd](%d) is not block grouped with %zd(%d)\n",
						 i, group_tests[i].group_id,
						 j, group_tests[j].group_id);
			}
		}
	}
	for (size_t i = 0; i < num_group_tests; i++) {
		cmemfree (super, mem[i]);
		void   *newmem = cmemalloc (super, group_tests[i].size);
		if (newmem != mem[i]) {
			fprintf (stderr,
					"%2zd bytes not recycled (%2zd,%2d) (%06zx %06zx)\n",
					 group_tests[i].size, i, group_tests[i].group_id,
					 pagemask (mem[i], ~~, super),
					 pagemask (newmem, ~~, super));
			ret = 0;
		}
		if (!((size_t) newmem % MEM_LINE_SIZE)) {
			fprintf (stderr, "newmem is aligned with chache line %p\n",
					 newmem);
			ret = 0;
		}
	}
	return ret;
}

static int
test_block_line (memsuper_t *super)
{
	void       *mem = cmemalloc (super, 2 * super->page_size);
	void       *line;
	memblock_t *block = super->memblocks;

	if ((size_t) block + super->page_size != (size_t) mem) {
		fprintf (stderr, "super memblocks do not point to mem\n");
		return 0;
	}
	if (block->size < MEM_LINE_SIZE) {
		// need to figure out a way to guarantee a shared block
		fprintf (stderr, "can't allocate line from block\n");
		return 0;
	}
	if (block->next != (memblock_t *) ((size_t) super & ~super->page_mask)) {
		fprintf (stderr, "excess blocks in super\n");
		return 0;
	}
	line = cmemalloc (super, MEM_LINE_SIZE);
	if (!((size_t) line & super->page_mask)) {
		fprintf (stderr, "line is page aligned\n");
		return 0;
	}
	if (0 && super->memblocks->next) {
		// need to figure out a way to guarantee a shared block
		fprintf (stderr, "mem and line not in same block\n");
		return 0;
	}
	cmemfree (super, mem);
	if (!super->memblocks) {
		fprintf (stderr, "shared block freed\n");
		return 0;
	}
	if (cmemalloc (super, super->page_size) != mem) {
		fprintf (stderr, "block not reused for mem\n");
		return 0;
	}
	//if (super->memblocks != block || super->memblocks->next) {
	if (super->memblocks != block || !super->memblocks->next
		|| super->memblocks->next->next) {
		// need to figure out a way to guarantee a shared block
		fprintf (stderr, "blocks corrupt\n");
		return 0;
	}
	cmemfree (super, line);
	if (!super->memblocks) {
		fprintf (stderr, "shared block freed 2\n");
		return 0;
	}
	cmemfree (super, mem);
	if (0 && super->memblocks) {
		fprintf (stderr, "shared block not freed\n");
		return 0;
	}
	return 1;
}

int
main (void)
{
	memsuper_t *super = new_memsuper ();
	int         i;
#if __WORDSIZE == 32
	if (sizeof (memsuper_t) != 1 * MEM_LINE_SIZE) {
		fprintf (stderr, "memsuper_t not 2 * cache size: %zd\n",
				 sizeof (memsuper_t));
		return 1;
	}
#else
	if (sizeof (memsuper_t) != 2 * MEM_LINE_SIZE) {
		fprintf (stderr, "memsuper_t not 2 * cache size: %zd\n",
				 sizeof (memsuper_t));
		return 1;
	}
#endif
	if (sizeof (memline_t) != MEM_LINE_SIZE) {
		fprintf (stderr, "memline_t not cache size: %zd\n",
				 sizeof (memline_t));
		return 1;
	}
	if (sizeof (memsline_t) != 2 * sizeof (void *)) {
		fprintf (stderr, "memsline_t not two pointers: %zd\n",
				 sizeof (memsline_t));
		return 1;
	}
	if (sizeof (memblock_t) != MEM_LINE_SIZE) {
		fprintf (stderr, "memblock_t not cache size: %zd\n",
				 sizeof (memblock_t));
		return 1;
	}
	if ((size_t) super & (MEM_LINE_SIZE - 1)) {
		fprintf (stderr, "super block not cache aligned: %p\n", super);
		return 1;
	}
	if (super->page_size != Sys_PageSize ()) {
		fprintf (stderr, "page size not equal to system page size: %zd, %zd\n",
				 super->page_size, Sys_PageSize ());
		return 1;
	}
	if (!super->page_size || (super->page_size & (super->page_size - 1))) {
		fprintf (stderr, "page size not power of two: %zd\n",
				 super->page_size);
		return 1;
	}
	if (super->page_mask + 1 != super->page_size) {
		fprintf (stderr, "page mask not page size - 1: %zx %zx\n",
				 super->page_mask, super->page_size);
		return 1;
	}
	if (!super->page_mask || (super->page_mask & (super->page_mask + 1))) {
		fprintf (stderr, "page mask not all 1s: %zx\n",
				 super->page_mask);
		return 1;
	}
	if (super->memblocks
		!= (memblock_t *) ((size_t) super & ~super->page_mask)) {
		fprintf (stderr, "superblock not in block a: %p %p\n", super->memblocks, super);
		return 1;
	}
	for (i = 4; i-- > 0; ) {
		if (super->last_freed[i]) {
			break;
		}
	}
	if (i >= 0) {
		fprintf (stderr, "super last_freed not all null\n");
		return 1;
	}
#if 0 // no longer valid
	for (i = MAX_CACHE_LINES; i-- > 0; ) {
		if (super->free_lines[i]) {
			break;
		}
	}
	if (i >= 0) {
		fprintf (stderr, "super free_lines not all null\n");
		return 1;
	}
#endif
	if (!test_block (super)) {
		fprintf (stderr, "block tests failed\n");
		return 1;
	}
	if (super->memblocks
		!= (memblock_t *) ((size_t) super & ~super->page_mask)) {
		fprintf (stderr, "superblock not in block b: %p %p\n", super->memblocks, super);
		return 1;
	}
	for (i = 4; i-- > 0; ) {
		if (super->last_freed[i]) {
			break;
		}
	}
	if (i >= 0) {
		fprintf (stderr, "super last_freed not all null 2\n");
		return 1;
	}
#if 0 // no longer valid
	for (i = MAX_CACHE_LINES; i-- > 0; ) {
		if (super->free_lines[i]) {
			break;
		}
	}
	if (i >= 0) {
		fprintf (stderr, "super free_lines not all null 2\n");
		return 1;
	}
#endif
	if (!test_line (super)) {
		fprintf (stderr, "line tests failed\n");
		return 1;
	}
	if (super->memblocks
		!= (memblock_t *) ((size_t) super & ~super->page_mask)) {
		fprintf (stderr, "superblock not in block c: %p %p\n", super->memblocks, super);
		return 1;
	}
	if (!test_block_line (super)) {
		fprintf (stderr, "block-line tests failed\n");
		return 1;
	}
	for (size_t i = 0; i < 2 * super->page_size / MEM_LINE_SIZE; i++) {
		void       *line = cmemalloc (super, MEM_LINE_SIZE);
		if (!line) {
			fprintf (stderr, "could not allocate %d byte line\n",
					 MEM_LINE_SIZE);
			return 1;
		}
		if ((size_t) line % MEM_LINE_SIZE) {
			fprintf (stderr, "line not cache-line aligned: %p %d\n",
					 line, MEM_LINE_SIZE);
			return 1;
		}
		if (!((size_t) line & super->page_mask)) {
			fprintf (stderr, "line is page aligned: %p %zd\n",
					 line, super->page_size);
			return 1;
		}
	}
	if (!test_sline (super)) {
		fprintf (stderr, "sub-line tests failed\n");
		return 1;
	}
	delete_memsuper (super);
	return 0;
}
