/* This file is part of Ganv.
 * Copyright 2007-2016 David Robillard <http://drobilla.net>
 *
 * Ganv is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or any later version.
 *
 * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for details.
 *
 * You should have received a copy of the GNU General Public License along
 * with Ganv.  If not, see <http://www.gnu.org/licenses/>.
 */

/* Based on GnomeCanvasWidget, by Federico Mena <federico@nuclecu.unam.mx>
 * Copyright 1997-2000 Free Software Foundation
 */

#include "ganv-private.h"
#include "gettext.h"

#include "ganv/canvas.h"
#include "ganv/item.h"
#include "ganv/widget.h"

#include <cairo.h>
#include <glib-object.h>
#include <glib.h>
#include <gobject/gclosure.h>
#include <gtk/gtk.h>
#include <gtk/gtktypebuiltins.h>

#include <math.h>
#include <stddef.h>

G_DEFINE_TYPE_WITH_CODE(GanvWidget, ganv_widget, GANV_TYPE_ITEM,
                        G_ADD_PRIVATE(GanvWidget))

static GanvItemClass* parent_class;

enum {
	PROP_0,
	PROP_WIDGET,
	PROP_X,
	PROP_Y,
	PROP_WIDTH,
	PROP_HEIGHT,
	PROP_ANCHOR,
	PROP_SIZE_PIXELS
};

static void
ganv_widget_init(GanvWidget* witem)
{
	GanvWidgetPrivate* impl =
	    (GanvWidgetPrivate*)ganv_widget_get_instance_private(witem);

	witem->impl              = impl;
	witem->impl->x           = 0.0;
	witem->impl->y           = 0.0;
	witem->impl->width       = 0.0;
	witem->impl->height      = 0.0;
	witem->impl->anchor      = GTK_ANCHOR_NW;
	witem->impl->size_pixels = FALSE;
}

static void
ganv_widget_destroy(GtkObject* object)
{
	g_return_if_fail(object != NULL);
	g_return_if_fail(GANV_IS_WIDGET(object));

	GanvWidget* witem = GANV_WIDGET(object);

	if (witem->impl->widget && !witem->impl->in_destroy) {
		g_signal_handler_disconnect(witem->impl->widget, witem->impl->destroy_id);
		gtk_widget_destroy(witem->impl->widget);
		witem->impl->widget = NULL;
	}

	if (GTK_OBJECT_CLASS(parent_class)->destroy) {
		(*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
	}
}

static void
recalc_bounds(GanvWidget* witem)
{
	GanvItem* item = GANV_ITEM(witem);

	/* Get world coordinates */

	double wx = witem->impl->x;
	double wy = witem->impl->y;
	ganv_item_i2w(item, &wx, &wy);

	/* Get canvas pixel coordinates */

	ganv_canvas_w2c(item->impl->canvas, wx, wy, &witem->impl->cx, &witem->impl->cy);

	/* Anchor widget item */

	switch (witem->impl->anchor) {
	case GTK_ANCHOR_N:
	case GTK_ANCHOR_CENTER:
	case GTK_ANCHOR_S:
		witem->impl->cx -= witem->impl->cwidth / 2;
		break;

	case GTK_ANCHOR_NE:
	case GTK_ANCHOR_E:
	case GTK_ANCHOR_SE:
		witem->impl->cx -= witem->impl->cwidth;
		break;

	default:
		break;
	}

	switch (witem->impl->anchor) {
	case GTK_ANCHOR_W:
	case GTK_ANCHOR_CENTER:
	case GTK_ANCHOR_E:
		witem->impl->cy -= witem->impl->cheight / 2;
		break;

	case GTK_ANCHOR_SW:
	case GTK_ANCHOR_S:
	case GTK_ANCHOR_SE:
		witem->impl->cy -= witem->impl->cheight;
		break;

	default:
		break;
	}

	/* Bounds */

	item->impl->x1 = witem->impl->cx;
	item->impl->y1 = witem->impl->cy;
	item->impl->x2 = witem->impl->cx + witem->impl->cwidth;
	item->impl->y2 = witem->impl->cy + witem->impl->cheight;

	int zoom_xofs = 0;
	int zoom_yofs = 0;
	ganv_canvas_get_zoom_offsets(item->impl->canvas, &zoom_xofs, &zoom_yofs);
	if (witem->impl->widget) {
		gtk_layout_move(GTK_LAYOUT(item->impl->canvas), witem->impl->widget,
		                witem->impl->cx + zoom_xofs,
		                witem->impl->cy + zoom_yofs);
	}
}

static void
do_destroy(GtkObject* object, gpointer data)
{
	(void)object;

	GanvWidget* witem = GANV_WIDGET(data);

	witem->impl->in_destroy = TRUE;
	gtk_object_destroy(GTK_OBJECT(data));
}

static void
ganv_widget_set_property(GObject*      object,
                         guint         param_id,
                         const GValue* value,
                         GParamSpec*   pspec)
{
	GanvItem*   item        = GANV_ITEM(object);
	GanvWidget* witem       = GANV_WIDGET(object);
	int         update      = FALSE;
	int         calc_bounds = FALSE;
	GObject*    obj         = NULL;

	switch (param_id) {
	case PROP_WIDGET:
		if (witem->impl->widget) {
			g_signal_handler_disconnect(witem->impl->widget, witem->impl->destroy_id);
			gtk_container_remove(GTK_CONTAINER(item->impl->canvas), witem->impl->widget);
		}

		obj = (GObject*)g_value_get_object(value);
		if (obj) {
			witem->impl->widget     = GTK_WIDGET(obj);
			witem->impl->destroy_id = g_signal_connect(obj, "destroy",
			                                     G_CALLBACK(do_destroy),
			                                     witem);
			int zoom_xofs = 0;
			int zoom_yofs = 0;
			ganv_canvas_get_zoom_offsets(item->impl->canvas, &zoom_xofs, &zoom_yofs);

			gtk_layout_put(GTK_LAYOUT(item->impl->canvas), witem->impl->widget,
			               witem->impl->cx + zoom_xofs,
			               witem->impl->cy + zoom_yofs);
		}

		update = TRUE;
		break;

	case PROP_X:
		if (witem->impl->x != g_value_get_double(value)) {
			witem->impl->x    = g_value_get_double(value);
			calc_bounds = TRUE;
		}
		break;

	case PROP_Y:
		if (witem->impl->y != g_value_get_double(value)) {
			witem->impl->y    = g_value_get_double(value);
			calc_bounds = TRUE;
		}
		break;

	case PROP_WIDTH:
		if (witem->impl->width != fabs(g_value_get_double(value))) {
			witem->impl->width = fabs(g_value_get_double(value));
			update       = TRUE;
		}
		break;

	case PROP_HEIGHT:
		if (witem->impl->height != fabs(g_value_get_double(value))) {
			witem->impl->height = fabs(g_value_get_double(value));
			update        = TRUE;
		}
		break;

	case PROP_ANCHOR:
		if (witem->impl->anchor != (GtkAnchorType)g_value_get_enum(value)) {
			witem->impl->anchor = (GtkAnchorType)g_value_get_enum(value);
			update        = TRUE;
		}
		break;

	case PROP_SIZE_PIXELS:
		if (witem->impl->size_pixels != g_value_get_boolean(value)) {
			witem->impl->size_pixels = g_value_get_boolean(value);
			update             = TRUE;
		}
		break;

	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
		break;
	}

	if (update) {
		(*GANV_ITEM_GET_CLASS(item)->update)(item, 0);
	}

	if (calc_bounds) {
		recalc_bounds(witem);
	}
}

static void
ganv_widget_get_property(GObject*    object,
                         guint       param_id,
                         GValue*     value,
                         GParamSpec* pspec)
{
	g_return_if_fail(object != NULL);
	g_return_if_fail(GANV_IS_WIDGET(object));

	GanvWidget* witem = GANV_WIDGET(object);

	switch (param_id) {
	case PROP_WIDGET:
		g_value_set_object(value, (GObject*)witem->impl->widget);
		break;

	case PROP_X:
		g_value_set_double(value, witem->impl->x);
		break;

	case PROP_Y:
		g_value_set_double(value, witem->impl->y);
		break;

	case PROP_WIDTH:
		g_value_set_double(value, witem->impl->width);
		break;

	case PROP_HEIGHT:
		g_value_set_double(value, witem->impl->height);
		break;

	case PROP_ANCHOR:
		g_value_set_enum(value, witem->impl->anchor);
		break;

	case PROP_SIZE_PIXELS:
		g_value_set_boolean(value, witem->impl->size_pixels);
		break;

	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
		break;
	}
}

static void
ganv_widget_update(GanvItem* item, int flags)
{
	GanvWidget* witem = GANV_WIDGET(item);

	if (parent_class->update) {
		(*parent_class->update)(item, flags);
	}

	if (witem->impl->widget) {
		const double pixels_per_unit = ganv_canvas_get_zoom(item->impl->canvas);
		if (witem->impl->size_pixels) {
			witem->impl->cwidth  = (int)(witem->impl->width + 0.5);
			witem->impl->cheight = (int)(witem->impl->height + 0.5);
		} else {
			witem->impl->cwidth  = (int)(witem->impl->width * pixels_per_unit + 0.5);
			witem->impl->cheight = (int)(witem->impl->height * pixels_per_unit + 0.5);
		}

		gtk_widget_set_size_request(witem->impl->widget, witem->impl->cwidth, witem->impl->cheight);
	} else {
		witem->impl->cwidth  = 0.0;
		witem->impl->cheight = 0.0;
	}

	recalc_bounds(witem);
}

static void
ganv_widget_draw(GanvItem* item,
                 cairo_t* cr, double cx, double cy, double cw, double ch)
{
	(void)cr;
	(void)cx;
	(void)cy;
	(void)cw;
	(void)ch;

	GanvWidget* witem = GANV_WIDGET(item);

	if (witem->impl->widget) {
		gtk_widget_queue_draw(witem->impl->widget);
	}
}

static double
ganv_widget_point(GanvItem* item, double x, double y, GanvItem** actual_item)
{
	GanvWidget* witem = GANV_WIDGET(item);

	*actual_item = item;

	double x1 = 0.0;
	double y1 = 0.0;
	ganv_canvas_c2w(item->impl->canvas, witem->impl->cx, witem->impl->cy, &x1, &y1);

	const double pixels_per_unit = ganv_canvas_get_zoom(item->impl->canvas);

	double x2 = x1 + (witem->impl->cwidth - 1) / pixels_per_unit;
	double y2 = y1 + (witem->impl->cheight - 1) / pixels_per_unit;

	/* Is point inside widget bounds? */

	if ((x >= x1) && (y >= y1) && (x <= x2) && (y <= y2)) {
		return 0.0;
	}

	/* Point is outside widget bounds */

	double dx = 0.0;
	if (x < x1) {
		dx = x1 - x;
	} else if (x > x2) {
		dx = x - x2;
	} else {
		dx = 0.0;
	}

	double dy = 0.0;
	if (y < y1) {
		dy = y1 - y;
	} else if (y > y2) {
		dy = y - y2;
	} else {
		dy = 0.0;
	}

	return sqrt(dx * dx + dy * dy);
}

static void
ganv_widget_bounds(GanvItem* item, double* x1, double* y1, double* x2, double* y2)
{
	GanvWidget* witem = GANV_WIDGET(item);

	*x1 = witem->impl->x;
	*y1 = witem->impl->y;

	switch (witem->impl->anchor) {
	case GTK_ANCHOR_NW:
	case GTK_ANCHOR_W:
	case GTK_ANCHOR_SW:
		break;

	case GTK_ANCHOR_N:
	case GTK_ANCHOR_CENTER:
	case GTK_ANCHOR_S:
		*x1 -= witem->impl->width / 2.0;
		break;

	case GTK_ANCHOR_NE:
	case GTK_ANCHOR_E:
	case GTK_ANCHOR_SE:
		*x1 -= witem->impl->width;
		break;

	default:
		break;
	}

	switch (witem->impl->anchor) {
	case GTK_ANCHOR_NW:
	case GTK_ANCHOR_N:
	case GTK_ANCHOR_NE:
		break;

	case GTK_ANCHOR_W:
	case GTK_ANCHOR_CENTER:
	case GTK_ANCHOR_E:
		*y1 -= witem->impl->height / 2.0;
		break;

	case GTK_ANCHOR_SW:
	case GTK_ANCHOR_S:
	case GTK_ANCHOR_SE:
		*y1 -= witem->impl->height;
		break;

	default:
		break;
	}

	*x2 = *x1 + witem->impl->width;
	*y2 = *y1 + witem->impl->height;
}

static void
ganv_widget_class_init(GanvWidgetClass* klass)
{
	GObjectClass*   gobject_class = (GObjectClass*)klass;
	GtkObjectClass* object_class  = (GtkObjectClass*)klass;
	GanvItemClass*  item_class    = (GanvItemClass*)klass;

	parent_class = (GanvItemClass*)g_type_class_peek_parent(klass);

	gobject_class->set_property = ganv_widget_set_property;
	gobject_class->get_property = ganv_widget_get_property;

	g_object_class_install_property(
		gobject_class, PROP_WIDGET, g_param_spec_object(
			"widget", _("Widget"),
			_("The widget to embed in this item."),
			GTK_TYPE_WIDGET,
			(GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE)));

	g_object_class_install_property(
		gobject_class, PROP_X, g_param_spec_double(
			"x", _("x"),
			_("The x coordinate of the anchor"),
			-G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
			G_PARAM_READWRITE));

	g_object_class_install_property(
		gobject_class, PROP_Y, g_param_spec_double(
			"y", _("y"),
			_("The x coordinate of the anchor"),
			-G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
			G_PARAM_READWRITE));

	g_object_class_install_property(
		gobject_class, PROP_WIDTH, g_param_spec_double(
			"width", _("Width"),
			_("The width of the widget."),
			-G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
			G_PARAM_READWRITE));

	g_object_class_install_property(
		gobject_class, PROP_HEIGHT, g_param_spec_double(
			"height", _("Height"),
			_("The height of the widget."),
			-G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
			G_PARAM_READWRITE));

	g_object_class_install_property(
		gobject_class, PROP_ANCHOR, g_param_spec_enum(
			"anchor", _("Anchor"),
			_("The anchor point of the widget."),
			GTK_TYPE_ANCHOR_TYPE,
			GTK_ANCHOR_NW,
			G_PARAM_READWRITE));

	g_object_class_install_property(
		gobject_class, PROP_SIZE_PIXELS, g_param_spec_boolean(
			"size-pixels", ("Size is in pixels"),
			_("Specifies whether the widget size is specified in pixels or"
			  " canvas units. If it is in pixels, then the widget will not"
			  " be scaled when the canvas zoom factor changes.  Otherwise,"
			  " it will be scaled."),
			FALSE,
			G_PARAM_READWRITE));

	object_class->destroy = ganv_widget_destroy;

	item_class->update = ganv_widget_update;
	item_class->point  = ganv_widget_point;
	item_class->bounds = ganv_widget_bounds;
	item_class->draw   = ganv_widget_draw;
}
