#include <math.h>
#include <stdio.h>

#include <posthaste/execute.h>
#include <posthaste/lower.h>
#include <posthaste/utils.h>
#include <posthaste/date.h>
#include <posthaste/vec.h>

static int64_t load(struct loc l, size_t sp, struct vec *stack,
                    struct vec *globals)
{
	/* extra runtime branches is another reason this bytecode instruction
	 * set is slow to interpret. To get rid of this if (), one
	 * option would be to add LOAD/STORE_GLOBAL instructions that move data
	 * between the global array and the stack, but this would make the JIT
	 * slower */
	if (l.l)
		return vect_at(int64_t, *stack, l.o + sp);

	return vect_at(int64_t, *globals, l.o);
}

static void store(struct loc l, int64_t v, size_t sp, struct vec *stack,
                  struct vec *globals)
{
	if (l.l) {
		vect_at(int64_t, *stack, l.o + sp) = v;
		return;
	}

	vect_at(int64_t, *globals, l.o) = v;
}

static int64_t interpret_func(struct fn *f, struct vec *args, size_t sp,
                              struct vec *stack, struct vec *globals)
{
	/* move args to formal locations */
	size_t formal = 0;
	foreach_vec(ai, *args) {
		int64_t a = vect_at(int64_t, *args, ai);
		vect_at(int64_t, *stack, sp + formal) = a;
		formal += 1;
	}

	vec_reset(args);

	/* load/store from location, selecting between global array and stack. Assumes
	 * parameter names, generally frowned upon. */
#define get(l) \
	load(l, sp, stack, globals)

#define put(l, v) \
	store(l, v, sp, stack, globals)

	/* use computed gotos to avoid extra overhead of loops/switch statements.
	 * Usually I'm not a huge fan of massive single functions and from a
	 * stylistic viewpoint maybe prefer breaking each instruction out into
	 * its own procedure, but this at least saves a bit of typing */
#define LINK(x) [x] = &&CASE_##x
	static void *labels[] = {
		LINK(LABEL),
		LINK(CALL),
		LINK(MOVE),
		LINK(ADD),
		LINK(SUB),
		LINK(MUL),
		LINK(DIV),
		LINK(ARG),
		LINK(RETVAL),
		LINK(PRINT_STRING),
		LINK(PRINT_INT),
		LINK(PRINT_BOOL),
		LINK(PRINT_DATE),
		LINK(PRINT_NEWLINE),
		LINK(PRINT_SPACE),
		LINK(DATE_ADD),
		LINK(DATE_SUB),
		LINK(DATE_DIFF),
		LINK(STORE_DAY),
		LINK(STORE_MONTH),
		LINK(STORE_YEAR),
		LINK(LOAD_DAY),
		LINK(LOAD_MONTH),
		LINK(LOAD_YEAR),
		LINK(LOAD_WEEKDAY),
		LINK(LOAD_WEEKNUM),
		LINK(TODAY),
		LINK(RET),
		LINK(STOP),
		LINK(CONST),
		LINK(EQ),
		LINK(LT),
		LINK(NEG),
		LINK(B),
		LINK(BZ),
		LINK(J),
	};
#undef LINK

#define JUMP() i = vect_at(struct insn, insns, pc); goto *labels[i.k];
#define DISPATCH() pc++; JUMP();

#define CASE(x) CASE_##x

	size_t pc = 0;
	int64_t retval = 0;
	struct vec insns = f->insns;
	struct insn i = {0};
	/* jump to first instruction */
	JUMP();

	CASE(STOP) : {return 0;}
	CASE(RET) : {return get(i.i0);}

	CASE(LABEL) : {DISPATCH();}
	CASE(RETVAL) : {put(i.o, retval); DISPATCH();}

	CASE(J) : {pc = i.v; JUMP();}

	CASE(B) : {
		int64_t i0 = get(i.i0);
		if (i0) {
			pc = i.v;
			JUMP();
		}

		DISPATCH();
	}

	CASE(BZ) : {
		int64_t i0 = get(i.i0);
		if (!i0) {
			pc = i.v;
			JUMP();
		}

		DISPATCH();
	}

	CASE(ARG) : {
		int64_t a = get(i.i0);
		vect_append(int64_t, *args, &a);
		DISPATCH();
	}

	CASE(CALL) : {
		struct fn *cf = find_fn(i.v);
		assert(cf);

		retval = interpret_func(cf, args, sp + f->max_sp + 1, stack,
		                        globals);
		DISPATCH();
	}

	CASE(MOVE) : {
		int64_t i0 = get(i.i0);
		put(i.o, i0);
		DISPATCH();
	}


	CASE(ADD) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);
		int64_t o = i0 + i1;
		put(i.o, o);
		DISPATCH();
	}

	CASE(SUB) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);
		int64_t o = i0 - i1;
		put(i.o, o);
		DISPATCH();
	}

	CASE(MUL) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);
		int64_t o = i0 * i1;
		put(i.o, o);
		DISPATCH();
	}

	CASE(DIV) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);
		int64_t o = i0 / i1;
		put(i.o, o);
		DISPATCH();
	}

	CASE(CONST) : {
		put(i.o, i.v);
		DISPATCH();
	}

	CASE(PRINT_DATE) : {
		int64_t i0 = get(i.i0);
		char str[11] = {0};
		date_to_string(str, i0);
		printf("%s", str);
		DISPATCH();
	}

	CASE(PRINT_INT) : {
		int64_t i0 = get(i.i0);
		printf("%lli", (long long)i0);
		DISPATCH();
	}

	CASE(PRINT_BOOL) : {
		int64_t i0 = get(i.i0);
		printf("%s", i0 ? "true" : "false");
		DISPATCH();
	}

	CASE(PRINT_STRING) : {
		int64_t i0 = get(i.i0);
		printf("%s", (const char *)i0);
		DISPATCH();
	}

	CASE(PRINT_NEWLINE) : {
		putchar('\n');
		DISPATCH();
	}

	CASE(PRINT_SPACE) : {
		putchar(' ');
		DISPATCH();
	}

	CASE(LOAD_DAY) : {
		int64_t i0 = get(i.i0);
		unsigned day = 0;
		date_split((ph_date_t)i0, NULL, NULL, &day);
		put(i.o, (int64_t)day);
		DISPATCH();
	}

	CASE(LOAD_MONTH) : {
		int64_t i0 = get(i.i0);
		unsigned month = 0;
		date_split((ph_date_t)i0, NULL, &month, NULL);
		put(i.o, (int64_t)month);
		DISPATCH();
	}

	CASE(LOAD_YEAR) : {
		int64_t i0 = get(i.i0);
		unsigned year = 0;
		date_split((ph_date_t)i0, &year, NULL, NULL);
		put(i.o, (int64_t)year);
		DISPATCH();
	}

	CASE(LOAD_WEEKDAY) : {
		int64_t i0 = get(i.i0);
		struct tm time = tm_from_date((ph_date_t)i0);
		put(i.o, (int64_t)time.tm_wday);
		DISPATCH();
	}

	CASE(LOAD_WEEKNUM) : {
		int64_t i0 = get(i.i0);
		struct tm time = tm_from_date((ph_date_t)i0);
		put(i.o, time.tm_yday / 7);
		DISPATCH();
	}

	CASE(STORE_DAY) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);

		unsigned year = 0;
		unsigned month = 0;
		date_split((ph_date_t)i0, &year, &month, NULL);
		ph_date_t date = date_from_numbers(year, month, i1);
		put(i.o, (int64_t)date);
		DISPATCH();
	}

	CASE(STORE_MONTH) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);

		unsigned year = 0;
		unsigned day = 0;
		date_split((ph_date_t)i0, &year, NULL, &day);
		ph_date_t date = date_from_numbers(year, i1, day);
		put(i.o, (int64_t)date);
		DISPATCH();
	}

	CASE(STORE_YEAR) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);

		unsigned month = 0;
		unsigned day = 0;
		date_split((ph_date_t)i0, NULL, &month, &day);
		ph_date_t date = date_from_numbers(i1, month, day);
		put(i.o, (int64_t)date);
		DISPATCH();
	}

	CASE(DATE_ADD) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);

		struct tm time = tm_from_date((ph_date_t)i0);
		time.tm_mday += i1;
		mktime(&time);

		ph_date_t date = date_from_tm(time);
		put(i.o, (int64_t)date);
		DISPATCH();
	}

	CASE(DATE_SUB) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);

		struct tm time = tm_from_date((ph_date_t)i0);
		time.tm_mday -= i1;
		mktime(&time);

		ph_date_t date = date_from_tm(time);
		put(i.o, (int64_t)date);
		DISPATCH();
	}

	CASE(DATE_DIFF) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);

		struct tm time0 = tm_from_date((ph_date_t)i0);
		struct tm time1 = tm_from_date((ph_date_t)i1);

		/* close enough at least */
		time_t t0 = mktime(&time0);
		time_t t1 = mktime(&time1);
		double seconds = difftime(t0, t1);
		int64_t days = round(seconds / 86400);
		put(i.o, days);
		DISPATCH();
	}

	CASE(TODAY) : {
		ph_date_t date = current_date();
		put(i.o, (int64_t)date);
		DISPATCH();
	}

	CASE(EQ) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);
		int64_t b = i0 == i1;
		put(i.o, b);
		DISPATCH();
	}

	CASE(LT) : {
		int64_t i0 = get(i.i0);
		int64_t i1 = get(i.i1);
		int64_t b = i0 < i1;
		put(i.o, b);
		DISPATCH();
	}

	CASE(NEG) : {
		int64_t i0 = get(i.i0);
		put(i.o, -i0);
		DISPATCH();
	}

#undef CASE
#undef JUMP
#undef DISPATCH
#undef get
#undef put
}

void interpret()
{
	struct fn *f = find_fn(0);
	assert(f);

	struct vec stack = vec_create(sizeof(int64_t));
	/* arbitrary amount of stack space, could potentially even be expanded
	 * if we run out but that's currently not implemented */
	vec_reserve(&stack, 65535);

	struct vec globals = vec_create(sizeof(int64_t));
	vec_reserve(&globals, num_globals());

	struct vec args = vec_create(sizeof(int64_t));

	interpret_func(f, &args, 0, &stack, &globals);

	vec_destroy(&args);
	vec_destroy(&stack);
	vec_destroy(&globals);
}