#include "mupdf/fitz.h"
#include "mupdf/pdf.h"

#if FZ_ENABLE_JS

#include "mujs.h"

#include <stdarg.h>
#include <string.h>

struct pdf_js_s
{
	fz_context *ctx;
	pdf_document *doc;
	pdf_obj *form;
	pdf_js_event event;
	js_State *imp;
};

FZ_NORETURN static void rethrow(pdf_js *js)
{
	js_newerror(js->imp, fz_caught_message(js->ctx));
	js_throw(js->imp);
}

/* Unpack argument object with named arguments into actual parameters. */
static pdf_js *unpack_arguments(js_State *J, ...)
{
	if (js_isobject(J, 1))
	{
		int i = 1;
		va_list args;

		js_copy(J, 1);

		va_start(args, J);
		for (;;)
		{
			const char *s = va_arg(args, const char *);
			if (!s)
				break;
			js_getproperty(J, -1, s);
			js_replace(J, i++);
		}
		va_end(args);

		js_pop(J, 1);
	}
	return js_getcontext(J);
}

static char *pdf_from_utf8(fz_context *ctx, const char *utf8)
{
	char *pdf = fz_malloc(ctx, strlen(utf8)+1);
	int i = 0;
	unsigned char c;

	while ((c = *utf8) != 0)
	{
		if ((c & 0x80) == 0 && pdf_doc_encoding[c] == c)
		{
			pdf[i++] = c;
			utf8++ ;
		}
		else
		{
			int rune;
			int j;

			utf8 += fz_chartorune(&rune, utf8);

			for (j = 0; j < sizeof(pdf_doc_encoding) && pdf_doc_encoding[j] != rune; j++)
				;

			if (j < sizeof(pdf_doc_encoding))
				pdf[i++] = j;
		}
	}

	pdf[i] = 0;

	return pdf;
}

static void app_alert(js_State *J)
{
	pdf_js *js = unpack_arguments(J, "cMsg", "nIcon", "nType", "cTitle", 0);
	pdf_alert_event event;

	event.message = js_tostring(J, 1);
	event.icon_type = js_tointeger(J, 2);
	event.button_group_type = js_tointeger(J, 3);
	event.title = js_tostring(J, 4);
	event.button_pressed = 0; /* WIP WIP WIP IS THIS CORRECT? */

	fz_try(js->ctx)
		pdf_event_issue_alert(js->ctx, js->doc, &event);
	fz_catch(js->ctx)
		rethrow(js);

	js_pushnumber(J, event.button_pressed);
}

static void app_execDialog(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	// monitor
	// inheritDialog
	// parentDoc

	fz_try(js->ctx)
		pdf_event_issue_exec_dialog(js->ctx, js->doc);
	fz_catch(js->ctx)
		rethrow(js);

	// return "ok" or "cancel"
	js_pushstring(J, "cancel");
}

static void app_execMenuItem(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	const char *cMenuItem = js_tostring(J, 1);
	fz_try(js->ctx)
		pdf_event_issue_exec_menu_item(js->ctx, js->doc, cMenuItem);
	fz_catch(js->ctx)
		rethrow(js);
}

static void app_launchURL(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	const char *cUrl = js_tostring(J, 1);
	int bNewFrame = js_toboolean(J, 1);
	fz_try(js->ctx)
		pdf_event_issue_launch_url(js->ctx, js->doc, cUrl, bNewFrame);
	fz_catch(js->ctx)
		rethrow(js);
}

static void field_finalize(js_State *J, void *p)
{
	pdf_js *js = js_getcontext(J);
	pdf_drop_obj(js->ctx, p);
}

static void field_buttonSetCaption(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	pdf_obj *field = js_touserdata(J, 0, "Field");
	const char *cCaption = js_tostring(J, 1);
	char *caption = pdf_from_utf8(js->ctx, cCaption);
	fz_try(js->ctx)
		pdf_field_set_button_caption(js->ctx, js->doc, field, caption);
	fz_always(js->ctx)
		fz_free(js->ctx, caption);
	fz_catch(js->ctx)
		rethrow(js);
}

static void field_getName(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	pdf_obj *field = js_touserdata(J, 0, "Field");
	char *name = NULL;
	fz_try(js->ctx)
		name = pdf_field_name(js->ctx, js->doc, field);
	fz_catch(js->ctx)
		rethrow(js);
	js_pushstring(J, name); /* to utf8? */
}

static void field_setName(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	fz_warn(js->ctx, "Unexpected call to field_setName");
}

static void field_getDisplay(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	pdf_obj *field = js_touserdata(J, 0, "Field");
	int display = 0;
	fz_try(js->ctx)
		display = pdf_field_display(js->ctx, js->doc, field);
	fz_catch(js->ctx)
		rethrow(js);
	js_pushnumber(J, display);
}

static void field_setDisplay(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	pdf_obj *field = js_touserdata(J, 0, "Field");
	int display = js_tonumber(J, 1);
	fz_try(js->ctx)
		pdf_field_set_display(js->ctx, js->doc, field, display);
	fz_catch(js->ctx)
		rethrow(js);
}

static pdf_obj *load_color(pdf_js *js, int idx)
{
	fz_context *ctx = js->ctx;
	pdf_document *doc = js->doc;
	js_State *J = js->imp;

	pdf_obj *color = NULL;
	int i, n;
	float c;

	n = js_getlength(J, idx);

	/* The only legitimate color expressed as an array of length 1
	 * is [T], meaning transparent. Return a NULL object to represent
	 * transparent */
	if (n <= 1)
		return NULL;

	fz_var(color);

	fz_try(ctx)
	{
		color = pdf_new_array(ctx, doc, n-1);
		for (i = 0; i < n-1; i++)
		{
			js_getindex(J, idx, i+1);
			c = js_tonumber(J, -1);
			js_pop(J, 1);

			pdf_array_push_real(ctx, color, c);
		}
	}
	fz_catch(ctx)
	{
		pdf_drop_obj(ctx, color);
		rethrow(js);
	}

	return color;
}

static void field_getFillColor(js_State *J)
{
	js_pushundefined(J);
}

static void field_setFillColor(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	pdf_obj *field = js_touserdata(J, 0, "Field");
	pdf_obj *color = load_color(js, 1);
	fz_try(js->ctx)
		pdf_field_set_fill_color(js->ctx, js->doc, field, color);
	fz_always(js->ctx)
		pdf_drop_obj(js->ctx, color);
	fz_catch(js->ctx)
		rethrow(js);
}

static void field_getTextColor(js_State *J)
{
	js_pushundefined(J);
}

static void field_setTextColor(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	pdf_obj *field = js_touserdata(J, 0, "Field");
	pdf_obj *color = load_color(js, 1);
	fz_try(js->ctx)
		pdf_field_set_text_color(js->ctx, js->doc, field, color);
	fz_always(js->ctx)
		pdf_drop_obj(js->ctx, color);
	fz_catch(js->ctx)
		rethrow(js);
}

static void field_getBorderStyle(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	pdf_obj *field = js_touserdata(J, 0, "Field");
	const char *border_style = NULL;
	fz_try(js->ctx)
		border_style = pdf_field_border_style(js->ctx, js->doc, field);
	fz_catch(js->ctx)
		rethrow(js);
	js_pushstring(J, border_style);
}

static void field_setBorderStyle(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	pdf_obj *field = js_touserdata(J, 0, "Field");
	const char *border_style = js_tostring(J, 1);
	fz_try(js->ctx)
		pdf_field_set_border_style(js->ctx, js->doc, field, border_style);
	fz_catch(js->ctx)
		rethrow(js);
}

static void field_getValue(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	pdf_obj *field = js_touserdata(J, 0, "Field");
	char *val = NULL;

	fz_try(js->ctx)
		val = pdf_field_value(js->ctx, js->doc, field);
	fz_catch(js->ctx)
		rethrow(js);

	js_pushstring(J, val ? val : "");

	fz_free(js->ctx, val);
}

static void field_setValue(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	pdf_obj *field = js_touserdata(J, 0, "Field");
	const char *value = js_tostring(J, 1);

	fz_try(js->ctx)
		(void)pdf_field_set_value(js->ctx, js->doc, field, value);
	fz_catch(js->ctx)
		rethrow(js);
}

static void event_getTarget(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	js_getregistry(J, "Field");
	js_newuserdata(J, "Field", pdf_keep_obj(js->ctx, js->event.target), field_finalize);
}

static void event_setTarget(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	fz_warn(js->ctx, "Unexpected call to event_setTarget");
}

static void event_getValue(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	const char *v = js->event.value;
	js_pushstring(J, v ? v : "");
}

static void event_setValue(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	const char *value = js_tostring(J, 1);
	fz_free(js->ctx, js->event.value);
	js->event.value = fz_strdup(js->ctx, value);
}

static void event_getWillCommit(js_State *J)
{
	js_pushnumber(J, 1);
}

static void event_setWillCommit(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	fz_warn(js->ctx, "Unexpected call to event_setWillCommit");
}

static void event_getRC(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	js_pushnumber(J, js->event.rc);
}

static void event_setRC(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	js->event.rc = js_tointeger(js->imp, 1);
}

static void doc_getField(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	fz_context *ctx = js->ctx;
	const char *cName = js_tostring(J, 1);
	char *name = pdf_from_utf8(ctx, cName);
	pdf_obj *dict = NULL;

	fz_try(ctx)
		dict = pdf_lookup_field(ctx, js->form, name);
	fz_always(ctx)
		fz_free(ctx, name);
	fz_catch(ctx)
		rethrow(js);

	if (dict)
	{
		js_getregistry(J, "Field");
		js_newuserdata(J, "Field", pdf_keep_obj(js->ctx, dict), field_finalize);
	}
	else
	{
		js_pushnull(J);
	}
}

static void reset_field(pdf_js *js, const char *cName)
{
	fz_context *ctx = js->ctx;
	if (cName)
	{
		char *name = pdf_from_utf8(ctx, cName);
		fz_try(ctx)
		{
			pdf_obj *field = js_touserdata(js->imp, 0, "Field");
			if (field)
				pdf_field_reset(ctx, js->doc, field);
		}
		fz_always(ctx)
			fz_free(ctx, name);
		fz_catch(ctx)
			rethrow(js);
	}
}

static void doc_resetForm(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	fz_context *ctx = js->ctx;
	int i, n;

	/* An array of fields has been passed in. Call pdf_reset_field on each item. */
	if (js_isarray(J, 1))
	{
		n = js_getlength(J, 1);
		for (i = 0; i < n; ++i)
		{
			js_getindex(J, 1, i);
			reset_field(js, js_tostring(J, -1));
			js_pop(J, 1);
		}
	}

	/* No argument or null passed in means reset all. */
	else
	{
		n = pdf_array_len(ctx, js->form);
		for (i = 0; i < n; i++)
		{
			fz_try(ctx)
				pdf_field_reset(ctx, js->doc, pdf_array_get(ctx, js->form, i));
			fz_catch(ctx)
				rethrow(js);
		}
	}
}

static void doc_print(js_State *J)
{
	pdf_js *js = js_getcontext(J);
	fz_try(js->ctx)
		pdf_event_issue_print(js->ctx, js->doc);
	fz_catch(js->ctx)
		rethrow(js);
}

static void doc_mailDoc(js_State *J)
{
	pdf_js *js = unpack_arguments(J, "bUI", "cTo", "cCc", "cBcc", "cSubject", "cMessage", 0);
	pdf_mail_doc_event event;

	event.ask_user = js_isdefined(J, 1) ? js_toboolean(J, 1) : 1;
	event.to = js_tostring(J, 2);
	event.cc = js_tostring(J, 3);
	event.bcc = js_tostring(J, 4);
	event.subject = js_tostring(J, 5);
	event.message = js_tostring(J, 6);

	fz_try(js->ctx)
		pdf_event_issue_mail_doc(js->ctx, js->doc, &event);
	fz_catch(js->ctx)
		rethrow(js);
}

static void addmethod(js_State *J, const char *name, js_CFunction fun, int n)
{
	const char *realname = strchr(name, '.');
	realname = realname ? realname + 1 : name;
	js_newcfunction(J, fun, name, n);
	js_defproperty(J, -2, realname, JS_READONLY | JS_DONTENUM | JS_DONTCONF);
}

static void addproperty(js_State *J, const char *name, js_CFunction getfun, js_CFunction setfun)
{
	const char *realname = strchr(name, '.');
	realname = realname ? realname + 1 : name;
	js_newcfunction(J, getfun, name, 0);
	js_newcfunction(J, setfun, name, 1);
	js_defaccessor(J, -3, realname, JS_READONLY | JS_DONTENUM | JS_DONTCONF);
}

static void declare_dom(pdf_js *js)
{
	js_State *J = js->imp;

	/* Allow access to the global environment via the 'global' name */
	js_pushglobal(J);
	js_defglobal(J, "global", JS_READONLY | JS_DONTCONF | JS_DONTENUM);

	/* Create the 'app' object */
	js_newobject(J);
	{
#ifdef _WIN32
		js_pushstring(J, "WIN");
#elif defined(__APPLE__)
		js_pushstring(J, "MAC");
#else
		js_pushstring(J, "UNIX");
#endif
		js_defproperty(J, -2, "app.platform", JS_READONLY | JS_DONTENUM | JS_DONTCONF);

		addmethod(J, "app.alert", app_alert, 4);
		addmethod(J, "app.execDialog", app_execDialog, 0);
		addmethod(J, "app.execMenuItem", app_execMenuItem, 1);
		addmethod(J, "app.launchURL", app_launchURL, 2);
	}
	js_defglobal(J, "app", JS_READONLY | JS_DONTCONF | JS_DONTENUM);

	/* Create the 'event' object */
	js_newobject(J);
	{
		addproperty(J, "event.target", event_getTarget, event_setTarget);
		addproperty(J, "event.value", event_getValue, event_setValue);
		addproperty(J, "event.willCommit", event_getWillCommit, event_setWillCommit);
		addproperty(J, "event.rc", event_getRC, event_setRC);
	}
	js_defglobal(J, "event", JS_READONLY | JS_DONTCONF | JS_DONTENUM);

	/* Create the Field prototype object */
	js_newobject(J);
	{
		addproperty(J, "Field.value", field_getValue, field_setValue);
		addproperty(J, "Field.borderStyle", field_getBorderStyle, field_setBorderStyle);
		addproperty(J, "Field.textColor", field_getTextColor, field_setTextColor);
		addproperty(J, "Field.fillColor", field_getFillColor, field_setFillColor);
		addproperty(J, "Field.display", field_getDisplay, field_setDisplay);
		addproperty(J, "Field.name", field_getName, field_setName);
		addmethod(J, "Field.buttonSetCaption", field_buttonSetCaption, 1);
	}
	js_setregistry(J, "Field");

	/* Create the Doc prototype object */
	js_newobject(J);
	{
		addmethod(J, "Doc.getField", doc_getField, 1);
		addmethod(J, "Doc.resetForm", doc_resetForm, 0);
		addmethod(J, "Doc.print", doc_print, 0);
		addmethod(J, "Doc.mailDoc", doc_mailDoc, 6);
	}
	js_setregistry(J, "Doc");

	js_getregistry(J, "Doc");
	js_setglobal(J, "MuPDF_Doc"); /* for pdf-util.js use */
}

extern const unsigned char fz_source_pdf_pdf_js_util_js[];

static void preload_helpers(pdf_js *js)
{
	/* When testing on the cluster:
	 * Use a fixed date for "new Date" and Date.now().
	 * Sadly, this breaks uses of the Date function without the new keyword.
	 * Return a fixed number from Math.random().
	 */
#ifdef CLUSTER
	js_dostring(js->imp,
"var MuPDFOldDate = Date\n"
"Date = function() { return new MuPDFOldDate(298252800000); }\n"
"Date.now = function() { return 298252800000; }\n"
"Date.UTC = function() { return 298252800000; }\n"
"Date.parse = MuPDFOldDate.parse;\n"
"Math.random = function() { return 1/4; }\n"
	);
#endif

	js_dostring(js->imp, (const char *)fz_source_pdf_pdf_js_util_js);
}

void pdf_drop_js(fz_context *ctx, pdf_js *js)
{
	if (js)
	{
		js_freestate(js->imp);
		fz_free(ctx, js->event.value);
		fz_free(ctx, js);
	}
}

static void *pdf_js_alloc(void *actx, void *ptr, int n)
{
	fz_context *ctx = actx;
	if (n == 0) {
		fz_free(ctx, ptr);
		return NULL;
	}
	return fz_resize_array_no_throw(ctx, ptr, n, 1);
}

static pdf_js *pdf_new_js(fz_context *ctx, pdf_document *doc)
{
	pdf_js *js = fz_malloc_struct(ctx, pdf_js);

	js->ctx = ctx;
	js->doc = doc;

	fz_try(ctx)
	{
		pdf_obj *root, *acroform;

		/* Find the form array */
		root = pdf_dict_get(ctx, pdf_trailer(ctx, doc), PDF_NAME_Root);
		acroform = pdf_dict_get(ctx, root, PDF_NAME_AcroForm);
		js->form = pdf_dict_get(ctx, acroform, PDF_NAME_Fields);

		/* Initialise the javascript engine, passing the fz_context for use in memory allocation. */
		js->imp = js_newstate(pdf_js_alloc, ctx, 0);
		if (!js->imp)
			fz_throw(ctx, FZ_ERROR_GENERIC, "cannot initialize javascript engine");

		/* Also set our pdf_js context, so we can retrieve it in callbacks. */
		js_setcontext(js->imp, js);

		declare_dom(js);
		preload_helpers(js);
	}
	fz_catch(ctx)
	{
		pdf_drop_js(ctx, js);
		fz_rethrow(ctx);
	}

	return js;
}

static void pdf_js_load_document_level(pdf_js *js)
{
	fz_context *ctx = js->ctx;
	pdf_document *doc = js->doc;
	pdf_obj *javascript;
	int len, i;

	javascript = pdf_load_name_tree(ctx, doc, PDF_NAME_JavaScript);
	len = pdf_dict_len(ctx, javascript);

	fz_try(ctx)
	{
		for (i = 0; i < len; i++)
		{
			pdf_obj *fragment = pdf_dict_get_val(ctx, javascript, i);
			pdf_obj *code = pdf_dict_get(ctx, fragment, PDF_NAME_JS);
			char *codebuf = pdf_load_stream_or_string_as_utf8(ctx, code);
			pdf_js_execute(js, codebuf);
			fz_free(ctx, codebuf);
		}
	}
	fz_always(ctx)
		pdf_drop_obj(ctx, javascript);
	fz_catch(ctx)
		fz_rethrow(ctx);
}

void pdf_js_setup_event(pdf_js *js, pdf_js_event *e)
{
	if (js)
	{
		fz_context *ctx = js->ctx;
		char *ev = e->value ? e->value : "";
		char *v = fz_strdup(ctx, ev);

		fz_free(ctx, js->event.value);
		js->event.value = v;

		js->event.target = e->target;
		js->event.rc = 1;
	}
}

pdf_js_event *pdf_js_get_event(pdf_js *js)
{
	return js ? &js->event : NULL;
}

void pdf_js_execute(pdf_js *js, char *source)
{
	if (js)
	{
		if (js_ploadstring(js->imp, "[pdf]", source))
		{
			fz_warn(js->ctx, "%s", js_tostring(js->imp, -1));
			js_pop(js->imp, 1);
			return;
		}
		js_getregistry(js->imp, "Doc"); /* set 'this' to the Doc object */
		if (js_pcall(js->imp, 0))
		{
			fz_warn(js->ctx, "%s", js_tostring(js->imp, -1));
			js_pop(js->imp, 1);
			return;
		}
		js_pop(js->imp, 1);
	}
}

void pdf_enable_js(fz_context *ctx, pdf_document *doc)
{
	if (!doc->js)
	{
		doc->js = pdf_new_js(ctx, doc);
		pdf_js_load_document_level(doc->js);
	}
}

void pdf_disable_js(fz_context *ctx, pdf_document *doc)
{
	pdf_drop_js(ctx, doc->js);
	doc->js = NULL;
}

int pdf_js_supported(fz_context *ctx, pdf_document *doc)
{
	return doc->js != NULL;
}

#else /* FZ_ENABLE_JS */

void pdf_drop_js(fz_context *ctx, pdf_js *js) { }
void pdf_enable_js(fz_context *ctx, pdf_document *doc) { }
void pdf_disable_js(fz_context *ctx, pdf_document *doc) { }
int pdf_js_supported(fz_context *ctx, pdf_document *doc) { return 0; }
void pdf_js_setup_event(pdf_js *js, pdf_js_event *e) { }
pdf_js_event *pdf_js_get_event(pdf_js *js) { return NULL; }
void pdf_js_execute(pdf_js *js, char *code) { }

#endif /* FZ_ENABLE_JS */
