#include "vm.h"
#include "util-jn.h"
#include "util-gd.h"
#include "rsrc.h"

#define _safe_wrap(gdt, ct) ({ \
	_Static_assert(sizeof(gdt) == sizeof(ct)); \
	*(typeof(ct)*)v; \
})

void
gdjn_dejanetize_typed
(	GDExtensionTypePtr v,
	GDExtensionVariantType const t,
	Janet val
) {
	switch (t) {
		case GDEXTENSION_VARIANT_TYPE_BOOL:
			assert(janet_type(val) == JANET_BOOLEAN);
			*(bool*)v = janet_unwrap_boolean(val);
			break;
		case GDEXTENSION_VARIANT_TYPE_INT:
			switch (janet_type(val)) {
				case JANET_NUMBER:
					*(int64_t*)v = janet_unwrap_integer(val); break;
				case JANET_INT_S64:
					*(int64_t*)v = janet_unwrap_s64(val); break;
				case JANET_INT_U64:
					*(int64_t*)v = janet_unwrap_u64(val); break;
				default: assert(false);
			}
			break;
		case GDEXTENSION_VARIANT_TYPE_STRING: 
		case GDEXTENSION_VARIANT_TYPE_STRING_NAME: {
			JanetString str;
			switch (janet_type(val)) {
				case JANET_STRING: str = janet_unwrap_string(val); break;
				case JANET_KEYWORD: str = janet_unwrap_keyword(val); break;
				case JANET_SYMBOL: str = janet_unwrap_symbol(val); break;
				default: assert(false);
			}
			size_t len = janet_string_length(str);
			if (t == GDEXTENSION_VARIANT_TYPE_STRING_NAME) {
				_t(stringName).newWithUtf8CharsAndLen(v, (char*)str, len);
			} else {
				_t(string).newWithUtf8CharsAndLen(v, (char*)str, len);
			}
			break;
		}
		case GDEXTENSION_VARIANT_TYPE_ARRAY: {
		}
		case GDEXTENSION_VARIANT_TYPE_DICTIONARY: {
		}
		default: {
			assert(false);
		}
	}
}

Janet
gdjn_janetize_typed
(	GDExtensionTypePtr     const v,
	GDExtensionVariantType const t
) {
	switch (t) {
		case GDEXTENSION_VARIANT_TYPE_NIL:
			return janet_wrap_nil();
		case GDEXTENSION_VARIANT_TYPE_BOOL:
			return janet_wrap_boolean(_safe_wrap(gd_bool, int8_t));
		case GDEXTENSION_VARIANT_TYPE_INT:
			return janet_wrap_s64(_safe_wrap(gd_int, int64_t));
		case GDEXTENSION_VARIANT_TYPE_FLOAT:
			_Static_assert(
				sizeof(gd_float) == sizeof(double) ||
				sizeof(gd_float) == sizeof(float)
			);
			return janet_wrap_number(
				(sizeof(gd_float) == sizeof(double)) ? *(double*)v :
				(sizeof(gd_float) == sizeof(float))  ? *(float*)v  :0);

		case GDEXTENSION_VARIANT_TYPE_STRING: {
			auto str = gdu_string_pdup((gd_string*)v);
			auto j = janet_stringv((void*)str.v, str.sz);
			_free(str.v);
			return j;
		};

		case GDEXTENSION_VARIANT_TYPE_STRING_NAME: {
		   /* we can reasonably assume syms will be small enough
			* to fit on the stack and avoid a pointless malloc */
			auto str = _gdu_stringName_stackp((gd_stringName*)v);
			auto j = janet_keywordv((void*)str.v, str.sz);
			return j;
		};

		case GDEXTENSION_VARIANT_TYPE_ARRAY: {
			auto sz = gd_array_size(v);
			auto ja = janet_array(sz);
			for (size_t i = 0; i < sz; ++i) {
				auto val = _t(array).operatorIndexConst(v, i);
				auto j = gdjn_janetize(val);
				janet_array_push(ja, j);
			}
			return janet_wrap_array(ja);
		};
		default: assert(false);
	}
}


typedef struct jn_closure {
	Janet (*fn)(void* data, int32_t argc, Janet* argv);
	void (*gc)(void* data);
	char alignas(max_align_t) data [];
} jn_closure;

typedef struct jn_hnd_dict {
	JanetAbstractHead header;
	GDExtensionVariantType key, val;
	gd_dictionary dict;
} jn_hnd_dict;

typedef struct jn_hnd_array {
	JanetAbstractHead header;
	GDExtensionVariantType ty;
	gd_array array;
} jn_hnd_array;

static int
api_del_closure(void* ptr, size_t sz) {
	jn_closure* c = ptr;
	if (c -> gc != nullptr) {
		(*c -> gc)(c -> data);
	}
	return 0;
}

static Janet
api_call_closure
(	void* ptr,
	int32_t argc,
	Janet* argv
) {
	jn_closure* c = ptr;
	return (c -> fn)(c -> data, argc, argv);
}

static int
api_del_dict(void* ptr, size_t sz) {
	jn_hnd_dict* dict = ptr;
	_t(dictionary).dtor(&dict -> dict);
	printf("drop dict\n");
	return 0;
}
static int
api_del_array(void* ptr, size_t sz) {
	jn_hnd_array* array = ptr;
	_t(array).dtor(&array -> array);
	printf("drop array\n");
	return 0;
}

const JanetAbstractType jn_closure_def = {
	.name = "closure",
	.call = api_call_closure,
	.gc = api_del_closure,
};

const JanetAbstractType jn_hnd_dict_def = {
	.name = "prim/type/dict",
	.gc = api_del_dict,
};

const JanetAbstractType jn_hnd_array_def = {
	.name = "prim/type/array",
	.gc = api_del_array,
};


static Janet
api_new_array(int32_t argc, Janet* argv) {
	auto a = (jn_hnd_array*)janet_abstract(&jn_hnd_array_def, sizeof(jn_hnd_array));
	_t(array).empty(&a -> array, nullptr);
	printf("create array\n");
	return janet_wrap_abstract(a);
}

static Janet
api_new_dict(int32_t argc, Janet* argv) {
	auto a = (jn_hnd_dict*)janet_abstract(&jn_hnd_dict_def, sizeof(jn_hnd_dict));
	_t(dictionary).empty(&a -> dict, nullptr);
	printf("create dict\n");
	return janet_wrap_abstract(a);
}


/* (prim/class-load [<ident> [<ident2>...]])
 * low-level class loader. run at compile time to
 * import a godot class */
static Janet
api_class_load(int32_t argc, Janet* argv) {
	return janet_wrap_nil(); /* FIXME */
}

static const JanetReg reg_core [] = {
	{"class-load", api_class_load,
		"(prim/class-load ident)\n\n"
		"low-level loading function for Godot classes"},
	{"type/array", api_new_array,
		"(prim/type/array [...])\n\n"
		"create a handle to a new godot array object"},
	{"type/dict", api_new_dict,
		"(prim/type/dict <key-type> <val-type> {...})\n\n"
		"create a handle to a new godot dictionary object"},
	{}
};


JanetTable*
gdjn_vm_api_spawnEnv (JanetTable* api) {
	/* create a clean new environment that can be used
	 * and discarded by a script without contaminating
	 * the global environment(s)
	 * yes this is ooky */
	auto env = jnu_table_extend(api, 8);
	auto sym_mc = janet_csymbolv("module/cache");
	auto cleancache = jnu_table_extend(
			janet_unwrap_table(janet_table_get(api, sym_mc)), 8);

	janet_table_put(env, janet_csymbolv("module/cache"), janet_wrap_table(cleancache));
	return env;
}


gdjn_vm_bind
gdjn_vm_meta
(	JanetTable* bind
) {
	gdjn_vm_bind b = {};

	if (gdjn_vm_metaFlag(bind, "private")) goto fail;
	if (!gdjn_vm_metaKey(bind, "value", &b.val)) goto fail;

	if (gdjn_vm_metaFlag(bind, "macro"))
		b.kind = gdjn_vm_bind_mac;
	else {
		if (gdjn_vm_metaKey(bind, "method", &b.meta))
			/* TODO assert callability */
			b.kind = gdjn_vm_bind_method;
		else if (gdjn_vm_metaFlag(bind, "class"))
			b.kind = gdjn_vm_bind_class;
		else if (gdjn_vm_metaKey(bind, "type", &b.meta)) {
			if (gdjn_vm_metaFlag(bind, "prop")) {
				b.kind = gdjn_vm_bind_prop;
			} else /* constant */ {
				b.kind = gdjn_vm_bind_const;
				goto succeed;
			}
			if (gdjn_vm_metaFlag(bind, "share"))
				b.kind |= gdjn_vm_bind_flag_static;
		} else {
			switch (janet_type(b.val)) {
				case JANET_ABSTRACT:
					if ((janet_abstract_type(&b.val)) != &jn_closure_def) {
						b.kind = gdjn_vm_bind_const; break;
					}
				case JANET_FUNCTION:
				case JANET_CFUNCTION:
					b.kind = gdjn_vm_bind_method_static;
					break;
				default: goto fail;
			}
		}
	}
	/* found a valid export, return it */
	succeed: return b;
	/* this binding is not marked correctly for gdexport */
	fail: return (gdjn_vm_bind){gdjn_vm_bind_none};
}

gdjn_vm_bind
gdjn_vm_resv
(	JanetTable* env,
	Janet       key
) {
	auto gchnd = janet_gclock();
	Janet def = janet_table_get(env, key);
	if (janet_type(def) == JANET_NIL)
		return (gdjn_vm_bind){};
	auto m = gdjn_vm_meta(janet_unwrap_table(def));
	janet_gcunlock(gchnd);
	return m;
}

void gdjn_vm_api_installCommon (JanetTable* tgt) {
	/* install primitives */
	janet_cfuns(tgt, "prim", reg_core);
	auto idmap = janet_env_lookup(tgt);
	int gc = janet_gclock();

	/* unpack API image */
	Janet apiEnv = janet_unmarshal(
		gdjn_rsrc_api_jimage,
		sizeof gdjn_rsrc_api_jimage,
		0,
		idmap,
		nullptr);
	printf("apienv type is %s\n",
			janet_type_names[janet_type(apiEnv)]);

	JanetTable* apiTbl = janet_unwrap_table(apiEnv);
	/* call the init function to precache base modules */
	Janet initDef = janet_table_get(apiTbl, janet_csymbolv("init"));
	if (janet_type(initDef) == JANET_NIL) {
		_err("no init fn in api envtbl");
		goto fail;
	}
	auto initFn = janet_unwrap_function(
			janet_table_get(janet_unwrap_table(initDef), 
				janet_ckeywordv("value")));
	Janet ret;
	auto e = janet_pcall(initFn, 1,
			(Janet[]) {janet_wrap_table(tgt)},
			&ret, nullptr);
	if (e == JANET_SIGNAL_ERROR) {
		_errMsg("failed to unpack the janet environment",
			(char*)janet_unwrap_string(ret));
		goto fail;
	}
	/* janet_table_merge_table(tgt, janet_unwrap_table(ret)); */
	printf("environment load complete\n");
fail:
	janet_gcunlock(gc);
	/* janet_collect(); */
}

JanetTable* gdjn_vm_api_build_compTime(void) {
	auto core = janet_core_env(nullptr);
	auto api = jnu_table_extend(core,32);
	gdjn_vm_api_installCommon(api);
	return api;
}

JanetTable* gdjn_vm_api_build_core(void) {
	auto core = janet_core_env(nullptr);
	auto api = jnu_table_extend(core,32);
	gdjn_vm_api_installCommon(api);
	return api;
}

JanetTable*
gdjn_vm_compile
(	pstr const  body,
	JanetTable* api,
	const char* ctx
) {
	if (!ctx) ctx = "<anon>";
	if (!api) api = gdjn_ctx -> jn.api;

	auto cls = jnu_table_extend(api, 8);
	janet_gcroot(janet_wrap_table(cls));
	/* printf("janet: doing bytes %d %s\n", (int)me -> src.sz, me -> src.v); */
	int e = janet_dobytes(cls,
		(uint8_t const*)body.v, body.sz,
		ctx, nullptr);
	/* we discard the return value; the environment is what
	 * we're really interested in here */
	/* printf("janet: bytes done, got %d\n",e); */
	if (e != 0) {
		_err("janet module could not be loaded");
		/* TODO capture parse error */
		janet_gcunroot(janet_wrap_table(cls));
		cls = nullptr;
	}
	return cls;
}


pstr
gdjn_vm_image
(	JanetTable* env,
	JanetTable* binds
) {
	return (pstr){}; //TODO
}
