/*
 * Copyright (C) 2011 Red Hat, 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.
 *
 * Author: David Zeuthen <davidz@redhat.com>
 */

/* Compile with
 *
 *   gcc -o gcs gcs.c -Wall `pkg-config --cflags --libs gio-2.0`
 */

#include <gio/gio.h>

/* Set the environment variable GCS_DEBUG to show debug */
static gboolean show_debug (void);
static void     print_debug (const gchar *str, ...);

#define BUS_NAME "org.gnome.Shell.CalendarServer"

static const gchar introspection_xml[] =
  "<node>"
  "  <interface name='org.gnome.Shell.CalendarServer'>"
  "    <method name='GetEvents'>"
  "      <arg type='x' name='since' direction='in'/>"
  "      <arg type='x' name='until' direction='in'/>"
  "      <arg type='b' name='force_reload' direction='in'/>"
  "      <arg type='a(sssbxxa{sv})' name='events' direction='out'/>"
  "    </method>"
  "    <signal name='Changed'/>"
  "    <property name='Since' type='x' access='read'/>"
  "    <property name='Until' type='x' access='read'/>"
  "  </interface>"
  "</node>";
static GDBusNodeInfo *introspection_data = NULL;

struct _App;
typedef struct _App App;

static GMainLoop    *loop = NULL;
static gboolean      opt_replace = FALSE;
static GOptionEntry  opt_entries[] =
{
  {"replace", 0, 0, G_OPTION_ARG_NONE, &opt_replace, "Replace existing server", NULL},
  {NULL }
};

static App *_global_app = NULL;

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

struct _App
{
  GDBusConnection *connection;
  time_t since;
  time_t until;

  guint changed_timeout_id;

  gboolean cache_invalid;
};

#if 0
static gboolean
on_app_schedule_changed_cb (gpointer user_data)
{
  App *app = user_data;
  print_debug ("Emitting changed");
  g_dbus_connection_emit_signal (app->connection,
                                 NULL, /* destination_bus_name */
                                 "/org/gnome/Shell/CalendarServer",
                                 "org.gnome.Shell.CalendarServer",
                                 "Changed",
                                 NULL, /* no params */
                                 NULL);
  app->changed_timeout_id = 0;
  return FALSE;
}

static void
app_schedule_changed (App *app)
{
  print_debug ("Scheduling changed");
  if (app->changed_timeout_id == 0)
    {
      app->changed_timeout_id = g_timeout_add (2000,
                                               on_app_schedule_changed_cb,
                                               app);
    }
}

static void
app_invalidate_cache (App *app)
{
  app->cache_invalid = TRUE;
}
#endif

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

  app = g_new0 (App, 1);
  app->connection = g_object_ref (connection);

  return app;
}

static void
app_free (App *app)
{
  g_object_unref (app->connection);
  if (app->changed_timeout_id != 0)
    g_source_remove (app->changed_timeout_id);
  g_free (app);
}

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

static void
app_load_events (App *app)
{
}

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

static void
add_appointment (GVariantBuilder *builder,
                 gint year, gint month, gint day,
                 gint hour, gint minute, gint second,
                 gint duration_seconds,
                 const gchar *summary_format,
                 ...)
{
  GVariantBuilder extras_builder;
  va_list var_args;
  gchar *summary;
  GDateTime *start_time;
  GDateTime *end_time;

  va_start (var_args, summary_format);
  summary = g_strdup_vprintf (summary_format, var_args);
  va_end (var_args);

  start_time = g_date_time_new_local (year, month, day,
                                      hour, minute, second);
  end_time = g_date_time_add_seconds (start_time, duration_seconds);

  g_variant_builder_init (&extras_builder, G_VARIANT_TYPE ("a{sv}")); /* unused escape hatch */
  g_variant_builder_add (builder,
                         "(sssbxxa{sv})",
                         "", /* uid - currently unused by the shell */
                         summary,
                         "", /* description - currently unused by the shell */
                         FALSE, /* is_all_day */
                         g_date_time_to_unix (start_time),
                         g_date_time_to_unix (end_time),
                         extras_builder);

  g_date_time_unref (start_time);
  g_date_time_unref (end_time);
  g_free (summary);
}

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

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;
  GDateTime *since_local;
  GDateTime *until_local;
  GDateTime *tmp;
  GDateTime *day_local_iter;

  since_local = NULL;
  until_local = NULL;
  day_local_iter = NULL;

  if (g_strcmp0 (method_name, "GetEvents") == 0)
    {
      GVariantBuilder builder;
      gint64 since;
      gint64 until;
      gboolean force_reload;
      gboolean window_changed;

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

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

      tmp = g_date_time_new_from_unix_utc (since);
      if (tmp == NULL)
        {
          g_dbus_method_invocation_return_dbus_error (invocation,
                                                      "org.gnome.Shell.CalendarServer.Error",
                                                      "The since param is outside the supported range of GDateTime.");
          goto out;
        }
      since_local = g_date_time_to_local (tmp);
      g_date_time_unref (tmp);
      tmp = g_date_time_new_from_unix_utc (until);
      if (tmp == NULL)
        {
          g_dbus_method_invocation_return_dbus_error (invocation,
                                                      "org.gnome.Shell.CalendarServer.Error",
                                                      "The until param is outside the supported range of GDateTime.");
          goto out;
        }
      until_local = g_date_time_to_local (tmp);
      g_date_time_unref (tmp);

      if (show_debug ())
        {
          gchar *since_str;
          gchar *until_str;
          since_str = g_date_time_format (since_local,"%Y-%m-%d %H:%M:%S %Z unix=%s");
          until_str = g_date_time_format (until_local,"%Y-%m-%d %H:%M:%S %Z unix=%s");
          print_debug ("Handling GetEvents()\n"
                       "  since:        %s\n"
                       "  until:        %s\n"
                       "  force_reload: %s",
                       since_str,
                       until_str,
                       force_reload ? "true" : "false");
          g_free (since_str);
          g_free (until_str);
        }

      window_changed = 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 */
                                         "/org/gnome/Shell/CalendarServer",
                                         "org.freedesktop.DBus.Properties",
                                         "PropertiesChanged",
                                         g_variant_new ("(sa{sv}as)",
                                                        "org.gnome.Shell.CalendarServer",
                                                        builder,
                                                        invalidated_builder),
                                         NULL); /* GError** */
        }

      /* reload events if necessary */
      if (window_changed || force_reload || app->cache_invalid)
        {
          app_load_events (app);
        }

      /* 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(sssbxxa{sv})"));

      day_local_iter = g_date_time_ref (since_local);
      while (g_date_time_difference (until_local, day_local_iter) > 0)
        {
          gint year, month, day;

          g_date_time_get_ymd (day_local_iter, &year, &month, &day);
          add_appointment (&builder,
                           year, month, day, 10, 0, 0, 5*60*60,
                           "Daily 10a-3p appt (%02d-%02d-%02d)",
                           year, month, day);

          /* advance */
          tmp = g_date_time_add_days (day_local_iter, 1);
          g_date_time_unref (day_local_iter);
          day_local_iter = tmp;
        }

#if 0
      GVariantBuilder extras_builder;
      g_variant_builder_init (&extras_builder, G_VARIANT_TYPE ("a{sv}"));
      g_variant_builder_add (&builder,
                             "(sssbxxa{sv})",
                             a->uid,
                             a->summary != NULL ? a->summary : "",
                             a->description != NULL ? a->description : "",
                             (gboolean) a->is_all_day,
                             (gint64) start_time,
                             (gint64) end_time,
                             extras_builder);
#endif
      g_dbus_method_invocation_return_value (invocation,
                                             g_variant_new ("(a(sssbxxa{sv}))", &builder));
    }
  else
    {
      g_assert_not_reached ();
    }

 out:
  if (day_local_iter != NULL)
    g_date_time_unref (day_local_iter);
  if (since_local != NULL)
    g_date_time_unref (since_local);
  if (until_local != NULL)
    g_date_time_unref (until_local);
}

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
    {
      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)
{
  GError *error;
  guint registration_id;

  _global_app = app_new (connection);

  error = NULL;
  registration_id = g_dbus_connection_register_object (connection,
                                                       "/org/gnome/Shell/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)",
                  error->message,
                  g_quark_to_string (error->domain),
                  error->code);
      g_error_free (error);
      _exit (1);
    }

  print_debug ("Connected to the session bus");

}

static void
on_name_lost (GDBusConnection *connection,
              const gchar     *name,
              gpointer         user_data)
{
  g_print ("gcs[%d]: Lost (or failed to acquire) the name " BUS_NAME " - exiting\n",
           (gint) getpid ());
  g_main_loop_quit (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)
{
  if (condition & G_IO_HUP)
    {
      g_print ("gcs[%d]: Got HUP on stdin - exiting\n",
               (gint) getpid ());
      g_main_loop_quit (loop);
    }
  else
    {
      g_warning ("Unhandled condition %d on GIOChannel for stdin", condition);
    }
  return FALSE; /* remove source */
}

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

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

  g_type_init ();

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

  opt_context = g_option_context_new ("GNOME Calendar Server");
  g_option_context_add_main_entries (opt_context, opt_entries, NULL);
  error = NULL;
  if (!g_option_context_parse (opt_context, &argc, &argv, &error))
    {
      g_printerr ("Error parsing options: %s", error->message);
      g_error_free (error);
      goto out;
    }

  stdin_channel = g_io_channel_unix_new (STDIN_FILENO);
  g_io_add_watch (stdin_channel,
                  G_IO_HUP,
                  stdin_channel_io_func,
                  NULL);

  loop = g_main_loop_new (NULL, FALSE);

  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,
                                  NULL,
                                  NULL);

  g_main_loop_run (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 (loop != NULL)
    g_main_loop_unref (loop);
  if (opt_context != NULL)
    g_option_context_free (opt_context);
  return ret;
}

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

static gboolean _show_debug = FALSE;

static gboolean
show_debug (void)
{
  return _show_debug;
}

static void
print_debug (const gchar *format, ...)
{
  gchar *s;
  va_list ap;
  gchar timebuf[64];
  GTimeVal now;
  time_t now_t;
  struct tm broken_down;
  static volatile gsize once_init_value = 0;
  static guint pid = 0;

  if (g_once_init_enter (&once_init_value))
    {
      _show_debug = (g_getenv ("GCS_DEBUG") != NULL);
      pid = getpid ();
      g_once_init_leave (&once_init_value, 1);
    }

  if (!_show_debug)
    goto out;

  g_get_current_time (&now);
  now_t = now.tv_sec;
  localtime_r (&now_t, &broken_down);
  strftime (timebuf, sizeof timebuf, "%H:%M:%S", &broken_down);

  va_start (ap, format);
  s = g_strdup_vprintf (format, ap);
  va_end (ap);

  g_print ("gcs[%d]: %s.%03d: %s\n", pid, timebuf, (gint) (now.tv_usec / 1000), s);
  g_free (s);
 out:
  ;
}
