/*
 * Copyright (C) 2011 Red Hat, Inc.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 * Author: David Zeuthen <davidz@redhat.com>
 *
 * Based on code from gnome-panel's clock-applet, file calendar-client.c, with Authors:
 *
 *     Mark McLoughlin  <mark@skynet.ie>
 *     William Jon McCann  <mccann@jhu.edu>
 *     Martin Grimme  <martin@pycage.de>
 *     Christian Kellner  <gicmo@xatom.net>
 *
 */

#include "phosh-config.h"

#include <string.h>
#include <sys/types.h>
#include <unistd.h>

#include <gio/gio.h>

#define HANDLE_LIBICAL_MEMORY
#define EDS_DISABLE_DEPRECATED
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
#include <libecal/libecal.h>
G_GNUC_END_IGNORE_DEPRECATIONS

#include "calendar-sources.h"

#define BUS_NAME PHOSH_APP_ID ".CalendarServer"

static const gchar introspection_xml[] =
  "<node>"
  "  <interface name='" PHOSH_APP_ID ".CalendarServer'>"
  "    <method name='SetTimeRange'>"
  "      <arg type='x' name='since' direction='in'/>"
  "      <arg type='x' name='until' direction='in'/>"
  "      <arg type='b' name='force_reload' direction='in'/>"
  "    </method>"
  "    <signal name='EventsAddedOrUpdated'>"
  "      <arg type='a(ssxxa{sv})' name='events' direction='out'/>"
  "    </signal>"
  "    <signal name='EventsRemoved'>"
  "      <arg type='as' name='ids' direction='out'/>"
  "    </signal>"
  "    <signal name='ClientDisappeared'>"
  "      <arg type='s' name='source_uid' direction='out'/>"
  "    </signal>"
  "    <property name='Since' type='x' access='read'/>"
  "    <property name='Until' type='x' access='read'/>"
  "    <property name='HasCalendars' type='b' access='read'/>"
  "  </interface>"
  "</node>";
static GDBusNodeInfo *introspection_data = NULL;

struct _App;
typedef struct _App App;

static gboolean      opt_replace = FALSE;
static GOptionEntry  opt_entries[] = {
  {"replace", 0, 0, G_OPTION_ARG_NONE, &opt_replace, "Replace existing daemon", NULL},
  {NULL }
};
static App *_global_app = NULL;

/* ---------------------------------------------------------------------------------------------------- */

/* While the UID is usually enough to identify an event,
 * only the triple of (source,UID,RID) is fully unambiguous;
 * neither may contain '\n', so we can safely use it to
 * create a unique ID from the triple
 */
static gchar *
create_event_id (const gchar *source_uid,
                 const gchar *comp_uid,
                 const gchar *comp_rid)
{
  return g_strconcat (
    source_uid ? source_uid : "",
    "\n",
    comp_uid ? comp_uid : "",
    "\n",
    comp_rid ? comp_rid : "",
    NULL);
}

typedef struct
{
  ECalClient *client;
  GSList **pappointments; /* CalendarAppointment * */
} CollectAppointmentsData;

typedef struct
{
  gchar  *id;
  gchar  *summary;
  time_t  start_time;
  time_t  end_time;
  char   *color;
} CalendarAppointment;

static gboolean
get_time_from_property (ECalClient            *cal,
                        ICalComponent         *icomp,
                        ICalPropertyKind       prop_kind,
                        ICalTime * (* get_prop_func) (const ICalProperty *prop),
                        ICalTimezone          *default_zone,
                        ICalTime              **out_itt,
                        ICalTimezone          **out_timezone)
{
  ICalProperty  *prop;
  ICalTime      *itt;
  ICalTimezone  *tz = NULL;

  prop = i_cal_component_get_first_property (icomp, prop_kind);
  if (!prop)
    return FALSE;

  itt = get_prop_func (prop);

  if (i_cal_time_is_utc (itt))
    tz = i_cal_timezone_get_utc_timezone ();
  else
   {
      ICalParameter *param;

      param = i_cal_property_get_first_parameter (prop, I_CAL_TZID_PARAMETER);
      if (param && !e_cal_client_get_timezone_sync (cal, i_cal_parameter_get_tzid (param), &tz, NULL, NULL))
        print_debug ("Failed to get timezone '%s'\n", i_cal_parameter_get_tzid (param));

      g_clear_object (&param);
   }

  if (tz == NULL)
    tz = default_zone;

  i_cal_time_set_timezone (itt, tz);

  g_clear_object (&prop);

  *out_itt = itt;
  *out_timezone = tz;

  return TRUE;
}

static inline time_t
get_ical_start_time (ECalClient    *cal,
                     ICalComponent *icomp,
                     ICalTimezone  *default_zone)
{
  ICalTime     *itt;
  ICalTimezone *tz;
  time_t        retval;

  if (!get_time_from_property (cal,
                               icomp,
                               I_CAL_DTSTART_PROPERTY,
                               i_cal_property_get_dtstart,
                               default_zone,
                               &itt,
                               &tz))
    {
      return 0;
    }

  retval = i_cal_time_as_timet_with_zone (itt, tz);

  g_clear_object (&itt);

  return retval;
}

static inline time_t
get_ical_end_time (ECalClient    *cal,
                   ICalComponent *icomp,
                   ICalTimezone  *default_zone)
{
  ICalTime     *itt;
  ICalTimezone *tz;
  time_t        retval;

  if (!get_time_from_property (cal,
                               icomp,
                               I_CAL_DTEND_PROPERTY,
                               i_cal_property_get_dtend,
                               default_zone,
                               &itt,
                               &tz))
    {
      if (!get_time_from_property (cal,
                                   icomp,
                                   I_CAL_DTSTART_PROPERTY,
                                   i_cal_property_get_dtstart,
                                   default_zone,
                                   &itt,
                                   &tz))
        {
          return 0;
        }

      if (i_cal_time_is_date (itt))
        i_cal_time_adjust (itt, 1, 0, 0, 0);
    }

  retval = i_cal_time_as_timet_with_zone (itt, tz);

  g_clear_object (&itt);

  return retval;
}

static CalendarAppointment *
calendar_appointment_new (ECalClient    *cal,
                          ECalComponent *comp)
{
  CalendarAppointment *appt;
  ICalTimezone *default_zone;
  ICalComponent *ical;
  ECalComponentId *id;
  ESource *source;

  default_zone = e_cal_client_get_default_timezone (cal);
  ical = e_cal_component_get_icalcomponent (comp);
  id = e_cal_component_get_id (comp);

  appt = g_new0 (CalendarAppointment, 1);

  appt->id          = create_event_id (e_source_get_uid (e_client_get_source (E_CLIENT (cal))),
                                       id ? e_cal_component_id_get_uid (id) : NULL,
                                       id ? e_cal_component_id_get_rid (id) : NULL);
  appt->summary     = g_strdup (i_cal_component_get_summary (ical));
  appt->start_time  = get_ical_start_time (cal, ical, default_zone);
  appt->end_time    = get_ical_end_time (cal, ical, default_zone);

  source = e_client_get_source (E_CLIENT (cal));
  if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR))
    {
      ESourceSelectable *ext = e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR);
      const char *color;

      color = e_source_selectable_get_color (ext);
      appt->color = g_strdup (color);
    }

  e_cal_component_id_free (id);

  return appt;
}

static void
calendar_appointment_free (gpointer ptr)
{
  CalendarAppointment *appt = ptr;

  if (appt)
    {
      g_free (appt->id);
      g_free (appt->summary);
      g_free (appt->color);
      g_free (appt);
    }
}

static time_t
timet_from_ical_time (ICalTime     *time,
                      ICalTimezone *default_zone)
{
  ICalTimezone *tz = NULL;

  tz = i_cal_time_get_timezone (time);
  if (tz == NULL)
    tz = default_zone;
  return i_cal_time_as_timet_with_zone (time, tz);
}

static gboolean
generate_instances_cb (ICalComponent *icomp,
                       ICalTime *instance_start,
                       ICalTime *instance_end,
                       gpointer user_data,
                       GCancellable *cancellable,
                       GError **error)
{
  CollectAppointmentsData *data = user_data;
  CalendarAppointment *appointment;
  ECalComponent *comp;
  ICalTimezone *default_zone;

  default_zone = e_cal_client_get_default_timezone (data->client);
  comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomp));

  appointment             = calendar_appointment_new (data->client, comp);
  appointment->start_time = timet_from_ical_time (instance_start, default_zone);
  appointment->end_time   = timet_from_ical_time (instance_end, default_zone);

  *(data->pappointments) = g_slist_prepend (*(data->pappointments), appointment);

  g_clear_object (&comp);

  return TRUE;
}

/* ---------------------------------------------------------------------------------------------------- */

struct _App
{
  GDBusConnection *connection;

  time_t since;
  time_t until;

  ICalTimezone *zone;

  CalendarSources *sources;
  gulong client_appeared_signal_id;
  gulong client_disappeared_signal_id;

  gchar *timezone_location;

  GSList *notify_appointments; /* CalendarAppointment *, for EventsAdded */
  GSList *notify_ids; /* gchar *, for EventsRemoved */

  GSList *live_views;
};

static void
app_update_timezone (App *app)
{
  g_autofree char *location = NULL;

  location = e_cal_system_timezone_get_location ();
  if (g_strcmp0 (location, app->timezone_location) != 0)
    {
      if (location == NULL)
        app->zone = i_cal_timezone_get_utc_timezone ();
      else
        app->zone = i_cal_timezone_get_builtin_timezone (location);
      g_free (app->timezone_location);
      app->timezone_location = g_steal_pointer (&location);
      print_debug ("Using timezone %s", app->timezone_location);
    }
}

static void
app_notify_events_added (App *app)
{
  GVariantBuilder builder;
  GSList *events, *link;

  events = g_slist_reverse (app->notify_appointments);
  app->notify_appointments = NULL;

  print_debug ("Emitting EventsAddedOrUpdated with %d events", g_slist_length (events));

  if (!events)
    return;

  /* The a{sv} is used as an escape hatch in case we want to provide more
   * information in the future without breaking ABI
   */
  g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ssxxa{sv})"));
  for (link = events; link; link = g_slist_next (link))
    {
      CalendarAppointment *appt = link->data;
      time_t start_time = appt->start_time;
      time_t end_time   = appt->end_time;
      GVariantBuilder extras_builder;

      if ((start_time >= app->since &&
           start_time < app->until) ||
          (start_time <= app->since &&
          (end_time - 1) > app->since))
        {
          g_variant_builder_init (&extras_builder, G_VARIANT_TYPE ("a{sv}"));
          if (appt->color)
            {
              g_variant_builder_add (&extras_builder,
                                     "{sv}",
                                     "color",
                                     g_variant_new_string (appt->color));
            }
          g_variant_builder_add (&builder,
                                 "(ssxxa{sv})",
                                 appt->id,
                                 appt->summary != NULL ? appt->summary : "",
                                 (gint64) start_time,
                                 (gint64) end_time,
                                 &extras_builder);
        }
    }

  g_dbus_connection_emit_signal (app->connection,
                                 NULL, /* destination_bus_name */
                                 PHOSH_DBUS_PATH_PREFIX "/CalendarServer",
                                 PHOSH_APP_ID ".CalendarServer",
                                 "EventsAddedOrUpdated",
                                 g_variant_new ("(a(ssxxa{sv}))", &builder),
                                 NULL);

  g_variant_builder_clear (&builder);

  g_slist_free_full (events, calendar_appointment_free);
}

static void
app_notify_events_removed (App *app)
{
  GVariantBuilder builder;
  GSList *ids, *link;

  ids = app->notify_ids;
  app->notify_ids = NULL;

  print_debug ("Emitting EventsRemoved with %d ids", g_slist_length (ids));

  if (!ids)
    return;

  g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
  for (link = ids; link; link = g_slist_next (link))
    {
      const gchar *id = link->data;

      g_variant_builder_add (&builder, "s", id);
    }

  g_dbus_connection_emit_signal (app->connection,
                                 NULL, /* destination_bus_name */
                                 PHOSH_DBUS_PATH_PREFIX "/CalendarServer",
                                 PHOSH_APP_ID ".CalendarServer",
                                 "EventsRemoved",
                                 g_variant_new ("(as)", &builder),
                                 NULL);
  g_variant_builder_clear (&builder);

  g_slist_free_full (ids, g_free);

  return;
}

static void
app_process_added_modified_objects (App *app,
                                    ECalClientView *view,
                                    GSList *objects) /* ICalComponent * */
{
  ECalClient *cal_client;
  GSList *link;
  gboolean expand_recurrences;

  cal_client = e_cal_client_view_ref_client (view);
  expand_recurrences = e_cal_client_get_source_type (cal_client) == E_CAL_CLIENT_SOURCE_TYPE_EVENTS;

  for (link = objects; link; link = g_slist_next (link))
    {
      ECalComponent *comp;
      ICalComponent *icomp = link->data;

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

      if (expand_recurrences &&
          !e_cal_util_component_is_instance (icomp) &&
          e_cal_util_component_has_recurrences (icomp))
        {
          CollectAppointmentsData data;

          data.client = cal_client;
          data.pappointments = &app->notify_appointments;

          e_cal_client_generate_instances_for_object_sync (cal_client, icomp, app->since, app->until, NULL,
                                                           generate_instances_cb, &data);
        }
      else
        {
          comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomp));
          if (!comp)
            continue;

          app->notify_appointments = g_slist_prepend (app->notify_appointments,
                                                      calendar_appointment_new (cal_client, comp));
          g_object_unref (comp);
        }
    }

  g_clear_object (&cal_client);

  if (app->notify_appointments)
    app_notify_events_added (app);
}

static void
on_objects_added (ECalClientView *view,
                  GSList         *objects,
                  gpointer        user_data)
{
  App *app = user_data;
  ECalClient *client;

  client = e_cal_client_view_ref_client (view);
  print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (objects), e_source_get_uid (e_client_get_source (E_CLIENT (client))));
  g_clear_object (&client);

  app_process_added_modified_objects (app, view, objects);
}

static void
on_objects_modified (ECalClientView *view,
                     GSList         *objects,
                     gpointer        user_data)
{
  App *app = user_data;
  ECalClient *client;

  client = e_cal_client_view_ref_client (view);
  print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (objects), e_source_get_uid (e_client_get_source (E_CLIENT (client))));
  g_clear_object (&client);

  app_process_added_modified_objects (app, view, objects);
}

static void
on_objects_removed (ECalClientView *view,
                    GSList         *uids,
                    gpointer        user_data)
{
  App *app = user_data;
  ECalClient *client;
  GSList *link;
  const gchar *source_uid;

  client = e_cal_client_view_ref_client (view);
  source_uid = e_source_get_uid (e_client_get_source (E_CLIENT (client)));

  print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (uids), source_uid);

  for (link = uids; link; link = g_slist_next (link))
    {
      ECalComponentId *id = link->data;

      if (!id)
        continue;

      app->notify_ids = g_slist_prepend (app->notify_ids,
                                         create_event_id (source_uid,
                                         e_cal_component_id_get_uid (id),
                                         e_cal_component_id_get_rid (id)));
    }

  g_clear_object (&client);

  if (app->notify_ids)
    app_notify_events_removed (app);
}

static gboolean
app_has_calendars (App *app)
{
  return app->live_views != NULL;
}

static ECalClientView *
app_start_view (App *app,
                ECalClient *cal_client)
{
  g_autofree char *since_iso8601 = NULL;
  g_autofree char *until_iso8601 = NULL;
  g_autofree char *query = NULL;
  const gchar *tz_location;
  ECalClientView *view = NULL;
  g_autoptr (GError) error = NULL;

  if (app->since <= 0 || app->since >= app->until)
    return NULL;

  if (!app->since || !app->until)
    {
      print_debug ("Skipping load of events, no time interval set yet");
      return NULL;
    }

  /* timezone could have changed */
  app_update_timezone (app);

  since_iso8601 = isodate_from_time_t (app->since);
  until_iso8601 = isodate_from_time_t (app->until);
  tz_location = i_cal_timezone_get_location (app->zone);

  print_debug ("Loading events since %s until %s for calendar '%s'",
               since_iso8601,
               until_iso8601,
               e_source_get_uid (e_client_get_source (E_CLIENT (cal_client))));

  query = g_strdup_printf ("occur-in-time-range? (make-time \"%s\") "
                           "(make-time \"%s\") \"%s\"",
                           since_iso8601,
                           until_iso8601,
                           tz_location);

  if (app->zone)
    e_cal_client_set_default_timezone (cal_client, app->zone);

  if (!e_cal_client_get_view_sync (cal_client, query, &view, NULL /* cancellable */, &error))
    {
      g_warning ("Error setting up live-query '%s' on calendar: %s\n", query, error ? error->message : "Unknown error");
      view = NULL;
    }
  else
    {
      g_signal_connect (view,
                        "objects-added",
                        G_CALLBACK (on_objects_added),
                        app);
      g_signal_connect (view,
                        "objects-modified",
                        G_CALLBACK (on_objects_modified),
                        app);
      g_signal_connect (view,
                        "objects-removed",
                        G_CALLBACK (on_objects_removed),
                        app);
      e_cal_client_view_start (view, NULL);
    }
  return view;
}

static void
app_stop_view (App *app,
               ECalClientView *view)
{
      e_cal_client_view_stop (view, NULL);

      g_signal_handlers_disconnect_by_func (view, on_objects_added, app);
      g_signal_handlers_disconnect_by_func (view, on_objects_modified, app);
      g_signal_handlers_disconnect_by_func (view, on_objects_removed, app);
}

static void
app_notify_has_calendars (App *app)
{
  GVariantBuilder dict_builder;

  g_variant_builder_init (&dict_builder, G_VARIANT_TYPE ("a{sv}"));
  g_variant_builder_add (&dict_builder, "{sv}", "HasCalendars",
                         g_variant_new_boolean (app_has_calendars (app)));

  g_dbus_connection_emit_signal (app->connection,
                                 NULL,
                                 PHOSH_DBUS_PATH_PREFIX "/CalendarServer",
                                 "org.freedesktop.DBus.Properties",
                                 "PropertiesChanged",
                                 g_variant_new ("(sa{sv}as)",
                                                PHOSH_APP_ID ".CalendarServer",
                                                &dict_builder,
                                                NULL),
                                 NULL);
  g_variant_builder_clear (&dict_builder);
}

static void
app_update_views (App *app)
{
  GSList *link, *clients;
  gboolean had_views, has_views;

  had_views = app->live_views != NULL;

  for (link = app->live_views; link; link = g_slist_next (link))
    {
      app_stop_view (app, link->data);
    }

  g_slist_free_full (app->live_views, g_object_unref);
  app->live_views = NULL;

  clients = calendar_sources_ref_clients (app->sources);

  for (link = clients; link; link = g_slist_next (link))
    {
      ECalClient *cal_client = link->data;
      ECalClientView *view;

      if (!cal_client)
        continue;

      view = app_start_view (app, cal_client);
      if (view)
        app->live_views = g_slist_prepend (app->live_views, view);
    }

  has_views = app->live_views != NULL;

  if (has_views != had_views)
    app_notify_has_calendars (app);

  g_slist_free_full (clients, g_object_unref);
}

static void
on_client_appeared_cb (CalendarSources *sources,
                       ECalClient *client,
                       gpointer user_data)
{
  App *app = user_data;
  ECalClientView *view;
  GSList *link;
  const gchar *source_uid;

  source_uid = e_source_get_uid (e_client_get_source (E_CLIENT (client)));

  print_debug ("Client appeared '%s'", source_uid);

  for (link = app->live_views; link; link = g_slist_next (link))
    {
      ECalClientView *v = link->data;
      ECalClient *cal_client;
      ESource *source;

      cal_client = e_cal_client_view_ref_client (v);
      source = e_client_get_source (E_CLIENT (cal_client));

      if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0)
        {
          g_clear_object (&cal_client);
          return;
        }

      g_clear_object (&cal_client);
    }

  view = app_start_view (app, client);

  if (view)
    {
      app->live_views = g_slist_prepend (app->live_views, view);

      /* It's the first view, notify that it has calendars now */
      if (!g_slist_next (app->live_views))
        app_notify_has_calendars (app);
    }
}

static void
on_client_disappeared_cb (CalendarSources *sources,
                          const gchar *source_uid,
                          gpointer user_data)
{
  App *app = user_data;
  GSList *link;

  print_debug ("Client disappeared '%s'", source_uid);

  for (link = app->live_views; link; link = g_slist_next (link))
    {
      ECalClientView *view = link->data;
      ECalClient *cal_client;
      ESource *source;

      cal_client = e_cal_client_view_ref_client (view);
      source = e_client_get_source (E_CLIENT (cal_client));

      if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0)
        {
          g_clear_object (&cal_client);
          app_stop_view (app, view);
          app->live_views = g_slist_remove (app->live_views, view);
          g_object_unref (view);

          print_debug ("Emitting ClientDisappeared for '%s'", source_uid);

          g_dbus_connection_emit_signal (app->connection,
                                         NULL, /* destination_bus_name */
                                         PHOSH_DBUS_PATH_PREFIX "/CalendarServer",
                                         PHOSH_APP_ID ".CalendarServer",
                                         "ClientDisappeared",
                                         g_variant_new ("(s)", source_uid),
                                         NULL);

          /* It was the last view, notify that it doesn't have calendars now */
          if (!app->live_views)
            app_notify_has_calendars (app);

          break;
        }

      g_clear_object (&cal_client);
    }
}

static App *
app_new (GDBusConnection *connection)
{
  App *app;

  app = g_new0 (App, 1);
  app->connection = g_object_ref (connection);
  app->sources = calendar_sources_get ();
  app->client_appeared_signal_id = g_signal_connect (app->sources,
                                                     "client-appeared",
                                                     G_CALLBACK (on_client_appeared_cb),
                                                     app);
  app->client_disappeared_signal_id = g_signal_connect (app->sources,
                                                        "client-disappeared",
                                                        G_CALLBACK (on_client_disappeared_cb),
                                                        app);

  app_update_timezone (app);

  return app;
}

static void
app_free (App *app)
{
  GSList *ll;

  for (ll = app->live_views; ll != NULL; ll = g_slist_next (ll))
    {
      ECalClientView *view = E_CAL_CLIENT_VIEW (ll->data);

      app_stop_view (app, view);
    }

  g_signal_handler_disconnect (app->sources,
                               app->client_appeared_signal_id);
  g_signal_handler_disconnect (app->sources,
                               app->client_disappeared_signal_id);

  g_free (app->timezone_location);

  g_slist_free_full (app->live_views, g_object_unref);
  g_slist_free_full (app->notify_appointments, calendar_appointment_free);
  g_slist_free_full (app->notify_ids, g_free);

  g_object_unref (app->connection);
  g_object_unref (app->sources);

  g_free (app);
}

/* ---------------------------------------------------------------------------------------------------- */

static void
handle_method_call (GDBusConnection       *connection,
                    const gchar           *sender,
                    const gchar           *object_path,
                    const gchar           *interface_name,
                    const gchar           *method_name,
                    GVariant              *parameters,
                    GDBusMethodInvocation *invocation,
                    gpointer               user_data)
{
  App *app = user_data;

  if (g_strcmp0 (method_name, "SetTimeRange") == 0)
    {
      gint64 since;
      gint64 until;
      gboolean force_reload = FALSE;
      gboolean window_changed = FALSE;

      g_variant_get (parameters,
                     "(xxb)",
                     &since,
                     &until,
                     &force_reload);

      if (until < since)
        {
          g_dbus_method_invocation_return_dbus_error (invocation,
                                                      PHOSH_APP_ID ".CalendarServer.Error.Failed",
                                                      "until cannot be before since");
          goto out;
        }

      print_debug ("Handling SetTimeRange (since=%" G_GINT64_FORMAT ", until=%" G_GINT64_FORMAT ", force_reload=%s)",
                   since,
                   until,
                   force_reload ? "true" : "false");

      if (app->until != until || app->since != since)
        {
          GVariantBuilder *builder;
          GVariantBuilder *invalidated_builder;

          app->until = until;
          app->since = since;
          window_changed = TRUE;

          builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
          invalidated_builder = g_variant_builder_new (G_VARIANT_TYPE ("as"));
          g_variant_builder_add (builder, "{sv}",
                                 "Until", g_variant_new_int64 (app->until));
          g_variant_builder_add (builder, "{sv}",
                                 "Since", g_variant_new_int64 (app->since));
          g_dbus_connection_emit_signal (app->connection,
                                         NULL, /* destination_bus_name */
                                         PHOSH_DBUS_PATH_PREFIX "/CalendarServer",
                                         "org.freedesktop.DBus.Properties",
                                         "PropertiesChanged",
                                         g_variant_new ("(sa{sv}as)",
                                                        PHOSH_APP_ID ".CalendarServer",
                                                        builder,
                                                        invalidated_builder),
                                         NULL); /* GError** */

          g_variant_builder_unref (builder);
          g_variant_builder_unref (invalidated_builder);
        }

      g_dbus_method_invocation_return_value (invocation, NULL);

      if (window_changed || force_reload)
        app_update_views (app);
    }
  else
    {
      g_assert_not_reached ();
    }

 out:
  ;
}

static GVariant *
handle_get_property (GDBusConnection *connection,
                     const gchar     *sender,
                     const gchar     *object_path,
                     const gchar     *interface_name,
                     const gchar     *property_name,
                     GError         **error,
                     gpointer         user_data)
{
  App *app = user_data;
  GVariant *ret;

  ret = NULL;
  if (g_strcmp0 (property_name, "Since") == 0)
    {
      ret = g_variant_new_int64 (app->since);
    }
  else if (g_strcmp0 (property_name, "Until") == 0)
    {
      ret = g_variant_new_int64 (app->until);
    }
  else if (g_strcmp0 (property_name, "HasCalendars") == 0)
    {
      ret = g_variant_new_boolean (app_has_calendars (app));
    }
  else
    {
      g_assert_not_reached ();
    }
  return ret;
}

static const GDBusInterfaceVTable interface_vtable =
{
  handle_method_call,
  handle_get_property,
  NULL  /* handle_set_property */
};

static void
on_bus_acquired (GDBusConnection *connection,
                 const gchar     *name,
                 gpointer         user_data)
{
  GMainLoop *main_loop = user_data;
  guint registration_id;
  g_autoptr (GError) error = NULL;

  _global_app = app_new (connection);

  registration_id = g_dbus_connection_register_object (connection,
                                                       PHOSH_DBUS_PATH_PREFIX "/CalendarServer",
                                                       introspection_data->interfaces[0],
                                                       &interface_vtable,
                                                       _global_app,
                                                       NULL,  /* user_data_free_func */
                                                       &error);
  if (registration_id == 0)
    {
      g_printerr ("Error exporting object: %s (%s %d)\n",
                  error->message,
                  g_quark_to_string (error->domain),
                  error->code);
      g_main_loop_quit (main_loop);
      return;
    }

  print_debug ("Connected to the session bus");
}

static void
on_name_lost (GDBusConnection *connection,
              const gchar     *name,
              gpointer         user_data)
{
  GMainLoop *main_loop = user_data;

  g_print (CALENDAR_SERVER_NAME "[%d]: Lost (or failed to acquire) the name " BUS_NAME " - exiting\n",
           (gint) getpid ());
  g_main_loop_quit (main_loop);
}

static void
on_name_acquired (GDBusConnection *connection,
                  const gchar     *name,
                  gpointer         user_data)
{
  print_debug ("Acquired the name " BUS_NAME);
}

static gboolean
stdin_channel_io_func (GIOChannel *source,
                       GIOCondition condition,
                       gpointer data)
{
  GMainLoop *main_loop = data;

  if (condition & G_IO_HUP)
    {
      g_debug (CALENDAR_SERVER_NAME "[%d]: Got HUP on stdin - exiting\n",
               (gint) getpid ());
      g_main_loop_quit (main_loop);
    }
  else
    {
      g_warning ("Unhandled condition %d on GIOChannel for stdin", condition);
    }
  return FALSE; /* remove source */
}

int
main (int    argc,
      char **argv)
{
  g_autoptr (GError) error = NULL;
  GOptionContext *opt_context;
  GMainLoop *main_loop;
  gint ret;
  guint name_owner_id;
  GIOChannel *stdin_channel;

  ret = 1;
  opt_context = NULL;
  name_owner_id = 0;
  stdin_channel = NULL;

  introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL);
  g_assert (introspection_data != NULL);

  opt_context = g_option_context_new (GETTEXT_PACKAGE "calendar server");
  g_option_context_add_main_entries (opt_context, opt_entries, NULL);
  if (!g_option_context_parse (opt_context, &argc, &argv, &error))
    {
      g_printerr ("Error parsing options: %s\n", error->message);
      goto out;
    }

  main_loop = g_main_loop_new (NULL, FALSE);

  stdin_channel = g_io_channel_unix_new (STDIN_FILENO);
  g_io_add_watch_full (stdin_channel,
                       G_PRIORITY_DEFAULT,
                       G_IO_HUP,
                       stdin_channel_io_func,
                       g_main_loop_ref (main_loop),
                       (GDestroyNotify) g_main_loop_unref);

  name_owner_id = g_bus_own_name (G_BUS_TYPE_SESSION,
                                  BUS_NAME,
                                  G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT |
                                   (opt_replace ? G_BUS_NAME_OWNER_FLAGS_REPLACE : 0),
                                  on_bus_acquired,
                                  on_name_acquired,
                                  on_name_lost,
                                  g_main_loop_ref (main_loop),
                                  (GDestroyNotify) g_main_loop_unref);

  g_main_loop_run (main_loop);

  g_main_loop_unref (main_loop);

  ret = 0;

 out:
  if (stdin_channel != NULL)
    g_io_channel_unref (stdin_channel);
  if (_global_app != NULL)
    app_free (_global_app);
  if (name_owner_id != 0)
    g_bus_unown_name (name_owner_id);
  if (opt_context != NULL)
    g_option_context_free (opt_context);

  return ret;
}
