/* 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/box.h"
#include "ganv/canvas.h"
#include "ganv/edge.h"
#include "ganv/item.h"
#include "ganv/module.h"
#include "ganv/node.h"
#include "ganv/port.h"
#include "ganv/text.h"
#include "ganv/types.h"

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

#include <math.h>
#include <stdarg.h>
#include <stdlib.h>

static const double PORT_LABEL_HPAD = 4.0;
static const double PORT_LABEL_VPAD = 1.0;

static void
ganv_port_update_control_slider(GanvPort* port, float value, gboolean force);

G_DEFINE_TYPE_WITH_CODE(GanvPort, ganv_port, GANV_TYPE_BOX,
                        G_ADD_PRIVATE(GanvPort))

static GanvBoxClass* parent_class;

enum {
	PROP_0,
	PROP_IS_INPUT,
	PROP_IS_CONTROLLABLE
};

enum {
	PORT_VALUE_CHANGED,
	PORT_LAST_SIGNAL
};

static guint port_signals[PORT_LAST_SIGNAL];

static void
ganv_port_init(GanvPort* port)
{
	port->impl = (GanvPortPrivate*)ganv_port_get_instance_private(port);

	port->impl->control         = NULL;
	port->impl->value_label     = NULL;
	port->impl->is_input        = TRUE;
	port->impl->is_controllable = FALSE;
}

static void
ganv_port_destroy(GtkObject* object)
{
	g_return_if_fail(object != NULL);
	g_return_if_fail(GANV_IS_PORT(object));

	GanvItem*   item   = GANV_ITEM(object);
	GanvPort*   port   = GANV_PORT(object);
	GanvCanvas* canvas = ganv_item_get_canvas(item);
	if (canvas) {
		if (port->impl->is_input) {
			ganv_canvas_for_each_edge_to(
				canvas, &port->box.node, (GanvEdgeFunc)ganv_edge_remove, NULL);
		} else {
			ganv_canvas_for_each_edge_from(
				canvas, &port->box.node, (GanvEdgeFunc)ganv_edge_remove, NULL);
		}
	}

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

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

	GanvPort* port = GANV_PORT(object);

	switch (prop_id) {
		SET_CASE(IS_INPUT, boolean, port->impl->is_input)
		SET_CASE(IS_CONTROLLABLE, boolean, port->impl->is_controllable)
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}

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

	GanvPort* port = GANV_PORT(object);

	switch (prop_id) {
		GET_CASE(IS_INPUT, boolean, port->impl->is_input)
		GET_CASE(IS_CONTROLLABLE, boolean, port->impl->is_controllable)
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}

static void
ganv_port_update(GanvItem* item, int flags)
{
	GanvPort*        port = GANV_PORT(item);
	GanvPortPrivate* impl = port->impl;

	if (impl->control) {
		ganv_item_invoke_update(GANV_ITEM(impl->control->rect), flags);
	}

	if (impl->value_label) {
		ganv_item_invoke_update(GANV_ITEM(port->impl->value_label), flags);
	}

	GanvItemClass* item_class = GANV_ITEM_CLASS(parent_class);
	item_class->update(item, flags);
}

static void
ganv_port_draw(GanvItem* item,
               cairo_t* cr, double cx, double cy, double cw, double ch)
{
	GanvPort*   port   = GANV_PORT(item);
	GanvCanvas* canvas = ganv_item_get_canvas(item);

	// Draw Box
	GanvItemClass* item_class = GANV_ITEM_CLASS(parent_class);
	item_class->draw(item, cr, cx, cy, cw, ch);

	if (port->impl->control) {
		// Clip to port boundaries (to stay within radiused borders)
		cairo_save(cr);
		const double  pad    = GANV_NODE(port)->impl->border_width / 2.0;
		GanvBoxCoords coords = GANV_BOX(port)->impl->coords;
		ganv_item_i2w_pair(GANV_ITEM(port),
		                   &coords.x1, &coords.y1, &coords.x2, &coords.y2);
		ganv_box_path(GANV_BOX(port), cr,
		              coords.x1 + pad, coords.y1 + pad,
		              coords.x2 - pad, coords.y2 - pad,
		              -pad);
		cairo_clip(cr);

		GanvItem* const rect = GANV_ITEM(port->impl->control->rect);
		GANV_ITEM_GET_CLASS(rect)->draw(rect, cr, cx, cy, cw, ch);

		cairo_restore(cr);
	}

	if (ganv_canvas_get_direction(canvas) == GANV_DIRECTION_DOWN ||
	    !GANV_NODE(port)->impl->show_label) {
		return;
	}

	GanvItem* labels[2] = {
		GANV_ITEM(GANV_NODE(item)->impl->label),
		port->impl->value_label ? GANV_ITEM(port->impl->value_label) : NULL
	};
	for (int i = 0; i < 2; ++i) {
		if (labels[i] && (labels[i]->object.flags & GANV_ITEM_VISIBLE)) {
			GANV_ITEM_GET_CLASS(labels[i])->draw(
				labels[i], cr, cx, cy, cw, ch);
		}
	}
}

static void
ganv_port_tail_vector(const GanvNode* self,
                      const GanvNode* head,
                      double*         x,
                      double*         y,
                      double*         dx,
                      double*         dy)
{
	(void)head;

	GanvPort*   port   = GANV_PORT(self);
	GanvItem*   item   = &port->box.node.item;
	GanvCanvas* canvas = ganv_item_get_canvas(item);

	const double px           = item->impl->x;
	const double py           = item->impl->y;
	const double border_width = GANV_NODE(port)->impl->border_width;

	switch (ganv_canvas_get_direction(canvas)) {
	case GANV_DIRECTION_RIGHT:
		*x  = px + ganv_box_get_width(&port->box) + (border_width / 2.0);
		*y  = py + ganv_box_get_height(&port->box) / 2.0;
		*dx = 1.0;
		*dy = 0.0;
		break;
	case GANV_DIRECTION_DOWN:
		*x  = px + ganv_box_get_width(&port->box) / 2.0;
		*y  = py + ganv_box_get_height(&port->box) + (border_width / 2.0);
		*dx = 0.0;
		*dy = 1.0;
		break;
	}

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

static void
ganv_port_head_vector(const GanvNode* self,
                      const GanvNode* tail,
                      double*         x,
                      double*         y,
                      double*         dx,
                      double*         dy)
{
	(void)tail;

	GanvPort*   port   = GANV_PORT(self);
	GanvItem*   item   = &port->box.node.item;
	GanvCanvas* canvas = ganv_item_get_canvas(item);

	const double px           = item->impl->x;
	const double py           = item->impl->y;
	const double border_width = GANV_NODE(port)->impl->border_width;

	switch (ganv_canvas_get_direction(canvas)) {
	case GANV_DIRECTION_RIGHT:
		*x  = px - (border_width / 2.0);
		*y  = py + ganv_box_get_height(&port->box) / 2.0;
		*dx = -1.0;
		*dy = 0.0;
		break;
	case GANV_DIRECTION_DOWN:
		*x  = px + ganv_box_get_width(&port->box) / 2.0;
		*y  = py - (border_width / 2.0);
		*dx = 0.0;
		*dy = -1.0;
		break;
	}

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

static void
ganv_port_place_labels(GanvPort* port)
{
	GanvCanvas*      canvas   = ganv_item_get_canvas(GANV_ITEM(port));
	GanvPortPrivate* impl     = port->impl;
	GanvText*        label    = GANV_NODE(port)->impl->label;
	const double     port_w   = ganv_box_get_width(&port->box);
	const double     port_h   = ganv_box_get_height(&port->box);
	double           vlabel_w = 0.0;
	if (impl->value_label) {
		const double vlabel_h = impl->value_label->impl->coords.height;
		vlabel_w = impl->value_label->impl->coords.width;
		if (ganv_canvas_get_direction(canvas) == GANV_DIRECTION_RIGHT) {
			ganv_item_set(GANV_ITEM(impl->value_label),
			              "x", PORT_LABEL_HPAD,
			              "y", (port_h - vlabel_h) / 2.0 - PORT_LABEL_VPAD,
			              NULL);
		} else {
			ganv_item_set(GANV_ITEM(impl->value_label),
			              "x", (port_w - vlabel_w) / 2.0,
			              "y", (port_h - vlabel_h) / 2.0 - PORT_LABEL_VPAD,
			              NULL);
		}
		vlabel_w += PORT_LABEL_HPAD;
	}
	if (label) {
		const double label_h = label->impl->coords.height;
		if (ganv_canvas_get_direction(canvas) == GANV_DIRECTION_RIGHT) {
			ganv_item_set(GANV_ITEM(label),
			              "x", vlabel_w + PORT_LABEL_HPAD,
			              "y", (port_h - label_h) / 2.0 - PORT_LABEL_VPAD,
			              NULL);
		}
	}
}

static void
ganv_port_resize(GanvNode* self)
{
	GanvPort* port   = GANV_PORT(self);
	GanvNode* node   = GANV_NODE(self);
	GanvText* label  = node->impl->label;
	GanvText* vlabel = port->impl->value_label;

	double label_w  = 0.0;
	double label_h  = 0.0;
	double vlabel_w = 0.0;
	double vlabel_h = 0.0;
	if (label && (GANV_ITEM(label)->object.flags & GANV_ITEM_VISIBLE)) {
		g_object_get(label, "width", &label_w, "height", &label_h, NULL);
	}
	if (vlabel && (GANV_ITEM(vlabel)->object.flags & GANV_ITEM_VISIBLE)) {
		g_object_get(vlabel, "width", &vlabel_w, "height", &vlabel_h, NULL);
	}

	if (label || vlabel) {
		double labels_w = label_w + PORT_LABEL_HPAD * 2.0;
		if (vlabel_w != 0.0) {
			labels_w += vlabel_w + PORT_LABEL_HPAD;
		}
		ganv_box_set_width(&port->box, labels_w);
		ganv_box_set_height(&port->box,
		                    MAX(label_h, vlabel_h) + (PORT_LABEL_VPAD * 2.0));

		ganv_port_place_labels(port);
	}

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

static void
ganv_port_redraw_text(GanvNode* node)
{
	GanvPort* port = GANV_PORT(node);
	if (port->impl->value_label) {
		ganv_text_layout(port->impl->value_label);
	}
	if (GANV_NODE_CLASS(parent_class)->redraw_text) {
		(*GANV_NODE_CLASS(parent_class)->redraw_text)(node);
	}
	ganv_port_place_labels(port);
}

static void
ganv_port_set_width(GanvBox* box,
                    double   width)
{
	GanvPort* port = GANV_PORT(box);
	parent_class->set_width(box, width);
	if (port->impl->control) {
		ganv_port_update_control_slider(port, port->impl->control->value, TRUE);
	}
	ganv_port_place_labels(port);
}

static void
ganv_port_set_height(GanvBox* box,
                     double   height)
{
	GanvPort* port = GANV_PORT(box);
	parent_class->set_height(box, height);
	if (port->impl->control) {
		ganv_item_set(GANV_ITEM(port->impl->control->rect),
		              "y1", box->impl->coords.border_width / 2.0,
		              "y2", height - box->impl->coords.border_width / 2.0,
		              NULL);
	}
	ganv_port_place_labels(port);
}

static gboolean
ganv_port_event(GanvItem* item, GdkEvent* event)
{
	GanvCanvas* canvas = ganv_item_get_canvas(item);

	return ganv_canvas_port_event(canvas, GANV_PORT(item), event);
}

static void
ganv_port_class_init(GanvPortClass* klass)
{
	GObjectClass*   gobject_class = (GObjectClass*)klass;
	GtkObjectClass* object_class  = (GtkObjectClass*)klass;
	GanvItemClass*  item_class    = (GanvItemClass*)klass;
	GanvNodeClass*  node_class    = (GanvNodeClass*)klass;
	GanvBoxClass*   box_class     = (GanvBoxClass*)klass;

	parent_class = GANV_BOX_CLASS(g_type_class_peek_parent(klass));

	gobject_class->set_property = ganv_port_set_property;
	gobject_class->get_property = ganv_port_get_property;

	g_object_class_install_property(
		gobject_class, PROP_IS_INPUT, g_param_spec_boolean(
			"is-input",
			_("Is input"),
			_("Whether this port is an input, rather than an output."),
			0,
			G_PARAM_READWRITE));

	g_object_class_install_property(
		gobject_class, PROP_IS_CONTROLLABLE, g_param_spec_boolean(
			"is-controllable",
			_("Is controllable"),
			_("Whether this port can be controlled by the user."),
			0,
			G_PARAM_READWRITE));

	port_signals[PORT_VALUE_CHANGED]
	    = g_signal_new("value-changed",
	                   G_TYPE_FROM_CLASS(klass),
	                   G_SIGNAL_RUN_LAST,
	                   0,
	                   NULL, NULL,
	                   NULL,
	                   G_TYPE_NONE, 1,
	                   G_TYPE_DOUBLE);

	object_class->destroy = ganv_port_destroy;

	item_class->update = ganv_port_update;
	item_class->event  = ganv_port_event;
	item_class->draw   = ganv_port_draw;

	node_class->tail_vector = ganv_port_tail_vector;
	node_class->head_vector = ganv_port_head_vector;
	node_class->resize      = ganv_port_resize;
	node_class->redraw_text = ganv_port_redraw_text;

	box_class->set_width  = ganv_port_set_width;
	box_class->set_height = ganv_port_set_height;
}

GanvPort*
ganv_port_new(GanvModule* module,
              gboolean    is_input,
              const char* first_prop_name, ...)
{
	GanvPort* port = GANV_PORT(g_object_new(ganv_port_get_type(), NULL));

	port->impl->is_input = is_input;

	GanvItem* item = GANV_ITEM(port);
	va_list args;
	va_start(args, first_prop_name);
	ganv_item_construct(item,
	                    GANV_ITEM(module),
	                    first_prop_name, args);
	va_end(args);

	GanvBox* box = GANV_BOX(port);
	box->impl->coords.border_width = 1.0;

	GanvNode* node = GANV_NODE(port);
	node->impl->can_tail     = !is_input;
	node->impl->can_head     = is_input;
	node->impl->draggable    = FALSE;
	node->impl->border_width = 2.0;

	GanvCanvas* canvas = ganv_item_get_canvas(GANV_ITEM(port));
	ganv_port_set_direction(port, ganv_canvas_get_direction(canvas));

	return port;
}

void
ganv_port_set_direction(GanvPort*     port,
                        GanvDirection direction)
{
	GanvNode* node     = GANV_NODE(port);
	GanvBox*  box      = GANV_BOX(port);
	gboolean  is_input = port->impl->is_input;
	switch (direction) {
	case GANV_DIRECTION_RIGHT:
		box->impl->radius_tl = (is_input ? 0.0 : 5.0);
		box->impl->radius_tr = (is_input ? 5.0 : 0.0);
		box->impl->radius_br = (is_input ? 5.0 : 0.0);
		box->impl->radius_bl = (is_input ? 0.0 : 5.0);
		break;
	case GANV_DIRECTION_DOWN:
		box->impl->radius_tl = (is_input ? 0.0 : 5.0);
		box->impl->radius_tr = (is_input ? 0.0 : 5.0);
		box->impl->radius_br = (is_input ? 5.0 : 0.0);
		box->impl->radius_bl = (is_input ? 5.0 : 0.0);
		break;
	}

	node->impl->must_resize = TRUE;
	ganv_item_request_update(GANV_ITEM(node));
}

void
ganv_port_show_control(GanvPort* port)
{
	if (port->impl->control) {
		return;
	}

	const guint  color        = 0xFFFFFF66;
	const double border_width = GANV_NODE(port)->impl->border_width;

	GanvPortControl* control = (GanvPortControl*)malloc(sizeof(GanvPortControl));
	port->impl->control = control;

	control->value      = 0.0f;
	control->min        = 0.0f;
	control->max        = 1.0f;
	control->is_toggle  = FALSE;
	control->is_integer = FALSE;
	control->rect       = GANV_BOX(
		ganv_item_new(GANV_ITEM(port),
		              ganv_box_get_type(),
		              "x1", border_width / 2.0,
		              "y1", border_width / 2.0,
		              "x2", 0.0,
		              "y2", ganv_box_get_height(&port->box) - border_width / 2.0,
		              "fill-color", color,
		              "border-color", color,
		              "border-width", 0.0,
		              "managed", TRUE,
		              NULL));
	ganv_item_show(GANV_ITEM(control->rect));
}

void
ganv_port_hide_control(GanvPort* port)
{
	gtk_object_destroy(GTK_OBJECT(port->impl->control->rect));
	free(port->impl->control);
	port->impl->control = NULL;
}

void
ganv_port_set_value_label(GanvPort*   port,
                          const char* str)
{
	GanvPortPrivate* impl = port->impl;

	if (!str || str[0] == '\0') {
		if (impl->value_label) {
			gtk_object_destroy(GTK_OBJECT(impl->value_label));
			impl->value_label = NULL;
		}
	} else if (impl->value_label) {
		ganv_item_set(GANV_ITEM(impl->value_label),
		              "text", str,
		              NULL);
	} else {
		impl->value_label = GANV_TEXT(ganv_item_new(GANV_ITEM(port),
		                                            ganv_text_get_type(),
		                                            "text", str,
		                                            "color", DIM_TEXT_COLOR,
		                                            "managed", TRUE,
		                                            NULL));
	}
}

static void
ganv_port_update_control_slider(GanvPort* port, float value, gboolean force)
{
	GanvPortPrivate* impl = port->impl;
	if (!impl->control) {
		return;
	}

	// Clamp to toggle or integer value if applicable
	if (impl->control->is_toggle) {
		if (value != 0.0f) {
			value = impl->control->max;
		} else {
			value = impl->control->min;
		}
	} else if (impl->control->is_integer) {
		value = lrintf(value);
	}

	// Clamp to range
	if (value < impl->control->min) {
		value = impl->control->min;
	}
	if (value > impl->control->max) {
		value = impl->control->max;
	}

	if (!force && value == impl->control->value) {
		return;  // No change, do nothing
	}

	const double span = (ganv_box_get_width(&port->box) -
	                     GANV_NODE(port)->impl->border_width);

	const double w = (value - impl->control->min)
		/ (impl->control->max - impl->control->min)
		* span;

	if (isnan(w)) {
		return;  // Shouldn't happen, but ignore crazy values
	}

	// Redraw port
	impl->control->value = value;
	ganv_box_set_width(impl->control->rect, MAX(0.0, w));
	ganv_box_request_redraw(
		GANV_ITEM(port), &GANV_BOX(port)->impl->coords, FALSE);
}

void
ganv_port_set_control_is_toggle(GanvPort* port,
                                gboolean  is_toggle)
{
	if (port->impl->control) {
		port->impl->control->is_toggle = is_toggle;
		ganv_port_update_control_slider(port, port->impl->control->value, TRUE);
	}
}

void
ganv_port_set_control_is_integer(GanvPort* port,
                                 gboolean  is_integer)
{
	if (port->impl->control) {
		port->impl->control->is_integer = is_integer;
		const float rounded = rintf(port->impl->control->value);
		ganv_port_update_control_slider(port, rounded, TRUE);
	}
}

void
ganv_port_set_control_value(GanvPort* port,
                            float     value)
{
	ganv_port_update_control_slider(port, value, FALSE);
}

void
ganv_port_set_control_value_internal(GanvPort* port,
                                     float     value)
{
	// Update slider
	ganv_port_set_control_value(port, value);

	// Fire signal to notify user value has changed
	const double dvalue = port->impl->control->value;
	g_signal_emit(port, port_signals[PORT_VALUE_CHANGED], 0, dvalue, NULL);
}

void
ganv_port_set_control_min(GanvPort* port,
                          float     min)
{
	if (port->impl->control) {
		const gboolean force = port->impl->control->min != min;
		port->impl->control->min = min;
		if (port->impl->control->max < min) {
			port->impl->control->max = min;
		}
		ganv_port_update_control_slider(port, port->impl->control->value, force);
	}
}

void
ganv_port_set_control_max(GanvPort* port,
                          float     max)
{
	if (port->impl->control) {
		const gboolean force = port->impl->control->max != max;
		port->impl->control->max = max;
		if (port->impl->control->min > max) {
			port->impl->control->min = max;
		}
		ganv_port_update_control_slider(port, port->impl->control->value, force);
	}
}

double
ganv_port_get_natural_width(const GanvPort* port)
{
	GanvCanvas* const canvas = ganv_item_get_canvas(GANV_ITEM(port));
	GanvText* const   label  = port->box.node.impl->label;
	double            w      = 0.0;
	if (ganv_canvas_get_direction(canvas) == GANV_DIRECTION_DOWN) {
		w = ganv_module_get_empty_port_breadth(ganv_port_get_module(port));
	} else if (label && (GANV_ITEM(label)->object.flags & GANV_ITEM_VISIBLE)) {
		double label_w = 0.0;
		g_object_get(port->box.node.impl->label, "width", &label_w, NULL);
		w = label_w + (PORT_LABEL_HPAD * 2.0);
	} else {
		w = ganv_module_get_empty_port_depth(ganv_port_get_module(port));
	}
	if (port->impl->value_label &&
	    (GANV_ITEM(port->impl->value_label)->object.flags
	     & GANV_ITEM_VISIBLE)) {
		double label_w = 0.0;
		g_object_get(port->impl->value_label, "width", &label_w, NULL);
		w += label_w + PORT_LABEL_HPAD;
	}
	return w;
}

GanvModule*
ganv_port_get_module(const GanvPort* port)
{
	return GANV_MODULE(GANV_ITEM(port)->impl->parent);
}

float
ganv_port_get_control_value(const GanvPort* port)
{
	return port->impl->control ? port->impl->control->value : 0.0f;
}

float
ganv_port_get_control_min(const GanvPort* port)
{
	return port->impl->control ? port->impl->control->min : 0.0f;
}

float
ganv_port_get_control_max(const GanvPort* port)
{
	return port->impl->control ? port->impl->control->max : 0.0f;
}

gboolean
ganv_port_is_input(const GanvPort* port)
{
	return port->impl->is_input;
}

gboolean
ganv_port_is_output(const GanvPort* port)
{
	return !port->impl->is_input;
}
