/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/* Evolution calendar - Timezone selector dialog
 *
 * Copyright (C) 2001 Ximian, Inc.
 *
 * Authors: Damon Chaplin <damon@ximian.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General Public
 * License as published by the Free Software Foundation.
 *
 * This program 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 more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include <config.h>
#include <gtk/gtksignal.h>
#include <gtk/gtkcombo.h>
#include <gtk/gtkentry.h>
#include <gtk/gtklabel.h>
#include <gnome.h>
#include <glade/glade.h>
#include <widgets/misc/e-map.h>
#include "e-timezone-dialog.h"

#define E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA 0xc070a0ff
#define E_TIMEZONE_DIALOG_MAP_POINT_HOVER_RGBA 0xffff60ff
#define E_TIMEZONE_DIALOG_MAP_POINT_SELECTED_1_RGBA 0xff60e0ff
#define E_TIMEZONE_DIALOG_MAP_POINT_SELECTED_2_RGBA 0x000000ff

struct _ETimezoneDialogPrivate {
	/* The TZID of the timezone. May be NULL for a 'local time' (i.e. when
	   the displayed name is "") or for builtin timezones which we haven't
	   loaded yet. */
	char *tzid;

	/* Glade XML data */
	GladeXML *xml;

	EMapPoint *point_selected;
	EMapPoint *point_hover;

	EMap *map;

	/* The timeout used to flash the nearest point. */
	guint timeout_id;

	/* Widgets from the Glade file */
	GtkWidget *app;
	GtkWidget *table;
	GtkWidget *map_window;
	GtkWidget *timezone_preview;
	GtkWidget *timezone_combo;
};


static void e_timezone_dialog_class_init	(ETimezoneDialogClass *class);
static void e_timezone_dialog_init		(ETimezoneDialog      *etd);
static void e_timezone_dialog_destroy		(GtkObject	*object);

static gboolean get_widgets			(ETimezoneDialog *etd);
static gboolean on_map_timeout			(gpointer	 data);
static gboolean on_map_motion			(GtkWidget	*widget,
						 GdkEventMotion *event,
						 gpointer	 data);
static gboolean on_map_leave			(GtkWidget	*widget,
						 GdkEventCrossing *event,
						 gpointer	 data);
static gboolean on_map_visibility_changed	(GtkWidget	*w,
						 GdkEventVisibility *event,
						 gpointer	 data);
static gboolean on_map_button_pressed		(GtkWidget	*w,
						 GdkEventButton *event,
						 gpointer	 data);

static char*	get_zone_from_point		(ETimezoneDialog *etd,
						 EMapPoint	*point);
static void	find_selected_point		(ETimezoneDialog *etd);
static void	on_combo_changed		(GtkEditable	*entry,
						 ETimezoneDialog *etd);


static GtkObjectClass *parent_class;


GtkType
e_timezone_dialog_get_type (void)
{
	static GtkType e_timezone_dialog_type = 0;

	if (!e_timezone_dialog_type) {
		static const GtkTypeInfo e_timezone_dialog_info = {
			"ETimezoneDialog",
			sizeof (ETimezoneDialog),
			sizeof (ETimezoneDialogClass),
			(GtkClassInitFunc) e_timezone_dialog_class_init,
			(GtkObjectInitFunc) e_timezone_dialog_init,
			NULL, /* reserved_1 */
			NULL, /* reserved_2 */
			(GtkClassInitFunc) NULL
		};

		e_timezone_dialog_type = gtk_type_unique (GTK_TYPE_OBJECT,
							  &e_timezone_dialog_info);
	}

	return e_timezone_dialog_type;
}

/* Class initialization function for the event editor */
static void
e_timezone_dialog_class_init (ETimezoneDialogClass *class)
{
	GtkObjectClass *object_class;

	object_class = (GtkObjectClass *) class;

	parent_class = gtk_type_class (GTK_TYPE_OBJECT);

	object_class->destroy = e_timezone_dialog_destroy;
}

/* Object initialization function for the event editor */
static void
e_timezone_dialog_init (ETimezoneDialog *etd)
{
	ETimezoneDialogPrivate *priv;

	GTK_OBJECT_UNSET_FLAGS (etd, GTK_FLOATING);

	priv = g_new0 (ETimezoneDialogPrivate, 1);
	etd->priv = priv;

	priv->tzid = NULL;
	priv->point_selected = NULL;
	priv->point_hover = NULL;
	priv->timeout_id = 0;
}

/* Destroy handler for the event editor */
static void
e_timezone_dialog_destroy (GtkObject *object)
{
	ETimezoneDialog *etd;
	ETimezoneDialogPrivate *priv;
	GtkWidget *dialog;

	g_return_if_fail (object != NULL);
	g_return_if_fail (E_IS_TIMEZONE_DIALOG (object));

	etd = E_TIMEZONE_DIALOG (object);
	priv = etd->priv;

	/* Destroy the actual dialog. */
	dialog = e_timezone_dialog_get_toplevel (etd);
	gtk_widget_destroy (dialog);

	g_free (priv->tzid);
	priv->tzid = NULL;

	if (priv->timeout_id) {
		g_source_remove (priv->timeout_id);
		priv->timeout_id = 0;
	}

	if (priv->xml) {
		gtk_object_unref (GTK_OBJECT (priv->xml));
		priv->xml = NULL;
	}

	g_free (priv);
	etd->priv = NULL;

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


static void
e_timezone_dialog_add_timezones (ETimezoneDialog *etd)
{
	ETimezoneDialogPrivate *priv;
	icalarray *zones;
	GtkWidget *listitem;
	GtkCombo *combo;
	int i;

	priv = etd->priv;

	combo = GTK_COMBO (priv->timezone_combo);

	/* Clear any existing items in the combo. */
	gtk_list_clear_items (GTK_LIST (combo->list), 0, -1);

	/* Put the "None" and "UTC" entries at the top of the combo's list.
	   When "None" is selected we want the field to be cleared. */
	listitem = gtk_list_item_new_with_label (_("None"));
	gtk_combo_set_item_string (combo, GTK_ITEM (listitem), "");
	gtk_widget_show (listitem);
	gtk_container_add (GTK_CONTAINER (combo->list), listitem);

	listitem = gtk_list_item_new_with_label (_("UTC"));
	gtk_widget_show (listitem);
	gtk_container_add (GTK_CONTAINER (combo->list), listitem);

	/* Get the array of builtin timezones. */
	zones = icaltimezone_get_builtin_timezones ();

	for (i = 0; i < zones->num_elements; i++) {
		icaltimezone *zone;
		char *location;

		zone = icalarray_element_at (zones, i);

		location = _(icaltimezone_get_location (zone));

		e_map_add_point (priv->map, location,
				 icaltimezone_get_longitude (zone),
				 icaltimezone_get_latitude (zone),
				 E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA);

		listitem = gtk_list_item_new_with_label (location);
		gtk_widget_show (listitem);
		gtk_container_add (GTK_CONTAINER (combo->list), listitem);
	}
}


ETimezoneDialog *
e_timezone_dialog_construct (ETimezoneDialog *etd)
{
	ETimezoneDialogPrivate *priv;
	GtkWidget *map;

	g_return_val_if_fail (etd != NULL, NULL);
	g_return_val_if_fail (E_IS_TIMEZONE_DIALOG (etd), NULL);

	priv = etd->priv;

	/* Load the content widgets */

	priv->xml = glade_xml_new (EVOLUTION_GLADEDIR "/e-timezone-dialog.glade", NULL, NULL);
	if (!priv->xml) {
		g_message ("e_timezone_dialog_construct(): Could not load the Glade XML file!");
		goto error;
	}

	if (!get_widgets (etd)) {
		g_message ("e_timezone_dialog_construct(): Could not find all widgets in the XML file!");
		goto error;
	}

	map = GTK_WIDGET (e_map_new ());
	priv->map = E_MAP (map);
	gtk_widget_set_events (map, gtk_widget_get_events (map)
			       | GDK_LEAVE_NOTIFY_MASK
			       | GDK_VISIBILITY_NOTIFY_MASK);

	gtk_entry_set_editable (GTK_ENTRY (GTK_COMBO (priv->timezone_combo)->entry), FALSE);

	e_timezone_dialog_add_timezones (etd);

	gtk_container_add (GTK_CONTAINER (priv->map_window), map);
	gtk_widget_show (map);

        gtk_signal_connect (GTK_OBJECT (map), "motion-notify-event",
			    GTK_SIGNAL_FUNC (on_map_motion), etd);
        gtk_signal_connect (GTK_OBJECT (map), "leave-notify-event",
			    GTK_SIGNAL_FUNC (on_map_leave), etd);
        gtk_signal_connect (GTK_OBJECT (map), "visibility-notify-event",
			    GTK_SIGNAL_FUNC (on_map_visibility_changed), etd);
	gtk_signal_connect (GTK_OBJECT (map), "button-press-event",
			    GTK_SIGNAL_FUNC (on_map_button_pressed), etd);

	gtk_signal_connect (GTK_OBJECT (GTK_COMBO (priv->timezone_combo)->entry), "changed", 
			    GTK_SIGNAL_FUNC (on_combo_changed), etd);

	return etd;

 error:

	gtk_object_unref (GTK_OBJECT (etd));
	return NULL;
}

/* Gets the widgets from the XML file and returns if they are all available.
 * For the widgets whose values can be simply set with e-dialog-utils, it does
 * that as well.
 */
static gboolean
get_widgets (ETimezoneDialog *etd)
{
	ETimezoneDialogPrivate *priv;

	priv = etd->priv;

#define GW(name) glade_xml_get_widget (priv->xml, name)

	priv->app		= GW ("timezone-dialog");
	priv->map_window	= GW ("map-window");
	priv->timezone_combo	= GW ("timezone-combo");
	priv->timezone_preview	= GW ("timezone-preview");
	priv->table             = GW ("table1");

	return (priv->app
		&& priv->map_window
		&& priv->timezone_combo
		&& priv->timezone_preview
		&& priv->table);
}


/**
 * e_timezone_dialog_new:
 *
 * Creates a new event editor dialog.
 *
 * Return value: A newly-created event editor dialog, or NULL if the event
 * editor could not be created.
 **/
ETimezoneDialog *
e_timezone_dialog_new (void)
{
	ETimezoneDialog *etd;

	etd = E_TIMEZONE_DIALOG (gtk_type_new (E_TYPE_TIMEZONE_DIALOG));
	return e_timezone_dialog_construct (E_TIMEZONE_DIALOG (etd));
}



/* This flashes the currently selected timezone in the map. */
static gboolean
on_map_timeout (gpointer data)
{
	ETimezoneDialog *etd;
	ETimezoneDialogPrivate *priv;

	etd = E_TIMEZONE_DIALOG (data);
	priv = etd->priv;

	if (!priv->point_selected)
		return TRUE;

        if (e_map_point_get_color_rgba (priv->point_selected)
	    == E_TIMEZONE_DIALOG_MAP_POINT_SELECTED_1_RGBA)
	        e_map_point_set_color_rgba (priv->map, priv->point_selected,
					    E_TIMEZONE_DIALOG_MAP_POINT_SELECTED_2_RGBA);
	else
	        e_map_point_set_color_rgba (priv->map, priv->point_selected,
					    E_TIMEZONE_DIALOG_MAP_POINT_SELECTED_1_RGBA);

	return TRUE;
}


static gboolean
on_map_motion (GtkWidget *widget, GdkEventMotion *event, gpointer data)
{
	ETimezoneDialog *etd;
	ETimezoneDialogPrivate *priv;
	double longitude, latitude;
	char *old_zone, *new_zone;

	etd = E_TIMEZONE_DIALOG (data);
	priv = etd->priv;

	e_map_window_to_world (priv->map, (double) event->x, (double) event->y,
			       &longitude, &latitude);

	if (priv->point_hover && priv->point_hover != priv->point_selected)
	        e_map_point_set_color_rgba (priv->map, priv->point_hover,
					    E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA);

	priv->point_hover = e_map_get_closest_point (priv->map, longitude,
						     latitude, TRUE);

	if (priv->point_hover != priv->point_selected)
	        e_map_point_set_color_rgba (priv->map, priv->point_hover,
					    E_TIMEZONE_DIALOG_MAP_POINT_HOVER_RGBA);

	gtk_label_get (GTK_LABEL (priv->timezone_preview), &old_zone);
	new_zone = get_zone_from_point (etd, priv->point_hover);
	if (strcmp (old_zone, new_zone))
		gtk_label_set_text (GTK_LABEL (priv->timezone_preview),
				    new_zone);

	return TRUE;
}


static gboolean
on_map_leave (GtkWidget *widget, GdkEventCrossing *event, gpointer data)
{
	ETimezoneDialog *etd;
	ETimezoneDialogPrivate *priv;
	char *old_zone;

	etd = E_TIMEZONE_DIALOG (data);
	priv = etd->priv;

	/* We only want to reset the hover point and the preview text if this
	   is a normal leave event. For some reason we are getting leave events
	   when the button is pressed in the map, which causes problems. */
	if (event->mode != GDK_CROSSING_NORMAL)
		return FALSE;

	if (priv->point_hover && priv->point_hover != priv->point_selected)
	        e_map_point_set_color_rgba (priv->map, priv->point_hover,
					    E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA);

	priv->point_hover = NULL;

	/* Clear the timezone preview label, if it isn't already empty. */
	gtk_label_get (GTK_LABEL (priv->timezone_preview), &old_zone);
	if (strcmp (old_zone, ""))
		gtk_label_set_text (GTK_LABEL (priv->timezone_preview), "");

	return FALSE;
}


static gboolean
on_map_visibility_changed (GtkWidget *w, GdkEventVisibility *event,
			   gpointer data)
{
	ETimezoneDialog *etd;
	ETimezoneDialogPrivate *priv;
	
	etd = E_TIMEZONE_DIALOG (data);
	priv = etd->priv;

	if (event->state != GDK_VISIBILITY_FULLY_OBSCURED) {
		/* Map is visible, at least partly, so make sure we flash the
		   selected point. */
		if (!priv->timeout_id)
			priv->timeout_id = gtk_timeout_add (100, on_map_timeout, etd);
	} else {
		/* Map is invisible, so don't waste resources on the timeout.*/
		if (priv->timeout_id) {
			g_source_remove (priv->timeout_id);
			priv->timeout_id = 0;
		}
	}

	return FALSE;
}


static gboolean
on_map_button_pressed (GtkWidget *w, GdkEventButton *event, gpointer data)
{
	ETimezoneDialog *etd;
	ETimezoneDialogPrivate *priv;
	double longitude, latitude;
	char *location;
	
	etd = E_TIMEZONE_DIALOG (data);
	priv = etd->priv;

	e_map_window_to_world (priv->map, (double) event->x, (double) event->y,
			       &longitude, &latitude);
	
	if (event->button != 1) {
	        e_map_zoom_out (priv->map);
	} else {
		if (e_map_get_magnification (priv->map) <= 1.0)
		        e_map_zoom_to_location (priv->map, longitude,
						latitude);
	
		if (priv->point_selected)
		        e_map_point_set_color_rgba (priv->map,
						    priv->point_selected,
						    E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA);
		priv->point_selected = priv->point_hover;
		
		location = get_zone_from_point (etd, priv->point_selected);
		gtk_entry_set_text (GTK_ENTRY (GTK_COMBO (priv->timezone_combo)->entry),
				    location);

		g_free (priv->tzid);
		priv->tzid = NULL;
	}
	
	return TRUE;
}


/* Returns the translated timezone location of the fiven EMapPoint,
   e.g. "Europe/London", in the current locale's encoding (not UTF-8). */
static char*
get_zone_from_point (ETimezoneDialog *etd,
		     EMapPoint *point)
{
	ETimezoneDialogPrivate *priv;
	icalarray *zones;
	double longitude, latitude;
	int i;

	priv = etd->priv;

	if (point == NULL)
		return "";

	e_map_point_get_location (point, &longitude, &latitude);

	/* Get the array of builtin timezones. */
	zones = icaltimezone_get_builtin_timezones ();

	for (i = 0; i < zones->num_elements; i++) {
		icaltimezone *zone;
		double zone_longitude, zone_latitude;

		zone = icalarray_element_at (zones, i);
		zone_longitude = icaltimezone_get_longitude (zone);
		zone_latitude = icaltimezone_get_latitude (zone);

		if (zone_longitude - 0.005 <= longitude &&
		    zone_longitude + 0.005 >= longitude &&
		    zone_latitude - 0.005 <= latitude &&
		    zone_latitude + 0.005 >= latitude)
		{
			return _(icaltimezone_get_location (zone));
		}
	}

	g_assert_not_reached ();

	return NULL;
}


/* Returns the TZID of the timezone set, and optionally its displayed name.
   The TZID may be NULL, in which case the builtin timezone with the city name
   of display_name should be used. If display_name is also NULL or "", then it
   is assumed to be a 'local time'. Note that display_name may be translated,
   so you need to convert it back to English before trying to load it. 
   It will be in the GTK+ encoding, i.e. not UTF-8. */
char*
e_timezone_dialog_get_timezone		(ETimezoneDialog  *etd,
					 const char **display_name)
{
	ETimezoneDialogPrivate *priv;

	g_return_val_if_fail (etd != NULL, NULL);
	g_return_val_if_fail (E_IS_TIMEZONE_DIALOG (etd), NULL);

	priv = etd->priv;

	if (display_name)
		*display_name = gtk_entry_get_text (GTK_ENTRY (GTK_COMBO (priv->timezone_combo)->entry));

	return priv->tzid;
}


/* Sets the TZID and displayed name of the timezone. The TZID may be NULL for
   a 'local time' (i.e. display_name is NULL or "") or if it is a builtin
   timezone which hasn't been loaded yet. (This is done so we don't load
   timezones until we really need them.) The display_name should be the
   translated name in the GTK+ - it will be displayed exactly as it is. */
void
e_timezone_dialog_set_timezone		(ETimezoneDialog  *etd,
					 char		  *tzid,
					 char		  *display_name)
{
	ETimezoneDialogPrivate *priv;

	g_return_if_fail (etd != NULL);
	g_return_if_fail (E_IS_TIMEZONE_DIALOG (etd));

	priv = etd->priv;

	if (priv->tzid)
		g_free (priv->tzid);

	priv->tzid = g_strdup (tzid);

	gtk_entry_set_text (GTK_ENTRY (GTK_COMBO (priv->timezone_combo)->entry),
			    display_name ? display_name : "");

	find_selected_point (etd);
}


GtkWidget *
e_timezone_dialog_get_toplevel	(ETimezoneDialog  *etd)
{
	ETimezoneDialogPrivate *priv;

	g_return_val_if_fail (etd != NULL, NULL);
	g_return_val_if_fail (E_IS_TIMEZONE_DIALOG (etd), NULL);

	priv = etd->priv;

	return priv->app;
}


/* This tries to find the timezone corresponding to the text in the combo,
   and selects the point so that it flashes. */
static void
find_selected_point (ETimezoneDialog *etd)
{
	ETimezoneDialogPrivate *priv;
	icalarray *zones;
	const char *current_zone;
	EMapPoint *point = NULL;
	int i;

	priv = etd->priv;

	current_zone = gtk_entry_get_text (GTK_ENTRY (GTK_COMBO (priv->timezone_combo)->entry));

	/* Get the array of builtin timezones. */
	zones = icaltimezone_get_builtin_timezones ();

	for (i = 0; i < zones->num_elements; i++) {
		icaltimezone *zone;
		char *location;

		zone = icalarray_element_at (zones, i);

		location = _(icaltimezone_get_location (zone));

		if (!strcmp (current_zone, location)) {
			double zone_longitude, zone_latitude;

			zone_longitude = icaltimezone_get_longitude (zone);
			zone_latitude = icaltimezone_get_latitude (zone);

			point = e_map_get_closest_point (priv->map,
							 zone_longitude,
							 zone_latitude,
							 FALSE);
			break;
		}
	}

	if (priv->point_selected)
		e_map_point_set_color_rgba (priv->map, priv->point_selected,
					    E_TIMEZONE_DIALOG_MAP_POINT_NORMAL_RGBA);

	priv->point_selected = point;
}


static void
on_combo_changed (GtkEditable *entry, ETimezoneDialog *etd)
{
	ETimezoneDialogPrivate *priv;

	priv = etd->priv;

	find_selected_point (etd);

	g_free (priv->tzid);
	priv->tzid = NULL;
}

/**
 * e_timezone_dialog_reparent:
 * @etd: #ETimezoneDialog.
 * @new_parent: The new parent widget.
 *
 * Takes the internal widgets out of the dialog and put them into @new_parent
 */
void
e_timezone_dialog_reparent (ETimezoneDialog *etd,
			    GtkWidget *new_parent)
{
	ETimezoneDialogPrivate *priv;

	priv = etd->priv;

	gtk_widget_reparent (priv->table, new_parent);
}


/* Returns the builtin timezone corresponding to display_name, which is
   the translated location, e.g. 'Europe/London', in the locale's encoding.
   If display_name is NULL or "" it returns NULL. */
icaltimezone*
e_timezone_dialog_get_builtin_timezone	(const char *display_name)
{
	icalarray *zones;
	int i;

	/* If the field is empty, return NULL (i.e. a floating time). */
	if (!display_name || !display_name[0])
		return NULL;

	/* Check for UTC. */
	if (!strcmp (display_name, _("UTC")))
		return icaltimezone_get_utc_timezone ();

	/* Get the array of builtin timezones. */
	zones = icaltimezone_get_builtin_timezones ();

	for (i = 0; i < zones->num_elements; i++) {
		icaltimezone *zone;
		char *location;

		zone = icalarray_element_at (zones, i);
		location = icaltimezone_get_location (zone);
		if (!strcmp (display_name, _(location)))
			return zone;
	}

	return NULL;
}
