/* 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/>.
 */

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

#include "ganv/canvas.h"
#include "ganv/circle.h"
#include "ganv/item.h"
#include "ganv/node.h"
#include "ganv/text.h"
#include "ganv/types.h"

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

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

G_DEFINE_TYPE_WITH_CODE(GanvCircle, ganv_circle, GANV_TYPE_NODE,
                        G_ADD_PRIVATE(GanvCircle))

static GanvNodeClass* parent_class;

enum {
	PROP_0,
	PROP_RADIUS,
	PROP_RADIUS_EMS,
	PROP_FIT_LABEL
};

static void
ganv_circle_init(GanvCircle* circle)
{
	circle->impl = (GanvCirclePrivate*)ganv_circle_get_instance_private(circle);

	memset(&circle->impl->coords, '\0', sizeof(GanvCircleCoords));
	circle->impl->coords.radius     = 0.0;
	circle->impl->coords.radius_ems = 1.0;
	circle->impl->coords.width      = 2.0;
	circle->impl->old_coords        = circle->impl->coords;
	circle->impl->fit_label         = TRUE;
}

static void
ganv_circle_destroy(GtkObject* object)
{
	g_return_if_fail(object != NULL);
	g_return_if_fail(GANV_IS_CIRCLE(object));

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

static void
ganv_circle_set_property(GObject*      object,
                         guint         prop_id,
                         const GValue* value,
                         GParamSpec*   pspec)
{
	g_return_if_fail(object != NULL);
	g_return_if_fail(GANV_IS_CIRCLE(object));

	GanvCircle* circle = GANV_CIRCLE(object);

	switch (prop_id) {
		SET_CASE(RADIUS, double, circle->impl->coords.radius)
		SET_CASE(RADIUS_EMS, double, circle->impl->coords.radius_ems)
		SET_CASE(FIT_LABEL, boolean, circle->impl->fit_label)
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}

	if (prop_id == PROP_RADIUS_EMS) {
		ganv_circle_set_radius_ems(circle, circle->impl->coords.radius_ems);
	}
}

static void
ganv_circle_get_property(GObject*    object,
                         guint       prop_id,
                         GValue*     value,
                         GParamSpec* pspec)
{
	g_return_if_fail(object != NULL);
	g_return_if_fail(GANV_IS_CIRCLE(object));

	GanvCircle* circle = GANV_CIRCLE(object);

	switch (prop_id) {
		GET_CASE(RADIUS, double, circle->impl->coords.radius)
		GET_CASE(RADIUS_EMS, double, circle->impl->coords.radius_ems)
		GET_CASE(FIT_LABEL, boolean, circle->impl->fit_label)
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}

static void
ganv_circle_resize(GanvNode* self)
{
	GanvNode*   node   = GANV_NODE(self);
	GanvCircle* circle = GANV_CIRCLE(self);
	GanvCanvas* canvas = GANV_CANVAS(GANV_ITEM(node)->impl->canvas);

	if (node->impl->label) {
		if (node->impl->label->impl->needs_layout) {
			ganv_text_layout(node->impl->label);
		}

		const double label_w = node->impl->label->impl->coords.width;
		const double label_h = node->impl->label->impl->coords.height;
		if (circle->impl->fit_label) {
			// Resize to fit text
			const double radius = MAX(label_w, label_h) / 2.0 + 3.0;
			if (radius != circle->impl->coords.radius) {
				ganv_item_set(GANV_ITEM(self),
				              "radius", radius,
				              NULL);
			}
		}

		// Center label
		ganv_item_set(GANV_ITEM(node->impl->label),
		              "x", label_w / -2.0,
		              "y", label_h / -2.0,
		              NULL);
	}

	if (parent_class->resize) {
		parent_class->resize(self);
	}

	ganv_canvas_for_each_edge_on(
		canvas, node, (GanvEdgeFunc)ganv_edge_update_location, NULL);
}

static void
ganv_circle_redraw_text(GanvNode* self)
{
	GanvCircle* circle = GANV_CIRCLE(self);
	if (circle->impl->coords.radius_ems) {
		ganv_circle_set_radius_ems(circle, circle->impl->coords.radius_ems);
	}

	if (parent_class->redraw_text) {
		parent_class->redraw_text(self);
	}
}

static gboolean
ganv_circle_is_within(const GanvNode* self,
                      double          x1,
                      double          y1,
                      double          x2,
                      double          y2)
{
	const double x = GANV_ITEM(self)->impl->x;
	const double y = GANV_ITEM(self)->impl->y;

	return x >= x1
		&& x <= x2
		&& y >= y1
		&& y <= y2;
}

static void
ganv_circle_vector(const GanvNode* self,
                   const GanvNode* other,
                   double*         x,
                   double*         y,
                   double*         dx,
                   double*         dy)
{
	GanvCircle* circle = GANV_CIRCLE(self);

	const double cx      = GANV_ITEM(self)->impl->x;
	const double cy      = GANV_ITEM(self)->impl->y;
	const double other_x = GANV_ITEM(other)->impl->x;
	const double other_y = GANV_ITEM(other)->impl->y;

	const double border  = circle->node.impl->border_width;
	const double xdist = other_x - cx;
	const double ydist = other_y - cy;
	const double h     = sqrt((xdist * xdist) + (ydist * ydist));
	const double theta = asin(xdist / (h + DBL_EPSILON));
	const double y_mod = (cy < other_y) ? 1 : -1;
	const double ret_h = h - circle->impl->coords.radius - border / 2.0;
	const double ret_x = other_x - sin(theta) * ret_h;
	const double ret_y = other_y - cos(theta) * ret_h * y_mod;

	*x  = ret_x;
	*y  = ret_y;
	*dx = 0.0;
	*dy = 0.0;

	ganv_item_i2w(GANV_ITEM(circle)->impl->parent, x, y);
}

static void
request_redraw(GanvItem*               item,
               const GanvCircleCoords* coords,
               gboolean                world)
{
	const double w = coords->width;

	double x1 = coords->x - coords->radius - w;
	double y1 = coords->y - coords->radius - w;
	double x2 = coords->x + coords->radius + w;
	double y2 = coords->y + coords->radius + w;

	if (!world) {
		// Convert from parent-relative coordinates to world coordinates
		ganv_item_i2w_pair(item, &x1, &y1, &x2, &y2);
	}

	ganv_canvas_request_redraw_w(item->impl->canvas, x1, y1, x2, y2);
}

static void
coords_i2w(GanvItem* item, GanvCircleCoords* coords)
{
	ganv_item_i2w(item, &coords->x, &coords->y);
}

static void
ganv_circle_bounds_item(GanvItem* item,
                        double* x1, double* y1,
                        double* x2, double* y2)
{
	const GanvCircle*       circle = GANV_CIRCLE(item);
	const GanvCircleCoords* coords = &circle->impl->coords;
	*x1 = coords->x - coords->radius - coords->width;
	*y1 = coords->y - coords->radius - coords->width;
	*x2 = coords->x + coords->radius + coords->width;
	*y2 = coords->y + coords->radius + coords->width;
}

static void
ganv_circle_bounds(GanvItem* item,
                   double* x1, double* y1,
                   double* x2, double* y2)
{
	ganv_circle_bounds_item(item, x1, y1, x2, y2);
}

static void
ganv_circle_update(GanvItem* item, int flags)
{
	GanvCircle*        circle = GANV_CIRCLE(item);
	GanvCirclePrivate* impl   = circle->impl;
	impl->coords.width = circle->node.impl->border_width;

	// Request redraw of old location
	request_redraw(item, &impl->old_coords, TRUE);

	// Store old coordinates in world relative coordinates in case the
	// group we are in moves between now and the next update
	impl->old_coords = impl->coords;
	coords_i2w(item, &impl->old_coords);

	// Update world-relative bounding box
	ganv_circle_bounds(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);
	ganv_item_i2w_pair(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);

	// Request redraw of new location
	request_redraw(item, &impl->coords, FALSE);

	GANV_ITEM_CLASS(parent_class)->update(item, flags);
}

static void
ganv_circle_draw(GanvItem* item,
                 cairo_t* cr, double cx, double cy, double cw, double ch)
{
	GanvNode*          node   = GANV_NODE(item);
	GanvCircle*        circle = GANV_CIRCLE(item);
	GanvCirclePrivate* impl   = circle->impl;

	double r = 0.0;
	double g = 0.0;
	double b = 0.0;
	double a = 0.0;

	double x = impl->coords.x;
	double y = impl->coords.y;
	ganv_item_i2w(item, &x, &y);

	double dash_length  = 0.0;
	double border_color = 0.0;
	double fill_color   = 0.0;
	ganv_node_get_draw_properties(
		&circle->node, &dash_length, &border_color, &fill_color);

	// Fill
	cairo_new_path(cr);
	cairo_arc(cr,
	          x,
	          y,
	          impl->coords.radius + (impl->coords.width / 2.0),
	          0, 2 * G_PI);
	color_to_rgba(fill_color, &r, &g, &b, &a);
	cairo_set_source_rgba(cr, r, g, b, a);
	cairo_fill(cr);

	// Border
	cairo_arc(cr,
	          x,
	          y,
	          impl->coords.radius,
	          0, 2 * G_PI);
	color_to_rgba(border_color, &r, &g, &b, &a);
	cairo_set_source_rgba(cr, r, g, b, a);
	cairo_set_line_width(cr, impl->coords.width);
	if (dash_length > 0) {
		cairo_set_dash(cr, &dash_length, 1, circle->node.impl->dash_offset);
	} else {
		cairo_set_dash(cr, &dash_length, 0, 0);
	}
	cairo_stroke(cr);

	// Draw label
	if (node->impl->label) {
		GanvItem* label_item = GANV_ITEM(node->impl->label);

		if (label_item->object.flags & GANV_ITEM_VISIBLE) {
			GANV_ITEM_GET_CLASS(label_item)->draw(
				label_item, cr, cx, cy, cw, ch);
		}
	}
}

static double
ganv_circle_point(GanvItem* item, double x, double y, GanvItem** actual_item)
{
	const GanvCircle*       circle = GANV_CIRCLE(item);
	const GanvCircleCoords* coords = &circle->impl->coords;

	*actual_item = item;

	const double dx = fabs(x - coords->x);
	const double dy = fabs(y - coords->y);
	const double d  = sqrt((dx * dx) + (dy * dy));

	if (d <= coords->radius + coords->width) {
		// Point is inside the circle
		return 0.0;
	} else {
		// Distance from the edge of the circle
		return d - (coords->radius + coords->width);
	}
}

static void
ganv_circle_class_init(GanvCircleClass* klass)
{
	GObjectClass*   gobject_class = (GObjectClass*)klass;
	GtkObjectClass* object_class  = (GtkObjectClass*)klass;
	GanvItemClass*  item_class    = (GanvItemClass*)klass;
	GanvNodeClass*  node_class    = (GanvNodeClass*)klass;

	parent_class = GANV_NODE_CLASS(g_type_class_peek_parent(klass));

	gobject_class->set_property = ganv_circle_set_property;
	gobject_class->get_property = ganv_circle_get_property;

	g_object_class_install_property(
		gobject_class, PROP_RADIUS, g_param_spec_double(
			"radius",
			_("Radius"),
			_("The radius of the circle."),
			0, G_MAXDOUBLE,
			0.0,
			G_PARAM_READWRITE));

	g_object_class_install_property(
		gobject_class, PROP_RADIUS_EMS, g_param_spec_double(
			"radius-ems",
			_("Radius in ems"),
			_("The radius of the circle in ems."),
			0, G_MAXDOUBLE,
			1.0,
			G_PARAM_READWRITE));

	g_object_class_install_property(
		gobject_class, PROP_FIT_LABEL, g_param_spec_boolean(
			"fit-label",
			_("Fit label"),
			_("If true, expand circle to fit its label"),
			TRUE,
			(GParamFlags)G_PARAM_READWRITE));

	object_class->destroy = ganv_circle_destroy;

	node_class->resize      = ganv_circle_resize;
	node_class->is_within   = ganv_circle_is_within;
	node_class->tail_vector = ganv_circle_vector;
	node_class->head_vector = ganv_circle_vector;
	node_class->redraw_text = ganv_circle_redraw_text;

	item_class->update = ganv_circle_update;
	item_class->bounds = ganv_circle_bounds;
	item_class->point  = ganv_circle_point;
	item_class->draw   = ganv_circle_draw;
}

GanvCircle*
ganv_circle_new(GanvCanvas* canvas,
                const char* first_property_name, ...)
{
	GanvCircle* circle = GANV_CIRCLE(
		g_object_new(ganv_circle_get_type(), "canvas", canvas, NULL));

	va_list args;
	va_start(args, first_property_name);
	g_object_set_valist(G_OBJECT(circle), first_property_name, args);
	va_end(args);

	return circle;
}

double
ganv_circle_get_radius(const GanvCircle* circle)
{
	return circle->impl->coords.radius;
}

void
ganv_circle_set_radius(GanvCircle* circle, double radius)
{
	circle->impl->coords.radius = radius;
	ganv_item_request_update(GANV_ITEM(circle));
}

double
ganv_circle_get_radius_ems(const GanvCircle* circle)
{
	return circle->impl->coords.radius_ems;
}

void
ganv_circle_set_radius_ems(GanvCircle* circle, double ems)
{
	GanvCanvas*  canvas = GANV_CANVAS(GANV_ITEM(circle)->impl->canvas);
	const double points = ganv_canvas_get_font_size(canvas);
	circle->impl->coords.radius_ems = ems;
	circle->impl->coords.radius     = points * ems;
	ganv_item_request_update(GANV_ITEM(circle));
}

gboolean
ganv_circle_get_fit_label(const GanvCircle* circle)
{
	return circle->impl->fit_label;
}

void
ganv_circle_set_fit_label(GanvCircle* circle, gboolean fit_label)
{
	circle->impl->fit_label = fit_label;
	ganv_item_request_update(GANV_ITEM(circle));
}
