/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */

/* Nautilus - Canvas item for floating selection.
 *
 * Copyright (C) 1997, 1998, 1999, 2000 Free Software Foundation
 * Copyright (C) 2011 Red Hat Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * Authors: Federico Mena <federico@nuclecu.unam.mx>
 *          Cosimo Cecchi <cosimoc@redhat.com>
 */

#include <config.h>

#include "nautilus-selection-canvas-item.h"

#include <math.h>

enum {
	PROP_X1 = 1,
	PROP_Y1,
	PROP_X2,
	PROP_Y2,
	PROP_FILL_COLOR_RGBA,
	PROP_OUTLINE_COLOR_RGBA,
	PROP_OUTLINE_STIPPLING,
	PROP_WIDTH_PIXELS,
	NUM_PROPERTIES
};

static GParamSpec *properties[NUM_PROPERTIES] = { NULL };

typedef struct {
  /*< public >*/
  int x0, y0, x1, y1;
}  Rect;

struct _NautilusSelectionCanvasItemDetails {
	Rect last_update_rect;
	Rect last_outline_update_rect;
	int last_outline_update_width;

	double x1, y1, x2, y2;		/* Corners of item */
	double width;			/* Outline width */

        GdkRGBA fill_color;
        GdkRGBA outline_color;

	gboolean outline_stippling;

	/* Configuration flags */

	unsigned int fill_set : 1;	/* Is fill color set? */
	unsigned int outline_set : 1;	/* Is outline color set? */

	double fade_out_fill_alpha;
	double fade_out_outline_alpha;

	double fade_out_fill_delta;
	double fade_out_outline_delta;	

	guint fade_out_handler_id;
};

G_DEFINE_TYPE (NautilusSelectionCanvasItem, nautilus_selection_canvas_item, EEL_TYPE_CANVAS_ITEM);

#define DASH_ON 0.8
#define DASH_OFF 1.7
static void
nautilus_selection_canvas_item_draw (EelCanvasItem *item,
				     cairo_t *cr,
				     cairo_region_t *region)
{
	NautilusSelectionCanvasItem *self;
	double x1, y1, x2, y2;
	int cx1, cy1, cx2, cy2;
	double i2w_dx, i2w_dy;

	self = NAUTILUS_SELECTION_CANVAS_ITEM (item);

	/* Get canvas pixel coordinates */
	i2w_dx = 0.0;
	i2w_dy = 0.0;
	eel_canvas_item_i2w (item, &i2w_dx, &i2w_dy);
	
	x1 = self->priv->x1 + i2w_dx;
	y1 = self->priv->y1 + i2w_dy;
	x2 = self->priv->x2 + i2w_dx;
	y2 = self->priv->y2 + i2w_dy;

	eel_canvas_w2c (item->canvas, x1, y1, &cx1, &cy1);
	eel_canvas_w2c (item->canvas, x2, y2, &cx2, &cy2);
	
	if (cx2 <= cx1 || cy2 <= cy1 ) {
		return;
	}

	cairo_save (cr);

	if (self->priv->fill_set) {
		GdkRGBA actual_fill;

		actual_fill = self->priv->fill_color;

		if (self->priv->fade_out_handler_id != 0) {
			actual_fill.alpha = self->priv->fade_out_fill_alpha;
		}

		gdk_cairo_set_source_rgba (cr, &actual_fill);
		cairo_rectangle (cr,
				 cx1, cy1,
				 cx2 - cx1 + 1,
				 cy2 - cy1 + 1);
		cairo_fill (cr);
	}

	if (self->priv->outline_set) {
		GdkRGBA actual_outline;

		actual_outline = self->priv->outline_color;

		if (self->priv->fade_out_handler_id != 0) {
			actual_outline.alpha = self->priv->fade_out_outline_alpha;
		}

		gdk_cairo_set_source_rgba (cr, &actual_outline);
		cairo_set_line_width (cr, (int) self->priv->width);

		if (self->priv->outline_stippling) {
			double dash[2] = { DASH_ON, DASH_OFF };

			cairo_set_dash (cr, dash, G_N_ELEMENTS (dash), 0);
		}

		cairo_rectangle (cr,
				 cx1 + 0.5, cy1 + 0.5,
				 cx2 - cx1,
				 cy2 - cy1);
		cairo_stroke (cr);
	}

	cairo_restore (cr);
}

static double
nautilus_selection_canvas_item_point (EelCanvasItem *item,
				      double x,
				      double y,
				      int cx,
				      int cy,
				      EelCanvasItem **actual_item)
{
	NautilusSelectionCanvasItem *self;
	double x1, y1, x2, y2;
	double hwidth;
	double dx, dy;
	double tmp;

	self = NAUTILUS_SELECTION_CANVAS_ITEM (item);
	*actual_item = item;

	/* Find the bounds for the rectangle plus its outline width */

	x1 = self->priv->x1;
	y1 = self->priv->y1;
	x2 = self->priv->x2;
	y2 = self->priv->y2;

	if (self->priv->outline_set) {
		hwidth = (self->priv->width / item->canvas->pixels_per_unit) / 2.0;

		x1 -= hwidth;
		y1 -= hwidth;
		x2 += hwidth;
		y2 += hwidth;
	} else
		hwidth = 0.0;

	/* Is point inside rectangle (which can be hollow if it has no fill set)? */

	if ((x >= x1) && (y >= y1) && (x <= x2) && (y <= y2)) {
		if (self->priv->fill_set || !self->priv->outline_set)
			return 0.0;

		dx = x - x1;
		tmp = x2 - x;
		if (tmp < dx)
			dx = tmp;

		dy = y - y1;
		tmp = y2 - y;
		if (tmp < dy)
			dy = tmp;

		if (dy < dx)
			dx = dy;

		dx -= 2.0 * hwidth;

		if (dx < 0.0)
			return 0.0;
		else
			return dx;
	}

	/* Point is outside rectangle */

	if (x < x1)
		dx = x1 - x;
	else if (x > x2)
		dx = x - x2;
	else
		dx = 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
request_redraw_borders (EelCanvas *canvas,
			Rect      *update_rect,
			int        width)
{
	eel_canvas_request_redraw (canvas,
				   update_rect->x0, update_rect->y0,
				   update_rect->x1, update_rect->y0 + width);
	eel_canvas_request_redraw (canvas,
				   update_rect->x0, update_rect->y1-width,
				   update_rect->x1, update_rect->y1);
	eel_canvas_request_redraw (canvas,
				   update_rect->x0,       update_rect->y0,
				   update_rect->x0+width, update_rect->y1);
	eel_canvas_request_redraw (canvas,
				   update_rect->x1-width, update_rect->y0,
				   update_rect->x1,       update_rect->y1);
}

static Rect make_rect (int x0, int y0, int x1, int y1);

static int
rect_empty (const Rect *src) {
  return (src->x1 <= src->x0 || src->y1 <= src->y0);
}

static gboolean
rects_intersect (Rect r1, Rect r2)
{
	if (r1.x0 >= r2.x1) {
		return FALSE;
	}
	if (r2.x0 >= r1.x1) {
		return FALSE;
	}
	if (r1.y0 >= r2.y1) {
		return FALSE;
	}
	if (r2.y0 >= r1.y1) {
		return FALSE;
	}
	return TRUE;
}

static void
diff_rects_guts (Rect ra, Rect rb, int *count, Rect result[4])
{
	if (ra.x0 < rb.x0) {
		result[(*count)++] = make_rect (ra.x0, ra.y0, rb.x0, ra.y1);
	}
	if (ra.y0 < rb.y0) {
		result[(*count)++] = make_rect (ra.x0, ra.y0, ra.x1, rb.y0);
	}
	if (ra.x1 < rb.x1) {
		result[(*count)++] = make_rect (ra.x1, rb.y0, rb.x1, rb.y1);
	}
	if (ra.y1 < rb.y1) {
		result[(*count)++] = make_rect (rb.x0, ra.y1, rb.x1, rb.y1);
	}
}

static void
diff_rects (Rect r1, Rect r2, int *count, Rect result[4])
{
	g_assert (count != NULL);
	g_assert (result != NULL);

	*count = 0;

	if (rects_intersect (r1, r2)) {
		diff_rects_guts (r1, r2, count, result);
		diff_rects_guts (r2, r1, count, result);
	} else {
		if (!rect_empty (&r1)) {
			result[(*count)++] = r1;
		}
		if (!rect_empty (&r2)) {
			result[(*count)++] = r2;
		}
	}
}

static Rect
make_rect (int x0, int y0, int x1, int y1)
{
	Rect r;

	r.x0 = x0;
	r.y0 = y0;
	r.x1 = x1;
	r.y1 = y1;
	return r;
}

static void
nautilus_selection_canvas_item_update (EelCanvasItem *item,
				       double i2w_dx,
				       double i2w_dy,
				       gint flags)
{
	NautilusSelectionCanvasItem *self;
	NautilusSelectionCanvasItemDetails *priv;
	double x1, y1, x2, y2;
	int cx1, cy1, cx2, cy2;
	int repaint_rects_count, i;
	int width_pixels;
	int width_lt, width_rb;
	Rect update_rect, repaint_rects[4];

	if (EEL_CANVAS_ITEM_CLASS (nautilus_selection_canvas_item_parent_class)->update)
		(* EEL_CANVAS_ITEM_CLASS (nautilus_selection_canvas_item_parent_class)->update) (item, i2w_dx, i2w_dy, flags);

	self = NAUTILUS_SELECTION_CANVAS_ITEM (item);
	priv = self->priv;

	x1 = priv->x1 + i2w_dx;
	y1 = priv->y1 + i2w_dy;
	x2 = priv->x2 + i2w_dx;
	y2 = priv->y2 + i2w_dy;

	eel_canvas_w2c (item->canvas, x1, y1, &cx1, &cy1);
	eel_canvas_w2c (item->canvas, x2, y2, &cx2, &cy2);

	update_rect = make_rect (cx1, cy1, cx2+1, cy2+1);
	diff_rects (update_rect, priv->last_update_rect,
		    &repaint_rects_count, repaint_rects);
	for (i = 0; i < repaint_rects_count; i++) {
		eel_canvas_request_redraw (item->canvas,
					   repaint_rects[i].x0, repaint_rects[i].y0,
					   repaint_rects[i].x1, repaint_rects[i].y1);
	}

	priv->last_update_rect = update_rect;

	if (priv->outline_set) {
		/* Outline and bounding box */
		width_pixels = (int) priv->width;
		width_lt = width_pixels / 2;
		width_rb = (width_pixels + 1) / 2;
		
		cx1 -= width_lt;
		cy1 -= width_lt;
		cx2 += width_rb;
		cy2 += width_rb;

		update_rect = make_rect (cx1, cy1, cx2, cy2);
		request_redraw_borders (item->canvas, &update_rect,
					(width_lt + width_rb));
		request_redraw_borders (item->canvas, &priv->last_outline_update_rect,
					priv->last_outline_update_width);
		priv->last_outline_update_rect = update_rect;
		priv->last_outline_update_width = width_lt + width_rb;
		
		item->x1 = cx1;
		item->y1 = cy1;
		item->x2 = cx2+1;
		item->y2 = cy2+1;
	} else {
		item->x1 = cx1;
		item->y1 = cy1;
		item->x2 = cx2+1;
		item->y2 = cy2+1;
	}
}

static void
nautilus_selection_canvas_item_translate (EelCanvasItem *item,
					  double dx,
					  double dy)
{
	NautilusSelectionCanvasItem *self;

	self = NAUTILUS_SELECTION_CANVAS_ITEM (item);

	self->priv->x1 += dx;
	self->priv->y1 += dy;
	self->priv->x2 += dx;
	self->priv->y2 += dy;
}

static void
nautilus_selection_canvas_item_bounds (EelCanvasItem *item,
				       double *x1,
				       double *y1,
				       double *x2,
				       double *y2)
{
	NautilusSelectionCanvasItem *self;
	double hwidth;

	self = NAUTILUS_SELECTION_CANVAS_ITEM (item);

	hwidth = (self->priv->width / item->canvas->pixels_per_unit) / 2.0;

	*x1 = self->priv->x1 - hwidth;
	*y1 = self->priv->y1 - hwidth;
	*x2 = self->priv->x2 + hwidth;
	*y2 = self->priv->y2 + hwidth;
}

#define FADE_OUT_STEPS 5
#define FADE_OUT_SPEED 30

static gboolean
fade_and_request_redraw (gpointer user_data)
{
	NautilusSelectionCanvasItem *self = user_data;

	if (self->priv->fade_out_fill_alpha <= 0 ||
	    self->priv->fade_out_outline_alpha <= 0) {
		self->priv->fade_out_handler_id = 0;
		eel_canvas_item_destroy (EEL_CANVAS_ITEM (self));

		return FALSE;
	}

	self->priv->fade_out_fill_alpha -= self->priv->fade_out_fill_delta;
	self->priv->fade_out_outline_alpha -= self->priv->fade_out_outline_delta;

	eel_canvas_item_request_redraw (EEL_CANVAS_ITEM (self));

	return TRUE;
}

void
nautilus_selection_canvas_item_fade_out (NautilusSelectionCanvasItem *self,
					 guint transition_time)
{
	self->priv->fade_out_fill_alpha = self->priv->fill_color.alpha;
	self->priv->fade_out_outline_alpha = self->priv->outline_color.alpha;

	self->priv->fade_out_fill_delta = self->priv->fade_out_fill_alpha / FADE_OUT_STEPS;
	self->priv->fade_out_outline_delta = self->priv->fade_out_outline_alpha / FADE_OUT_STEPS;

	self->priv->fade_out_handler_id =
		g_timeout_add ((guint) (transition_time / FADE_OUT_STEPS),
			       fade_and_request_redraw, self);
}

static void
nautilus_selection_canvas_item_dispose (GObject *obj)
{
	NautilusSelectionCanvasItem *self = NAUTILUS_SELECTION_CANVAS_ITEM (obj);

	if (self->priv->fade_out_handler_id != 0) {
		g_source_remove (self->priv->fade_out_handler_id);
		self->priv->fade_out_handler_id = 0;
	}

	G_OBJECT_CLASS (nautilus_selection_canvas_item_parent_class)->dispose (obj);
}

static void
do_set_fill (NautilusSelectionCanvasItem *self,
	     gboolean fill_set)
{
	if (self->priv->fill_set != fill_set) {
		self->priv->fill_set = fill_set;
		eel_canvas_item_request_update (EEL_CANVAS_ITEM (self));
	}
}

static void
do_set_outline (NautilusSelectionCanvasItem *self,
		gboolean outline_set)
{
	if (self->priv->outline_set != outline_set) {
		self->priv->outline_set = outline_set;
		eel_canvas_item_request_update (EEL_CANVAS_ITEM (self));
	}
}

static void
nautilus_selection_canvas_item_set_property (GObject *object,
					     guint param_id,
					     const GValue *value,
					     GParamSpec *pspec)
{
	EelCanvasItem *item;
	NautilusSelectionCanvasItem *self;

	self = NAUTILUS_SELECTION_CANVAS_ITEM (object);
	item = EEL_CANVAS_ITEM (object);

	switch (param_id) {
	case PROP_X1:
		self->priv->x1 = g_value_get_double (value);

		eel_canvas_item_request_update (item);
		break;

	case PROP_Y1:
		self->priv->y1 = g_value_get_double (value);

		eel_canvas_item_request_update (item);
		break;

	case PROP_X2:
		self->priv->x2 = g_value_get_double (value);

		eel_canvas_item_request_update (item);
		break;

	case PROP_Y2:
		self->priv->y2 = g_value_get_double (value);

		eel_canvas_item_request_update (item);
		break;

	case PROP_FILL_COLOR_RGBA: {
		GdkRGBA *color;

		color = g_value_get_boxed (value);

		do_set_fill (self, color != NULL);

		if (color != NULL) {
			self->priv->fill_color = *color;
		}

		eel_canvas_item_request_redraw (item);		
		break;
	}

	case PROP_OUTLINE_COLOR_RGBA: {
		GdkRGBA *color;

		color = g_value_get_boxed (value);

		do_set_outline (self, color != NULL);

		if (color != NULL) {
			self->priv->outline_color = *color;
		}

		eel_canvas_item_request_redraw (item);		
		break;
	}

	case PROP_OUTLINE_STIPPLING:
		self->priv->outline_stippling = g_value_get_boolean (value);

		eel_canvas_item_request_redraw (item);
		break;

	case PROP_WIDTH_PIXELS:
		self->priv->width = g_value_get_uint (value);

		eel_canvas_item_request_update (item);
		break;

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

static void
nautilus_selection_canvas_item_get_property (GObject *object,
					     guint param_id,
					     GValue *value,
					     GParamSpec *pspec)
{
	NautilusSelectionCanvasItem *self;

	self = NAUTILUS_SELECTION_CANVAS_ITEM (object);

	switch (param_id) {
	case PROP_X1:
		g_value_set_double (value,  self->priv->x1);
		break;

	case PROP_Y1:
		g_value_set_double (value,  self->priv->y1);
		break;

	case PROP_X2:
		g_value_set_double (value,  self->priv->x2);
		break;

	case PROP_Y2:
		g_value_set_double (value,  self->priv->y2);
		break;

	case PROP_FILL_COLOR_RGBA:
		g_value_set_boxed (value,  &self->priv->fill_color);
		break;

	case PROP_OUTLINE_COLOR_RGBA:
		g_value_set_boxed (value,  &self->priv->outline_color);
		break;

	case PROP_OUTLINE_STIPPLING:
		g_value_set_boolean (value, self->priv->outline_stippling);
		break;
	case PROP_WIDTH_PIXELS:
		g_value_set_uint (value, self->priv->width);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
		break;
	}
}

static void
nautilus_selection_canvas_item_class_init (NautilusSelectionCanvasItemClass *klass)
{
	EelCanvasItemClass *item_class;
	GObjectClass *gobject_class;

	gobject_class = G_OBJECT_CLASS (klass);
	item_class = EEL_CANVAS_ITEM_CLASS (klass);

	gobject_class->set_property = nautilus_selection_canvas_item_set_property;
	gobject_class->get_property = nautilus_selection_canvas_item_get_property;
	gobject_class->dispose = nautilus_selection_canvas_item_dispose;

	item_class->draw = nautilus_selection_canvas_item_draw;
	item_class->point = nautilus_selection_canvas_item_point;
	item_class->update = nautilus_selection_canvas_item_update;
	item_class->bounds = nautilus_selection_canvas_item_bounds;
	item_class->translate = nautilus_selection_canvas_item_translate;

	properties[PROP_X1] = 
                 g_param_spec_double ("x1", NULL, NULL,
				      -G_MAXDOUBLE, G_MAXDOUBLE, 0,
				      G_PARAM_READWRITE);
	properties[PROP_Y1] =
                 g_param_spec_double ("y1", NULL, NULL,
				      -G_MAXDOUBLE, G_MAXDOUBLE, 0,
				      G_PARAM_READWRITE);
	properties[PROP_X2] =
		g_param_spec_double ("x2", NULL, NULL,
				      -G_MAXDOUBLE, G_MAXDOUBLE, 0,
				      G_PARAM_READWRITE);
	properties[PROP_Y2] =
		g_param_spec_double ("y2", NULL, NULL,
				      -G_MAXDOUBLE, G_MAXDOUBLE, 0,
				      G_PARAM_READWRITE);
	properties[PROP_FILL_COLOR_RGBA] = 
                 g_param_spec_boxed ("fill-color-rgba", NULL, NULL,
				     GDK_TYPE_RGBA,
				     G_PARAM_READWRITE);
	properties[PROP_OUTLINE_COLOR_RGBA] =
		g_param_spec_boxed ("outline-color-rgba", NULL, NULL,
				     GDK_TYPE_RGBA,
				     G_PARAM_READWRITE);
	properties[PROP_OUTLINE_STIPPLING] =
		 g_param_spec_boolean ("outline-stippling", NULL, NULL,
				       FALSE, G_PARAM_READWRITE);
	properties[PROP_WIDTH_PIXELS] =
                 g_param_spec_uint ("width-pixels", NULL, NULL,
				    0, G_MAXUINT, 0,
				    G_PARAM_READWRITE);

	g_object_class_install_properties (gobject_class, NUM_PROPERTIES, properties);
	g_type_class_add_private (klass, sizeof (NautilusSelectionCanvasItemDetails));
}

static void
nautilus_selection_canvas_item_init (NautilusSelectionCanvasItem *self)
{
	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, NAUTILUS_TYPE_SELECTION_CANVAS_ITEM,
						  NautilusSelectionCanvasItemDetails);
}



