/*
 * Copyright (C) 2004 Free Software Foundation, Inc.
 *
 * This program 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 2 of the
 * License, or (at your option) any later version.
 *
 * 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.
 *
 * Authors:
 *     Mark McLoughlin  <mark@skynet.ie>
 *     William Jon McCann  <mccann@jhu.edu>
 *     Martin Grimme  <martin@pycage.de>
 *     Christian Kellner  <gicmo@xatom.net>
 */

#include <config.h>

#include "calendar-client.h"

#include <libintl.h>
#include <string.h>
#define HANDLE_LIBICAL_MEMORY
#include <libecal/e-cal.h>
#include <libecal/e-cal-time-util.h>
#include <libecal/e-cal-recur.h>

#include "calendar-sources.h"

#undef CALENDAR_ENABLE_DEBUG
#include "calendar-debug.h"

#define CALENDAR_CONFIG_PREFIX   "/apps/evolution/calendar"
#define CALENDAR_CONFIG_TIMEZONE CALENDAR_CONFIG_PREFIX "/display/timezone"

#ifndef _
#define _(x) gettext(x)
#endif

#ifndef N_
#define N_(x) x
#endif

#define CALENDAR_CLIENT_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), CALENDAR_TYPE_CLIENT, CalendarClientPrivate))

typedef struct _CalendarClientQuery  CalendarClientQuery;
typedef struct _CalendarClientSource CalendarClientSource;

struct _CalendarClientQuery
{
  ECalView   *view;
  GHashTable *events;
};

struct _CalendarClientSource
{
  CalendarClient      *client;
  ECal                *source;

  CalendarClientQuery  completed_query;
  CalendarClientQuery  in_progress_query;

  guint                changed_signal_id;

  guint                query_completed : 1;
  guint                query_in_progress : 1;
};

struct _CalendarClientPrivate
{
  CalendarSources     *calendar_sources;

  GSList              *appointment_sources;
  GSList              *task_sources;

  icaltimezone        *zone;

  guint                zone_listener;
  MateConfClient         *mateconf_client;

  guint                day;
  guint                month;
  guint                year;
};

static void calendar_client_class_init   (CalendarClientClass *klass);
static void calendar_client_init         (CalendarClient      *client);
static void calendar_client_finalize     (GObject             *object);
static void calendar_client_set_property (GObject             *object,
					  guint                prop_id,
					  const GValue        *value,
					  GParamSpec          *pspec);
static void calendar_client_get_property (GObject             *object,
					  guint                prop_id,
					  GValue              *value,
					  GParamSpec          *pspec);

static GSList *calendar_client_update_sources_list         (CalendarClient       *client,
							    GSList               *sources,
							    GSList               *esources,
							    guint                 changed_signal_id);
static void    calendar_client_appointment_sources_changed (CalendarClient       *client);
static void    calendar_client_task_sources_changed        (CalendarClient       *client);

static void calendar_client_stop_query  (CalendarClient       *client,
					 CalendarClientSource *source,
					 CalendarClientQuery  *query);
static void calendar_client_start_query (CalendarClient       *client,
					 CalendarClientSource *source,
					 const char           *query);

static void calendar_client_source_finalize (CalendarClientSource *source);
static void calendar_client_query_finalize  (CalendarClientQuery  *query);

static void
calendar_client_update_appointments (CalendarClient *client);
static void
calendar_client_update_tasks (CalendarClient *client);

enum
{
  PROP_O,
  PROP_DAY,
  PROP_MONTH,
  PROP_YEAR
};

enum
{
  APPOINTMENTS_CHANGED,
  TASKS_CHANGED,
  LAST_SIGNAL
};

static GObjectClass *parent_class = NULL;
static guint         signals [LAST_SIGNAL] = { 0, };

GType
calendar_client_get_type (void)
{
  static GType client_type = 0;
  
  if (!client_type)
    {
      static const GTypeInfo client_info =
      {
	sizeof (CalendarClientClass),
	NULL,		/* base_init */
	NULL,		/* base_finalize */
	(GClassInitFunc) calendar_client_class_init,
	NULL,           /* class_finalize */
	NULL,		/* class_data */
	sizeof (CalendarClient),
	0,		/* n_preallocs */
	(GInstanceInitFunc) calendar_client_init,
      };
      
      client_type = g_type_register_static (G_TYPE_OBJECT,
					    "CalendarClient",
					    &client_info, 0);
    }
  
  return client_type;
}

static void
calendar_client_class_init (CalendarClientClass *klass)
{
  GObjectClass *gobject_class = (GObjectClass *) klass;

  parent_class = g_type_class_peek_parent (klass);

  gobject_class->finalize     = calendar_client_finalize;
  gobject_class->set_property = calendar_client_set_property;
  gobject_class->get_property = calendar_client_get_property;

  g_type_class_add_private (klass, sizeof (CalendarClientPrivate));

  g_object_class_install_property (gobject_class,
				   PROP_DAY,
				   g_param_spec_uint ("day",
						      "Day",
						      "The currently monitored day between 1 and 31 (0 denotes unset)",
						      0, G_MAXUINT, 0,
						      G_PARAM_READWRITE));

  g_object_class_install_property (gobject_class,
				   PROP_MONTH,
				   g_param_spec_uint ("month",
						      "Month",
						      "The currently monitored month between 0 and 11",
						      0, G_MAXUINT, 0,
						      G_PARAM_READWRITE));

  g_object_class_install_property (gobject_class,
				   PROP_YEAR,
				   g_param_spec_uint ("year",
						      "Year",
						      "The currently monitored year",
						      0, G_MAXUINT, 0,
						      G_PARAM_READWRITE));

  signals [APPOINTMENTS_CHANGED] =
    g_signal_new ("appointments-changed",
		  G_TYPE_FROM_CLASS (gobject_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (CalendarClientClass, tasks_changed),
		  NULL,
		  NULL,
		  g_cclosure_marshal_VOID__VOID,
		  G_TYPE_NONE,
		  0);

  signals [TASKS_CHANGED] =
    g_signal_new ("tasks-changed",
		  G_TYPE_FROM_CLASS (gobject_class),
		  G_SIGNAL_RUN_LAST,
		  G_STRUCT_OFFSET (CalendarClientClass, tasks_changed),
		  NULL,
		  NULL,
		  g_cclosure_marshal_VOID__VOID,
		  G_TYPE_NONE,
		  0);
}

/* Timezone code adapted from evolution/calendar/gui/calendar-config.c */
/* The current timezone, e.g. "Europe/London". It may be NULL, in which case
   you should assume UTC. */
static gchar *
calendar_client_config_get_timezone (MateConfClient *mateconf_client)
{
  char *location;

  location = mateconf_client_get_string (mateconf_client,
                                      CALENDAR_CONFIG_TIMEZONE,
                                      NULL);

  return location;
}

static icaltimezone *
calendar_client_config_get_icaltimezone (MateConfClient *mateconf_client)
{
  char         *location;
  icaltimezone *zone = NULL;
	
  location = calendar_client_config_get_timezone (mateconf_client);
  if (!location)
    return icaltimezone_get_utc_timezone ();

  zone = icaltimezone_get_builtin_timezone (location);
  g_free (location);
	
  return zone;
}

static void
calendar_client_set_timezone (CalendarClient *client) 
{
  GSList *l;
  GSList *esources;

  client->priv->zone = calendar_client_config_get_icaltimezone (client->priv->mateconf_client);

  esources = calendar_sources_get_appointment_sources (client->priv->calendar_sources);
  for (l = esources; l; l = l->next) {
    ECal *source = l->data;
			
    e_cal_set_default_timezone (source, client->priv->zone, NULL);
  }
}

static void
calendar_client_timezone_changed_cb (MateConfClient    *mateconf_client,
                                     guint           id,
                                     MateConfEntry     *entry,
                                     CalendarClient *client)
{
  calendar_client_set_timezone (client);
}

static void
cal_opened_cb (ECal                 *ecal,
               ECalendarStatus       status,
               CalendarClientSource *cl_source)
{
  ECalSourceType  s_type;
  CalendarClient *client = cl_source->client;

  s_type = e_cal_get_source_type (ecal);

  if (status == E_CALENDAR_STATUS_BUSY &&
      e_cal_get_load_state (ecal) == E_CAL_LOAD_NOT_LOADED)
    {
      e_cal_open_async (ecal, FALSE);
      return;
    }
  
  g_signal_handlers_disconnect_by_func (ecal, cal_opened_cb, cl_source);

  if (status != E_CALENDAR_STATUS_OK)
    {
      if (s_type == E_CAL_SOURCE_TYPE_EVENT)
        client->priv->appointment_sources = g_slist_remove (client->priv->appointment_sources,
                                                            cl_source);
      else
        client->priv->task_sources = g_slist_remove (client->priv->task_sources,
                                                     cl_source);

      calendar_client_source_finalize (cl_source);
      g_free (cl_source);

      return;
    }

  if (s_type == E_CAL_SOURCE_TYPE_EVENT)
    calendar_client_update_appointments (client);
  else
    calendar_client_update_tasks (client);
}

static void
load_calendars (CalendarClient    *client,
                CalendarEventType  type) 
{
  GSList *l, *clients;

  switch (type)
    {
      case CALENDAR_EVENT_APPOINTMENT:
        clients = client->priv->appointment_sources;
        break;
      case CALENDAR_EVENT_TASK:
        clients = client->priv->task_sources;
        break;
      default:
        g_assert_not_reached ();
    }

  for (l = clients; l != NULL; l = l->next)
    {
      ECal *ecal;	
      CalendarClientSource *cl_source = l->data;

      ecal = cl_source->source;

      if (e_cal_get_load_state (ecal) == E_CAL_LOAD_LOADED)
        continue;

      g_signal_connect (G_OBJECT (ecal), "cal_opened",
                        G_CALLBACK (cal_opened_cb), cl_source);
      e_cal_open_async (ecal, TRUE);
    }
}

static void
calendar_client_init (CalendarClient *client)
{
  GSList *esources;

  client->priv = CALENDAR_CLIENT_GET_PRIVATE (client);

  client->priv->calendar_sources = calendar_sources_get ();
  client->priv->mateconf_client = mateconf_client_get_default ();

  esources = calendar_sources_get_appointment_sources (client->priv->calendar_sources);
  client->priv->appointment_sources =
    calendar_client_update_sources_list (client, NULL, esources, signals [APPOINTMENTS_CHANGED]);

  esources = calendar_sources_get_task_sources (client->priv->calendar_sources);
  client->priv->task_sources =
    calendar_client_update_sources_list (client, NULL, esources, signals [TASKS_CHANGED]);
 
  /* set the timezone before loading the clients */ 
  calendar_client_set_timezone (client);
  load_calendars (client, CALENDAR_EVENT_APPOINTMENT);
  load_calendars (client, CALENDAR_EVENT_TASK);

  g_signal_connect_swapped (client->priv->calendar_sources,
			    "appointment-sources-changed",
			    G_CALLBACK (calendar_client_appointment_sources_changed),
			    client);
  g_signal_connect_swapped (client->priv->calendar_sources,
			    "task-sources-changed",
			    G_CALLBACK (calendar_client_task_sources_changed),
			    client);

  mateconf_client_add_dir (client->priv->mateconf_client,
			CALENDAR_CONFIG_PREFIX,
			MATECONF_CLIENT_PRELOAD_NONE,
			NULL);

  client->priv->zone_listener = mateconf_client_notify_add (client->priv->mateconf_client,
                                                         CALENDAR_CONFIG_TIMEZONE,
                                                         (MateConfClientNotifyFunc) calendar_client_timezone_changed_cb,
                                                         client, NULL, NULL);

  client->priv->day   = -1;
  client->priv->month = -1;
  client->priv->year  = -1;
}

static void
calendar_client_finalize (GObject *object)
{
  CalendarClient *client = CALENDAR_CLIENT (object);
  GSList         *l;

  if (client->priv->zone_listener)
    {
      mateconf_client_notify_remove (client->priv->mateconf_client,
                                  client->priv->zone_listener);
      client->priv->zone_listener = 0;
    }

  mateconf_client_remove_dir (client->priv->mateconf_client,
                           CALENDAR_CONFIG_PREFIX,
                           NULL);

  if (client->priv->mateconf_client)
    g_object_unref (client->priv->mateconf_client);
  client->priv->mateconf_client = NULL;

  for (l = client->priv->appointment_sources; l; l = l->next)
    {
      calendar_client_source_finalize (l->data);
      g_free (l->data);
    }
  g_slist_free (client->priv->appointment_sources);
  client->priv->appointment_sources = NULL;

  for (l = client->priv->task_sources; l; l = l->next)
    {
      calendar_client_source_finalize (l->data);
      g_free (l->data);
    }
  g_slist_free (client->priv->task_sources);
  client->priv->task_sources = NULL;

  if (client->priv->calendar_sources)
    g_object_unref (client->priv->calendar_sources);
  client->priv->calendar_sources = NULL;

  if (G_OBJECT_CLASS (parent_class)->finalize)
    G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
calendar_client_set_property (GObject      *object,
			      guint         prop_id,
			      const GValue *value,
			      GParamSpec   *pspec)
{
  CalendarClient *client = CALENDAR_CLIENT (object);

  switch (prop_id)
    {
    case PROP_DAY:
      calendar_client_select_day (client, g_value_get_uint (value));
      break;
    case PROP_MONTH:
      calendar_client_select_month (client,
				    g_value_get_uint (value),
				    client->priv->year);
      break;
    case PROP_YEAR:
      calendar_client_select_month (client,
				    client->priv->month,
				    g_value_get_uint (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
calendar_client_get_property (GObject    *object,
			      guint       prop_id,
			      GValue     *value,
			      GParamSpec *pspec)
{
  CalendarClient *client = CALENDAR_CLIENT (object);

  switch (prop_id)
    {
    case PROP_DAY:
      g_value_set_uint (value, client->priv->day);
      break;
    case PROP_MONTH:
      g_value_set_uint (value, client->priv->month);
      break;
    case PROP_YEAR:
      g_value_set_uint (value, client->priv->year);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

CalendarClient *
calendar_client_new (void)
{
  return g_object_new (CALENDAR_TYPE_CLIENT, NULL);
}

/* @day and @month can happily be out of range as
 * mktime() will normalize them correctly. From mktime(3):
 *
 * "If structure members are outside their legal interval,
 *  they will be normalized (so that, e.g., 40 October is
 *  changed into 9 November)."
 *
 * "What?", you say, "Something useful in libc?"
 */
static inline time_t
make_time_for_day_begin (int day,
			 int month,
			 int year)
{
  struct tm localtime_tm = { 0, };

  localtime_tm.tm_mday  = day;
  localtime_tm.tm_mon   = month;
  localtime_tm.tm_year  = year - 1900;
  localtime_tm.tm_isdst = -1;

  return mktime (&localtime_tm);
}

static inline char *
make_isodate_for_day_begin (int day,
			    int month,
			    int year)
{
  time_t utctime;

  utctime = make_time_for_day_begin (day, month, year);

  return utctime != -1 ? isodate_from_time_t (utctime) : NULL;
}

static time_t
get_time_from_property (icalcomponent         *ical,
			icalproperty_kind      prop_kind,
			struct icaltimetype (* get_prop_func) (const icalproperty *prop),
                        icaltimezone          *default_zone)
{
  icalproperty        *prop;
  struct icaltimetype  ical_time;
  icalparameter       *param;
  icaltimezone        *timezone = NULL;
  
  prop = icalcomponent_get_first_property (ical, prop_kind);
  if (!prop)
    return 0;

  ical_time = get_prop_func (prop);

  param = icalproperty_get_first_parameter (prop, ICAL_TZID_PARAMETER);
  if (param)
    timezone = icaltimezone_get_builtin_timezone_from_tzid (icalparameter_get_tzid (param));
  else if (icaltime_is_utc (ical_time))
    timezone = icaltimezone_get_utc_timezone ();
  else 
    timezone = default_zone;

  return icaltime_as_timet_with_zone (ical_time, timezone);
}

static char *
get_ical_uid (icalcomponent *ical)
{
  return g_strdup (icalcomponent_get_uid (ical));
}

static char *
get_ical_rid (icalcomponent *ical)
{
  icalproperty        *prop;
  struct icaltimetype  ical_time;
  
  prop = icalcomponent_get_first_property (ical, ICAL_RECURRENCEID_PROPERTY);
  if (!prop)
    return NULL;

  ical_time = icalproperty_get_recurrenceid (prop);

  return icaltime_is_valid_time (ical_time) && !icaltime_is_null_time (ical_time) ? 
    g_strdup (icaltime_as_ical_string (ical_time)) : NULL;
}

static char *
get_ical_summary (icalcomponent *ical)
{
  icalproperty *prop;

  prop = icalcomponent_get_first_property (ical, ICAL_SUMMARY_PROPERTY);
  if (!prop)
    return NULL;

  return g_strdup (icalproperty_get_summary (prop));
}

static char *
get_ical_description (icalcomponent *ical)
{
  icalproperty *prop;

  prop = icalcomponent_get_first_property (ical, ICAL_DESCRIPTION_PROPERTY);
  if (!prop)
    return NULL;

  return g_strdup (icalproperty_get_description (prop));
}

static inline time_t
get_ical_start_time (icalcomponent *ical,
                     icaltimezone  *default_zone)
{
  return get_time_from_property (ical,
				 ICAL_DTSTART_PROPERTY,
				 icalproperty_get_dtstart,
                                 default_zone);
}

static inline time_t
get_ical_end_time (icalcomponent *ical,
                   icaltimezone  *default_zone)
{
  return get_time_from_property (ical,
				 ICAL_DTEND_PROPERTY,
				 icalproperty_get_dtend,
                                 default_zone);
}

static gboolean
get_ical_is_all_day (icalcomponent *ical,
                     time_t         start_time,
                     icaltimezone  *default_zone)
{
  icalproperty            *prop;
  struct tm               *start_tm;
  time_t                   end_time;
  struct icaldurationtype  duration;
  struct icaltimetype      start_icaltime;

  start_icaltime = icalcomponent_get_dtstart (ical);
  if (start_icaltime.is_date)
    return TRUE;

  start_tm = gmtime (&start_time);
  if (start_tm->tm_sec  != 0 ||
      start_tm->tm_min  != 0 ||
      start_tm->tm_hour != 0)
    return FALSE;

  if ((end_time = get_ical_end_time (ical, default_zone)))
    return (end_time - start_time) % 86400 == 0;

  prop = icalcomponent_get_first_property (ical, ICAL_DURATION_PROPERTY);
  if (!prop)
    return FALSE;

  duration = icalproperty_get_duration (prop);

  return icaldurationtype_as_int (duration) % 86400 == 0;
}

static inline time_t
get_ical_due_time (icalcomponent *ical,
                   icaltimezone  *default_zone)
{
  return get_time_from_property (ical,
				 ICAL_DUE_PROPERTY,
				 icalproperty_get_due,
                                 default_zone);
}

static guint
get_ical_percent_complete (icalcomponent *ical)
{
  icalproperty *prop;
  icalproperty_status status;
  int           percent_complete;

  status = icalcomponent_get_status (ical);
  if (status == ICAL_STATUS_COMPLETED)
    return 100;

  prop = icalcomponent_get_first_property (ical, ICAL_COMPLETED_PROPERTY);
  if (prop)
    return 100;

  prop = icalcomponent_get_first_property (ical, ICAL_PERCENTCOMPLETE_PROPERTY);
  if (!prop)
    return 0;

  percent_complete = icalproperty_get_percentcomplete (prop);

  return CLAMP (percent_complete, 0, 100);
}

static inline time_t
get_ical_completed_time (icalcomponent *ical,
                         icaltimezone  *default_zone)
{
  return get_time_from_property (ical,
				 ICAL_COMPLETED_PROPERTY,
				 icalproperty_get_completed,
                                 default_zone);
}

static int
get_ical_priority (icalcomponent *ical)
{
  icalproperty *prop;

  prop = icalcomponent_get_first_property (ical, ICAL_PRIORITY_PROPERTY);
  if (!prop)
    return -1;

  return icalproperty_get_priority (prop);
}

static char *
get_source_color (ECal *esource)
{
  ESource *source;

  g_return_val_if_fail (E_IS_CAL (esource), NULL);

  source = e_cal_get_source (esource);

  return g_strdup (e_source_peek_color_spec (source));
}

static gchar *
get_source_uri (ECal *esource)
{
    ESource *source;
    gchar   *string;
    gchar  **list;

    g_return_val_if_fail (E_IS_CAL (esource), NULL);

    source = e_cal_get_source (esource);
    string = g_strdup (e_source_get_uri (source));
    if (string) {
        list = g_strsplit (string, ":", 2);
        g_free (string);

        if (list[0]) {
            string = g_strdup (list[0]);
            g_strfreev (list);
            return string;
        }
	g_strfreev (list);
    }
    return NULL;
}

static inline int
null_safe_strcmp (const char *a,
		  const char *b)
{
  return (!a && !b) ? 0 : (a && !b) || (!a && b) ? 1 : strcmp (a, b);
}

static inline gboolean
calendar_appointment_equal (CalendarAppointment *a,
			    CalendarAppointment *b)
{
  GSList *la, *lb;

  if (g_slist_length (a->occurrences) != g_slist_length (b->occurrences))
      return FALSE;

  for (la = a->occurrences, lb = b->occurrences; la && lb; la = la->next, lb = lb->next)
    {
      CalendarOccurrence *oa = la->data;
      CalendarOccurrence *ob = lb->data;

      if (oa->start_time != ob->start_time ||
	  oa->end_time   != ob->end_time)
	return FALSE;
    }

  return
    null_safe_strcmp (a->uid,          b->uid)          == 0 &&
    null_safe_strcmp (a->uri,          b->uri)          == 0 &&
    null_safe_strcmp (a->summary,      b->summary)      == 0 &&
    null_safe_strcmp (a->description,  b->description)  == 0 &&
    null_safe_strcmp (a->color_string, b->color_string) == 0 &&
    a->start_time == b->start_time                         &&
    a->end_time   == b->end_time                           &&
    a->is_all_day == b->is_all_day;
}

static void
calendar_appointment_copy (CalendarAppointment *appointment,
			   CalendarAppointment *appointment_copy)
{
  GSList *l;

  g_assert (appointment != NULL);
  g_assert (appointment_copy != NULL);

  appointment_copy->occurrences = g_slist_copy (appointment->occurrences);
  for (l = appointment_copy->occurrences; l; l = l->next)
    {
      CalendarOccurrence *occurrence = l->data;
      CalendarOccurrence *occurrence_copy;

      occurrence_copy             = g_new0 (CalendarOccurrence, 1);
      occurrence_copy->start_time = occurrence->start_time;
      occurrence_copy->end_time   = occurrence->end_time;

      l->data = occurrence_copy;
    }

  appointment_copy->uid          = g_strdup (appointment->uid);
  appointment_copy->uri          = g_strdup (appointment->uri);
  appointment_copy->summary      = g_strdup (appointment->summary);
  appointment_copy->description  = g_strdup (appointment->description);
  appointment_copy->color_string = g_strdup (appointment->color_string);
  appointment_copy->start_time   = appointment->start_time;
  appointment_copy->end_time     = appointment->end_time;
  appointment_copy->is_all_day   = appointment->is_all_day;
}

static void
calendar_appointment_finalize (CalendarAppointment *appointment)
{
  GSList *l;

  for (l = appointment->occurrences; l; l = l->next)
    g_free (l->data);
  g_slist_free (appointment->occurrences);
  appointment->occurrences = NULL;

  g_free (appointment->uid);
  appointment->uid = NULL;

  g_free (appointment->rid);
  appointment->rid = NULL;

  g_free (appointment->uri);
  appointment->uri = NULL;

  g_free (appointment->summary);
  appointment->summary = NULL;

  g_free (appointment->description);
  appointment->description = NULL;

  g_free (appointment->color_string);
  appointment->color_string = NULL;

  appointment->start_time = 0;
  appointment->is_all_day = FALSE;
}

static void
calendar_appointment_init (CalendarAppointment  *appointment,
			   icalcomponent        *ical,
                           CalendarClientSource *source,
                           icaltimezone         *default_zone)
{
  appointment->uid          = get_ical_uid (ical);
  appointment->rid          = get_ical_rid (ical);
  appointment->uri          = get_source_uri (source->source);
  appointment->summary      = get_ical_summary (ical);
  appointment->description  = get_ical_description (ical);
  appointment->color_string = get_source_color (source->source);
  appointment->start_time   = get_ical_start_time (ical, default_zone);
  appointment->end_time     = get_ical_end_time (ical, default_zone);
  appointment->is_all_day   = get_ical_is_all_day (ical,
                                                   appointment->start_time,
                                                   default_zone);
}

static icaltimezone *
resolve_timezone_id (const char *tzid,
		     ECal       *source)
{
  icaltimezone *retval;

  retval = icaltimezone_get_builtin_timezone_from_tzid (tzid);
  if (!retval)
    {
      e_cal_get_timezone (source, tzid, &retval, NULL);
    }

  return retval;
}

static gboolean
calendar_appointment_collect_occurrence (ECalComponent  *component,
					 time_t          occurrence_start,
					 time_t          occurrence_end,
					 gpointer        data)
{
  CalendarOccurrence *occurrence;
  GSList **collect_loc = data;

  occurrence             = g_new0 (CalendarOccurrence, 1);
  occurrence->start_time = occurrence_start;
  occurrence->end_time   = occurrence_end;

  *collect_loc = g_slist_prepend (*collect_loc, occurrence);

  return TRUE;
}

static void
calendar_appointment_generate_ocurrences (CalendarAppointment *appointment,
					  icalcomponent       *ical,
					  ECal                *source,
					  time_t               start,
					  time_t               end,
                                          icaltimezone        *default_zone)
{
  ECalComponent *ecal;

  g_assert (appointment->occurrences == NULL);

  ecal = e_cal_component_new ();
  e_cal_component_set_icalcomponent (ecal,
				     icalcomponent_new_clone (ical));

  e_cal_recur_generate_instances (ecal,
				  start,
				  end,
				  calendar_appointment_collect_occurrence,
				  &appointment->occurrences,
				  (ECalRecurResolveTimezoneFn) resolve_timezone_id,
				  source,
				  default_zone);

  g_object_unref (ecal);

  appointment->occurrences = g_slist_reverse (appointment->occurrences);
}

static inline gboolean
calendar_task_equal (CalendarTask *a,
		     CalendarTask *b)
{
  return
    null_safe_strcmp (a->uid,          b->uid)          == 0 &&
    null_safe_strcmp (a->summary,      b->summary)      == 0 &&
    null_safe_strcmp (a->description,  b->description)  == 0 &&
    null_safe_strcmp (a->color_string, b->color_string) == 0 &&
    a->start_time       == b->start_time                   &&
    a->due_time         == b->due_time                     &&
    a->percent_complete == b->percent_complete             &&
    a->completed_time   == b->completed_time               &&
    a->priority         == b->priority;
}

static void
calendar_task_copy (CalendarTask *task,
		    CalendarTask *task_copy)
{
  g_assert (task != NULL);
  g_assert (task_copy != NULL);

  task_copy->uid              = g_strdup (task->uid);
  task_copy->summary          = g_strdup (task->summary);
  task_copy->description      = g_strdup (task->description);
  task_copy->color_string     = g_strdup (task->color_string);
  task_copy->start_time       = task->start_time;
  task_copy->due_time         = task->due_time;
  task_copy->percent_complete = task->percent_complete;
  task_copy->completed_time   = task->completed_time;
  task_copy->priority         = task->priority;
}

static void
calendar_task_finalize (CalendarTask *task)
{
  g_free (task->uid);
  task->uid = NULL;

  g_free (task->summary);
  task->summary = NULL;

  g_free (task->description);
  task->description = NULL;

  g_free (task->color_string);
  task->color_string = NULL;

  task->percent_complete = 0;
}

static void
calendar_task_init (CalendarTask         *task,
		    icalcomponent        *ical,
                    CalendarClientSource *source,
                    icaltimezone         *default_zone)
{
  task->uid              = get_ical_uid (ical);
  task->summary          = get_ical_summary (ical);
  task->description      = get_ical_description (ical);
  task->color_string     = get_source_color (source->source);
  task->start_time       = get_ical_start_time (ical, default_zone);
  task->due_time         = get_ical_due_time (ical, default_zone);
  task->percent_complete = get_ical_percent_complete (ical);
  task->completed_time   = get_ical_completed_time (ical, default_zone);
  task->priority         = get_ical_priority (ical);
}

void
calendar_event_free (CalendarEvent *event)
{
  switch (event->type)
    {
    case CALENDAR_EVENT_APPOINTMENT:
      calendar_appointment_finalize (CALENDAR_APPOINTMENT (event));
      break;
    case CALENDAR_EVENT_TASK:
      calendar_task_finalize (CALENDAR_TASK (event));
      break;
    default:
      g_assert_not_reached ();
      break;
    }

  g_free (event);
}

static CalendarEvent *
calendar_event_new (icalcomponent        *ical,
                    CalendarClientSource *source,
                    icaltimezone         *default_zone)
{
  CalendarEvent *event;

  event = g_new0 (CalendarEvent, 1);

  switch (icalcomponent_isa (ical))
    {
    case ICAL_VEVENT_COMPONENT:
      event->type = CALENDAR_EVENT_APPOINTMENT;
      calendar_appointment_init (CALENDAR_APPOINTMENT (event),
                                 ical,
                                 source,
                                 default_zone);
      break;
    case ICAL_VTODO_COMPONENT:
      event->type = CALENDAR_EVENT_TASK;
      calendar_task_init (CALENDAR_TASK (event),
                          ical,
                          source,
                          default_zone);
      break;
    default:
      g_warning ("Unknown calendar component type: %d\n",
                 icalcomponent_isa (ical));
      g_free (event);
      return NULL;
    }

  return event;
}

static CalendarEvent *
calendar_event_copy (CalendarEvent *event)
{
  CalendarEvent *retval;

  if (!event)
    return NULL;

  retval = g_new0 (CalendarEvent, 1);

  retval->type = event->type;

  switch (event->type)
    {
    case CALENDAR_EVENT_APPOINTMENT:
      calendar_appointment_copy (CALENDAR_APPOINTMENT (event),
				 CALENDAR_APPOINTMENT (retval));
      break;
    case CALENDAR_EVENT_TASK:
      calendar_task_copy (CALENDAR_TASK (event),
			  CALENDAR_TASK (retval));
      break;
    default:
      g_assert_not_reached ();
      break;
    }

  return retval;
}

static char *
calendar_event_get_uid (CalendarEvent *event)
{
  switch (event->type)
    {
    case CALENDAR_EVENT_APPOINTMENT:
      return g_strdup_printf ("%s%s", CALENDAR_APPOINTMENT (event)->uid, CALENDAR_APPOINTMENT (event)->rid ? CALENDAR_APPOINTMENT (event)->rid : ""); 
      break;
    case CALENDAR_EVENT_TASK:
      return g_strdup (CALENDAR_TASK (event)->uid);
      break;
    default:
      g_assert_not_reached ();
      break;
    }

  return NULL;
}

static gboolean
calendar_event_equal (CalendarEvent *a,
		      CalendarEvent *b)
{
  if (!a && !b)
    return TRUE;

  if ((a && !b) || (!a && b))
    return FALSE;

  if (a->type != b->type)
    return FALSE;

  switch (a->type)
    {
    case CALENDAR_EVENT_APPOINTMENT:
      return calendar_appointment_equal (CALENDAR_APPOINTMENT (a),
					 CALENDAR_APPOINTMENT (b));
    case CALENDAR_EVENT_TASK:
      return calendar_task_equal (CALENDAR_TASK (a),
				  CALENDAR_TASK (b));
    default:
      break;
    }
 
  g_assert_not_reached ();

  return FALSE;
}

static void
calendar_event_generate_ocurrences (CalendarEvent *event,
				    icalcomponent *ical,
				    ECal          *source,
				    time_t         start,
				    time_t         end,
                                    icaltimezone  *default_zone)
{
  if (event->type != CALENDAR_EVENT_APPOINTMENT)
    return;

  calendar_appointment_generate_ocurrences (CALENDAR_APPOINTMENT (event),
					    ical,
					    source,
					    start,
					    end,
                                            default_zone);
}

static inline void
calendar_event_debug_dump (CalendarEvent *event)
{
#ifdef CALENDAR_ENABLE_DEBUG
  switch (event->type)
    {
    case CALENDAR_EVENT_APPOINTMENT:
      {
	char   *start_str;
	char   *end_str;
	GSList *l;

	start_str = CALENDAR_APPOINTMENT (event)->start_time ?
	                    isodate_from_time_t (CALENDAR_APPOINTMENT (event)->start_time) :
	                    g_strdup ("(undefined)");
	end_str = CALENDAR_APPOINTMENT (event)->end_time ?
	                    isodate_from_time_t (CALENDAR_APPOINTMENT (event)->end_time) :
	                    g_strdup ("(undefined)");
	  
	dprintf ("Appointment: uid '%s', summary '%s', description '%s', "
		 "start_time '%s', end_time '%s', is_all_day %s\n",
		 CALENDAR_APPOINTMENT (event)->uid,
		 CALENDAR_APPOINTMENT (event)->summary,
		 CALENDAR_APPOINTMENT (event)->description,
		 start_str,
		 end_str,
		 CALENDAR_APPOINTMENT (event)->is_all_day ? "(true)" : "(false)");

	g_free (start_str);
	g_free (end_str);

	dprintf ("  Occurrences:\n");
	for (l = CALENDAR_APPOINTMENT (event)->occurrences; l; l = l->next)
	  {
	    CalendarOccurrence *occurrence = l->data;

	    start_str = occurrence->start_time ?
	      isodate_from_time_t (occurrence->start_time) :
	      g_strdup ("(undefined)");
	    
	    end_str = occurrence->end_time ?
	      isodate_from_time_t (occurrence->end_time) :
	      g_strdup ("(undefined)");

	    dprintf ("    start_time '%s', end_time '%s'\n",
		     start_str, end_str);

	    g_free (start_str);
	    g_free (end_str);
	  }
      }
      break;
    case CALENDAR_EVENT_TASK:
      {
	char *start_str;
	char *due_str;
	char *completed_str;

	start_str = CALENDAR_TASK (event)->start_time ?
	                    isodate_from_time_t (CALENDAR_TASK (event)->start_time) :
	                    g_strdup ("(undefined)");
	due_str = CALENDAR_TASK (event)->due_time ?
	                    isodate_from_time_t (CALENDAR_TASK (event)->due_time) :
	                    g_strdup ("(undefined)");
	completed_str = CALENDAR_TASK (event)->completed_time ?
	                    isodate_from_time_t (CALENDAR_TASK (event)->completed_time) :
	                    g_strdup ("(undefined)");

	dprintf ("Task: uid '%s', summary '%s', description '%s', "
		 "start_time '%s', due_time '%s', percent_complete %d, completed_time '%s'\n",
		 CALENDAR_TASK (event)->uid,
		 CALENDAR_TASK (event)->summary,
		 CALENDAR_TASK (event)->description,
		 start_str,
		 due_str,
		 CALENDAR_TASK (event)->percent_complete,
		 completed_str);

	g_free (completed_str);
      }
      break;
    default:
      g_assert_not_reached ();
      break;
    }
#endif
}

static inline CalendarClientQuery *
goddamn_this_is_crack (CalendarClientSource *source,
		       ECalView             *view,
		       gboolean             *emit_signal)
{
  g_assert (view != NULL);

  if (source->completed_query.view == view)
    {
      if (emit_signal)
	*emit_signal = TRUE;
      return &source->completed_query;
    }
  else if (source->in_progress_query.view == view)
    {
      if (emit_signal)
	*emit_signal = FALSE;
      return &source->in_progress_query;
    }

  g_assert_not_reached ();

  return NULL;
}

static void
calendar_client_handle_query_completed (CalendarClientSource *source,
					ECalendarStatus       status,
					ECalView             *view)
{
  CalendarClientQuery *query;

  query = goddamn_this_is_crack (source, view, NULL);
  
  dprintf ("Query %p completed: %s\n", query, e_cal_get_error_message (status));

  if (status != E_CALENDAR_STATUS_OK)
    {
      g_warning ("Calendar query failed: %s\n",
		 e_cal_get_error_message (status));
      calendar_client_stop_query (source->client, source, query);
      return;
    }

  g_assert (source->query_in_progress != FALSE);
  g_assert (query == &source->in_progress_query);

  calendar_client_query_finalize (&source->completed_query);

  source->completed_query = source->in_progress_query;
  source->query_completed = TRUE;

  source->query_in_progress        = FALSE;
  source->in_progress_query.view   = NULL;
  source->in_progress_query.events = NULL;

  g_signal_emit (source->client, source->changed_signal_id, 0);
}

static void
calendar_client_handle_query_result (CalendarClientSource *source,
				     GList                *objects,
				     ECalView             *view)
{
  CalendarClientQuery *query;
  CalendarClient      *client;
  gboolean             emit_signal;
  gboolean             events_changed;
  GList               *l;
  time_t               month_begin;
  time_t               month_end;

  client = source->client;

  query = goddamn_this_is_crack (source, view, &emit_signal);

  dprintf ("Query %p result: %d objects:\n",
	   query, g_list_length (objects));

  month_begin = make_time_for_day_begin (1,
					 client->priv->month,
					 client->priv->year);

  month_end = make_time_for_day_begin (1,
				       client->priv->month + 1,
				       client->priv->year);

  events_changed = FALSE;
  for (l = objects; l; l = l->next)
    {
      CalendarEvent *event;
      CalendarEvent *old_event;
      icalcomponent *ical = l->data;
      char          *uid;
      
      event = calendar_event_new (ical, source, client->priv->zone);
      if (!event)
	      continue;

      calendar_event_generate_ocurrences (event,
					  ical,
					  source->source,
					  month_begin,
					  month_end,
                                          client->priv->zone);

      uid = calendar_event_get_uid (event);
      
      old_event = g_hash_table_lookup (query->events, uid);

      if (!calendar_event_equal (event, old_event))
	{
 	  dprintf ("Event %s: ", old_event ? "modified" : "added");

	  calendar_event_debug_dump (event);

	  g_hash_table_replace (query->events, uid, event);

	  events_changed = TRUE;
	}
      else
	{
	  g_free (uid);
	}		
    }

  if (emit_signal && events_changed)
    {
      g_signal_emit (source->client, source->changed_signal_id, 0);
    }
}

static gboolean
check_object_remove (gpointer key,
                     gpointer value,
                     gpointer data)
{
  char             *uid = data;
  ssize_t           len;

  len = strlen (uid);
  
  if (len <= strlen (key) && strncmp (uid, key, len) == 0)
    {
      dprintf ("Event removed: ");

      calendar_event_debug_dump (value);

      return TRUE;
    }

  return FALSE;
}

static void
calendar_client_handle_objects_removed (CalendarClientSource *source,
					GList                *ids,
					ECalView             *view)
{
  CalendarClientQuery *query;
  gboolean             emit_signal;
  gboolean             events_changed;
  GList               *l;

  query = goddamn_this_is_crack (source, view, &emit_signal);

  events_changed = FALSE;
  for (l = ids; l; l = l->next)
    {
      CalendarEvent   *event;
      ECalComponentId *id = l->data;
      char            *uid = g_strdup_printf ("%s%s", id->uid, id->rid ? id->rid : "");

      if (!id->rid || !(*id->rid))
	{
	  int size = g_hash_table_size (query->events);

	  g_hash_table_foreach_remove (query->events, check_object_remove, id->uid);

		if (size != g_hash_table_size (query->events))
			events_changed = TRUE;		
	}
      else if ((event = g_hash_table_lookup (query->events, uid)))
	{
	  dprintf ("Event removed: ");

	  calendar_event_debug_dump (event);

	  g_assert (g_hash_table_remove (query->events, uid));

	  events_changed = TRUE;
	}
      g_free (uid);
    }

  if (emit_signal && events_changed)
    {
      g_signal_emit (source->client, source->changed_signal_id, 0);
    }
}

static void
calendar_client_query_finalize (CalendarClientQuery *query)
{
  if (query->view)
    g_object_unref (query->view);
  query->view = NULL;

  if (query->events)
    g_hash_table_destroy (query->events);
  query->events = NULL;
}

static void
calendar_client_stop_query (CalendarClient       *client,
			    CalendarClientSource *source,
			    CalendarClientQuery  *query)
{
  if (query == &source->in_progress_query)
    {
      dprintf ("Stopping in progress query %p\n", query);

      g_assert (source->query_in_progress != FALSE);

      source->query_in_progress = FALSE;
    }
  else if (query == &source->completed_query)
    {
      dprintf ("Stopping completed query %p\n", query);

      g_assert (source->query_completed != FALSE);

      source->query_completed = FALSE;
    }
  else
    g_assert_not_reached ();
  
  calendar_client_query_finalize (query);
}

static void
calendar_client_start_query (CalendarClient       *client,
			     CalendarClientSource *source,
			     const char           *query)
{
  ECalView *view = NULL;
  GError   *error = NULL;

  if (!e_cal_get_query (source->source, query, &view, &error))
    {
      g_warning ("Error preparing the query: '%s': %s\n",
		 query, error->message);
      g_error_free (error);
      return;
    }

  g_assert (view != NULL);

  if (source->query_in_progress)
    calendar_client_stop_query (client, source, &source->in_progress_query);
  
  dprintf ("Starting query %p: '%s'\n", &source->in_progress_query, query);

  source->query_in_progress        = TRUE;
  source->in_progress_query.view   = view;
  source->in_progress_query.events =
    g_hash_table_new_full (g_str_hash,
			   g_str_equal,
			   g_free,
			   (GDestroyNotify) calendar_event_free);

  g_signal_connect_swapped (view, "objects-added",
			    G_CALLBACK (calendar_client_handle_query_result),
			    source);
  g_signal_connect_swapped (view, "objects-modified",
			    G_CALLBACK (calendar_client_handle_query_result),
			    source);
  g_signal_connect_swapped (view, "objects-removed",
			    G_CALLBACK (calendar_client_handle_objects_removed),
			    source);
  g_signal_connect_swapped (view, "view-done",
			    G_CALLBACK (calendar_client_handle_query_completed),
			    source);

  e_cal_view_start (view);
}

static void
calendar_client_update_appointments (CalendarClient *client)
{
  GSList *l;
  char   *query;
  char   *month_begin;
  char   *month_end;

  if (client->priv->month == -1 ||
      client->priv->year  == -1)
    return;

  month_begin = make_isodate_for_day_begin (1,
					    client->priv->month,
					    client->priv->year);

  month_end = make_isodate_for_day_begin (1,
					  client->priv->month + 1,
					  client->priv->year);

  query = g_strdup_printf ("occur-in-time-range? (make-time \"%s\") "
			                        "(make-time \"%s\")",
			   month_begin, month_end);

  for (l = client->priv->appointment_sources; l; l = l->next)
    {
      CalendarClientSource *cs = l->data;
                  
      if (e_cal_get_load_state (cs->source) != E_CAL_LOAD_LOADED)  
        continue;

      calendar_client_start_query (client, cs, query);
    }

  g_free (month_begin);
  g_free (month_end);
  g_free (query);
}

/* FIXME:
 * perhaps we should use evo's "hide_completed_tasks" pref?
 */
static void
calendar_client_update_tasks (CalendarClient *client)
{
  GSList *l;
  char   *query;

#ifdef FIX_BROKEN_TASKS_QUERY
  /* FIXME: this doesn't work for tasks without a start or
   *        due date
   *        Look at filter_task() to see the behaviour we
   *        want.
   */
  
  char   *day_begin;
  char   *day_end;

  if (client->priv->day   == -1 ||
      client->priv->month == -1 ||
      client->priv->year  == -1)
    return;

  day_begin = make_isodate_for_day_begin (client->priv->day,
					  client->priv->month,
					  client->priv->year);

  day_end = make_isodate_for_day_begin (client->priv->day + 1,
					client->priv->month,
					client->priv->year);
  if (!day_begin || !day_end)
    {
      g_warning ("Cannot run query with invalid date: %dd %dy %dm\n",
		 client->priv->day,
		 client->priv->month,
		 client->priv->year);
      g_free (day_begin);
      g_free (day_end);
      return;
    }
  
  query = g_strdup_printf ("(and (occur-in-time-range? (make-time \"%s\") "
                                                      "(make-time \"%s\")) "
                             "(or (not is-completed?) "
                               "(and (is-completed?) "
                                    "(not (completed-before? (make-time \"%s\"))))))",
			   day_begin, day_end, day_begin);
#else
  query = g_strdup ("#t");
#endif /* FIX_BROKEN_TASKS_QUERY */

  for (l = client->priv->task_sources; l; l = l->next)
    {
      CalendarClientSource *cs = l->data;

      if (e_cal_get_load_state (cs->source) != E_CAL_LOAD_LOADED)  
        continue;

      calendar_client_start_query (client, cs, query);
    }

#ifdef FIX_BROKEN_TASKS_QUERY
  g_free (day_begin);
  g_free (day_end);
#endif
  g_free (query);
}

static void
calendar_client_source_finalize (CalendarClientSource *source)
{
  source->client = NULL;

  if (source->source) {
    g_signal_handlers_disconnect_by_func (source->source,
                                          cal_opened_cb, source);
    g_object_unref (source->source);
  }
  source->source = NULL;

  calendar_client_query_finalize (&source->completed_query);
  calendar_client_query_finalize (&source->in_progress_query);
  
  source->query_completed   = FALSE;
  source->query_in_progress = FALSE;
}

static int
compare_calendar_sources (CalendarClientSource *s1,
			  CalendarClientSource *s2)
{
  return (s1->source == s2->source) ? 0 : 1;
}

static GSList *
calendar_client_update_sources_list (CalendarClient *client,
				     GSList         *sources,
				     GSList         *esources,
				     guint           changed_signal_id)
{
  GSList *retval, *l;

  retval = NULL;

  for (l = esources; l; l = l->next)
    {
      CalendarClientSource  dummy_source;
      CalendarClientSource *new_source;
      GSList               *s;
      ECal                 *esource = l->data;

      dummy_source.source = esource;

      dprintf ("update_sources_list: adding client %s: ",
	       e_source_peek_uid (e_cal_get_source (esource)));

      if ((s = g_slist_find_custom (sources,
				    &dummy_source,
				    (GCompareFunc) compare_calendar_sources)))
	{
	  dprintf ("already on list\n");
	  new_source = s->data;
	  sources = g_slist_delete_link (sources, s);
	}
      else
	{
	  dprintf ("added\n");
	  new_source                    = g_new0 (CalendarClientSource, 1);
	  new_source->client            = client;
	  new_source->source            = g_object_ref (esource);
	  new_source->changed_signal_id = changed_signal_id;
	}

      retval = g_slist_prepend (retval, new_source);
    }

  for (l = sources; l; l = l->next)
    {
      CalendarClientSource *source = l->data;

      dprintf ("Removing client %s from list\n",
	       e_source_peek_uid (e_cal_get_source (source->source)));

      calendar_client_source_finalize (source);
      g_free (source);
    }
  g_slist_free (sources);

  return retval;
}

static void
calendar_client_appointment_sources_changed (CalendarClient  *client)
{
  GSList *esources;

  dprintf ("appointment_sources_changed: updating ...\n");

  esources = calendar_sources_get_appointment_sources (client->priv->calendar_sources);

  client->priv->appointment_sources = 
    calendar_client_update_sources_list (client,
					 client->priv->appointment_sources,
					 esources,
					 signals [APPOINTMENTS_CHANGED]);

  load_calendars (client, CALENDAR_EVENT_APPOINTMENT);
  calendar_client_update_appointments (client);
}

static void
calendar_client_task_sources_changed (CalendarClient  *client)
{
  GSList *esources;

  dprintf ("task_sources_changed: updating ...\n");

  esources = calendar_sources_get_task_sources (client->priv->calendar_sources);

  client->priv->task_sources = 
    calendar_client_update_sources_list (client,
					 client->priv->task_sources,
					 esources,
					 signals [TASKS_CHANGED]);

  load_calendars (client, CALENDAR_EVENT_TASK);
  calendar_client_update_tasks (client);
}

void
calendar_client_get_date (CalendarClient *client,
                          guint          *year,
                          guint          *month,
                          guint          *day)
{
  g_return_if_fail (CALENDAR_IS_CLIENT (client));

  if (year)
    *year = client->priv->year;

  if (month)
    *month = client->priv->month;

  if (day)
    *day = client->priv->day;
}

void
calendar_client_select_month (CalendarClient *client,
			      guint           month,
			      guint           year)
{
  g_return_if_fail (CALENDAR_IS_CLIENT (client));
  g_return_if_fail (month <= 11);

  if (client->priv->year != year || client->priv->month != month)
    {
      client->priv->month = month;
      client->priv->year  = year;

      calendar_client_update_appointments (client);
      calendar_client_update_tasks (client);

      g_object_freeze_notify (G_OBJECT (client));
      g_object_notify (G_OBJECT (client), "month");
      g_object_notify (G_OBJECT (client), "year");
      g_object_thaw_notify (G_OBJECT (client));
    }
}

void
calendar_client_select_day (CalendarClient *client,
			    guint           day)
{
  g_return_if_fail (CALENDAR_IS_CLIENT (client));
  g_return_if_fail (day <= 31);

  if (client->priv->day != day)
    {
      client->priv->day = day;

      /* don't need to update appointments unless
       * the selected month changes
       */
#ifdef FIX_BROKEN_TASKS_QUERY
      calendar_client_update_tasks (client);
#endif

      g_object_notify (G_OBJECT (client), "day");
    }
}

typedef struct
{
  CalendarClient *client;
  GSList         *events;
  time_t          start_time;
  time_t          end_time;
} FilterData;

typedef void (* CalendarEventFilterFunc) (const char    *uid,
					  CalendarEvent *event,
					  FilterData    *filter_data);

static void
filter_appointment (const char    *uid,
		    CalendarEvent *event,
		    FilterData    *filter_data)
{
  GSList *occurrences, *l;

  if (event->type != CALENDAR_EVENT_APPOINTMENT)
    return;

  occurrences = CALENDAR_APPOINTMENT (event)->occurrences;
  CALENDAR_APPOINTMENT (event)->occurrences = NULL;

  for (l = occurrences; l; l = l->next)
    {
      CalendarOccurrence *occurrence = l->data;
      time_t start_time = occurrence->start_time;
      time_t end_time   = occurrence->end_time;

      if ((start_time >= filter_data->start_time &&
           start_time < filter_data->end_time) ||
          (start_time <= filter_data->start_time &&
           (end_time - 1) > filter_data->start_time))
	{
	  CalendarEvent *new_event;

	  new_event = calendar_event_copy (event);
	      
	  CALENDAR_APPOINTMENT (new_event)->start_time = occurrence->start_time;
	  CALENDAR_APPOINTMENT (new_event)->end_time   = occurrence->end_time;
	      
	  filter_data->events = g_slist_prepend (filter_data->events, new_event);
	}
    }

  CALENDAR_APPOINTMENT (event)->occurrences = occurrences;
}

static void
filter_task (const char    *uid,
	     CalendarEvent *event,
	     FilterData    *filter_data)
{
#ifdef FIX_BROKEN_TASKS_QUERY
  CalendarTask *task;
#endif

  if (event->type != CALENDAR_EVENT_TASK)
    return;

#ifdef FIX_BROKEN_TASKS_QUERY
  task = CALENDAR_TASK (event);

  if (task->start_time && task->start_time > filter_data->start_time)
    return;

  if (task->completed_time && 
      (task->completed_time < filter_data->start_time ||
       task->completed_time > filter_data->end_time))
    return;
#endif /* FIX_BROKEN_TASKS_QUERY */

  filter_data->events = g_slist_prepend (filter_data->events,
					 calendar_event_copy (event));
}

static GSList *
calendar_client_filter_events (CalendarClient          *client,
			       GSList                  *sources,
			       CalendarEventFilterFunc  filter_func,
			       time_t                   start_time,
			       time_t                   end_time)
{
  FilterData  filter_data;
  GSList     *l;
  GSList     *retval;

  if (!sources)
    return NULL;

  filter_data.client     = client;
  filter_data.events     = NULL;
  filter_data.start_time = start_time;
  filter_data.end_time   = end_time;

  retval = NULL;
  for (l = sources; l; l = l->next)
    {
      CalendarClientSource *source = l->data;

      if (source->query_completed)
	{
	  filter_data.events = NULL;
	  g_hash_table_foreach (source->completed_query.events,
				(GHFunc) filter_func,
				&filter_data);

	  filter_data.events = g_slist_reverse (filter_data.events);

	  retval = g_slist_concat (retval, filter_data.events);
	}
    }

  return retval;
}

GSList *
calendar_client_get_events (CalendarClient    *client,
			    CalendarEventType  event_mask)
{
  GSList *appointments;
  GSList *tasks;
  time_t  day_begin;
  time_t  day_end;

  g_return_val_if_fail (CALENDAR_IS_CLIENT (client), NULL);
  g_return_val_if_fail (client->priv->day   != -1 &&
			client->priv->month != -1 &&
			client->priv->year  != -1, NULL);

  day_begin = make_time_for_day_begin (client->priv->day,
				       client->priv->month,
				       client->priv->year);
  day_end   = make_time_for_day_begin (client->priv->day + 1,
				       client->priv->month,
				       client->priv->year);

  appointments = NULL;
  if (event_mask & CALENDAR_EVENT_APPOINTMENT)
    {
      appointments = calendar_client_filter_events (client,
						    client->priv->appointment_sources,
						    filter_appointment,
						    day_begin,
						    day_end);
    }

  tasks = NULL;
  if (event_mask & CALENDAR_EVENT_TASK)
    {
      tasks = calendar_client_filter_events (client,
					     client->priv->task_sources,
					     filter_task,
					     day_begin,
					     day_end);
    }

  return g_slist_concat (appointments, tasks);
}

static inline int
day_from_time_t (time_t t)
{
  struct tm *tm = localtime (&t);

  g_assert (tm == NULL || (tm->tm_mday >=1 && tm->tm_mday <= 31));

  return tm ? tm->tm_mday : 0;
}

void
calendar_client_foreach_appointment_day (CalendarClient  *client,
					 CalendarDayIter  iter_func,
					 gpointer         user_data)
{
  GSList   *appointments, *l;
  gboolean  marked_days [32] = { FALSE, };
  time_t    month_begin;
  time_t    month_end;
  int       i;

  g_return_if_fail (CALENDAR_IS_CLIENT (client));
  g_return_if_fail (iter_func != NULL);
  g_return_if_fail (client->priv->month != -1 &&
		    client->priv->year  != -1);

  month_begin = make_time_for_day_begin (1,
					 client->priv->month,
					 client->priv->year);
  month_end   = make_time_for_day_begin (1,
					 client->priv->month + 1,
					 client->priv->year);
  
  appointments = calendar_client_filter_events (client,
						client->priv->appointment_sources,
						filter_appointment,
						month_begin,
						month_end);
  for (l = appointments; l; l = l->next)
    {
      CalendarAppointment *appointment = l->data;

      if (appointment->start_time)
        {
          time_t day_time = appointment->start_time;

          if (day_time >= month_begin)
            marked_days [day_from_time_t (day_time)] = TRUE;
      
          if (appointment->end_time)
            {
              int day_offset;
              int duration = appointment->end_time - appointment->start_time;
	      /* mark the days for the appointment, no need to add an extra one when duration is a multiple of 86400 */
              for (day_offset = 1; day_offset <= duration / 86400 && duration != day_offset * 86400; day_offset++)
                {
                  time_t day_tm = appointment->start_time + day_offset * 86400;

                  if (day_tm > month_end)
                    break;
                  if (day_tm >= month_begin)
                    marked_days [day_from_time_t (day_tm)] = TRUE;
                }
            }
        }
      calendar_event_free (CALENDAR_EVENT (appointment));
    }

  g_slist_free (appointments);

  for (i = 1; i < 32; i++)
    {
      if (marked_days [i])
	iter_func (client, i, user_data);
    }
}

void
calendar_client_set_task_completed (CalendarClient *client,
				    char           *task_uid,
				    gboolean        task_completed,
				    guint           percent_complete)
{
  GSList              *l;
  ECal                *esource;
  icalcomponent       *ical;
  icalproperty        *prop;
  icalproperty_status  status;

  g_return_if_fail (CALENDAR_IS_CLIENT (client));
  g_return_if_fail (task_uid != NULL);
  g_return_if_fail (task_completed == FALSE || percent_complete == 100);

  ical = NULL;
  esource = NULL;
  for (l = client->priv->task_sources; l; l = l->next)
    {
      CalendarClientSource *source = l->data;

      esource = source->source;
      e_cal_get_object (esource, task_uid, NULL, &ical, NULL);
      if (ical)
	break;
    }

  if (!ical)
    {
      g_warning ("Cannot locate task with uid = '%s'\n", task_uid);
      return;
    }

  g_assert (esource != NULL);

  /* Completed time */
  prop = icalcomponent_get_first_property (ical,
					   ICAL_COMPLETED_PROPERTY);
  if (task_completed)
    {
      struct icaltimetype  completed_time;

      completed_time = icaltime_current_time_with_zone (client->priv->zone);
      if (!prop)
	{
	  icalcomponent_add_property (ical,
				      icalproperty_new_completed (completed_time));
	}
      else
	{
	  icalproperty_set_completed (prop, completed_time);
	}
    }
  else if (prop)
    {
      icalcomponent_remove_property (ical, prop);
    }

  /* Percent complete */
  prop = icalcomponent_get_first_property (ical,
					   ICAL_PERCENTCOMPLETE_PROPERTY);
  if (!prop)
    {
      icalcomponent_add_property (ical,
				  icalproperty_new_percentcomplete (percent_complete));
    }
  else
    {
      icalproperty_set_percentcomplete (prop, percent_complete);
    }

  /* Status */
  status = task_completed ? ICAL_STATUS_COMPLETED : ICAL_STATUS_NEEDSACTION;
  prop = icalcomponent_get_first_property (ical, ICAL_STATUS_PROPERTY);
  if (prop)
    {
      icalproperty_set_status (prop, status);
    }
  else
    {
      icalcomponent_add_property (ical,
				  icalproperty_new_status (status));
    }

  e_cal_modify_object (esource, ical, CALOBJ_MOD_ALL, NULL);
}