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

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


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

/* Padding between day borders and event text */
#define EVENT_PADDING 3


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;
}

/* Creates the quick view when a day is clicked in the month view */
static void
do_quick_view_popup (MonthView *mv, GdkEventButton *event, int day)
{
	time_t day_begin_time, day_end_time;
	GList *list;
	GtkWidget *qv;
	char date_str[256];

	day_begin_time = time_from_day (mv->year, mv->month, day);
	day_end_time = time_day_end (day_begin_time);

	list = calendar_get_events_in_range (mv->calendar->cal, day_begin_time, day_end_time);

	strftime (date_str, sizeof (date_str), _("%a %b %d %Y"), localtime (&day_begin_time));
	qv = quick_view_new (mv->calendar, date_str, list);

	quick_view_do_popup (QUICK_VIEW (qv), event);

	gtk_widget_destroy (qv);
	calendar_destroy_event_list (list);
}

/* Callback used to destroy the popup menu when the month view is destroyed */
static void
destroy_menu (GtkWidget *widget, gpointer data)
{
	gtk_widget_destroy (GTK_WIDGET (data));
}

/* Creates a new appointment in the current day */
static void
new_appointment (GtkWidget *widget, gpointer data)
{
	MonthView *mv;
	time_t *t;

	mv = MONTH_VIEW (data);
	t = gtk_object_get_data (GTK_OBJECT (widget), "time_data");

	event_editor_new_whole_day (mv->calendar, *t);
}

/* Convenience functions to jump to a view and set the time */
static void
do_jump (GtkWidget *widget, gpointer data, char *view_name)
{
	MonthView *mv;
	time_t *t;

	mv = MONTH_VIEW (data);

	/* Get the time data from the menu item */

	t = gtk_object_get_data (GTK_OBJECT (widget), "time_data");

	/* Set the view and time */

	gnome_calendar_set_view (mv->calendar, view_name);
	gnome_calendar_goto (mv->calendar, *t);
}

/* The following three callbacks set the view in the calendar and change the time */

static void
jump_to_day (GtkWidget *widget, gpointer data)
{
	do_jump (widget, data, "dayview");
}

static void
jump_to_week (GtkWidget *widget, gpointer data)
{
	do_jump (widget, data, "weekview");
}

static void
jump_to_year (GtkWidget *widget, gpointer data)
{
	do_jump (widget, data, "yearview");
}

static GnomeUIInfo mv_popup_menu[] = {
	GNOMEUIINFO_ITEM_STOCK (N_("_New appointment in this day..."), NULL, new_appointment, GNOME_STOCK_MENU_NEW),

	GNOMEUIINFO_SEPARATOR,

	GNOMEUIINFO_ITEM_STOCK (N_("Jump to this _day"), NULL, jump_to_day, GNOME_STOCK_MENU_JUMP_TO),
	GNOMEUIINFO_ITEM_STOCK (N_("Jump to this _week"), NULL, jump_to_week, GNOME_STOCK_MENU_JUMP_TO),
	GNOMEUIINFO_ITEM_STOCK (N_("Jump to this _year"), NULL, jump_to_year, GNOME_STOCK_MENU_JUMP_TO),
	GNOMEUIINFO_END
};

/* Creates the popup menu for the month view if it does not yet exist, and attaches it to the month
 * view object so that it can be destroyed when appropriate.
 */
static GtkWidget *
get_popup_menu (MonthView *mv)
{
	GtkWidget *menu;

	menu = gtk_object_get_data (GTK_OBJECT (mv), "popup_menu");
	
	if (!menu) {
		menu = gnome_popup_menu_new (mv_popup_menu);
		gtk_object_set_data (GTK_OBJECT (mv), "popup_menu", menu);
		gtk_signal_connect (GTK_OBJECT (mv), "destroy",
				    (GtkSignalFunc) destroy_menu,
				    menu);
	}

	return menu;
}

/* Pops up the menu for the month view. */
static void
do_popup_menu (MonthView *mv, GdkEventButton *event, int day)
{
	GtkWidget *menu;
	static time_t t;

	menu = get_popup_menu (mv);

	/* Enable or disable items as appropriate */

	gtk_widget_set_sensitive (mv_popup_menu[0].widget, day != 0);
	gtk_widget_set_sensitive (mv_popup_menu[2].widget, day != 0);
	gtk_widget_set_sensitive (mv_popup_menu[3].widget, day != 0);

	if (day == 0)
		day = 1;

	/* Store the time for the menu item callbacks to use */

	t = time_from_day (mv->year, mv->month, day);

	gtk_object_set_data (GTK_OBJECT (mv_popup_menu[0].widget), "time_data", &t);
	gtk_object_set_data (GTK_OBJECT (mv_popup_menu[2].widget), "time_data", &t);
	gtk_object_set_data (GTK_OBJECT (mv_popup_menu[3].widget), "time_data", &t);
	gtk_object_set_data (GTK_OBJECT (mv_popup_menu[4].widget), "time_data", &t);

	gnome_popup_menu_do_popup (menu, NULL, NULL, event, mv);
}

/* Event handler for day groups.  When mouse button 1 is pressed, it will pop up a quick view with
 * the events in that day.  When mouse button 3 is pressed, it will pop up a menu.
 */
static gint
day_event (GnomeCanvasItem *item, GdkEvent *event, gpointer data)
{
	MonthView *mv;
	int child_num;
	int day;

	mv = MONTH_VIEW (data);

	child_num = gnome_month_item_child2num (GNOME_MONTH_ITEM (mv->mitem), item);
	day = gnome_month_item_num2day (GNOME_MONTH_ITEM (mv->mitem), child_num);

	switch (event->type) {
	case GDK_BUTTON_PRESS:
		if ((event->button.button == 1) && (day != 0)) {
			do_quick_view_popup (mv, (GdkEventButton *) event, day);
			return TRUE;
		} else if (event->button.button == 3) {
			do_popup_menu (mv, (GdkEventButton *) event, day);
			return TRUE;
		}

		break;

	default:
		break;
	}

	return FALSE;
}

/* Returns the index of the specified arrow in the array of arrows */
static int
get_arrow_index (MonthView *mv, GnomeCanvasItem *arrow)
{
	int i;

	for (i = 0; i < 42; i++)
		if (mv->up[i] == arrow)
			return i;
		else if (mv->down[i] == arrow)
			return i + 42;

	g_warning ("Eeeek, arrow %p not found in month view %p", arrow, mv);
	return -1;
}

/* Checks whether arrows need to be displayed at the specified day index or not */
static void
check_arrow_visibility (MonthView *mv, int day_index)
{
	GtkArg args[3];
	double text_height;
	double clip_height;
	double y_offset;

	args[0].name = "text_height";
	args[1].name = "clip_height";
	args[2].name = "y_offset";
	gtk_object_getv (GTK_OBJECT (mv->text[day_index]), 3, args);

	text_height = GTK_VALUE_DOUBLE (args[0]);
	clip_height = GTK_VALUE_DOUBLE (args[1]);
	y_offset = GTK_VALUE_DOUBLE (args[2]);

	/* Check up arrow */

	if (y_offset < 0.0)
		gnome_canvas_item_show (mv->up[day_index]);
	else
		gnome_canvas_item_hide (mv->up[day_index]);

	if (y_offset > (clip_height - text_height))
		gnome_canvas_item_show (mv->down[day_index]);
	else
		gnome_canvas_item_hide (mv->down[day_index]);
}

/* Finds which arrow was clicked and scrolls the corresponding text item in the month view */
static void
do_arrow_click (MonthView *mv, GnomeCanvasItem *arrow)
{
	int arrow_index;
	int day_index;
	int up;
	GtkArg args[4];
	double text_height, clip_height;
	double y_offset;
	GdkFont *font;

	arrow_index = get_arrow_index (mv, arrow);
	up = (arrow_index < 42);
	day_index = up ? arrow_index : (arrow_index - 42);

	/* See how much we can scroll */

	args[0].name = "text_height";
	args[1].name = "clip_height";
	args[2].name = "y_offset";
	args[3].name = "font_gdk";
	gtk_object_getv (GTK_OBJECT (mv->text[day_index]), 4, args);

	text_height = GTK_VALUE_DOUBLE (args[0]);
	clip_height = GTK_VALUE_DOUBLE (args[1]);
	y_offset = GTK_VALUE_DOUBLE (args[2]);
	font = GTK_VALUE_BOXED (args[3]);

	if (up)
		y_offset += font->ascent + font->descent;
	else
		y_offset -= font->ascent + font->descent;

	if (y_offset > 0.0)
		y_offset = 0.0;
	else if (y_offset < (clip_height - text_height))
		y_offset = clip_height - text_height;

	/* Scroll */

	gnome_canvas_item_set (mv->text[day_index],
			       "y_offset", y_offset,
			       NULL);

	check_arrow_visibility (mv, day_index);
}

/* Event handler for the scroll arrows in the month view */
static gint
arrow_event (GnomeCanvasItem *item, GdkEvent *event, gpointer data)
{
	MonthView *mv;

	mv = MONTH_VIEW (data);

	switch (event->type) {
	case GDK_ENTER_NOTIFY:
		gnome_canvas_item_set (item,
				       "fill_color", color_spec_from_prop (COLOR_PROP_PRELIGHT_DAY_BG),
				       NULL);
		return TRUE;

	case GDK_LEAVE_NOTIFY:
		gnome_canvas_item_set (item,
				       "fill_color", color_spec_from_prop (COLOR_PROP_DAY_FG),
				       NULL);
		return TRUE;

	case GDK_BUTTON_PRESS:
		if (event->button.button != 1)
			break;

		do_arrow_click (mv, item);
		return TRUE;

	default:
		break;
	}

	return FALSE;
}

/* Creates a new arrow out of the specified points and connects the proper signals to it */
static GnomeCanvasItem *
new_arrow (MonthView *mv, GnomeCanvasGroup *group, GnomeCanvasPoints *points)
{
	GnomeCanvasItem *item;
	char *color_spec;

	color_spec = color_spec_from_prop (COLOR_PROP_DAY_FG);

	item = gnome_canvas_item_new (GNOME_CANVAS_GROUP (group),
				      gnome_canvas_polygon_get_type (),
				      "points", points,
				      "fill_color", color_spec,
				      "outline_color", color_spec,
				      NULL);

	gtk_signal_connect (GTK_OBJECT (item), "event",
			    (GtkSignalFunc) arrow_event,
			    mv);

	return item;
}

static void
month_view_init (MonthView *mv)
{
	int i;
	GnomeCanvasItem *day_group;
	GnomeCanvasPoints *points;

	/* Title */

	mv->title = gnome_canvas_item_new (gnome_canvas_root (GNOME_CANVAS (mv)),
					   gnome_canvas_text_get_type (),
					   "anchor", GTK_ANCHOR_N,
					   "fontset", HEADING_FONTSET,
					   "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_fontset", BIG_DAY_HEADING_FONTSET,
			       "day_fontset", BIG_NORMAL_DAY_FONTSET,
			       NULL);

	/* Arrows and text items.  The arrows start hidden by default; they will be shown as
	 * appropriate by the item adjustment code.  Also, connect to the event signal of the
	 * day groups so that we can pop up the quick view when appropriate.
	 */

	points = gnome_canvas_points_new (3);

	for (i = 0; i < 42; i++) {
		day_group = gnome_month_item_num2child (GNOME_MONTH_ITEM (mv->mitem),
							i + GNOME_MONTH_ITEM_DAY_GROUP);
		gtk_signal_connect (GTK_OBJECT (day_group), "event",
				    (GtkSignalFunc) day_event,
				    mv);

		/* Up arrow */

		points->coords[0] = 3;
		points->coords[1] = 10;
		points->coords[2] = 11;
		points->coords[3] = 10;
		points->coords[4] = 7;
		points->coords[5] = 3;

		mv->up[i] = new_arrow (mv, GNOME_CANVAS_GROUP (day_group), points);

		/* Down arrow */

		points->coords[0] = 13;
		points->coords[1] = 3;
		points->coords[2] = 17;
		points->coords[3] = 10;
		points->coords[4] = 21;
		points->coords[5] = 3;

		mv->down[i] = new_arrow (mv, GNOME_CANVAS_GROUP (day_group), points);

		/* Text item */

		mv->text[i] = gnome_canvas_item_new (GNOME_CANVAS_GROUP (day_group),
						     gnome_canvas_text_get_type (),
						     "fontset", EVENT_FONTSET,
						     "anchor", GTK_ANCHOR_NW,
						     "fill_color", color_spec_from_prop (COLOR_PROP_DAY_FG),
						     "clip", TRUE,
						     NULL);
	}
	gnome_canvas_points_free (points);
	
	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 the text items for events in the month view to the appropriate size.  It also makes the
 * corresponding arrows visible or invisible, as appropriate.
 */
static void
adjust_children (MonthView *mv)
{
	int i;
	GnomeCanvasItem *item;
	double x1, y1, x2, y2;
	GtkArg arg;

	for (i = 0; i < 42; i++) {
		/* Get dimensions of the day group */

		item = gnome_month_item_num2child (GNOME_MONTH_ITEM (mv->mitem), i + GNOME_MONTH_ITEM_DAY_GROUP);
		gnome_canvas_item_get_bounds (item, &x1, &y1, &x2, &y2);

		/* Normalize and add paddings */

		x2 -= x1 + EVENT_PADDING;
		x1 = EVENT_PADDING;
		y2 -= y1 + EVENT_PADDING;
		y1 = EVENT_PADDING;

		/* Add height of day label to y1 */

		item = gnome_month_item_num2child (GNOME_MONTH_ITEM (mv->mitem), i + GNOME_MONTH_ITEM_DAY_LABEL);

		arg.name = "text_height";
		gtk_object_getv (GTK_OBJECT (item), 1, &arg);
		y1 += GTK_VALUE_DOUBLE (arg);

		/* Set the position and clip size */

		gnome_canvas_item_set (mv->text[i],
				       "x", x1,
				       "y", y1,
				       "clip_width", x2 - x1,
				       "clip_height", y2 - y1,
				       "x_offset", 0.0,
				       "y_offset", 0.0,
				       NULL);

		/* See what visibility state the arrows should be set to */

		check_arrow_visibility (mv, i);
	}
}

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);
}

/* This defines the environment for the calendar iterator function that is used to populate the
 * month view with events.
 */
struct iter_info {
	MonthView *mv;			/* The month view we are creating children for */
	int first_day_index;		/* Index of the first day of the month within the month item */
	time_t month_begin, month_end;	/* Beginning and end of month */
	GString **strings;		/* Array of strings to populate */
};

/* This is the calendar iterator function used to populate the string array with event information.
 * For each event, it iterates through all the days that the event touches and appends the proper
 * information to the string array in the iter_info structure.
 */
static int
add_event (iCalObject *ico, time_t start, time_t end, void *data)
{
	struct iter_info *ii;
	struct tm *tm;
	time_t t;
	time_t day_begin_time, day_end_time;

	ii = data;

	/* Get the first day of the event */

	t = MAX (start, ii->month_begin);
	day_begin_time = time_day_begin (t);
	day_end_time = time_day_end (day_begin_time);

	/* Loop until the event ends or the month ends.  For each day touched, append the proper
	 * information to the corresponding string.
	 */

	do {
		tm = localtime (&day_begin_time);
		g_string_sprintfa (ii->strings[ii->first_day_index + tm->tm_mday - 1], "%s\n", ico->summary);

		/* Next day */

		day_begin_time = time_add_day (day_begin_time, 1);
		day_end_time = time_day_end (day_begin_time);
	} while ((end > day_begin_time) && (day_begin_time < ii->month_end));

	return TRUE; /* this means we are not finished yet with event generation */
}

void
month_view_update (MonthView *mv, iCalObject *object, int flags)
{
	struct iter_info ii;
	GString *strings[42];
	int i;
	time_t t;

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

	ii.mv = mv;

	/* Create an array of empty GStrings */

	ii.strings = strings;

	for (i = 0; i < 42; i++)
		strings[i] = g_string_new (NULL);

	ii.first_day_index = gnome_month_item_day2index (GNOME_MONTH_ITEM (mv->mitem), 1);
	g_assert (ii.first_day_index != -1);

	/* Populate the array of strings with events */

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

	calendar_iterate (mv->calendar->cal, ii.month_begin, ii.month_end, add_event, &ii);

	for (i = 0; i < 42; i++) {
		/* Delete the last character if it is a newline */

		if (strings[i]->str && strings[i]->len && (strings[i]->str[strings[i]->len - 1] == '\n'))
			g_string_truncate (strings[i], strings[i]->len - 1);
		
		gnome_canvas_item_set (mv->text[i],
				       "text", strings[i]->str,
				       NULL);
		g_string_free (strings[i], TRUE);
	}

	/* Adjust children for scrolling */

	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),
				       "fontset", BIG_NORMAL_DAY_FONTSET,
				       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),
				       "fontset", BIG_CURRENT_DAY_FONTSET,
				       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 */
}