/* Window binding for browser using duktape and libdom
 *
 * Copyright 2015 Vincent Sanders <vince@netsurf-browser.org>
 *
 * This file is part of NetSurf, http://www.netsurf-browser.org/
 *
 * Released under the terms of the MIT License,
 *         http://www.opensource.org/licenses/mit-license
 */

class Window {
	private struct browser_window * win;
	private struct html_content * htmlc;
	private struct window_schedule_s * schedule_ring;
	private bool closed_down;
	prologue %{
#include "utils/corestrings.h"
#include "utils/nsurl.h"
#include "netsurf/browser_window.h"
#include "content/hlcache.h"
#include "html/html.h"
#include "html/private.h"
#include "desktop/gui_internal.h"
#include "netsurf/misc.h"
#include "utils/ring.h"
#include "netsurf/inttypes.h"

#define WINDOW_CALLBACKS MAGIC(WindowCallbacks)
#define HANDLER_MAGIC MAGIC(HANDLER_MAP)

static size_t next_handle = 0;

typedef struct window_schedule_s {
	window_private_t *owner;
	duk_context *ctx;
	struct window_schedule_s *r_next;
	struct window_schedule_s *r_prev;
	size_t handle;
	int repeat_timeout;
	bool running;
} window_schedule_t;

static void window_remove_callback_bits(duk_context *ctx, size_t handle) {
	/* stack is ... */
	duk_push_global_object(ctx);
	duk_get_prop_string(ctx, -1, WINDOW_CALLBACKS);
	/* stack is ..., win, cbt */
	duk_push_int(ctx, (duk_int_t)handle);
	/* ..., win, cbt, handle */
	duk_del_prop(ctx, -2);
	/* ..., win, cbt */
	duk_pop_2(ctx);
	/* ... */
}

static void
window_call_callback(duk_context *ctx, size_t handle, bool clear_entry)
{
	NSLOG(dukky, DEEPDEBUG, "ctx=%p, handle=%"PRIsizet, ctx, handle);
	/* Stack is ... */
	duk_push_global_object(ctx);
	/* ..., win */
	duk_get_prop_string(ctx, -1, WINDOW_CALLBACKS);
	/* ..., win, cbt */
	duk_push_int(ctx, (duk_int_t)handle);
	/* ..., win, cbt, handle */
	duk_get_prop(ctx, -2);
	/* ..., win, cbt, cbo */
	//dukky_log_stack_frame(ctx, "On entry to callback");
	/* ..., win, cbt, cbo */
	/* What we want to do is call cbo.func passing all of cbo.args */
	duk_get_prop_string(ctx, -1, "func");
	duk_get_prop_string(ctx, -2, "args");
	/* ..., win, cbt, cbo, func, argarr */
	duk_size_t arrlen = duk_get_length(ctx, -1);
	for (duk_size_t i = 0; i < arrlen; ++i) {
		duk_push_int(ctx, (duk_int_t)i);
		duk_get_prop(ctx, -(2+i));
	}
	/* ..., win, cbt, cbo, func, argarr, args... */
	duk_remove(ctx, -(arrlen+1));
	/* ..., win, cbt, cbo, func, args... */
	//dukky_log_stack_frame(ctx, "Just before call");
	(void) dukky_pcall(ctx, arrlen, true);
	/* ..., win, cbt, cbo, retval */
	if (clear_entry) {
		NSLOG(dukky, DEEPDEBUG, "Not recurring callback, removing from cbt");
		duk_pop_n(ctx, 2);
		/* ..., win, cbt */
		duk_push_int(ctx, (duk_int_t)handle);
		/* ..., win, cbt, handle */
		duk_del_prop(ctx, -2);
		/* ..., win, cbt */
		duk_pop_n(ctx, 2);
	} else {
		duk_pop_n(ctx, 4);
	}
	/* ... */
	//dukky_log_stack_frame(ctx, "On leaving callback");
}


static void
window_schedule_callback(void *p)
{
	window_schedule_t *priv = (window_schedule_t *)p;

	NSLOG(dukky, DEEPDEBUG,
	      "Entered window scheduler callback: %"PRIsizet, priv->handle);

	priv->running = true;
	window_call_callback(priv->ctx,
			     priv->handle,
			     priv->repeat_timeout == 0);
	priv->running = false;

	if (priv->repeat_timeout > 0) {
		/* Reschedule */
		NSLOG(dukky, DEEPDEBUG,
		      "Rescheduling repeating callback %"PRIsizet,
		      priv->handle);
		guit->misc->schedule(priv->repeat_timeout,
				     window_schedule_callback,
				     priv);
	} else {
		NSLOG(dukky, DEEPDEBUG,
		      "Removing completed callback %"PRIsizet, priv->handle);
		/* Remove this from the ring */
		RING_REMOVE(priv->owner->schedule_ring, priv);
		window_remove_callback_bits(priv->ctx, priv->handle);
		free(priv);
	}
}

static size_t
window_alloc_new_callback(duk_context *ctx,
			  window_private_t *window,
			  bool repeating,
			  int timeout)
{
	size_t new_handle = next_handle++;
	window_schedule_t *sched = calloc(sizeof *sched, 1);
	if (sched == NULL) {
		return new_handle;
	}
	sched->owner = window;
	sched->ctx = ctx;
	sched->handle = new_handle;
	sched->repeat_timeout = repeating ? timeout : 0;
	sched->running = false;

	RING_INSERT(window->schedule_ring, sched);

	/* Next, the duktape stack looks like: func, timeout, ...
	 * In order to proceed, we want to put into the WINDOW_CALLBACKS
	 * keyed by the handle, an object containing the call to make and
	 * the array of arguments to call the function with
	 */
	duk_idx_t nargs = duk_get_top(ctx) - 2;
	duk_push_global_object(ctx);
	duk_get_prop_string(ctx, -1, WINDOW_CALLBACKS);
	duk_push_int(ctx, (duk_int_t)new_handle);
	duk_push_object(ctx);
	/* stack is: func, timeout, ..., win, cbt, handle, cbo */

	/* put the function into the cbo */
	duk_dup(ctx, 0);
	duk_put_prop_string(ctx, -2, "func");

	/* Now the arguments */
	duk_push_array(ctx);
	for (duk_idx_t i = 0; i < nargs; ++i) {
		duk_dup(ctx, 2 + i); /* Dup the arg */
		duk_put_prop_index(ctx, -2, i); /* arr[i] = arg[i] */
	}
	duk_put_prop_string(ctx, -2, "args");
	/* stack is: func, timeout, ..., win, cbt, handle, cbo */
	duk_put_prop(ctx, -3);
	/* stack is: func, timeout, ..., win, cbt */
	duk_pop_2(ctx);
	/* And we're back to func, timeout, ... */

	guit->misc->schedule(timeout, window_schedule_callback, sched);
	NSLOG(dukky, DEEPDEBUG, "Scheduled callback %"PRIsizet" for %d ms from now", new_handle, timeout);

	return new_handle;
}

static void
window_remove_callback_by_handle(duk_context *ctx,
				 window_private_t *window,
				 size_t handle)
{
	int res;

	RING_ITERATE_START(window_schedule_t, window->schedule_ring, sched) {
		if (sched->handle == handle) {
			if (sched->running) {
				NSLOG(dukky, DEEPDEBUG,
				      "Cancelling in-train callback %"PRIsizet,
				      sched->handle);
				sched->repeat_timeout = 0;
			} else {
				NSLOG(dukky, DEEPDEBUG,
				      "Cancelled callback %"PRIsizet,
				      sched->handle);
				res = guit->misc->schedule(-1,
							   window_schedule_callback,
							   sched);
				assert(res == NSERROR_OK);
				RING_REMOVE(window->schedule_ring, sched);
				window_remove_callback_bits(ctx, sched->handle);
				free(sched);
			}
			RING_ITERATE_STOP(window->schedule_ring, sched);
		}
	} RING_ITERATE_END(window->schedule_ring, sched);
}

/* This is the dodgy thread closedown method */
static duk_ret_t dukky_window_closedown_thread(duk_context *ctx)
{
	window_private_t *priv = NULL;

	duk_push_global_object(ctx);
	duk_get_prop_string(ctx, -1, dukky_magic_string_private);
	priv = duk_get_pointer(ctx, -1);
	duk_pop_2(ctx);

	if (priv == NULL) {
		return 0;
	}

	priv->closed_down = true;

	NSLOG(dukky, DEEPDEBUG, "Closing down thread");
	while (priv->schedule_ring != NULL) {
		window_schedule_t *to_remove = NULL;
		// Find a schedule item to remove
		RING_ITERATE_START(window_schedule_t, priv->schedule_ring, sched) {
			if (sched->running == false) {
				// This one is not running, we can remove it
				to_remove = sched;
				RING_ITERATE_STOP(window->schedule_ring, sched);
			} else if (sched->repeat_timeout != 0) {
				// This one is running and has yet to be
				// cancelled, so prevent it rescheduling itself
				NSLOG(dukky, DEEPDEBUG,
				      "Cancelling in-train callback %"PRIsizet,
				      sched->handle);
				sched->repeat_timeout = 0;
			}
		} RING_ITERATE_END(priv->schedule_ring, sched);

		if (to_remove == NULL) {
			// We didn't find any non-running callbacks
			// so let's log that and break out of the closedown
			// loop so we can continue and hopefully close down
			NSLOG(dukky, DEEPDEBUG,
			      "Leaving in-train callbacks to unwind");
			break;
		}
		
		// Remove the handle we found, this will reduce the callback
		// scheduler ring by one and perhaps leave it empty so we can
		// finish the closedown.
		window_remove_callback_by_handle(ctx,
						 priv,
						 to_remove->handle);
	}

	return 0;
}

%};
};

init Window(struct browser_window *win, struct html_content *htmlc)
%{
	/* It makes no sense if win or htmlc are NULL */
	assert(win != NULL);
	assert(htmlc != NULL);
	/* element window */
	priv->win = win;
	priv->htmlc = htmlc;
	priv->schedule_ring = NULL;
	priv->closed_down = false;
	NSLOG(netsurf, DEEPDEBUG, "win=%p htmlc=%p", priv->win, priv->htmlc);

	NSLOG(netsurf, DEEPDEBUG,
	      "URL is %s", nsurl_access(browser_window_access_url(priv->win)));
	duk_push_object(ctx);
	duk_put_prop_string(ctx, 0, WINDOW_CALLBACKS);
%}

fini Window()
%{
	NSLOG(dukky, DEEPDEBUG, "Shutting down Window %p", priv->win);
	/* Cheaply iterate the schedule ring, cancelling any pending callbacks */
	while (priv->schedule_ring != NULL) {
		window_remove_callback_by_handle(ctx, priv, priv->schedule_ring->handle);
	}
%}

prototype Window()
%{
#define EXPOSE(v) \
	duk_get_global_string(ctx, #v); \
	duk_put_prop_string(ctx, 0, #v)
	/* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects */
	/* ** Value properties */
	EXPOSE(Infinity);
	EXPOSE(NaN);
	EXPOSE(undefined);
	EXPOSE(null);
	EXPOSE(globalThis);

	/* ** Function properties */
	EXPOSE(eval);
	/* EXPOSE(uneval); */ /* Not standard, maybe not available */
	EXPOSE(isFinite);
	EXPOSE(isNaN);
	EXPOSE(parseFloat);
	EXPOSE(parseInt);
	EXPOSE(decodeURI);
	EXPOSE(decodeURIComponent);
	EXPOSE(encodeURI);
	EXPOSE(encodeURIComponent);
	EXPOSE(escape);
	EXPOSE(unescape);

	/* ** Fundamental Objects */
	EXPOSE(Object);
	EXPOSE(Function);
	EXPOSE(Boolean);
	EXPOSE(Symbol);
	EXPOSE(Error);
	EXPOSE(EvalError);
	EXPOSE(InternalError);
	EXPOSE(RangeError);
	EXPOSE(ReferenceError);
	EXPOSE(SyntaxError);
	EXPOSE(TypeError);
	EXPOSE(URIError);

	/* ** Numbers and Dates */
	EXPOSE(Number);
	EXPOSE(BigInt);
	EXPOSE(Math);
	EXPOSE(Date);

	/* ** Text Processing */
	EXPOSE(String);
	EXPOSE(RegExp);

	/* ** Indexed Collections */
	EXPOSE(Array);
	EXPOSE(Int8Array);
	EXPOSE(Uint8Array);
	EXPOSE(Uint8ClampedArray);
	EXPOSE(Int16Array);
	EXPOSE(Uint16Array);
	EXPOSE(Int32Array);
	EXPOSE(Uint32Array);
	EXPOSE(Float32Array);
	EXPOSE(Float64Array);
	/* EXPOSE(BigInt64Array); */ /* Duktape lacks this - nonstandard API */
	/* EXPOSE(BigUint64Array); */ /* Duktape lacks this - nonstandard API */

	/* ** Keyed Collections */
	/* EXPOSE(Map); */ /* Duktape lacks this - ES6 */
	/* EXPOSE(Set); */ /* Duktape lacks this - ES6 */
	/* EXPOSE(WeakMap); */ /* Duktape lacks this - ES6 */
	/* EXPOSE(WeakSet); */ /* Duktape lacks this - ES6 */

	/* Structured Data */
	EXPOSE(ArrayBuffer);
	/* EXPOSE(SharedArrayBuffer); */ /* Duktape lacks this - experimental API */
	/* EXPOSE(Atomics); */ /* Duktape lacks this - experimental API */
	EXPOSE(DataView);
	EXPOSE(JSON);

	/* ** Control abstraction properties */
	/* EXPOSE(Promise); */ /* Probably ought to be one of ours? Also ES6 */
	/* EXPOSE(Generator); */ /* Duktape and async? ES6 */
	/* EXPOSE(GeneratorFunction); */ /* Duktape and async? ES6 */
	/* EXPOSE(AsyncFunction); */ /* Duktape lacks this - experimental API */

	/* Reflection */
	EXPOSE(Reflect);
	EXPOSE(Proxy);

	/* ** Internationalisation */
	/* Duktape lacks Intl - Maybe polyfill it? */
	/* There is suggestion that cdn.polyfill.io exists for it */

	/* ** WebAssembly */
	/* As yet, Duktape lacks WA */
#undef EXPOSE
	/* Add s3kr1t method to close the JS thread (browsing context) */
	duk_dup(ctx, 0);
	duk_push_string(ctx, MAGIC(closedownThread));
	duk_push_c_function(ctx, dukky_window_closedown_thread, DUK_VARARGS);
	duk_def_prop(ctx, -3,
		     DUK_DEFPROP_HAVE_VALUE |
		     DUK_DEFPROP_HAVE_WRITABLE |
		     DUK_DEFPROP_HAVE_ENUMERABLE |
		     DUK_DEFPROP_ENUMERABLE |
		     DUK_DEFPROP_HAVE_CONFIGURABLE);
	duk_pop(ctx);
%}

getter Window::document()
%{
	NSLOG(netsurf, DEBUG, "priv=%p", priv);
	dom_document *doc = priv->htmlc->document;
	dukky_push_node(ctx, (struct dom_node *)doc);
	return 1;
%}

getter Window::window()
%{
	duk_push_this(ctx);
	return 1;
%}

getter Window::console()
%{
	duk_push_this(ctx);
	duk_get_prop_string(ctx, -1, MAGIC(Console));
	if (duk_is_undefined(ctx, -1)) {
		duk_pop(ctx);
		if (dukky_create_object(ctx, PROTO_NAME(CONSOLE), 0) != DUK_EXEC_SUCCESS) {
			return duk_error(ctx, DUK_ERR_ERROR, "Unable to create console object");
		}
		duk_dup(ctx, -1);
		duk_put_prop_string(ctx, -3, MAGIC(Console));
	}
	return 1;
%}

getter Window::location()
%{
	/* obtain location object for this window (if it exists) */
	duk_push_this(ctx);
	duk_get_prop_string(ctx, -1, MAGIC(Location));
	if (duk_is_undefined(ctx, -1)) {
		/* location object did not previously exist so create it */
		duk_pop(ctx);

		duk_push_pointer(ctx, llcache_handle_get_url(priv->htmlc->base.llcache));

		if (dukky_create_object(ctx, PROTO_NAME(LOCATION), 1) != DUK_EXEC_SUCCESS) {
			return duk_error(ctx, DUK_ERR_ERROR, "Unable to create location object");
		}
		duk_dup(ctx, -1);
		duk_put_prop_string(ctx, -3, MAGIC(Location));
	}
	return 1;
%}

getter Window::navigator()
%{
	duk_push_this(ctx);
	duk_get_prop_string(ctx, -1, MAGIC(Navigator));
	if (duk_is_undefined(ctx, -1)) {
		duk_pop(ctx);

		if (dukky_create_object(ctx,
					PROTO_NAME(NAVIGATOR),
					0) != DUK_EXEC_SUCCESS) {
			return duk_error(ctx,
				  DUK_ERR_ERROR,
				  "Unable to create navigator object");
		}
		duk_dup(ctx, -1);
		duk_put_prop_string(ctx, -3, MAGIC(Navigator));
	}
	return 1;
%}

getter Window::name()
%{
	const char *name;
	browser_window_get_name(priv->win, &name);
	duk_push_string(ctx, name);
	return 1;
%}

setter Window::name()
%{
	const char *name;
	name = duk_to_string(ctx, -1);
	browser_window_set_name(priv->win, name);
	return 0;
%}

method Window::alert()
%{
	duk_idx_t dukky_argc = duk_get_top(ctx);
	if (dukky_argc == 0) {
		NSLOG(netsurf, INFO, "JS ALERT");
	} else {
		duk_size_t msg_len;
		const char *msg;

		if (!duk_is_string(ctx, 0)) {
			duk_to_string(ctx, 0);
		}
		msg = duk_safe_to_lstring(ctx, 0, &msg_len);
		NSLOG(netsurf, INFO, "JS ALERT: %*s", (int)msg_len, msg);
	}
	return 0;
%}

method Window::setTimeout()
%{
	duk_idx_t argc = duk_get_top(ctx);
	duk_int_t timeout = 10;

	if (priv->closed_down == true) {
		return 0; /* coerced to undefined */
	}

	if (argc >= 2) {
		timeout = duk_get_int(ctx, 1);
	}
	/* func, [timeout, args...] */
	if (timeout < 10) { timeout = 10; }
	size_t handle = window_alloc_new_callback(ctx, priv, false, (int)timeout);

	duk_push_int(ctx, (duk_int_t)handle);
	return 1;
%}

method Window::setInterval()
%{
	duk_idx_t argc = duk_get_top(ctx);
	duk_int_t timeout = 10;

	if (priv->closed_down == true) {
		return 0; /* coerced to undefined */
	}

	if (argc >= 2) {
		timeout = duk_get_int(ctx, 1);
	}
	/* func, [timeout, args...] */
	if (timeout < 10) { timeout = 10; }
	size_t handle = window_alloc_new_callback(ctx, priv, true, (int)timeout);

	duk_push_int(ctx, (duk_int_t)handle);
	return 1;
%}

method Window::clearTimeout()
%{
	duk_int_t handle = duk_get_int(ctx, 0);
	window_remove_callback_by_handle(ctx, priv, (size_t) handle);

	return 0;
%}

method Window::clearInterval()
%{
	duk_int_t handle = duk_get_int(ctx, 0);
	window_remove_callback_by_handle(ctx, priv, (size_t) handle);

	return 0;
%}

getter Window::onabort();
setter Window::onabort();
getter Window::onafterprint();
setter Window::onafterprint();
getter Window::onautocompleteerror();
setter Window::onautocompleteerror();
getter Window::onautocomplete();
setter Window::onautocomplete();
getter Window::onbeforeprint();
setter Window::onbeforeprint();
getter Window::onbeforeunload();
setter Window::onbeforeunload();
getter Window::onblur();
setter Window::onblur();
getter Window::oncancel();
setter Window::oncancel();
getter Window::oncanplaythrough();
setter Window::oncanplaythrough();
getter Window::oncanplay();
setter Window::oncanplay();
getter Window::onchange();
setter Window::onchange();
getter Window::onclick();
setter Window::onclick();
getter Window::onclose();
setter Window::onclose();
getter Window::oncontextmenu();
setter Window::oncontextmenu();
getter Window::oncuechange();
setter Window::oncuechange();
getter Window::ondblclick();
setter Window::ondblclick();
getter Window::ondragend();
setter Window::ondragend();
getter Window::ondragenter();
setter Window::ondragenter();
getter Window::ondragexit();
setter Window::ondragexit();
getter Window::ondragleave();
setter Window::ondragleave();
getter Window::ondragover();
setter Window::ondragover();
getter Window::ondragstart();
setter Window::ondragstart();
getter Window::ondrag();
setter Window::ondrag();
getter Window::ondrop();
setter Window::ondrop();
getter Window::ondurationchange();
setter Window::ondurationchange();
getter Window::onemptied();
setter Window::onemptied();
getter Window::onended();
setter Window::onended();
getter Window::onerror();
setter Window::onerror();
getter Window::onfocus();
setter Window::onfocus();
getter Window::onhashchange();
setter Window::onhashchange();
getter Window::oninput();
setter Window::oninput();
getter Window::oninvalid();
setter Window::oninvalid();
getter Window::onkeydown();
setter Window::onkeydown();
getter Window::onkeypress();
setter Window::onkeypress();
getter Window::onkeyup();
setter Window::onkeyup();
getter Window::onlanguagechange();
setter Window::onlanguagechange();
getter Window::onloadeddata();
setter Window::onloadeddata();
getter Window::onloadedmetadata();
setter Window::onloadedmetadata();
getter Window::onloadstart();
setter Window::onloadstart();
getter Window::onload();
setter Window::onload();
getter Window::onmessage();
setter Window::onmessage();
getter Window::onmousedown();
setter Window::onmousedown();
getter Window::onmouseenter();
setter Window::onmouseenter();
getter Window::onmouseleave();
setter Window::onmouseleave();
getter Window::onmousemove();
setter Window::onmousemove();
getter Window::onmouseout();
setter Window::onmouseout();
getter Window::onmouseover();
setter Window::onmouseover();
getter Window::onmouseup();
setter Window::onmouseup();
getter Window::onoffline();
setter Window::onoffline();
getter Window::ononline();
setter Window::ononline();
getter Window::onpagehide();
setter Window::onpagehide();
getter Window::onpageshow();
setter Window::onpageshow();
getter Window::onpause();
setter Window::onpause();
getter Window::onplaying();
setter Window::onplaying();
getter Window::onplay();
setter Window::onplay();
getter Window::onpopstate();
setter Window::onpopstate();
getter Window::onprogress();
setter Window::onprogress();
getter Window::onratechange();
setter Window::onratechange();
getter Window::onreset();
setter Window::onreset();
getter Window::onresize();
setter Window::onresize();
getter Window::onscroll();
setter Window::onscroll();
getter Window::onseeked();
setter Window::onseeked();
getter Window::onseeking();
setter Window::onseeking();
getter Window::onselect();
setter Window::onselect();
getter Window::onshow();
setter Window::onshow();
getter Window::onsort();
setter Window::onsort();
getter Window::onstalled();
setter Window::onstalled();
getter Window::onstorage();
setter Window::onstorage();
getter Window::onsubmit();
setter Window::onsubmit();
getter Window::onsuspend();
setter Window::onsuspend();
getter Window::ontimeupdate();
setter Window::ontimeupdate();
getter Window::ontoggle();
setter Window::ontoggle();
getter Window::onunload();
setter Window::onunload();
getter Window::onvolumechange();
setter Window::onvolumechange();
getter Window::onwaiting();
setter Window::onwaiting();
getter Window::onwheel();
setter Window::onwheel();
