#include <setjmp.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "QF/va.h"

#include "QF/simd/types.h"

#define num_tests (sizeof (tests) / sizeof (tests[0]))
static int test_enabled[num_tests] = { 0 };

#include "getopt.h"

#include "QF/cmd.h"
#include "QF/cvar.h"

static bfunction_t test_functions[] = {
	{},	// null function
	{ .first_statement = 0 }
};
static dprograms_t test_progs = {
	.version = PROG_VERSION,
};
static progs_t test_pr;

static jmp_buf jump_buffer;

static void
test_debug_handler (prdebug_t event, void *param, void *data)
{
	progs_t    *pr = data;

	switch (event) {
		case prd_breakpoint:
			if (verbose > 0) {
				printf ("debug: %s\n", prdebug_names[event]);
			}
			longjmp (jump_buffer, 1);
		case prd_subenter:
			if (verbose > 0) {
				printf ("debug: subenter %d\n", *(pr_func_t *) param);
			}
		case prd_subexit:
			break;
		case prd_trace:
			dstatement_t *st = test_pr.pr_statements + test_pr.pr_xstatement;
			if (verbose > 1) {
				printf ("---\n");
				printf ("debug: trace %05x %04x %04x %04x %04x%s\n",
						test_pr.pr_xstatement, st->op, st->a, st->b, st->c,
						pr->globals.stack ? va (0, " %05x", *pr->globals.stack)
										  : "");
				printf ("                        %04x %04x %04x\n",
						st->a + PR_BASE (pr, st, A),
						st->b + PR_BASE (pr, st, B),
						st->c + PR_BASE (pr, st, C));
			}
			if (verbose > 0) {
				PR_PrintStatement (&test_pr, st, 0);
			}
			if (pr->globals.stack) {
				if (*pr->globals.stack & 3) {
					printf ("stack not aligned: %d\n", *pr->globals.stack);
					longjmp (jump_buffer, 3);
				}
			}
			break;
		case prd_runerror:
			printf ("debug: %s: %s\n", prdebug_names[event], (char *)param);
			longjmp (jump_buffer, 3);
		case prd_watchpoint:
		case prd_begin:
		case prd_terminate:
		case prd_error:
		case prd_none:
			printf ("debug: unexpected:%s %p\n", prdebug_names[event], param);
			longjmp (jump_buffer, 2);
	}
}

static void
setup_test (test_t *test)
{
	memset (&test_pr, 0, sizeof (test_pr));
	PR_Init (&test_pr);
	PR_Debug_Init (&test_pr);
	test_pr.progs = &test_progs;
	test_pr.debug_handler = test_debug_handler;
	test_pr.debug_data = &test_pr;
	test_pr.pr_trace = 1;
	test_pr.pr_trace_depth = -1;
	if (test->num_functions && test->functions) {
		test_pr.function_table = calloc ((test->num_functions + 2),
										 sizeof (bfunction_t));
		memcpy (test_pr.function_table, test_functions,
				2 * sizeof (bfunction_t));
		memcpy (test_pr.function_table + 2, test->functions,
				test->num_functions * sizeof (bfunction_t));
	} else {
		test_pr.function_table = test_functions;
	}

	pr_uint_t   num_globals = test->num_globals;
	num_globals += test->extra_globals + test->stack_size;

	test_pr.globals_size = num_globals;
	test_pr.pr_globals = Sys_Alloc (num_globals * sizeof (pr_type_t));
	memcpy (test_pr.pr_globals, test->init_globals,
			test->num_globals * sizeof (pr_type_t));
	memset (test_pr.pr_globals + test->num_globals, 0,
			test->extra_globals * sizeof (pr_type_t));
	if (test->stack_size) {
		pr_ptr_t    stack = num_globals - test->stack_size;
		test_pr.stack_bottom = stack + 4;
		test_pr.globals.stack = (pr_ptr_t *) (test_pr.pr_globals + stack);
		*test_pr.globals.stack = num_globals;
	}
	if (test->edict_area) {
		test_pr.pr_edict_area = test_pr.pr_globals + test->edict_area;
	}
	if (test->double_time || test->float_time) {
		test_pr.fields.nextthink = test->nextthink;
		test_pr.fields.frame = test->frame;
		test_pr.fields.think = test->think;
		test_pr.globals.self = (pr_uint_t *) &test_pr.pr_globals[test->self];
		if (test->double_time) {
			test_pr.globals.dtime = (double *)&test_pr.pr_globals[test->dtime];
			*test_pr.globals.dtime = *test->double_time;
		}
		if (test->float_time) {
			test_pr.globals.ftime = (float *) &test_pr.pr_globals[test->ftime];
			*test_pr.globals.ftime = *test->float_time;
		}
	}

	test_progs.statements.count = test->num_statements + 1;
	test_pr.pr_statements
		= malloc ((test->num_statements + 1) * sizeof (dstatement_t));
	memcpy (test_pr.pr_statements, test->statements,
			(test->num_statements + 1) * sizeof (dstatement_t));
	test_pr.pr_statements[test->num_statements] =
		(dstatement_t) { OP_BREAK, 0, 0, 0 };

	test_pr.pr_strings = (char *) test->strings;
	test_pr.pr_stringsize = test->string_size;
}

static int
check_result (test_t *test)
{
	int         ret = 0;

	if (memcmp (test_pr.pr_globals, test->expect_globals,
				test->num_globals * sizeof (pr_int_t)) == 0) {
		ret = 1;
		printf ("test #%zd: %s: OK\n", test - tests, test->desc);
	} else {
		printf ("test #%zd: %s: words differ\n", test - tests, test->desc);
		for (pr_uint_t i = 0; i < test->num_globals; i += 4) {
			pr_ivec4_t *a = (pr_ivec4_t *) &test->expect_globals[i];
			pr_ivec4_t *b = (pr_ivec4_t *) &test_pr.pr_globals[i];
			if (memcmp (a, b, sizeof (pr_ivec4_t))) {
				printf ("-%4x { %8x, %8x %8x %8x }\n", i, VEC4_EXP (*a));
				printf ("+%4x { %8x, %8x %8x %8x }\n", i, VEC4_EXP (*b));
			}
		}
	}
	return ret;
}

static int
run_test (test_t *test)
{
	int         jump_ret;
	int         ret = 0;

	setup_test (test);

	if (!(jump_ret = setjmp (jump_buffer))) {
		PR_ExecuteProgram (&test_pr, 1);
		printf ("returned from progs\n");
	}
	if (jump_ret == 1) {
		ret = check_result (test);
	} else {
		printf ("test #%zd: %s: critical failure\n", test - tests, test->desc);
	}

	pr_uint_t   num_globals = test->num_globals;
	num_globals += test->extra_globals + test->stack_size;
	if (test->num_functions && test->functions) {
		free (test_pr.function_table);
	}
	Sys_Free (test_pr.pr_globals, num_globals * sizeof (pr_type_t));

	free (test_pr.pr_statements);
	return ret;
}

int
main (int argc, char **argv)
{
	int         c;
	size_t      i, test;
	int         pass = 1;

	Cmd_Init_Hash ();
	Cvar_Init_Hash ();
	Cmd_Init ();
	Cvar_Init ();
	PR_Init_Cvars ();
	pr_boundscheck = 1;

	while ((c = getopt (argc, argv, "qvt:")) != EOF) {
		switch (c) {
			case 'q':
				verbose--;
				break;
			case 'v':
				verbose++;
				break;
			case 't':
				test = atoi (optarg);
				if (test < num_tests) {
					test_enabled[test] = 1;
				} else {
					fprintf (stderr, "Bad test number (0 - %zd)\n", num_tests);
					return 1;
				}
				break;
			default:
				fprintf (stderr, "-q (quiet) -v (verbose) and/or -t TEST "
								 "(test number)\n");
				return 1;
		}
	}

	for (i = 0; i < num_tests; i++)
		if (test_enabled[i])
			break;
	if (i == num_tests) {
		for (i = 0; i < num_tests; i++)
			test_enabled[i] = 1;
	}

	for (i = 0; i < num_tests; i++) {
		if (!test_enabled[i])
			continue;
		pass &= run_test (&tests[i]);
	}

	return !pass;
}
