/* HTML canvas element rendering context binding using duktape and libdom
 *
 * Copyright 2020 Daniel Silverstone <dsilvers@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 CanvasRenderingContext2D {
      private struct dom_html_canvas_element *canvas;
      private struct bitmap *bitmap;
      private int width;
      private int height;
      private size_t stride;
      private dom_event_listener *listener;
      prologue %{
/* prologue */
#include "desktop/gui_internal.h"
#include "desktop/gui_table.h"
#include "netsurf/bitmap.h"
#include "utils/corestrings.h"
/* It's a smidge naughty of us to read
 * this particular header, but we're needing
 * to redraw the node we represent
 */
#include "content/handlers/html/private.h"

static void redraw_node(dom_node *node)
{
	struct box *box = NULL;
	html_content *htmlc = NULL;
	dom_exception exc;
	dom_document *doc;

	exc = dom_node_get_user_data(node,
				     corestring_dom___ns_key_box_node_data,
				     &box);
	if (exc != DOM_NO_ERR || box == NULL) {
		return;
	}

	exc = dom_node_get_owner_document(node, &doc);
	if (exc != DOM_NO_ERR || doc == NULL) {
		return;
	}

	exc = dom_node_get_user_data(doc,
				     corestring_dom___ns_key_html_content_data,
				     &htmlc);
	if (exc != DOM_NO_ERR || htmlc == NULL) {
		dom_node_unref(doc);
		return;
	}

	html__redraw_a_box(htmlc, box);

	dom_node_unref(doc);
}

/**
 * deal with events from the DOM for canvas node user data
 *
 * \param operation The DOM operation happening
 * \param key The user data key
 * \param data The user data (our bitmap)
 * \param src The DOM node emitting the event (our <canvas>)
 * \param dst The target DOM node if applicable
 */
static void
canvas2d_user_data_handler(dom_node_operation operation,
			   dom_string *key,
			   void *data,
			   struct dom_node *src,
			   struct dom_node *dst)
{
	struct bitmap *newbitmap, *bitmap = (struct bitmap*)data, *oldbitmap = NULL;
	int width, height;
	size_t stride;

	if (dom_string_isequal(key,corestring_dom___ns_key_canvas_node_data) == false || data == NULL) {
		/* Not for us */
		return;
	}

	switch (operation) {
	case DOM_NODE_CLONED:
		width = guit->bitmap->get_width(bitmap);
		height = guit->bitmap->get_height(bitmap);
		stride = guit->bitmap->get_rowstride(bitmap);
		newbitmap = guit->bitmap->create(width, height,
						 BITMAP_NONE);
		if (newbitmap != NULL) {
			if (guit->bitmap->get_rowstride(newbitmap) == stride) {
				// Compatible bitmap, bung the data over
				memcpy(guit->bitmap->get_buffer(newbitmap),
				       guit->bitmap->get_buffer(bitmap),
				       stride * height);
				guit->bitmap->modified(newbitmap);
			}
		}
		if (dom_node_set_user_data(dst,
					   corestring_dom___ns_key_canvas_node_data,
					   newbitmap, canvas2d_user_data_handler,
					   &oldbitmap) == DOM_NO_ERR) {
			if (oldbitmap != NULL)
				guit->bitmap->destroy(oldbitmap);
		}
		break;

	case DOM_NODE_RENAMED:
	case DOM_NODE_IMPORTED:
	case DOM_NODE_ADOPTED:
		break;

	case DOM_NODE_DELETED:
		guit->bitmap->destroy(bitmap);
		break;
	default:
		NSLOG(netsurf, INFO, "User data operation not handled.");
		assert(0);
	}
}

/**
 * Give the canvas element an appropriately sized bitmap
 *
 * \param node The DOM node being inserted
 * \param[out] bitmap_out The bitmap created
 * \return NSERROR_OK on success else appropriate error code
 */
static nserror canvas2d_create_bitmap(dom_node *node, struct bitmap **bitmap_out)
{
	dom_exception exc;
	dom_string *width_s = NULL, *height_s = NULL;
	unsigned long width = 300, height = 150;
	struct bitmap *bitmap, *oldbitmap = NULL;

	exc = dom_element_get_attribute(node,
					corestring_dom_width,
					&width_s);
	if (exc == DOM_NO_ERR && width_s != NULL) {
		const char *ptr = (const char *)dom_string_data(width_s);
		const char *endptr = ptr + dom_string_length(width_s);
		char * ended;
		unsigned long width_n = strtoul(ptr, &ended, 10);

		if (ended == endptr || strcasecmp(ended, "px") == 0) {
			/* parsed it all */
			width = width_n;
		}

		dom_string_unref(width_s);
	}

	exc = dom_element_get_attribute(node,
					corestring_dom_height,
					&height_s);
	if (exc == DOM_NO_ERR && height_s != NULL) {
		const char *ptr = (const char *)dom_string_data(height_s);
		const char *endptr = ptr + dom_string_length(height_s);
		char * ended;
		unsigned long height_n = strtoul(ptr, &ended, 10);

		if (ended == endptr || strcasecmp(ended, "px") == 0) {
			/* parsed it all */
			height = height_n;
		}

		dom_string_unref(height_s);
	}

	bitmap = guit->bitmap->create(
		(int)width, (int)height,
		BITMAP_NONE);

	if (bitmap == NULL) {
		return NSERROR_NOMEM;
	}

	memset(guit->bitmap->get_buffer(bitmap),
	       0, /* Transparent black */
	       height * guit->bitmap->get_rowstride(bitmap));
	guit->bitmap->modified(bitmap);

	exc = dom_node_set_user_data(node,
				     corestring_dom___ns_key_canvas_node_data,
				     bitmap,
				     canvas2d_user_data_handler,
				     &oldbitmap);

	if (exc != DOM_NO_ERR) {
		guit->bitmap->destroy(bitmap);
		return NSERROR_DOM;
	}

	assert(oldbitmap == NULL);

	if (bitmap_out != NULL)
		*bitmap_out = bitmap;

	return NSERROR_OK;
}

/**
 * Handle subtree modified events for our canvas node
 *
 * If width or height has changed relative to our priv, then
 * we need to recreate the bitmap and reset our cached width
 * and height values in order to be safe.  Plus redraw ourselves.
 *
 * \param evt The event which occurred
 * \param pw The private pointer for our canvas object
 */
static void
canvas2d__handle_dom_event(dom_event *evt, void *pw)
{
	canvas_rendering_context2d_private_t *priv = pw;
	dom_ulong width;
	dom_ulong height;
	dom_exception exc;
	struct bitmap *newbitmap, *oldbitmap = NULL;
	size_t stride;
	dom_event_flow_phase phase;
	
	exc = dom_event_get_event_phase(evt, &phase);
	assert(exc == DOM_NO_ERR);
	/* If we're not being hit right now, we're not up for it */
	if (phase != DOM_AT_TARGET) return;
	
	/* Rather than being complex about things, let's just work out
	 * what the width and height are and hope nothing else matters
	 */
	
	exc = dom_html_canvas_element_get_width(priv->canvas, &width);
	if (exc != DOM_NO_ERR) return;
	exc = dom_html_canvas_element_get_height(priv->canvas, &height);
	if (exc != DOM_NO_ERR) return;
	
	if ((int)height == priv->height && (int)width == priv->width) return;
	
	/* Okay, we need to reallocate our bitmap and re-cache values */
	
	newbitmap = guit->bitmap->create(width, height, BITMAP_NONE);
	stride = guit->bitmap->get_rowstride(newbitmap);

	if (newbitmap != NULL) {
		memset(guit->bitmap->get_buffer(newbitmap),
		       0,
		       stride * height);
		guit->bitmap->modified(newbitmap);
	}

	if (dom_node_set_user_data(priv->canvas,
				   corestring_dom___ns_key_canvas_node_data,
				   newbitmap, canvas2d_user_data_handler,
				   &oldbitmap) == DOM_NO_ERR) {
		if (oldbitmap != NULL)
			guit->bitmap->destroy(oldbitmap);
	} else {
		guit->bitmap->destroy(newbitmap);
		/* We'll stick with the old, odd though that might be */
		return;
	}

	/* Cache the new values */
	priv->width = (int)width;
	priv->height = (int)height;
	priv->stride = stride;
	priv->bitmap = newbitmap;
}

typedef struct {
	uint8_t *ptr;
	size_t stride;
	ssize_t width;
	ssize_t height;
} raw_bitmap;

typedef struct {
	raw_bitmap src;
	raw_bitmap dst;
	/* These are relative to the destination top/left */
	ssize_t dst_x;
	ssize_t dst_y;
	/* These are relative to the source top/left */
	ssize_t x1;
	ssize_t y1;
	/* And these are +1, so a 1x1 copy will have x2==x1+1 etc */
	ssize_t x2;
	ssize_t y2;
} copy_operation;

/**
 * Copy from src to dst
 *
 * Note, this is destructive to its copy_operation input
 *
 * \param op The copy operation to perform
 * \return Whether the destination bitmap was altered
 */
static bool
canvas2d__copy_bitmap_to_bitmap(copy_operation *op)
{
	/* Constrain src rectangle to src bitmap size */
	if (op->x1 < 0) op->x1 = 0;
	if (op->y1 < 0) op->y1 = 0;
	if (op->x2 > op->src.width) op->x2 = op->src.width;
	if (op->y2 > op->src.height) op->y2 = op->src.height;
	/* Offset the rectangle into dst coordinates */
	op->x1 += op->dst_x;
	op->x2 += op->dst_x;
	op->y1 += op->dst_y;
	op->y2 += op->dst_y;
	/* Constrain dst rectangle to dst bitmap */
	if (op->x1 < 0) op->x1 = 0;
	if (op->y1 < 0) op->y1 = 0;
	if (op->x2 > op->dst.width) op->x2 = op->dst.width;
	if (op->y2 > op->dst.height) op->y2 = op->dst.height;
	/* If we have nothing to copy, stop now */
	if ((op->x2 - op->x1) < 1 ||
	    (op->y2 - op->y1) < 1)
		return false;
	/* Okay, stuff to copy, so let's begin */
	op->src.ptr +=
		(op->src.stride * (op->y1 - op->dst_y)) + /* move down y1 rows */
		(op->x1 - op->dst_x) * 4; /* and across x1 pixels */
	op->dst.ptr +=
		(op->dst.stride * op->y1) + /* down down y1 rows */
		(op->x1 * 4); /* and across x1 pixels */
	for (ssize_t rowctr = op->y2 - op->y1; rowctr > 0; --rowctr) {
		memcpy(op->dst.ptr, op->src.ptr, (op->x2 - op->x1) * 4);
		op->src.ptr += op->src.stride;
		op->dst.ptr += op->dst.stride;
	}
	return true;
}

/* prologue ends */
%};
};

init CanvasRenderingContext2D(struct dom_html_canvas_element *canvas)
%{
	struct bitmap *bitmap;
	dom_exception exc;

	assert(canvas != NULL);
	
	priv->canvas = canvas;
	dom_node_ref(canvas);
	
	exc = dom_event_listener_create(canvas2d__handle_dom_event,
					priv,
					&priv->listener);
	assert(exc == DOM_NO_ERR);
	
	exc = dom_event_target_add_event_listener(
		canvas,
		corestring_dom_DOMSubtreeModified,
		priv->listener,
		false);
	assert(exc == DOM_NO_ERR);
	
	exc = dom_node_get_user_data(canvas,
				     corestring_dom___ns_key_canvas_node_data,
				     &bitmap);
	assert(exc == DOM_NO_ERR);
	
	if (bitmap == NULL) {
		if (canvas2d_create_bitmap((dom_node *)canvas,
					   &bitmap) != NSERROR_OK) {
			priv->bitmap = NULL;
			priv->width = -1;
			priv->height = -1;
			priv->stride = 0;
			return;
		}
	}

	assert(bitmap != NULL);
	
	priv->bitmap = bitmap;
	priv->width = guit->bitmap->get_width(bitmap);
	priv->height = guit->bitmap->get_height(bitmap);
	priv->stride = guit->bitmap->get_rowstride(bitmap);
%}

fini CanvasRenderingContext2D()
%{
	dom_exception exc;
	exc = dom_event_target_remove_event_listener(
		priv->canvas,
		corestring_dom_DOMSubtreeModified,
		priv->listener,
		false);
	assert(exc == DOM_NO_ERR);
	dom_event_listener_unref(priv->listener);
	dom_node_unref(priv->canvas);
%}

getter CanvasRenderingContext2D::canvas()
%{
	dukky_push_node(ctx, (dom_node *)priv->canvas);
	return 1;
%}

getter CanvasRenderingContext2D::width()
%{
	dom_exception exc;
	dom_ulong width;
	
	exc = dom_html_canvas_element_get_width(priv->canvas, &width);
	if (exc != DOM_NO_ERR) return 0;
	
	duk_push_number(ctx, (duk_double_t)width);
	return 1;
%}

setter CanvasRenderingContext2D::width()
%{
	dom_exception exc;
	dom_ulong width = duk_get_uint(ctx, 0);

	exc = dom_html_canvas_element_set_width(priv->canvas, width);
	if (exc != DOM_NO_ERR) return 0;

	return 1;
%}

getter CanvasRenderingContext2D::height()
%{
	dom_exception exc;
	dom_ulong height;
	
	exc = dom_html_canvas_element_get_height(priv->canvas, &height);
	if (exc != DOM_NO_ERR) return 0;
	
	duk_push_number(ctx, (duk_double_t)height);
	return 1;
%}

setter CanvasRenderingContext2D::height()
%{
	dom_exception exc;
	dom_ulong height = duk_get_uint(ctx, 0);

	exc = dom_html_canvas_element_set_height(priv->canvas, height);
	if (exc != DOM_NO_ERR) return 0;

	return 1;
%}

method CanvasRenderingContext2D::createImageData()
%{
	/* Can be called either with width and height, or with a reference
	 * imagedata object
	 */
	image_data_private_t *idpriv;
	int width, height;

	if (duk_get_top(ctx) == 2) {
		width = duk_to_int(ctx, 0);
		height = duk_to_int(ctx, 1);
	} else if (dukky_instanceof(ctx, 0, PROTO_NAME(IMAGEDATA))) {
		duk_get_prop_string(ctx, 0, dukky_magic_string_private);
		idpriv = duk_get_pointer(ctx, -1);
		width = idpriv->width;
		height = idpriv->height;
		duk_pop(ctx);
	} else {
		duk_push_null(ctx);
		return 1;
	}

	duk_push_int(ctx, width);
	duk_push_int(ctx, height);
	if (dukky_create_object(ctx,
				PROTO_NAME(IMAGEDATA),
				2) != DUK_EXEC_SUCCESS) {
		return duk_error(ctx,
				 DUK_ERR_ERROR,
				 "Unable to create ImageData");
	}
	return 1;
%}

method CanvasRenderingContext2D::getImageData()
%{
	/* called with x, y, width, height */
	int x = duk_get_int(ctx, 0);
	int y = duk_get_int(ctx, 1);
	int width = duk_get_int(ctx, 2);
	int height = duk_get_int(ctx, 3);
	image_data_private_t *idpriv;
	copy_operation copyop;

	if (priv->bitmap == NULL)
		return duk_generic_error(ctx, "Canvas in bad state, sorry");

	duk_push_int(ctx, width);
	duk_push_int(ctx, height);
	if (dukky_create_object(ctx,
				PROTO_NAME(IMAGEDATA),
				2) != DUK_EXEC_SUCCESS) {
		return duk_error(ctx,
				 DUK_ERR_ERROR,
				 "Unable to create ImageData");
	}

	/* ... imgdata */
	duk_get_prop_string(ctx, -1, dukky_magic_string_private);
	idpriv = duk_get_pointer(ctx, -1);
	duk_pop(ctx);

	/* We now have access to the imagedata private, so we need to copy
	 * the pixel range out of ourselves
	 */
	copyop.src.ptr = guit->bitmap->get_buffer(priv->bitmap);
	copyop.src.stride = priv->stride;
	copyop.src.width = priv->width;
	copyop.src.height = priv->height;

	copyop.dst.ptr = idpriv->data;
	copyop.dst.stride = idpriv->width * 4;
	copyop.dst.width = idpriv->width;
	copyop.dst.height = idpriv->height;

	/* Copying to top/left of our new bitmap */
	copyop.dst_x = 0;
	copyop.dst_y = 0;

	/* Copying from x,y for width,height */
	copyop.x1 = x;
	copyop.x2 = x + width;
	copyop.y1 = y;
	copyop.y2 = y + height;

	/* We don't care if the copy operation wrote or not because
	 * we don't need to invalidate ImageData objects
	 */
	(void)canvas2d__copy_bitmap_to_bitmap(&copyop);
	return 1;
%}

method CanvasRenderingContext2D::putImageData()
%{
	/* imgdata, x, y[, clipx, clipy, clipw, cliph] */
	/* If provided, the clip coordinates are within the input image data */
	/* We pretend the image is placed at x,y within ourselves, and then we
	 * copy the clip rectangle (defaults to whole image)
	 */
	image_data_private_t *idpriv;
	copy_operation copyop;

	if (!dukky_instanceof(ctx, 0, PROTO_NAME(IMAGEDATA))) {
		return duk_generic_error(ctx, "Expected ImageData as first argument");
	}

	if (priv->bitmap == NULL)
		return duk_generic_error(ctx, "Canvas in bad state, sorry");

	duk_get_prop_string(ctx, 0, dukky_magic_string_private);
	idpriv = duk_get_pointer(ctx, -1);
	duk_pop(ctx);

	/* Copying from the input ImageData object */
	copyop.src.ptr = idpriv->data;
	copyop.src.stride = idpriv->width * 4;
	copyop.src.width = idpriv->width;
	copyop.src.height = idpriv->height;

	/* Copying to ourselves */
	copyop.dst.ptr = guit->bitmap->get_buffer(priv->bitmap);
	copyop.dst.stride = priv->stride;
	copyop.dst.width = priv->width;
	copyop.dst.height = priv->height;

	/* X Y target coordinates */
	copyop.dst_x = duk_to_int(ctx, 1);
	copyop.dst_y = duk_to_int(ctx, 2);

	if (duk_get_top(ctx) < 7) {
		/* Clipping data not provided */
		copyop.x1 = 0;
		copyop.y1 = 0;
		copyop.x2 = idpriv->width;
		copyop.y2 = idpriv->height;
	} else {
		copyop.x1 = duk_to_int(ctx, 3);
		copyop.y1 = duk_to_int(ctx, 4);
		copyop.x2 = copyop.x1 + duk_to_int(ctx, 5);
		copyop.y2 = copyop.y1 + duk_to_int(ctx, 6);
	}

	if (canvas2d__copy_bitmap_to_bitmap(&copyop)) {
		guit->bitmap->modified(priv->bitmap);
		redraw_node((dom_node *)(priv->canvas));
	}

	return 0;
%}
