#include "mupdf/pdf.h"

#include <mujs.h>

#define MAXARGS 16

#define OBJ(i) ((pdf_jsimp_obj*)((intptr_t)(i)))
#define IDX(p) ((intptr_t)(p))
#define NEWOBJ(J,x) OBJ(js_gettop(J) + (x))

struct pdf_jsimp_s
{
	fz_context *ctx;
	void *jsctx;
	js_State *J;
};

static void *alloc(void *ud, void *ptr, unsigned int n)
{
	fz_context *ctx = ud;
	if (n == 0) {
		fz_free(ctx, ptr);
		return NULL;
	}
	if (ptr)
		return fz_resize_array(ctx, ptr, n, 1);
	return fz_malloc_array(ctx, n, 1);
}

pdf_jsimp *pdf_new_jsimp(fz_context *ctx, void *jsctx)
{
	js_State *J;
	pdf_jsimp *imp;

	J = js_newstate(alloc, ctx);

	js_pushnull(J); /* prototype for jsctx userdata object */
	js_newuserdata(J, "jsctx", jsctx); /* create jsctx userdata object */
	js_setregistry(J, "jsctx"); /* hide it in the registry */

	imp = fz_malloc_struct(ctx, pdf_jsimp);
	imp->ctx = ctx;
	imp->jsctx = jsctx;
	imp->J = J;
	return imp;
}

void pdf_drop_jsimp(pdf_jsimp *imp)
{
	if (imp)
	{
		js_freestate(imp->J);
		fz_free(imp->ctx, imp);
	}
}

pdf_jsimp_type *pdf_jsimp_new_type(pdf_jsimp *imp, pdf_jsimp_dtr *dtr, char *name)
{
	js_State *J = imp->J;
	js_newobject(J);
	js_setregistry(J, name);
	return (pdf_jsimp_type*)name;
}

void pdf_jsimp_drop_type(pdf_jsimp *imp, pdf_jsimp_type *type)
{
	if (imp && type)
	{
		js_State *J = imp->J;
		js_delregistry(J, (const char *)type);
	}
}

static void wrapmethod(js_State *J)
{
	pdf_jsimp_obj *args[MAXARGS];
	pdf_jsimp_obj *ret;
	pdf_jsimp_method *meth;
	const char *type;
	void *jsctx;
	void *obj;
	int i;

	int argc = js_gettop(J) - 1;

	js_getregistry(J, "jsctx");
	jsctx = js_touserdata(J, "jsctx", -1);
	js_pop(J, 1);

	js_currentfunction(J);
	{
		js_getproperty(J, -1, "__call");
		meth = js_touserdata(J, "method", -1);
		js_pop(J, 1);

		js_getproperty(J, -1, "__type");
		type = js_tostring(J, -1);
		js_pop(J, 1);
	}
	js_pop(J, 1);

	if (js_isuserdata(J, type, 0))
		obj = js_touserdata(J, type, 0);
	else
		obj = NULL;

	if (argc > MAXARGS)
		js_rangeerror(J, "too many arguments");

	for (i = 0; i < argc; ++i)
		args[i] = OBJ(i+1);
	ret = meth(jsctx, obj, argc, args);
	if (ret)
		js_copy(J, IDX(ret));
	else
		js_pushundefined(J);
}

static void wrapgetter(js_State *J)
{
	pdf_jsimp_obj *ret;
	pdf_jsimp_getter *get;
	const char *type;
	void *jsctx;
	void *obj;

	js_getregistry(J, "jsctx");
	jsctx = js_touserdata(J, "jsctx", -1);
	js_pop(J, 1);

	js_currentfunction(J);
	{
		js_getproperty(J, -1, "__get");
		get = js_touserdata(J, "getter", -1);
		js_pop(J, 1);

		js_getproperty(J, -1, "__type");
		type = js_tostring(J, -1);
		js_pop(J, 1);
	}
	js_pop(J, 1);

	if (js_isuserdata(J, type, 0))
		obj = js_touserdata(J, type, 0);
	else
		obj = NULL;

	ret = get(jsctx, obj);
	if (ret)
		js_copy(J, IDX(ret));
	else
		js_pushundefined(J);
}

static void wrapsetter(js_State *J)
{
	pdf_jsimp_setter *set;
	const char *type;
	void *jsctx;
	void *obj;

	js_getregistry(J, "jsctx");
	jsctx = js_touserdata(J, "jsctx", -1);
	js_pop(J, 1);

	js_currentfunction(J);
	{
		js_getproperty(J, -1, "__set");
		set = js_touserdata(J, "setter", -1);
		js_pop(J, 1);

		js_getproperty(J, -1, "__type");
		type = js_tostring(J, -1);
		js_pop(J, 1);
	}
	js_pop(J, 1);

	if (js_isuserdata(J, type, 0))
		obj = js_touserdata(J, type, 0);
	else
		obj = NULL;

	set(jsctx, obj, OBJ(1));

	js_pushundefined(J);
}

void pdf_jsimp_addmethod(pdf_jsimp *imp, pdf_jsimp_type *type, char *name, pdf_jsimp_method *meth)
{
	js_State *J = imp->J;
	js_getregistry(J, (const char *)type);
	{
		js_newcfunction(J, wrapmethod, 0);
		{
			js_pushnull(J);
			js_newuserdata(J, "method", meth);
			js_defproperty(J, -2, "__call", JS_READONLY | JS_DONTENUM | JS_DONTCONF);
			js_pushstring(J, (const char *)type);
			js_defproperty(J, -2, "__type", JS_READONLY | JS_DONTENUM | JS_DONTCONF);
		}
		js_defproperty(J, -2, name, JS_READONLY | JS_DONTENUM | JS_DONTCONF);
	}
	js_pop(J, 1);
}

void pdf_jsimp_addproperty(pdf_jsimp *imp, pdf_jsimp_type *type, char *name, pdf_jsimp_getter *get, pdf_jsimp_setter *set)
{
	js_State *J = imp->J;
	js_getregistry(J, (const char *)type);
	{
		js_newcfunction(J, wrapgetter, 0);
		{
			js_pushnull(J);
			js_newuserdata(J, "getter", get);
			js_defproperty(J, -2, "__get", JS_READONLY | JS_DONTENUM | JS_DONTCONF);
			js_pushstring(J, (const char *)type);
			js_defproperty(J, -2, "__type", JS_READONLY | JS_DONTENUM | JS_DONTCONF);
		}
		js_newcfunction(J, wrapsetter, 0);
		{
			js_pushnull(J);
			js_newuserdata(J, "setter", set);
			js_defproperty(J, -2, "__set", JS_READONLY | JS_DONTENUM | JS_DONTCONF);
			js_pushstring(J, (const char *)type);
			js_defproperty(J, -2, "__type", JS_READONLY | JS_DONTENUM | JS_DONTCONF);
		}
		js_defaccessor(J, -3, name, JS_READONLY | JS_DONTENUM | JS_DONTCONF);
	}
	js_pop(J, 1);
}

void pdf_jsimp_set_global_type(pdf_jsimp *imp, pdf_jsimp_type *type)
{
	js_State *J = imp->J;
	js_getregistry(J, (const char *)type);
	js_setglobal(J, "global");
	js_dostring(J, "Object.getOwnPropertyNames(global).forEach(function(k){this[k]=global[k]}, this)", 0);
	js_dostring(J, "delete global", 0);
}

pdf_jsimp_obj *pdf_jsimp_new_obj(pdf_jsimp *imp, pdf_jsimp_type *type, void *natobj)
{
	js_State *J = imp->J;
	js_getregistry(J, (const char *)type);
	js_newuserdata(J, (const char *)type, natobj);
	return NEWOBJ(J, -1);
}

void pdf_jsimp_drop_obj(pdf_jsimp *imp, pdf_jsimp_obj *obj)
{
}

int pdf_jsimp_to_type(pdf_jsimp *imp, pdf_jsimp_obj *obj)
{
	js_State *J = imp->J;
	if (js_isnull(J, IDX(obj))) return JS_TYPE_NULL;
	if (js_isboolean(J, IDX(obj))) return JS_TYPE_BOOLEAN;
	if (js_isnumber(J, IDX(obj))) return JS_TYPE_NUMBER;
	if (js_isstring(J, IDX(obj))) return JS_TYPE_STRING;
	if (js_isarray(J, IDX(obj))) return JS_TYPE_ARRAY;
	return JS_TYPE_UNKNOWN;
}

pdf_jsimp_obj *pdf_jsimp_from_string(pdf_jsimp *imp, char *str)
{
	js_State *J = imp->J;
	js_pushstring(J, str);
	return NEWOBJ(J, -1);
}

char *pdf_jsimp_to_string(pdf_jsimp *imp, pdf_jsimp_obj *obj)
{
	/* cast away const :( */
	return (char*)js_tostring(imp->J, IDX(obj));
}

pdf_jsimp_obj *pdf_jsimp_from_number(pdf_jsimp *imp, double num)
{
	js_State *J = imp->J;
	js_pushnumber(J, num);
	return NEWOBJ(J, -1);
}

double pdf_jsimp_to_number(pdf_jsimp *imp, pdf_jsimp_obj *obj)
{
	return js_tonumber(imp->J, IDX(obj));
}

int pdf_jsimp_array_len(pdf_jsimp *imp, pdf_jsimp_obj *obj)
{
	js_State *J = imp->J;
	return js_getlength(J, IDX(obj));
}

pdf_jsimp_obj *pdf_jsimp_array_item(pdf_jsimp *imp, pdf_jsimp_obj *obj, int i)
{
	js_State *J = imp->J;
	js_getindex(J, IDX(obj), i);
	return NEWOBJ(J, -1);
}

pdf_jsimp_obj *pdf_jsimp_property(pdf_jsimp *imp, pdf_jsimp_obj *obj, char *prop)
{
	js_State *J = imp->J;
	js_getproperty(J, IDX(obj), prop);
	return NEWOBJ(J, -1);
}

void pdf_jsimp_execute(pdf_jsimp *imp, char *code)
{
	js_State *J = imp->J;
	js_dostring(J, code, 0);
}

void pdf_jsimp_execute_count(pdf_jsimp *imp, char *code, int count)
{
	char *terminated = fz_malloc(imp->ctx, count+1);
	memcpy(terminated, code, count);
	terminated[count] = 0;
	pdf_jsimp_execute(imp, terminated);
	fz_free(imp->ctx, terminated);
}
