/* This file is part of Ganv.
 * Copyright 2007-2015 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 GnomeCanvasGroup, by Federico Mena <federico@nuclecu.unam.mx>
 * and Raph Levien <raph@gimp.org>
 * Copyright 1997-2000 Free Software Foundation
 */

#include "ganv-private.h"

#include "ganv/group.h"
#include "ganv/item.h"

#include <cairo.h>
#include <float.h>
#include <glib-object.h>
#include <glib.h>
#include <gtk/gtk.h>

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

enum {
	GROUP_PROP_0
};

G_DEFINE_TYPE_WITH_CODE(GanvGroup, ganv_group, GANV_TYPE_ITEM,
                        G_ADD_PRIVATE(GanvGroup))

static GanvItemClass* group_parent_class;

static void
ganv_group_init(GanvGroup* group)
{
	GanvGroupPrivate* impl =
	    (GanvGroupPrivate*)ganv_group_get_instance_private(group);

	group->impl                = impl;
	group->impl->item_list     = NULL;
	group->impl->item_list_end = NULL;
}

static void
ganv_group_set_property(GObject* gobject, guint param_id,
                        const GValue* value, GParamSpec* pspec)
{
	(void)value;

	g_return_if_fail(GANV_IS_GROUP(gobject));

	switch (param_id) {
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, param_id, pspec);
		break;
	}
}

static void
ganv_group_get_property(GObject* gobject, guint param_id,
                        GValue* value, GParamSpec* pspec)
{
	(void)value;

	g_return_if_fail(GANV_IS_GROUP(gobject));

	switch (param_id) {
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, param_id, pspec);
		break;
	}
}

static void
ganv_group_destroy(GtkObject* object)
{
	GanvGroup* group = NULL;

	g_return_if_fail(GANV_IS_GROUP(object));

	group = GANV_GROUP(object);

	while (group->impl->item_list) {
		// child is unref'ed by the child's group_remove().
		gtk_object_destroy(GTK_OBJECT(group->impl->item_list->data));
	}
	if (GTK_OBJECT_CLASS(group_parent_class)->destroy) {
		(*GTK_OBJECT_CLASS(group_parent_class)->destroy)(object);
	}
}

static void
ganv_group_update(GanvItem* item, int flags)
{
	GanvGroup* group = GANV_GROUP(item);

	double min_x = 0.0;
	double min_y = 0.0;
	double max_x = 0.0;
	double max_y = 0.0;

	for (GList* list = group->impl->item_list; list; list = list->next) {
		GanvItem* i = (GanvItem*)list->data;

		ganv_item_invoke_update(i, flags);

		min_x = fmin(min_x, fmin(i->impl->x1, i->impl->x2));
		min_y = fmin(min_y, fmin(i->impl->y1, i->impl->y2));
		max_x = fmax(max_x, fmax(i->impl->x1, i->impl->x2));
		max_y = fmax(max_y, fmax(i->impl->y2, i->impl->y2));
	}
	item->impl->x1 = min_x;
	item->impl->y1 = min_y;
	item->impl->x2 = max_x;
	item->impl->y2 = max_y;

	(*group_parent_class->update)(item, flags);
}

static void
ganv_group_realize(GanvItem* item)
{
	GanvGroup* group = NULL;
	GList*     list  = NULL;
	GanvItem*  i     = NULL;

	group = GANV_GROUP(item);

	for (list = group->impl->item_list; list; list = list->next) {
		i = (GanvItem*)list->data;

		if (!(i->object.flags & GANV_ITEM_REALIZED)) {
			(*GANV_ITEM_GET_CLASS(i)->realize)(i);
		}
	}

	(*group_parent_class->realize)(item);
}

static void
ganv_group_unrealize(GanvItem* item)
{
	GanvGroup* group = NULL;
	GList*     list  = NULL;
	GanvItem*  i     = NULL;

	group = GANV_GROUP(item);

	for (list = group->impl->item_list; list; list = list->next) {
		i = (GanvItem*)list->data;

		if (i->object.flags & GANV_ITEM_REALIZED) {
			(*GANV_ITEM_GET_CLASS(i)->unrealize)(i);
		}
	}

	(*group_parent_class->unrealize)(item);
}

static void
ganv_group_map(GanvItem* item)
{
	GanvGroup* group = NULL;
	GList*     list  = NULL;
	GanvItem*  i     = NULL;

	group = GANV_GROUP(item);

	for (list = group->impl->item_list; list; list = list->next) {
		i = (GanvItem*)list->data;

		if (!(i->object.flags & GANV_ITEM_MAPPED)) {
			(*GANV_ITEM_GET_CLASS(i)->map)(i);
		}
	}

	(*group_parent_class->map)(item);
}

static void
ganv_group_unmap(GanvItem* item)
{
	GanvGroup* group = NULL;
	GList*     list  = NULL;
	GanvItem*  i     = NULL;

	group = GANV_GROUP(item);

	for (list = group->impl->item_list; list; list = list->next) {
		i = (GanvItem*)list->data;

		if (i->object.flags & GANV_ITEM_MAPPED) {
			(*GANV_ITEM_GET_CLASS(i)->unmap)(i);
		}
	}

	(*group_parent_class->unmap)(item);
}

static void
ganv_group_draw(GanvItem* item,
                cairo_t* cr, double cx, double cy, double cw, double ch)
{
	GanvGroup* group = GANV_GROUP(item);

	// TODO: Layered drawing

	for (GList* list = group->impl->item_list; list; list = list->next) {
		GanvItem* child = (GanvItem*)list->data;

		if (((child->object.flags & GANV_ITEM_VISIBLE)
		     && ((child->impl->x1 < (cx + cw))
		         && (child->impl->y1 < (cy + ch))
		         && (child->impl->x2 > cx)
		         && (child->impl->y2 > cy)))) {
			if (GANV_ITEM_GET_CLASS(child)->draw) {
				(*GANV_ITEM_GET_CLASS(child)->draw)(
					child, cr, cx, cy, cw, ch);
			}
		}
	}
}

static double
ganv_group_point(GanvItem* item, double x, double y, GanvItem** actual_item)
{
	GanvGroup* group = GANV_GROUP(item);

	const double x1 = x - GANV_CLOSE_ENOUGH;
	const double y1 = y - GANV_CLOSE_ENOUGH;
	const double x2 = x + GANV_CLOSE_ENOUGH;
	const double y2 = y + GANV_CLOSE_ENOUGH;

	double dist = 0.0;
	double best = 0.0;

	*actual_item = NULL;

	for (GList* list = group->impl->item_list; list; list = list->next) {
		GanvItem* child = (GanvItem*)list->data;
		if ((child->impl->x1 > x2) || (child->impl->y1 > y2) || (child->impl->x2 < x1) || (child->impl->y2 < y1)) {
			continue;
		}

		GanvItem* point_item = NULL;

		int has_point = FALSE;
		if ((child->object.flags & GANV_ITEM_VISIBLE)
		    && GANV_ITEM_GET_CLASS(child)->point) {
			dist = GANV_ITEM_GET_CLASS(child)->point(
				child,
				x - child->impl->x, y - child->impl->y,
				&point_item);
			has_point = TRUE;
		}

		if (has_point
		    && point_item
		    && ((int)(dist + 0.5) <= GANV_CLOSE_ENOUGH)) {
			best         = dist;
			*actual_item = point_item;
		}
	}

	if (*actual_item) {
		return best;
	} else {
		*actual_item = item;
		return 0.0;
	}
}

/* Get bounds of child item in group-relative coordinates. */
static void
get_child_bounds(GanvItem* child, double* x1, double* y1, double* x2, double* y2)
{
	ganv_item_get_bounds(child, x1, y1, x2, y2);

	// Make bounds relative to the item's parent coordinate system
	*x1 -= child->impl->x;
	*y1 -= child->impl->y;
	*x2 -= child->impl->x;
	*y2 -= child->impl->y;
}

static void
ganv_group_bounds(GanvItem* item, double* x1, double* y1, double* x2, double* y2)
{
	GanvGroup* group = GANV_GROUP(item);
	GanvItem*  child = NULL;
	GList*     list  = NULL;
	double     tx1   = 0.0;
	double     ty1   = 0.0;
	double     tx2   = 0.0;
	double     ty2   = 0.0;
	double     minx  = DBL_MAX;
	double     miny  = DBL_MAX;
	double     maxx  = DBL_MIN;
	double     maxy  = DBL_MIN;
	int        set   = FALSE;

	/* Get the bounds of the first visible item */

	for (list = group->impl->item_list; list; list = list->next) {
		child = (GanvItem*)list->data;

		if (child->object.flags & GANV_ITEM_VISIBLE) {
			set = TRUE;
			get_child_bounds(child, &minx, &miny, &maxx, &maxy);
			break;
		}
	}

	/* If there were no visible items, return an empty bounding box */

	if (!set) {
		*x1 = *y1 = *x2 = *y2 = 0.0;
		return;
	}

	/* Now we can grow the bounds using the rest of the items */

	list = list->next;

	for (; list; list = list->next) {
		child = (GanvItem*)list->data;

		if (!(child->object.flags & GANV_ITEM_VISIBLE)) {
			continue;
		}

		get_child_bounds(child, &tx1, &ty1, &tx2, &ty2);

		if (tx1 < minx) {
			minx = tx1;
		}

		if (ty1 < miny) {
			miny = ty1;
		}

		if (tx2 > maxx) {
			maxx = tx2;
		}

		if (ty2 > maxy) {
			maxy = ty2;
		}
	}

	*x1 = minx;
	*y1 = miny;
	*x2 = maxx;
	*y2 = maxy;
}

static void
ganv_group_add(GanvItem* parent, GanvItem* item)
{
	GanvGroup* group = GANV_GROUP(parent);
	g_object_ref_sink(G_OBJECT(item));

	if (!group->impl->item_list) {
		group->impl->item_list     = g_list_append(group->impl->item_list, item);
		group->impl->item_list_end = group->impl->item_list;
	} else {
		group->impl->item_list_end = g_list_append(group->impl->item_list_end, item)->next;
	}

	if (group->item.object.flags & GANV_ITEM_REALIZED) {
		(*GANV_ITEM_GET_CLASS(item)->realize)(item);
	}

	if (group->item.object.flags & GANV_ITEM_MAPPED) {
		(*GANV_ITEM_GET_CLASS(item)->map)(item);
	}

	g_object_notify(G_OBJECT(item), "parent");
}

static void
ganv_group_remove(GanvItem* parent, GanvItem* item)
{
	GanvGroup* group    = GANV_GROUP(parent);
	GList*     children = NULL;

	g_return_if_fail(GANV_IS_GROUP(group));
	g_return_if_fail(GANV_IS_ITEM(item));

	for (children = group->impl->item_list; children; children = children->next) {
		if (children->data == item) {
			if (item->object.flags & GANV_ITEM_MAPPED) {
				(*GANV_ITEM_GET_CLASS(item)->unmap)(item);
			}

			if (item->object.flags & GANV_ITEM_REALIZED) {
				(*GANV_ITEM_GET_CLASS(item)->unrealize)(item);
			}

			/* Unparent the child */

			item->impl->parent = NULL;
			g_object_unref(G_OBJECT(item));

			/* Remove it from the list */

			if (children == group->impl->item_list_end) {
				group->impl->item_list_end = children->prev;
			}

			group->impl->item_list = g_list_remove_link(group->impl->item_list, children);
			g_list_free(children);
			break;
		}
	}
}

static void
ganv_group_class_init(GanvGroupClass* klass)
{
	GObjectClass*   gobject_class = (GObjectClass*)klass;
	GtkObjectClass* object_class  = (GtkObjectClass*)klass;
	GanvItemClass*  item_class    = (GanvItemClass*)klass;

	group_parent_class = (GanvItemClass*)g_type_class_peek_parent(klass);

	gobject_class->set_property = ganv_group_set_property;
	gobject_class->get_property = ganv_group_get_property;

	object_class->destroy = ganv_group_destroy;

	item_class->add       = ganv_group_add;
	item_class->remove    = ganv_group_remove;
	item_class->update    = ganv_group_update;
	item_class->realize   = ganv_group_realize;
	item_class->unrealize = ganv_group_unrealize;
	item_class->map       = ganv_group_map;
	item_class->unmap     = ganv_group_unmap;
	item_class->draw      = ganv_group_draw;
	item_class->point     = ganv_group_point;
	item_class->bounds    = ganv_group_bounds;
}
