/* Month view display for gncal
 *
 * Copyright (C) 1998 Red Hat Software, Inc.
 *
 * Author: Federico Mena <federico@nuclecu.unam.mx>
 */

#include <config.h>
#include <libgnomeui/gnome-canvas-text.h>
#include "layout.h"
#include "month-view.h"
#include "main.h"
#include "mark.h"
#include "timeutil.h"


#define SPACING 4		/* Spacing between title and calendar */


/* This is a child in the month view.  Each child has a number of canvas items associated to it --
 * it can be more than one box because an event may span several weeks.
 */
struct child {
	iCalObject *ico;	/* The calendar object this child refers to */
	time_t start, end;	/* Start and end times for the instance of the event */
	int slot_start;		/* The first slot this child uses */
	int slots_used;		/* The number of slots occupied by this child */
	GList *segments;	/* The list of segments needed to display this child */
};

/* Each child is composed of one or more segments.  Each segment can be considered to be
 * the entire child clipped to a particular week, as events may span several weeks in the
 * month view.
 */
struct segment {
	time_t start, end;	/* Start/end times for this segment */
	GnomeCanvasItem *item;	/* Canvas item used to display this segment */
};


static void month_view_class_init    (MonthViewClass *class);
static void month_view_init          (MonthView      *mv);
static void month_view_size_request  (GtkWidget      *widget,
				      GtkRequisition *requisition);
static void month_view_size_allocate (GtkWidget      *widget,
				      GtkAllocation  *allocation);


static GnomeCanvasClass *parent_class;


GtkType
month_view_get_type (void)
{
	static GtkType month_view_type = 0;

	if (!month_view_type) {
		GtkTypeInfo month_view_info = {
			"MonthView",
			sizeof (MonthView),
			sizeof (MonthViewClass),
			(GtkClassInitFunc) month_view_class_init,
			(GtkObjectInitFunc) month_view_init,
			NULL, /* reserved_1 */
			NULL, /* reserved_2 */
			(GtkClassInitFunc) NULL
		};

		month_view_type = gtk_type_unique (gnome_canvas_get_type (), &month_view_info);
	}

	return month_view_type;
}

static void
month_view_class_init (MonthViewClass *class)
{
	GtkWidgetClass *widget_class;

	widget_class = (GtkWidgetClass *) class;

	parent_class = gtk_type_class (gnome_canvas_get_type ());

	widget_class->size_request = month_view_size_request;
	widget_class->size_allocate = month_view_size_allocate;
}

static void
month_view_init (MonthView *mv)
{
	/* Title */

	mv->title = gnome_canvas_item_new (gnome_canvas_root (GNOME_CANVAS (mv)),
					   gnome_canvas_text_get_type (),
					   "anchor", GTK_ANCHOR_N,
					   "font", HEADING_FONT,
					   "fill_color", "black",
					   NULL);

	/* Month item */

	mv->mitem = gnome_month_item_new (gnome_canvas_root (GNOME_CANVAS (mv)));
	gnome_canvas_item_set (mv->mitem,
			       "x", 0.0,
			       "anchor", GTK_ANCHOR_NW,
			       "day_anchor", GTK_ANCHOR_NE,
			       "start_on_monday", week_starts_on_monday,
			       "heading_padding", 2.0,
			       "heading_font", BIG_DAY_HEADING_FONT,
			       "day_font", BIG_NORMAL_DAY_FONT,
			       NULL);

	mv->old_current_index = -1;
}

GtkWidget *
month_view_new (GnomeCalendar *calendar, time_t month)
{
	MonthView *mv;

	g_return_val_if_fail (calendar != NULL, NULL);
	g_return_val_if_fail (GNOME_IS_CALENDAR (calendar), NULL);

	mv = gtk_type_new (month_view_get_type ());
	mv->calendar = calendar;

	month_view_colors_changed (mv);
	month_view_set (mv, month);
	return GTK_WIDGET (mv);
}

static void
month_view_size_request (GtkWidget *widget, GtkRequisition *requisition)
{
	g_return_if_fail (widget != NULL);
	g_return_if_fail (IS_MONTH_VIEW (widget));
	g_return_if_fail (requisition != NULL);

	if (GTK_WIDGET_CLASS (parent_class)->size_request)
		(* GTK_WIDGET_CLASS (parent_class)->size_request) (widget, requisition);

	requisition->width = 200;
	requisition->height = 150;
}

/* Adjusts a single segment from a child */
static void
adjust_segment (MonthView *mv, struct child *child, struct segment *seg)
{
	struct tm start, end;
	GnomeCanvasItem *start_item, *end_item;
	GnomeCanvasItem *item;
	int start_day_index, end_day_index;
	double ix1, iy1, ix2, iy2;
	double ly2;
	double width, height;
	time_t day_begin, day_end;
	double start_factor, end_factor;
	double slot_height;

	/* Find the days that the segment intersects and get their bounds */

	start = *localtime (&seg->start);
	end = *localtime (&seg->end);

	start_day_index = gnome_month_item_day2index (GNOME_MONTH_ITEM (mv->mitem), start.tm_mday);
	g_assert (start_day_index != -1);

	end_day_index = gnome_month_item_day2index (GNOME_MONTH_ITEM (mv->mitem), end.tm_mday);
	g_assert (end_day_index != -1);

	start_item = gnome_month_item_num2child (GNOME_MONTH_ITEM (mv->mitem),
						 start_day_index + GNOME_MONTH_ITEM_DAY_GROUP);
	end_item = gnome_month_item_num2child (GNOME_MONTH_ITEM (mv->mitem),
					       end_day_index + GNOME_MONTH_ITEM_DAY_GROUP);

	gnome_canvas_item_get_bounds (start_item, &ix1, &iy1, NULL, &iy2);
	gnome_canvas_item_get_bounds (end_item, NULL, NULL, &ix2, NULL);

	/* Get the lower edge of the day label */

	item = gnome_month_item_num2child (GNOME_MONTH_ITEM (mv->mitem), start_day_index + GNOME_MONTH_ITEM_DAY_LABEL);
	gnome_canvas_item_get_bounds (item, NULL, NULL, NULL, &ly2);

	/* Calculate usable space */

	iy1 += ly2;
	width = ix2 - ix1;
	height = iy2 - iy1;

	/* Set the segment's item coordinates */

	day_begin = time_day_begin (seg->start);
	day_end = time_day_end (seg->end);

	start_factor = (double) (seg->start - day_begin) / (day_end - day_begin);
	end_factor = (double) (seg->end - day_begin) / (day_end - day_begin);

	slot_height = height / mv->num_slots;

	gnome_canvas_item_set (seg->item,
			       "x1", ix1 + width * start_factor,
			       "y1", iy1 + slot_height * child->slot_start,
			       "x2", ix1 + width * end_factor,
			       "y2", iy1 + slot_height * (child->slot_start + child->slots_used),
			       NULL);
}

/* Adjusts the child events of the month view to the appropriate size and position */
static void
adjust_children (MonthView *mv)
{
	GList *children;
	struct child *child;
	GList *segments;
	struct segment *seg;

	for (children = mv->children; children; children = children->next) {
		child = children->data;

		for (segments = child->segments; segments; segments = segments->next) {
			seg = segments->data;

			adjust_segment (mv, child, seg);
		}
	}
}

static void
month_view_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
{
	MonthView *mv;
	GdkFont *font;
	GtkArg arg;
	int y;

	g_return_if_fail (widget != NULL);
	g_return_if_fail (IS_MONTH_VIEW (widget));
	g_return_if_fail (allocation != NULL);

	mv = MONTH_VIEW (widget);

	if (GTK_WIDGET_CLASS (parent_class)->size_allocate)
		(* GTK_WIDGET_CLASS (parent_class)->size_allocate) (widget, allocation);

	gnome_canvas_set_scroll_region (GNOME_CANVAS (mv), 0, 0, allocation->width, allocation->height);

	/* Adjust items to new size */

	arg.name = "font_gdk";
	gtk_object_getv (GTK_OBJECT (mv->title), 1, &arg);
	font = GTK_VALUE_BOXED (arg);

	gnome_canvas_item_set (mv->title,
			       "x", (double) allocation->width / 2.0,
			       "y", (double) SPACING,
			       NULL);

	y = font->ascent + font->descent + 2 * SPACING;
	gnome_canvas_item_set (mv->mitem,
			       "y", (double) y,
			       "width", (double) (allocation->width - 1),
			       "height", (double) (allocation->height - y - 1),
			       NULL);

	/* Adjust children */

	adjust_children (mv);
}

/* Destroys a child structure and its associated canvas items */
static void
child_destroy (MonthView *mv, struct child *child)
{
	GList *list;
	struct segment *seg;

	/* Destroy the segments */

	for (list = child->segments; list; list = list->next) {
		seg = list->data;

		gtk_object_destroy (GTK_OBJECT (seg->item));
		g_free (seg);
	}

	g_list_free (child->segments);

	/* Destroy the child */

	g_free (child);
}

/* Creates the list of segments that are used to display a child.  Each child may have several
 * segments, because it may span several weeks in the month view.  This function only creates the
 * segment structures and the associated canvas items, but it does not set their final position in
 * the month view canvas -- that is done by the adjust_children() function.
 */
static void
child_create_segments (MonthView *mv, struct child *child)
{
	time_t t;
	time_t month_begin, month_end;
	time_t week_begin, week_end;
	time_t left, right;
	struct segment *seg;

	/* Get the month's extents */

	t = time_from_day (mv->year, mv->month, 1);
	month_begin = time_month_begin (t);
	month_end = time_month_end (t);

	/* Get the first week of the event */

	t = MAX (child->start, month_begin);
	week_begin = time_week_begin (t);

	if (week_starts_on_monday)
		time_add_day (week_begin, 1);

	week_end = time_add_week (week_begin, 1);

	/* Loop until the event ends or the month ends -- the segments list is created in reverse
	 * order.
	 */

	do {
		seg = g_new (struct segment, 1);

		/* Clip the child to this week */

		left = MAX (week_begin, month_begin);
		right = MIN (week_end, month_end);

		seg->start = MAX (child->start, left);
		seg->end = MIN (child->end, right);

		seg->item = gnome_canvas_item_new (GNOME_CANVAS_GROUP (mv->mitem),
						   gnome_canvas_rect_get_type (),
						   "fill_color", color_spec_from_prop (COLOR_PROP_MARK_DAY_BG),
						   "outline_color", "black",
						   "width_pixels", 0,
						   NULL);

		child->segments = g_list_prepend (child->segments, seg);

		/* Next week */

		week_begin = time_add_week (week_begin, 1);
		week_end = time_add_week (week_end, 1);
	} while ((child->end > week_begin) && (week_begin < month_end));

	/* Reverse the list to put it in increasing order */

	child->segments = g_list_reverse (child->segments);
}

/* Comparison function used to create the sorted list of children.  Sorts first by increasing start
 * time and then by decreasing end time, so that "longer" events are first in the list.
 */
static gint
child_compare (gconstpointer a, gconstpointer b)
{
	const struct child *ca, *cb;
	time_t diff;

	ca = a;
	cb = b;

	diff = ca->start - cb->start;

	if (diff == 0)
		diff = cb->end - ca->end;

	return (diff < 0) ? -1 : ((diff > 0) ? 1 : 0);
}

/* This is the callback function used from the calendar iterator.  It adds events to the list of
 * children in the month view.
 */
static int
add_event (iCalObject *ico, time_t start, time_t end, void *data)
{
	MonthView *mv;
	struct child *child;

	mv = MONTH_VIEW (data);

	child = g_new (struct child, 1);
	child->ico = ico;
	child->start = start;
	child->end = end;
	child->segments = NULL;

	child_create_segments (mv, child);

	/* Add it to the list of children */

	mv->children = g_list_insert_sorted (mv->children, child, child_compare);

	return TRUE; /* means "we are not yet finished" */
}

/* Time query function for the layout engine */
static void
child_query_func (GList *list, time_t *start, time_t *end)
{
	struct child *child;

	child = list->data;

	*start = child->start;
	*end = child->end;
}

/* Uses the generic event layout engine to set the children's layout information */
static void
layout_children (MonthView *mv)
{
	GList *list;
	struct child *child;
	int *allocations;
	int *slots;
	int i;

	layout_events (mv->children, child_query_func, &mv->num_slots, &allocations, &slots);

	if (mv->num_slots == 0)
		return;

	for (list = mv->children, i = 0; list; list = list->next, i++) {
		child = list->data;
		child->slot_start = allocations[i];
		child->slots_used = slots[i];
	}

	g_free (allocations);
	g_free (slots);
}

void
month_view_update (MonthView *mv, iCalObject *object, int flags)
{
	GList *list;
	time_t t;
	time_t month_begin, month_end;

	g_return_if_fail (mv != NULL);
	g_return_if_fail (IS_MONTH_VIEW (mv));

	/* Destroy the old list of children */

	for (list = mv->children; list; list = list->next)
		child_destroy (mv, list->data);

	g_list_free (mv->children);
	mv->children = NULL;

	/* Create a new list of children and lay them out */

	t = time_from_day (mv->year, mv->month, 1);
	month_begin = time_month_begin (t);
	month_end = time_month_end (t);

	calendar_iterate (mv->calendar->cal, month_begin, month_end, add_event, mv);
	layout_children (mv);
	adjust_children (mv);
}

/* Unmarks the old day that was marked as current and marks the current day if appropriate */
static void
mark_current_day (MonthView *mv)
{
	time_t t;
	struct tm *tm;
	GnomeCanvasItem *item;

	/* Unmark the old day */

	if (mv->old_current_index != -1) {
		item = gnome_month_item_num2child (GNOME_MONTH_ITEM (mv->mitem),
						   GNOME_MONTH_ITEM_DAY_LABEL + mv->old_current_index);
		gnome_canvas_item_set (item,
				       "fill_color", color_spec_from_prop (COLOR_PROP_DAY_FG),
				       "font", BIG_NORMAL_DAY_FONT,
				       NULL);

		mv->old_current_index = -1;
	}

	/* Mark the new day */

	t = time (NULL);
	tm = localtime (&t);

	if (((tm->tm_year + 1900) == mv->year) && (tm->tm_mon == mv->month)) {
		mv->old_current_index = gnome_month_item_day2index (GNOME_MONTH_ITEM (mv->mitem), tm->tm_mday);
		g_assert (mv->old_current_index != -1);

		item = gnome_month_item_num2child (GNOME_MONTH_ITEM (mv->mitem),
						   GNOME_MONTH_ITEM_DAY_LABEL + mv->old_current_index);
		gnome_canvas_item_set (item,
				       "fill_color", color_spec_from_prop (COLOR_PROP_CURRENT_DAY_FG),
				       "font", BIG_CURRENT_DAY_FONT,
				       NULL);
	}
}

void
month_view_set (MonthView *mv, time_t month)
{
	struct tm *tm;
	char buf[100];

	g_return_if_fail (mv != NULL);
	g_return_if_fail (IS_MONTH_VIEW (mv));

	/* Title */

	tm = localtime (&month);

	mv->year = tm->tm_year + 1900;
	mv->month = tm->tm_mon;
	
	strftime (buf, 100, "%B %Y", tm);

	gnome_canvas_item_set (mv->title,
			       "text", buf,
			       NULL);

	/* Month item */

	gnome_canvas_item_set (mv->mitem,
			       "year", mv->year,
			       "month", mv->month,
			       NULL);

	/* Update events */

	month_view_update (mv, NULL, 0);
	mark_current_day (mv);
}

void
month_view_time_format_changed (MonthView *mv)
{
	g_return_if_fail (mv != NULL);
	g_return_if_fail (IS_MONTH_VIEW (mv));

	gnome_canvas_item_set (mv->mitem,
			       "start_on_monday", week_starts_on_monday,
			       NULL);

	month_view_set (mv, time_month_begin (time_from_day (mv->year, mv->month, 1)));
}

void
month_view_colors_changed (MonthView *mv)
{
	g_return_if_fail (mv != NULL);
	g_return_if_fail (IS_MONTH_VIEW (mv));

	colorify_month_item (GNOME_MONTH_ITEM (mv->mitem), default_color_func, NULL);
	mark_current_day (mv);

	/* FIXME: set children to the marked color */
}