/* gcal-calendar-monitor.c
 *
 * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN "GcalCalendarMonitor"

#include "gcal-calendar-monitor.h"
#include "gcal-core-macros.h"
#include "gcal-date-time-utils.h"
#include "gcal-debug.h"
#include "gcal-event.h"

#include <gio/gio.h>
#include <libecal/libecal.h>

typedef struct
{
  GcalCalendarMonitor *monitor;
  GPtrArray           *events;
  GPtrArray           *event_ids;
  gboolean             complete;
} IdleData;

typedef enum
{
  INVALID_EVENT,
  CREATE_VIEW,
  REMOVE_VIEW,
  RANGE_UPDATED,
  FILTER_UPDATED,
  QUIT,
} MonitorThreadEvent;

struct _GcalCalendarMonitor
{
  GObject             parent;

  GThread            *thread;
  GCancellable       *cancellable;
  GMainContext       *thread_context;
  GMainContext       *main_context;

  GAsyncQueue        *messages;
  GcalCalendar       *calendar;
  gboolean            complete;

  const GcalCalendarMonitorListener *listener;
  gpointer                           listener_user_data;

  /*
   * These fields are only accessed on the monitor thread, and
   * never on the main thread.
   */
  struct {
    gboolean          populated;
    GPtrArray        *events_to_add;
    ECalClientView   *view;
  } monitor_thread;

  /*
   * Accessing any of the fields below must be happen inside
   * a locked muxed.
   */
  struct {
    GRWLock           lock;
    GHashTable       *events; /* gchar* -> GcalEvent* */
    GcalRange        *range;
    gchar            *filter;
  } shared;
};

static gboolean      add_events_to_timeline_in_idle_cb           (gpointer           user_data);
static gboolean      update_events_in_idle_cb                    (gpointer           user_data);
static gboolean      remove_events_from_timeline_in_idle_cb      (gpointer           user_data);
static gboolean      complete_in_idle_cb                         (gpointer           user_data);

G_DEFINE_TYPE (GcalCalendarMonitor, gcal_calendar_monitor, G_TYPE_OBJECT)

enum
{
  PROP_0,
  PROP_CALENDAR,
  PROP_COMPLETE,
  N_PROPS
};

static GParamSpec *properties [N_PROPS] = { NULL, };

/*
 * Threads
 *
 * These methods must *never* be executed in the main thread.
 */

static gchar*
build_subscriber_filter (GcalRange   *range,
                         const gchar *filter)
{
  g_autoptr (GDateTime) inclusive_range_end = NULL;
  g_autoptr (GDateTime) utc_range_start = NULL;
  g_autoptr (GDateTime) utc_range_end = NULL;
  g_autoptr (GDateTime) range_start = NULL;
  g_autoptr (GDateTime) range_end = NULL;
  g_autofree gchar *start_str = NULL;
  g_autofree gchar *end_str = NULL;
  g_autofree gchar *result = NULL;

  /*
   * XXX: E-D-S ISO8601 parser is incomplete and doesn't accept the output of
   * g_date_time_format_iso8601().
   */

  range_start = gcal_range_get_start (range);
  utc_range_start = g_date_time_to_utc (range_start);
  start_str = g_date_time_format (utc_range_start, "%Y%m%dT%H%M%SZ");

  range_end = gcal_range_get_end (range);
  inclusive_range_end = g_date_time_add_seconds (range_end, -1);
  utc_range_end = g_date_time_to_utc (inclusive_range_end);
  end_str = g_date_time_format (utc_range_end, "%Y%m%dT%H%M%SZ");

  if (filter)
    {
      result = g_strdup_printf ("(and (occur-in-time-range? (make-time \"%s\") (make-time \"%s\")) %s)",
                                start_str,
                                end_str,
                                filter);
    }
  else
    {
      result = g_strdup_printf ("(occur-in-time-range? (make-time \"%s\") (make-time \"%s\"))",
                                start_str,
                                end_str);
    }

  return g_steal_pointer (&result);
}

static void
maybe_init_event_arrays (GcalCalendarMonitor *self)
{
  if (self->monitor_thread.populated)
    return;

  if (!self->monitor_thread.events_to_add)
    self->monitor_thread.events_to_add = g_ptr_array_new_with_free_func (g_object_unref);
}

static GcalRange*
get_monitor_ranges (GcalCalendarMonitor  *self)
{
  g_autoptr (GcalRange) range = NULL;

  g_rw_lock_reader_lock (&self->shared.lock);
  range = gcal_range_copy (self->shared.range);
  g_rw_lock_reader_unlock (&self->shared.lock);

  return g_steal_pointer (&range);
}

static void
idle_data_free (IdleData *data)
{
  g_clear_object (&data->monitor);
  g_clear_pointer (&data->events, g_ptr_array_unref);
  g_clear_pointer (&data->event_ids, g_ptr_array_unref);
  g_free (data);
}

static void
add_events_in_idle (GcalCalendarMonitor *self,
                    GPtrArray           *events)
{
  IdleData *idle_data;

  g_assert (GCAL_IS_THREAD (self->thread));

  idle_data = g_new0 (IdleData, 1);
  idle_data->monitor = g_object_ref (self);
  idle_data->events = g_ptr_array_ref (events);

  g_main_context_invoke_full (self->main_context,
                              G_PRIORITY_DEFAULT_IDLE,
                              add_events_to_timeline_in_idle_cb,
                              idle_data,
                              (GDestroyNotify) idle_data_free);
}

static void
update_events_in_idle (GcalCalendarMonitor *self,
                       GPtrArray           *events)
{
  IdleData *idle_data;

  g_assert (GCAL_IS_THREAD (self->thread));

  idle_data = g_new0 (IdleData, 1);
  idle_data->monitor = g_object_ref (self);
  idle_data->events = g_ptr_array_ref (events);

  g_main_context_invoke_full (self->main_context,
                              G_PRIORITY_DEFAULT_IDLE,
                              update_events_in_idle_cb,
                              idle_data,
                              (GDestroyNotify) idle_data_free);
}

static void
remove_events_in_idle (GcalCalendarMonitor *self,
                       GPtrArray           *events)
{
  IdleData *idle_data;

  g_assert (GCAL_IS_THREAD (self->thread));

  idle_data = g_new0 (IdleData, 1);
  idle_data->monitor = g_object_ref (self);
  idle_data->event_ids = g_ptr_array_ref (events);

  g_main_context_invoke_full (self->main_context,
                              G_PRIORITY_DEFAULT_IDLE,
                              remove_events_from_timeline_in_idle_cb,
                              idle_data,
                              (GDestroyNotify) idle_data_free);
}

static void
set_complete_in_idle (GcalCalendarMonitor *self,
                      gboolean             complete)
{
  IdleData *idle_data;

  g_assert (GCAL_IS_THREAD (self->thread));

  idle_data = g_new0 (IdleData, 1);
  idle_data->monitor = g_object_ref (self);
  idle_data->complete = complete;

  g_main_context_invoke_full (self->main_context,
                              G_PRIORITY_DEFAULT_IDLE,
                              complete_in_idle_cb,
                              idle_data,
                              (GDestroyNotify) idle_data_free);
}

typedef struct
{
  GcalCalendarMonitor *monitor;
  GPtrArray           *expanded_events;
} GenerateRecurrencesData;

static gboolean
client_instance_generated_cb (ICalComponent  *icomponent,
                              ICalTime       *instance_start,
                              ICalTime       *instance_end,
                              gpointer        user_data,
                              GCancellable   *cancellable,
                              GError        **error)
{
  g_autoptr (GcalEvent) event = NULL;
  g_autoptr (GError) local_error = NULL;
  GenerateRecurrencesData *data;
  GcalCalendarMonitor *self;
  ECalComponent *ecomponent;

  data = user_data;
  self = data->monitor;

  g_assert (GCAL_IS_THREAD (self->thread));

  if (g_cancellable_set_error_if_cancelled (cancellable, error))
    return FALSE;

  ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent));
  if (!ecomponent)
    return TRUE;

  event = gcal_event_new (self->calendar, ecomponent, &local_error);
  g_clear_object (&ecomponent);
  if (local_error)
    {
      g_propagate_error (error, local_error);
      return TRUE;
    }

  g_ptr_array_add (data->expanded_events, g_steal_pointer (&event));

  return TRUE;
}

static void
on_client_view_objects_added_cb (ECalClientView      *view,
                                 const GSList        *objects,
                                 GcalCalendarMonitor *self)
{
  g_autoptr (GPtrArray) components_to_expand = NULL;
  g_autoptr (GPtrArray) events_to_add = NULL;
  g_autoptr (GcalRange) range = NULL;
  const GSList *l;
  gint i;

  GCAL_ENTRY;

  g_assert (GCAL_IS_THREAD (self->thread));

  range = get_monitor_ranges (self);

  maybe_init_event_arrays (self);
  components_to_expand = g_ptr_array_new ();
  events_to_add = g_ptr_array_new_with_free_func (g_object_unref);

  for (l = objects; l; l = l->next)
    {
      g_autoptr (GcalEvent) event = NULL;
      g_autoptr (GError) error = NULL;
      ICalComponent *icomponent;
      ECalComponent *ecomponent;

      icomponent = l->data;

      if (g_cancellable_is_cancelled (self->cancellable))
        return;

      if (!icomponent || !i_cal_component_get_uid (icomponent))
        continue;

      /* Recurrent events will be processed later */
      if (e_cal_util_component_has_recurrences (icomponent) &&
          !e_cal_util_component_is_instance (icomponent))
        {
          GCAL_TRACE_MSG ("Component %s (%s) needs to be expanded",
                          i_cal_component_get_summary (icomponent),
                          i_cal_component_get_uid (icomponent));
          g_ptr_array_add (components_to_expand, icomponent);
          continue;
        }

      ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent));

      if (!ecomponent)
        continue;

      event = gcal_event_new (self->calendar, ecomponent, &error);
      g_clear_object (&ecomponent);

      if (error)
        {
          g_warning ("Error creating event: %s", error->message);
          continue;
        }

      if (!self->monitor_thread.populated)
        g_ptr_array_add (self->monitor_thread.events_to_add, g_object_ref (event));
      else
        g_ptr_array_add (events_to_add, g_object_ref (event));
    }

  /* Recurrent events */
  if (components_to_expand->len > 0)
    {
      g_autoptr (GDateTime) range_start = NULL;
      g_autoptr (GDateTime) range_end = NULL;
      ECalClient *client;
      time_t range_start_time;
      time_t range_end_time;

      GCAL_TRACE_MSG ("Expanding recurrencies of %d events", components_to_expand->len);

      client = gcal_calendar_get_client (self->calendar);

      range_start = gcal_range_get_start (range);
      range_end = gcal_range_get_end (range);
      range_start_time = g_date_time_to_unix (range_start);
      range_end_time = g_date_time_to_unix (range_end) - 1;

      /* Generate the instances */
      for (i = 0; i < components_to_expand->len; i++)
        {
          GenerateRecurrencesData recurrences_data;
          ICalComponent *icomponent;
#if GCAL_ENABLE_TRACE
          gint old_size;
#endif

          if (g_cancellable_is_cancelled (self->cancellable))
            return;

          icomponent = g_ptr_array_index (components_to_expand, i);

          recurrences_data.monitor = self;

          if (!self->monitor_thread.populated)
            recurrences_data.expanded_events = self->monitor_thread.events_to_add;
          else
            recurrences_data.expanded_events = events_to_add;

#if GCAL_ENABLE_TRACE
          old_size = recurrences_data.expanded_events->len;
#endif

          e_cal_client_generate_instances_for_object_sync (client,
                                                           icomponent,
                                                           range_start_time,
                                                           range_end_time,
                                                           self->cancellable,
                                                           client_instance_generated_cb,
                                                           &recurrences_data);

#if GCAL_ENABLE_TRACE
            {
              g_autofree gchar *range_str = gcal_range_to_string (range);

              GCAL_TRACE_MSG ("Component %s (%s) added %d instance(s) between %s",
                              i_cal_component_get_summary (icomponent),
                              i_cal_component_get_uid (icomponent),
                              recurrences_data.expanded_events->len - old_size,
                              range_str);
            }
#endif
        }
    }

  if (events_to_add->len > 0)
    add_events_in_idle (self, events_to_add);

  GCAL_EXIT;
}

static void
on_client_view_objects_modified_cb (ECalClientView      *view,
                                    const GSList        *objects,
                                    GcalCalendarMonitor *self)
{
  g_autoptr (GRWLockReaderLocker) reader_locker = NULL;
  g_autoptr (GHashTable) events_to_remove = NULL;
  g_autoptr (GPtrArray) components_to_expand = NULL;
  g_autoptr (GPtrArray) event_ids_to_remove = NULL;
  g_autoptr (GPtrArray) events_to_update = NULL;
  g_autoptr (GcalRange) range = NULL;
  const GSList *l;

  GCAL_ENTRY;

  g_assert (GCAL_IS_THREAD (self->thread));

  if (!self->monitor_thread.populated && self->monitor_thread.events_to_add)
    {
      g_clear_pointer (&self->monitor_thread.events_to_add, g_ptr_array_unref);
      return;
    }

  reader_locker = g_rw_lock_reader_locker_new (&self->shared.lock);
  components_to_expand = g_ptr_array_new ();
  events_to_remove = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
  events_to_update = g_ptr_array_new_with_free_func (g_object_unref);
  range = gcal_range_copy (self->shared.range);

  for (l = objects; l; l = l->next)
    {
      g_autoptr (GcalEvent) event = NULL;
      g_autoptr (GError) error = NULL;
      g_autofree gchar *event_id = NULL;
      ECalComponentId *component_id;
      ICalComponent *icomponent;
      ECalComponent *ecomponent;
      gboolean recurrence_main;

      icomponent = l->data;

      if (!icomponent || !i_cal_component_get_uid (icomponent))
        continue;

      ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent));

      if (!ecomponent)
        continue;

      recurrence_main = e_cal_util_component_has_recurrences (icomponent) &&
                        !e_cal_util_component_is_instance (icomponent);

      /*
       * If the event has no recurrence id, it is either a main event, or a
       * regular non-recurring event. If we're receiving the 'objects-modified'
       * signal because this event went from recurring to non-recurring, the
       * instances of the old recurrency are still here. They need to be removed.
       */
      component_id = e_cal_component_get_id (ecomponent);

      event_id = g_strdup_printf ("%s:%s",
                                  gcal_calendar_get_id (self->calendar),
                                  e_cal_component_id_get_uid (component_id));

      if (!e_cal_component_id_get_rid (component_id))
        {
          GHashTableIter iter;
          const gchar *aux;

          g_hash_table_iter_init (&iter, self->shared.events);
          while (g_hash_table_iter_next (&iter, (gpointer*) &aux, NULL))
            {
              if (!g_str_has_prefix (aux, event_id))
                continue;

              if (recurrence_main || !g_str_equal (aux, event_id))
                g_hash_table_add (events_to_remove, g_strdup (aux));
            }
        }
      g_clear_pointer (&component_id, e_cal_component_id_free);

      /* Recurrent events will be processed later */
      if (recurrence_main)
        {
          GCAL_TRACE_MSG ("Component %s (%s) needs to be expanded",
                          i_cal_component_get_summary (icomponent),
                          i_cal_component_get_uid (icomponent));
          g_ptr_array_add (components_to_expand, icomponent);
          continue;
        }

      event = gcal_event_new (self->calendar, ecomponent, &error);
      g_clear_object (&ecomponent);

      if (error)
        {
          g_warning ("Error creating event: %s", error->message);
          continue;
        }

      g_ptr_array_add (events_to_update, g_steal_pointer (&event));
    }

  /* Recurrent events */
  if (components_to_expand->len > 0)
    {
      g_autoptr (GPtrArray) expanded_events = NULL;
      g_autoptr (GPtrArray) events_to_add = NULL;
      g_autoptr (GDateTime) range_start = NULL;
      g_autoptr (GDateTime) range_end = NULL;
      ECalClient *client;
      time_t range_start_time;
      time_t range_end_time;

      GCAL_TRACE_MSG ("Expanding recurrencies of %d events", components_to_expand->len);

      client = gcal_calendar_get_client (self->calendar);

      range_start = gcal_range_get_start (range);
      range_end = gcal_range_get_end (range);
      range_start_time = g_date_time_to_unix (range_start);
      range_end_time = g_date_time_to_unix (range_end) - 1;

      expanded_events = g_ptr_array_new_with_free_func (g_object_unref);
      events_to_add = g_ptr_array_new_with_free_func (g_object_unref);

      /* Generate the instances */
      for (guint i = 0; i < components_to_expand->len; i++)
        {
          GenerateRecurrencesData recurrences_data;
          ICalComponent *icomponent;
#if GCAL_ENABLE_TRACE
          gint old_size;
#endif

          if (g_cancellable_is_cancelled (self->cancellable))
            return;

          icomponent = g_ptr_array_index (components_to_expand, i);

          recurrences_data.monitor = self;
          recurrences_data.expanded_events = expanded_events;

#if GCAL_ENABLE_TRACE
          old_size = expanded_events->len;
#endif

          e_cal_client_generate_instances_for_object_sync (client,
                                                           icomponent,
                                                           range_start_time,
                                                           range_end_time,
                                                           self->cancellable,
                                                           client_instance_generated_cb,
                                                           &recurrences_data);

#if GCAL_ENABLE_TRACE
            {
              g_autofree gchar *range_str = gcal_range_to_string (range);

              GCAL_TRACE_MSG ("Component %s (%s) added %d instance(s) between %s",
                              i_cal_component_get_summary (icomponent),
                              i_cal_component_get_uid (icomponent),
                              expanded_events->len - old_size,
                              range_str);
            }
#endif
        }

      for (guint i = 0; i < expanded_events->len; i++)
        {
          GcalEvent *event = g_ptr_array_index (expanded_events, i);
          GcalEvent *old_event;

          if (g_cancellable_is_cancelled (self->cancellable))
            return;

          old_event = g_hash_table_lookup (self->shared.events, gcal_event_get_uid (event));
          if (old_event)
            {
              g_hash_table_remove (events_to_remove, gcal_event_get_uid (event));
              g_ptr_array_add (events_to_update, g_object_ref (event));
            }
          else
            {
              g_ptr_array_add (events_to_add, g_object_ref (event));
            }
        }

      if (events_to_add->len > 0)
        add_events_in_idle (self, events_to_add);
    }

  if (events_to_update->len > 0)
    update_events_in_idle (self, events_to_update);

  /* Now remove lingering events */
  event_ids_to_remove = g_hash_table_steal_all_keys (events_to_remove);
  if (event_ids_to_remove->len > 0)
    remove_events_in_idle (self, event_ids_to_remove);

  GCAL_EXIT;
}

static void
on_client_view_objects_removed_cb (ECalClientView      *view,
                                   const GSList        *objects,
                                   GcalCalendarMonitor *self)
{
  g_autoptr (GPtrArray) event_ids = NULL;
  const GSList *l;

  GCAL_ENTRY;

  g_assert (GCAL_IS_THREAD (self->thread));

  event_ids = g_ptr_array_new_with_free_func (g_free);

  for (l = objects; l; l = l->next)
    {
      g_autofree gchar *event_id = NULL;
      ECalComponentId *component_id;

      if (g_cancellable_is_cancelled (self->cancellable))
        return;

      component_id = l->data;

      if (e_cal_component_id_get_rid (component_id))
        {
          event_id = g_strdup_printf ("%s:%s:%s",
                                      gcal_calendar_get_id (self->calendar),
                                      e_cal_component_id_get_uid (component_id),
                                      e_cal_component_id_get_rid (component_id));
        }
      else
        {
          g_autoptr (GRWLockReaderLocker) reader_locker = NULL;
          GHashTableIter iter;
          const gchar *aux;

          event_id = g_strdup_printf ("%s:%s",
                                      gcal_calendar_get_id (self->calendar),
                                      e_cal_component_id_get_uid (component_id));

          /*
           * If this is the main component, remove the expanded recurrency instances
           * as well.
           */
          reader_locker = g_rw_lock_reader_locker_new (&self->shared.lock);

          g_hash_table_iter_init (&iter, self->shared.events);
          while (g_hash_table_iter_next (&iter, (gpointer*) &aux, NULL))
            {
              if (!g_str_equal (aux, event_id) && g_str_has_prefix (aux, event_id))
                g_ptr_array_add (event_ids, g_strdup (aux));
            }
        }

      g_ptr_array_add (event_ids, g_strdup (event_id));
    }

  if (event_ids->len > 0)
    remove_events_in_idle (self, event_ids);

  GCAL_EXIT;
}

static void
on_client_view_complete_cb (ECalClientView      *view,
                            const GError        *error,
                            GcalCalendarMonitor *self)
{
  g_autoptr (GPtrArray) events_to_add = NULL;

  GCAL_ENTRY;

  g_assert (GCAL_IS_THREAD (self->thread));
  g_assert (!self->monitor_thread.populated);

  events_to_add = g_steal_pointer (&self->monitor_thread.events_to_add);

  if (events_to_add)
    add_events_in_idle (self, events_to_add);

  self->monitor_thread.populated = TRUE;

  set_complete_in_idle (self, TRUE);

  g_debug ("Finished initial loading of calendar '%s'", gcal_calendar_get_name (self->calendar));

  GCAL_EXIT;
}

static void
create_view (GcalCalendarMonitor *self)
{
  g_autoptr (GRWLockReaderLocker) reader_locker = NULL;
  g_autofree gchar *filter = NULL;
  g_autoptr (GError) error = NULL;
  ECalClientView *view;
  ECalClient *client;

  GCAL_ENTRY;

  g_assert (GCAL_IS_THREAD (self->thread));

  reader_locker = g_rw_lock_reader_locker_new (&self->shared.lock);

  g_assert (self->cancellable == NULL);
  self->cancellable = g_cancellable_new ();

  if (!self->shared.range)
    GCAL_RETURN ();

  filter = build_subscriber_filter (self->shared.range, self->shared.filter);

  g_clear_pointer (&reader_locker, g_rw_lock_reader_locker_free);

  if (!gcal_calendar_get_visible (self->calendar))
    GCAL_RETURN ();

  client = gcal_calendar_get_client (self->calendar);
  e_cal_client_get_view_sync (client, filter, &view, self->cancellable, &error);

  if (error)
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        g_warning ("Error creating view: %s", error->message);
      GCAL_RETURN ();
    }

  GCAL_TRACE_MSG ("Initialized ECalClientView with query \"%s\"", filter);

  g_signal_connect (view, "objects-added", G_CALLBACK (on_client_view_objects_added_cb), self);
  g_signal_connect (view, "objects-modified", G_CALLBACK (on_client_view_objects_modified_cb), self);
  g_signal_connect (view, "objects-removed", G_CALLBACK (on_client_view_objects_removed_cb), self);
  g_signal_connect (view, "complete", G_CALLBACK (on_client_view_complete_cb), self);

  if (g_cancellable_is_cancelled (self->cancellable))
    GCAL_RETURN ();

  g_debug ("Starting ECalClientView for calendar '%s'", gcal_calendar_get_name (self->calendar));

  e_cal_client_view_start (view, &error);

  if (error)
    {
      g_warning ("Error starting up view: %s", error->message);
      g_clear_object (&view);
      GCAL_RETURN ();
    }

  self->monitor_thread.view = g_steal_pointer (&view);

  set_complete_in_idle (self, FALSE);

  GCAL_EXIT;
}

static void
remove_view (GcalCalendarMonitor *self)
{
  g_autoptr (GError) error = NULL;

  GCAL_ENTRY;

  g_assert (GCAL_IS_THREAD (self->thread));

  g_clear_object (&self->cancellable);

  if (!self->monitor_thread.view)
    GCAL_RETURN ();

  g_debug ("Tearing down view for calendar %s", gcal_calendar_get_id (self->calendar));

  e_cal_client_view_stop (self->monitor_thread.view, &error);

  if (error)
    {
      g_warning ("Error stopping view: %s", error->message);
      GCAL_RETURN ();
    }

  g_clear_object (&self->monitor_thread.view);
  self->monitor_thread.populated = FALSE;

  GCAL_EXIT;
}

typedef struct
{
  GSource              parent;
  GcalCalendarMonitor *monitor;
  GMainLoop           *mainloop;
} MessageQueueSource;

static gboolean
message_queue_source_prepare (GSource *source,
                              gint    *timeout)
{
  GcalCalendarMonitor *self;
  MessageQueueSource *queue_source;

  queue_source = (MessageQueueSource*) source;
  self = queue_source->monitor;

  /*
   * The prepare function can be called before g_thread_new()
   * returns, and that's valid. However, in that case, no messages
   * should be available in the queue, as the main thread didn't
   * return execution to the mainloop yet. Protect against that.
   */
  g_assert (GCAL_IS_THREAD (self->thread) ||
            (!self->thread && g_async_queue_length (self->messages) == 0));

  return g_async_queue_length (self->messages) > 0;
}

static gboolean
message_queue_source_dispatch (GSource     *source,
                               GSourceFunc  callback,
                               gpointer     user_data)
{
  GcalCalendarMonitor *self;
  MessageQueueSource *queue_source;
  MonitorThreadEvent event;

  GCAL_ENTRY;

  queue_source = (MessageQueueSource*) source;
  self = queue_source->monitor;
  event = GPOINTER_TO_INT (g_async_queue_pop (self->messages));

  g_assert (GCAL_IS_THREAD (self->thread));

  switch (event)
    {
    case FILTER_UPDATED:
    case RANGE_UPDATED:
      remove_view (self);
      create_view (self);
      break;

    case CREATE_VIEW:
      create_view (self);
      break;

    case REMOVE_VIEW:
      remove_view (self);
      break;

    case INVALID_EVENT:
    case QUIT:
      remove_view (self);
      g_main_loop_quit (queue_source->mainloop);
      GCAL_RETURN (G_SOURCE_REMOVE);
    }

  GCAL_RETURN (G_SOURCE_CONTINUE);
}

static GSourceFuncs monitor_queue_source_funcs =
{
  message_queue_source_prepare,
  NULL,
  message_queue_source_dispatch,
  NULL,
};

static MessageQueueSource*
views_monitor_source_new (GcalCalendarMonitor *self,
                          GMainLoop           *mainloop)
{
  MessageQueueSource *queue_source;
  GSource *source;

  /* Prepare the message queue source */
  source = g_source_new (&monitor_queue_source_funcs, sizeof (MessageQueueSource));
  queue_source = (MessageQueueSource*) source;
  queue_source->monitor = self;
  queue_source->mainloop = mainloop;
  g_source_set_name (source, "Message Queue Source");

  return queue_source;
}

static gpointer
calendar_view_thread_func (gpointer data)
{
  g_autoptr (GMainLoop) mainloop = NULL;
  MessageQueueSource *queue_source;
  GcalCalendarMonitor *self;

  GCAL_ENTRY;

  self = data;
  mainloop = g_main_loop_new (self->thread_context, FALSE);
  g_main_context_push_thread_default (self->thread_context);

  /* Events source */
  queue_source = views_monitor_source_new (self, mainloop);
  g_source_attach ((GSource*) queue_source, self->thread_context);

  g_main_loop_run (mainloop);

  g_main_context_pop_thread_default (self->thread_context);

  GCAL_RETURN (NULL);
}

/*
 * Auxiliary methods
 */

static void
notify_view_thread (GcalCalendarMonitor *self,
                    MonitorThreadEvent   event)
{
  g_assert (GCAL_IS_MAIN_THREAD ());

  g_async_queue_push (self->messages, GINT_TO_POINTER (event));
  g_main_context_wakeup (self->thread_context);
}

static void
maybe_spawn_view_thread (GcalCalendarMonitor *self)
{
  g_autofree gchar *thread_name = NULL;

  g_assert (GCAL_IS_MAIN_THREAD ());

  if (self->thread || !self->shared.range)
    return;

  thread_name = g_strdup_printf ("GcalCalendarMonitor (%s)", gcal_calendar_get_id (self->calendar));
  self->thread = g_thread_new (thread_name, calendar_view_thread_func, self);

  g_debug ("Spawning thread %s", thread_name);
}

static void
remove_events_outside_range (GcalCalendarMonitor *self,
                             GcalRange           *range)
{
  g_autoptr (GRWLockWriterLocker) writer_locker = NULL;
  g_autoptr (GPtrArray) events_to_remove = NULL;
  GHashTableIter iter;
  GcalEvent *event;

  g_assert (GCAL_IS_MAIN_THREAD ());
  g_assert (range != NULL);

  GCAL_TRACE_MSG ("Removing events outside range from monitor");

  writer_locker = g_rw_lock_writer_locker_new (&self->shared.lock);

  events_to_remove = g_ptr_array_new_full (g_hash_table_size (self->shared.events),
                                           g_object_unref);

  g_hash_table_iter_init (&iter, self->shared.events);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &event))
    {
      GcalRange *event_range = gcal_event_get_range (event);

      if (gcal_range_calculate_overlap (range, event_range, NULL) != GCAL_RANGE_NO_OVERLAP)
        continue;

      g_ptr_array_add (events_to_remove, event);
      g_hash_table_iter_steal (&iter);
    }

  if (events_to_remove->len > 0)
    self->listener->remove_events (self, events_to_remove, self->listener_user_data);
}

static void
remove_all_events (GcalCalendarMonitor *self)
{
  g_autoptr (GRWLockWriterLocker) writer_locker = NULL;
  g_autoptr (GPtrArray) events_to_remove = NULL;

  g_assert (GCAL_IS_MAIN_THREAD ());

  GCAL_TRACE_MSG ("Removing all events from view");

  writer_locker = g_rw_lock_writer_locker_new (&self->shared.lock);

  events_to_remove = g_hash_table_steal_all_values (self->shared.events);

  if (events_to_remove->len > 0)
    self->listener->remove_events (self, events_to_remove, self->listener_user_data);
}

static void
set_complete (GcalCalendarMonitor *self,
              gboolean             complete)
{
  g_assert (GCAL_IS_MAIN_THREAD ());

  if (self->complete == complete)
    return;

  GCAL_TRACE_MSG ("Setting complete to %s", complete ? "TRUE" : "FALSE");

  self->complete = complete;
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMPLETE]);
}


/*
 * Callbacks
 */

static gboolean
add_events_to_timeline_in_idle_cb (gpointer user_data)
{
  g_autoptr (GRWLockWriterLocker) writer_locker = NULL;
  g_autoptr (GPtrArray) events_to_add = NULL;
  GcalCalendarMonitor *self;
  GPtrArray *events;
  IdleData *idle_data;

  GCAL_ENTRY;

  g_assert (GCAL_IS_MAIN_THREAD ());

  idle_data = user_data;
  self = idle_data->monitor;
  events = idle_data->events;
  g_assert (idle_data->event_ids == NULL);

  events_to_add = g_ptr_array_sized_new (events->len);

  writer_locker = g_rw_lock_writer_locker_new (&self->shared.lock);
  for (guint i = 0; i < events->len; i++)
    {
      GcalEvent *event;
      const gchar *uid;

      event = g_ptr_array_index (events, i);
      uid = gcal_event_get_uid (event);

      if (!g_hash_table_contains (self->shared.events, uid))
        {
          g_hash_table_insert (self->shared.events, g_strdup (uid), g_object_ref (event));
          g_ptr_array_add (events_to_add, event);
        }
    }

  if (events_to_add->len > 0)
    self->listener->add_events (self, events_to_add, self->listener_user_data);

  GCAL_RETURN (G_SOURCE_REMOVE);
}

static gboolean
update_events_in_idle_cb (gpointer user_data)
{
  g_autoptr (GRWLockWriterLocker) writer_locker = NULL;
  g_autoptr (GPtrArray) old_events = NULL;
  g_autoptr (GPtrArray) new_events = NULL;
  g_autoptr (GPtrArray) events = NULL;
  GcalCalendarMonitor *self;
  IdleData *idle_data;

  GCAL_ENTRY;

  g_assert (GCAL_IS_MAIN_THREAD ());

  idle_data = user_data;
  self = idle_data->monitor;
  events = g_steal_pointer (&idle_data->events);
  g_assert (idle_data->event_ids == NULL);

  old_events = g_ptr_array_new_full (events->len, g_object_unref);
  new_events = g_ptr_array_sized_new (events->len);

  writer_locker = g_rw_lock_writer_locker_new (&self->shared.lock);
  for (guint i = 0; i < events->len; i++)
    {
      g_autoptr (GcalEvent) old_event = NULL;
      g_autofree gchar *old_event_id = NULL;
      GcalEvent *event;
      const gchar *event_id;

      event = g_ptr_array_index (events, i);
      event_id = gcal_event_get_uid (event);

      if (g_hash_table_steal_extended (self->shared.events,
                                       event_id,
                                       (gpointer*) &old_event_id,
                                       (gpointer*) &old_event))
        {
          g_hash_table_insert (self->shared.events, g_strdup (event_id), g_object_ref (event));
          g_ptr_array_add (old_events, g_steal_pointer (&old_event));
          g_ptr_array_add (new_events, event);
        }
    }

  g_assert (old_events->len == new_events->len);

  if (new_events->len > 0)
    self->listener->update_events (self, old_events, new_events, self->listener_user_data);

  GCAL_RETURN (G_SOURCE_REMOVE);
}

static gboolean
remove_events_from_timeline_in_idle_cb (gpointer user_data)
{
  g_autoptr (GRWLockWriterLocker) writer_locker = NULL;
  g_autoptr (GPtrArray) events_to_remove = NULL;
  g_autoptr (GPtrArray) event_ids = NULL;
  GcalCalendarMonitor *self;
  IdleData *idle_data;

  GCAL_ENTRY;

  g_assert (GCAL_IS_MAIN_THREAD ());

  idle_data = user_data;
  self = idle_data->monitor;
  event_ids = g_steal_pointer (&idle_data->event_ids);
  g_assert (idle_data->events == NULL);

  events_to_remove = g_ptr_array_new_full (event_ids->len, g_object_unref);

  writer_locker = g_rw_lock_writer_locker_new (&self->shared.lock);

  for (guint i = 0; i < event_ids->len; i++)
    {
      const gchar *event_id;
      GcalEvent *event;

      event_id = g_ptr_array_index (event_ids, i);
      event = g_hash_table_lookup (self->shared.events, event_id);

      if (event)
        {
          /* Keep the event alive until the listener process it*/
          g_ptr_array_add (events_to_remove, g_object_ref (event));
          g_hash_table_remove (self->shared.events, event_id);
        }
    }

  if (events_to_remove->len > 0)
    self->listener->remove_events (self, events_to_remove, self->listener_user_data);

  GCAL_RETURN (G_SOURCE_REMOVE);
}

static void
on_calendar_visible_changed_cb (GcalCalendar        *calendar,
                                GParamSpec          *pspec,
                                GcalCalendarMonitor *self)
{
  if (gcal_calendar_get_visible (calendar))
    {
      notify_view_thread (self, CREATE_VIEW);
    }
  else
    {
      remove_all_events (self);
      notify_view_thread (self, REMOVE_VIEW);
    }
}

static gboolean
complete_in_idle_cb (gpointer user_data)
{
  GcalCalendarMonitor *self;
  IdleData *idle_data;

  GCAL_ENTRY;

  g_assert (GCAL_IS_MAIN_THREAD ());

  idle_data = user_data;
  self = idle_data->monitor;
  g_assert (idle_data->events == NULL);
  g_assert (idle_data->event_ids == NULL);

  set_complete (self, idle_data->complete);

  GCAL_RETURN (G_SOURCE_REMOVE);
}


/*
 * GObject overrides
 */

static void
gcal_calendar_monitor_dispose (GObject *object)
{
  GcalCalendarMonitor *self = (GcalCalendarMonitor *)object;

  g_cancellable_cancel (self->cancellable);
  notify_view_thread (self, QUIT);

  if (self->thread)
    {
      g_thread_join (self->thread);
      self->thread = NULL;
    }

  remove_all_events (self);

  g_clear_object (&self->cancellable);

  G_OBJECT_CLASS (gcal_calendar_monitor_parent_class)->dispose (object);
}

static void
gcal_calendar_monitor_finalize (GObject *object)
{
  GcalCalendarMonitor *self = (GcalCalendarMonitor *)object;

  g_clear_object (&self->calendar);
  g_clear_pointer (&self->thread_context, g_main_context_unref);
  g_clear_pointer (&self->main_context, g_main_context_unref);
  g_clear_pointer (&self->messages, g_async_queue_unref);
  g_clear_pointer (&self->shared.events, g_hash_table_destroy);
  g_clear_pointer (&self->shared.filter, g_free);
  g_clear_pointer (&self->shared.range, gcal_range_unref);

  G_OBJECT_CLASS (gcal_calendar_monitor_parent_class)->finalize (object);
}

static void
gcal_calendar_monitor_get_property (GObject    *object,
                                    guint       prop_id,
                                    GValue     *value,
                                    GParamSpec *pspec)
{
  GcalCalendarMonitor *self = GCAL_CALENDAR_MONITOR (object);

  switch (prop_id)
    {
    case PROP_CALENDAR:
      g_value_set_object (value, self->calendar);
      break;

    case PROP_COMPLETE:
      g_value_set_boolean (value, self->complete);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gcal_calendar_monitor_set_property (GObject      *object,
                                    guint         prop_id,
                                    const GValue *value,
                                    GParamSpec   *pspec)
{
  GcalCalendarMonitor *self = GCAL_CALENDAR_MONITOR (object);

  switch (prop_id)
    {
    case PROP_CALENDAR:
      g_assert (self->calendar == NULL);
      self->calendar = g_value_dup_object (value);
      g_signal_connect (self->calendar,
                        "notify::visible",
                        G_CALLBACK (on_calendar_visible_changed_cb),
                        self);
      break;

    case PROP_COMPLETE:
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gcal_calendar_monitor_class_init (GcalCalendarMonitorClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = gcal_calendar_monitor_dispose;
  object_class->finalize = gcal_calendar_monitor_finalize;
  object_class->get_property = gcal_calendar_monitor_get_property;
  object_class->set_property = gcal_calendar_monitor_set_property;

  properties[PROP_CALENDAR] = g_param_spec_object ("calendar",
                                                   "Calendar",
                                                   "Calendar to be monitored",
                                                   GCAL_TYPE_CALENDAR,
                                                   G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);


  properties[PROP_COMPLETE] = g_param_spec_boolean ("complete",
                                                    "Complete",
                                                    "Whether",
                                                    FALSE,
                                                    G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
gcal_calendar_monitor_init (GcalCalendarMonitor *self)
{
  self->shared.events = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
  self->thread_context = g_main_context_new ();
  self->main_context = g_main_context_ref_thread_default ();
  self->messages = g_async_queue_new ();
  self->complete = FALSE;

  g_rw_lock_init (&self->shared.lock);
}

GcalCalendarMonitor*
gcal_calendar_monitor_new (GcalCalendar                      *calendar,
                           const GcalCalendarMonitorListener *listener,
                           gpointer                           user_data)
{
  g_autoptr (GcalCalendarMonitor) monitor = NULL;

  g_assert (calendar != NULL && GCAL_IS_CALENDAR (calendar));
  g_assert (listener != NULL);

  monitor = g_object_new (GCAL_TYPE_CALENDAR_MONITOR,
                          "calendar", calendar,
                          NULL);

  monitor->listener = listener;
  monitor->listener_user_data = user_data;

  return g_steal_pointer (&monitor);
}

/**
 * gcal_calendar_monitor_set_range:
 * @self: a #GcalCalendarMonitor
 * @range: a #GcalRange
 *
 * Updates the range of @self. This usually will result in
 * @self creating a new view, and gathering the events from
 * the events server.
 */
void
gcal_calendar_monitor_set_range (GcalCalendarMonitor *self,
                                 GcalRange           *range)
{
  g_autoptr (GRWLockWriterLocker) writer_locker = NULL;
  gboolean range_changed;

  g_return_if_fail (GCAL_IS_CALENDAR_MONITOR (self));

  GCAL_ENTRY;

  writer_locker = g_rw_lock_writer_locker_new (&self->shared.lock);

  range_changed =
    !self->shared.range ||
    !range ||
    gcal_range_calculate_overlap (self->shared.range, range, NULL) != GCAL_RANGE_EQUAL;

  if (!range_changed)
    GCAL_RETURN ();

  g_clear_pointer (&self->shared.range, gcal_range_unref);
  self->shared.range = range ? gcal_range_copy (range) : NULL;

  g_clear_pointer (&writer_locker, g_rw_lock_writer_locker_free);

  maybe_spawn_view_thread (self);

  if (range)
    remove_events_outside_range (self, range);
  else
    remove_all_events (self);

  g_cancellable_cancel (self->cancellable);

  if (gcal_calendar_get_visible (self->calendar))
    notify_view_thread (self, RANGE_UPDATED);

  GCAL_EXIT;
}

/**
 * gcal_calendar_monitor_get_cached_event:
 * @self: a #GcalCalendarMonitor
 * @event_id: the id of the #GcalEvent
 *
 * Returns: (transfer full)(nullable): a #GcalEvent, or %NULL
 */
GcalEvent*
gcal_calendar_monitor_get_cached_event (GcalCalendarMonitor *self,
                                        const gchar         *event_id)
{
  GcalEvent *event;

  g_return_val_if_fail (GCAL_IS_CALENDAR_MONITOR (self), NULL);
  g_return_val_if_fail (event_id, NULL);

  g_rw_lock_reader_lock (&self->shared.lock);
  event = g_hash_table_lookup (self->shared.events, event_id);
  g_rw_lock_reader_unlock (&self->shared.lock);

  return event ? g_object_ref (event) : NULL;
}

void
gcal_calendar_monitor_set_filter (GcalCalendarMonitor *self,
                                  const gchar         *filter)
{
  g_return_if_fail (GCAL_IS_CALENDAR_MONITOR (self));

  g_rw_lock_writer_lock (&self->shared.lock);

  g_clear_pointer (&self->shared.filter, g_free);
  self->shared.filter = g_strdup (filter);

  g_rw_lock_writer_unlock (&self->shared.lock);

  remove_all_events (self);

  if (gcal_calendar_get_visible (self->calendar))
    notify_view_thread (self, FILTER_UPDATED);
}

gboolean
gcal_calendar_monitor_is_complete (GcalCalendarMonitor *self)
{
  g_return_val_if_fail (GCAL_IS_CALENDAR_MONITOR (self), FALSE);

  return self->complete;
}
