/* vi:set et ai sw=2 sts=2 ts=2: */
/*-
 * Copyright (c) 2009-2011 Jannis Pohlmann <jannis@xfce.org>
 * Copyright (c) 2015      Ali Abdallah    <ali@xfce.org>
 *
 * 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., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "tumbler-group-scheduler.h"
#include "tumbler-lifo-scheduler.h"
#include "tumbler-scheduler.h"
#include "tumbler-service-gdbus.h"
#include "tumbler-service.h"
#include "tumbler-utils.h"

#include "tumbler/tumbler.h"

#include <gio/gio.h>
#include <glib-object.h>
#include <glib/gi18n.h>


#define THUMBNAILER_PATH TUMBLER_SERVICE_PATH_PREFIX "/Thumbnailer1"
#define THUMBNAILER_SERVICE TUMBLER_SERVICE_NAME_PREFIX ".Thumbnailer1"
#define THUMBNAILER_IFACE TUMBLER_SERVICE_NAME_PREFIX ".Thumbnailer1"



/* property identifiers */
enum
{
  PROP_0,
  PROP_CONNECTION,
  PROP_REGISTRY,
};



typedef struct _SchedulerIdleInfo SchedulerIdleInfo;



static void
tumbler_service_constructed (GObject *object);
static void
tumbler_service_finalize (GObject *object);
static void
tumbler_service_get_property (GObject *object,
                              guint prop_id,
                              GValue *value,
                              GParamSpec *pspec);
static void
tumbler_service_set_property (GObject *object,
                              guint prop_id,
                              const GValue *value,
                              GParamSpec *pspec);
static gboolean
tumbler_service_queue_cb (TumblerExportedService *skeleton,
                          GDBusMethodInvocation *invocation,
                          const gchar *const *uris,
                          const gchar *const *mime_hints,
                          const gchar *flavor_name,
                          const gchar *scheduler_name,
                          guint handle_to_dequeue,
                          TumblerService *service);
static gboolean
tumbler_service_dequeue_cb (TumblerExportedService *skeleton,
                            GDBusMethodInvocation *invocation,
                            guint handle,
                            TumblerService *service);
static gboolean
tumbler_service_get_schedulers_cb (TumblerExportedService *skeleton,
                                   GDBusMethodInvocation *invocation,
                                   TumblerService *service);
static gboolean
tumbler_service_get_supported_cb (TumblerExportedService *skeleton,
                                  GDBusMethodInvocation *invocation,
                                  TumblerService *service);
static gboolean
tumbler_service_get_flavors_cb (TumblerExportedService *skeleton,
                                GDBusMethodInvocation *invocation,
                                TumblerService *service);
static void
tumbler_service_scheduler_error (TumblerScheduler *scheduler,
                                 guint32 handle,
                                 const gchar *const *failed_uris,
                                 GQuark error_domain,
                                 gint error_code,
                                 const gchar *message,
                                 const gchar *origin,
                                 TumblerService *service);
static void
tumbler_service_scheduler_finished (TumblerScheduler *scheduler,
                                    guint32 handle,
                                    const gchar *origin,
                                    TumblerService *service);
static void
tumbler_service_scheduler_ready (TumblerScheduler *scheduler,
                                 guint32 handle,
                                 const gchar *const *uris,
                                 const gchar *origin,
                                 TumblerService *service);
static void
tumbler_service_scheduler_started (TumblerScheduler *scheduler,
                                   guint32 handle,
                                   const gchar *origin,
                                   TumblerService *service);
static void
tumbler_service_pre_unmount (TumblerService *service,
                             GMount *mount,
                             GVolumeMonitor *monitor);
static void
scheduler_idle_info_free (SchedulerIdleInfo *info);



struct _TumblerService
{
  TumblerComponent __parent__;

  GDBusConnection *connection;
  TumblerExportedService *skeleton;
  gboolean dbus_interface_exported;

  TumblerRegistry *registry;
  TUMBLER_MUTEX (mutex);
  GList *schedulers;

  GVolumeMonitor *volume_monitor;
};

struct _SchedulerIdleInfo
{
  TumblerScheduler *scheduler;
  TumblerService *service;
  gchar **uris;
  gchar *message;
  gchar *origin;
  guint handle;
  gint error_code;
};



G_DEFINE_TYPE (TumblerService, tumbler_service, TUMBLER_TYPE_COMPONENT);



static void
tumbler_service_class_init (TumblerServiceClass *klass)
{
  GObjectClass *gobject_class;

  gobject_class = G_OBJECT_CLASS (klass);
  gobject_class->constructed = tumbler_service_constructed;
  gobject_class->finalize = tumbler_service_finalize;
  gobject_class->get_property = tumbler_service_get_property;
  gobject_class->set_property = tumbler_service_set_property;

  g_object_class_install_property (gobject_class, PROP_CONNECTION,
                                   g_param_spec_object ("connection",
                                                        "connection",
                                                        "connection",
                                                        G_TYPE_DBUS_CONNECTION,
                                                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (gobject_class, PROP_REGISTRY,
                                   g_param_spec_object ("registry",
                                                        "registry",
                                                        "registry",
                                                        TUMBLER_TYPE_REGISTRY,
                                                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
}



static void
tumbler_service_init (TumblerService *service)
{
  tumbler_mutex_create (service->mutex);
  service->schedulers = NULL;

  service->volume_monitor = g_volume_monitor_get ();
  g_signal_connect_swapped (service->volume_monitor, "mount-pre-unmount",
                            G_CALLBACK (tumbler_service_pre_unmount), service);
}



static void
tumbler_service_add_scheduler (TumblerService *service,
                               TumblerScheduler *scheduler)
{
  /* add the scheduler to the list */
  service->schedulers = g_list_append (service->schedulers, g_object_ref (scheduler));

  /* connect to the scheduler signals */
  g_signal_connect (scheduler, "error",
                    G_CALLBACK (tumbler_service_scheduler_error), service);
  g_signal_connect (scheduler, "finished",
                    G_CALLBACK (tumbler_service_scheduler_finished), service);
  g_signal_connect (scheduler, "ready",
                    G_CALLBACK (tumbler_service_scheduler_ready), service);
  g_signal_connect (scheduler, "started",
                    G_CALLBACK (tumbler_service_scheduler_started), service);
}



static void
tumbler_service_remove_scheduler (TumblerScheduler *scheduler,
                                  TumblerService *service)
{
  g_return_if_fail (TUMBLER_IS_SCHEDULER (scheduler));
  g_return_if_fail (TUMBLER_IS_SERVICE (service));

  g_signal_handlers_disconnect_matched (scheduler, G_SIGNAL_MATCH_DATA,
                                        0, 0, NULL, NULL, service);
  g_object_unref (scheduler);
}



static void
tumbler_service_constructed (GObject *object)
{
  TumblerScheduler *scheduler;
  TumblerService *service = TUMBLER_SERVICE (object);
  GError *error = NULL;

  /* chain up to parent classes */
  if (G_OBJECT_CLASS (tumbler_service_parent_class)->constructed != NULL)
    (G_OBJECT_CLASS (tumbler_service_parent_class)->constructed) (object);

  /* create the foreground scheduler */
  scheduler = tumbler_lifo_scheduler_new ("foreground");
  tumbler_service_add_scheduler (service, scheduler);
  g_object_unref (scheduler);

  /* create the background scheduler */
  scheduler = tumbler_group_scheduler_new ("background");
  tumbler_service_add_scheduler (service, scheduler);
  g_object_unref (scheduler);

  /* everything is fine, install the generic thumbnailer D-Bus info */
  service->skeleton = tumbler_exported_service_skeleton_new ();

  g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (service->skeleton),
                                    service->connection,
                                    THUMBNAILER_PATH,
                                    &error);
  if (error != NULL)
    {
      g_critical ("error exporting thumbnail service on session bus: %s", error->message);
      g_error_free (error);
      service->dbus_interface_exported = FALSE;
    }
  else
    {
      service->dbus_interface_exported = TRUE;

      g_signal_connect (service->skeleton, "handle-queue",
                        G_CALLBACK (tumbler_service_queue_cb), service);

      g_signal_connect (service->skeleton, "handle-dequeue",
                        G_CALLBACK (tumbler_service_dequeue_cb), service);

      g_signal_connect (service->skeleton, "handle-get-supported",
                        G_CALLBACK (tumbler_service_get_supported_cb), service);

      g_signal_connect (service->skeleton, "handle-get-schedulers",
                        G_CALLBACK (tumbler_service_get_schedulers_cb), service);

      g_signal_connect (service->skeleton, "handle-get-flavors",
                        G_CALLBACK (tumbler_service_get_flavors_cb), service);
    }
}



static void
tumbler_service_finalize (GObject *object)
{
  TumblerService *service = TUMBLER_SERVICE (object);

  /* disconnect from the volume monitor */
  g_signal_handlers_disconnect_matched (service->volume_monitor, G_SIGNAL_MATCH_DATA,
                                        0, 0, NULL, NULL, service);

  /* release the volume monitor */
  g_object_unref (service->volume_monitor);

  /* release all schedulers and the scheduler list */
  g_list_foreach (service->schedulers, (GFunc) tumbler_service_remove_scheduler, service);
  g_list_free (service->schedulers);

  /* release the reference on the thumbnailer registry */
  g_object_unref (service->registry);

  /* Unexport from dbus */
  if (service->dbus_interface_exported)
    g_dbus_interface_skeleton_unexport_from_connection (
      G_DBUS_INTERFACE_SKELETON (service->skeleton),
      service->connection);

  /* release the Skeleton object */
  g_object_unref (service->skeleton);

  /* release the D-Bus connection object */
  g_object_unref (service->connection);

  tumbler_mutex_free (service->mutex);

  (*G_OBJECT_CLASS (tumbler_service_parent_class)->finalize) (object);
}



static void
tumbler_service_get_property (GObject *object,
                              guint prop_id,
                              GValue *value,
                              GParamSpec *pspec)
{
  TumblerService *service = TUMBLER_SERVICE (object);

  switch (prop_id)
    {
    case PROP_CONNECTION:
      g_value_set_object (value, service->connection);
      break;
    case PROP_REGISTRY:
      g_value_set_object (value, service->registry);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}



static void
tumbler_service_set_property (GObject *object,
                              guint prop_id,
                              const GValue *value,
                              GParamSpec *pspec)
{
  TumblerService *service = TUMBLER_SERVICE (object);

  switch (prop_id)
    {
    case PROP_CONNECTION:
      service->connection = g_object_ref (g_value_get_object (value));
      break;
    case PROP_REGISTRY:
      service->registry = g_value_dup_object (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}



static gboolean
tumbler_service_error_idle (gpointer user_data)
{
  TumblerCache *cache;
  SchedulerIdleInfo *info = user_data;
  GVariant *signal_variant;

  g_return_val_if_fail (info != NULL, FALSE);
  g_return_val_if_fail (TUMBLER_IS_SCHEDULER (info->scheduler), FALSE);
  g_return_val_if_fail (info->uris != NULL && info->uris[0] != NULL && *info->uris[0] != '\0', FALSE);
  g_return_val_if_fail (info->message != NULL && *info->message != '\0', FALSE);
  g_return_val_if_fail (info->origin != NULL && *info->origin != '\0', FALSE);
  g_return_val_if_fail (TUMBLER_IS_SERVICE (info->service), FALSE);

  /* cache cleanup: any previous thumbnail is now invalid */
  cache = tumbler_cache_get_default ();
  tumbler_cache_delete (cache, (const gchar *const *) info->uris);
  g_object_unref (cache);

  /* signal variant */
  signal_variant = g_variant_new ("(u^asis)",
                                  info->handle,
                                  info->uris,
                                  info->error_code,
                                  info->message);

  /* send the signal message over D-Bus */
  g_dbus_connection_emit_signal (info->service->connection,
                                 info->origin,
                                 THUMBNAILER_PATH,
                                 THUMBNAILER_IFACE,
                                 "Error",
                                 signal_variant,
                                 NULL);

  scheduler_idle_info_free (info);

  return FALSE;
}



static void
tumbler_service_scheduler_error (TumblerScheduler *scheduler,
                                 guint32 handle,
                                 const gchar *const *failed_uris,
                                 GQuark error_domain,
                                 gint error_code,
                                 const gchar *message,
                                 const gchar *origin,
                                 TumblerService *service)
{
  SchedulerIdleInfo *info;

  g_return_if_fail (TUMBLER_IS_SCHEDULER (scheduler));
  g_return_if_fail (failed_uris != NULL);
  g_return_if_fail (message != NULL && *message != '\0');
  g_return_if_fail (origin != NULL && *origin != '\0');
  g_return_if_fail (TUMBLER_IS_SERVICE (service));

  /* request cancelled at some level after being handled by the thumbnailer: no signal,
   * just as if it had been cancelled before */
  if (error_domain == G_IO_ERROR && error_code == G_IO_ERROR_CANCELLED)
    return;

  info = g_slice_new0 (SchedulerIdleInfo);

  info->scheduler = g_object_ref (scheduler);
  info->handle = handle;
  info->uris = g_strdupv ((gchar **) failed_uris);
  info->origin = g_strdup (origin);
  info->service = g_object_ref (service);
  if (error_domain == TUMBLER_ERROR)
    {
      info->error_code = error_code;
      info->message = g_strdup (message);
    }
  else
    {
      info->error_code = TUMBLER_ERROR_OTHER_ERROR_DOMAIN;
      info->message = g_strdup_printf ("(domain %s, code %d) %s",
                                       g_quark_to_string (error_domain), error_code, message);
    }

  g_debug ("Error signal for job %d: Code %d, message: %s",
           handle, info->error_code, info->message);
  tumbler_util_dump_strv (G_LOG_DOMAIN, "URIs", failed_uris);

  g_idle_add (tumbler_service_error_idle, info);
}



static gboolean
tumbler_service_finished_idle (gpointer user_data)
{
  SchedulerIdleInfo *info = user_data;
  GVariant *signal_variant;

  g_return_val_if_fail (info != NULL, FALSE);
  g_return_val_if_fail (info->origin != NULL && *info->origin != '\0', FALSE);
  g_return_val_if_fail (TUMBLER_IS_SERVICE (info->service), FALSE);

  signal_variant = g_variant_new ("(u)",
                                  info->handle);

  /* send the signal message over D-Bus */
  g_dbus_connection_emit_signal (info->service->connection,
                                 info->origin,
                                 THUMBNAILER_PATH,
                                 THUMBNAILER_IFACE,
                                 "Finished",
                                 signal_variant,
                                 NULL);

  /* allow the lifecycle manager to shut down the service again (unless there
   * are other requests still being processed) */
  tumbler_component_decrement_use_count (TUMBLER_COMPONENT (info->service));

  scheduler_idle_info_free (info);

  return FALSE;
}



static void
tumbler_service_scheduler_finished (TumblerScheduler *scheduler,
                                    guint32 handle,
                                    const gchar *origin,
                                    TumblerService *service)
{
  SchedulerIdleInfo *info;

  g_return_if_fail (TUMBLER_IS_SCHEDULER (scheduler));
  g_return_if_fail (origin != NULL && *origin != '\0');
  g_return_if_fail (TUMBLER_IS_SERVICE (service));

  g_debug ("Finishing job %d\n", handle);

  info = g_slice_new0 (SchedulerIdleInfo);

  info->scheduler = g_object_ref (scheduler);
  info->handle = handle;
  info->origin = g_strdup (origin);
  info->service = g_object_ref (service);

  g_idle_add (tumbler_service_finished_idle, info);
}



static gboolean
tumbler_service_ready_idle (gpointer user_data)
{
  SchedulerIdleInfo *info = user_data;
  GVariant *signal_variant;

  g_return_val_if_fail (info != NULL, FALSE);
  g_return_val_if_fail (TUMBLER_IS_SCHEDULER (info->scheduler), FALSE);
  g_return_val_if_fail (info->uris != NULL && info->uris[0] != NULL && *info->uris[0] != '\0', FALSE);
  g_return_val_if_fail (info->origin != NULL && *info->origin != '\0', FALSE);
  g_return_val_if_fail (TUMBLER_IS_SERVICE (info->service), FALSE);

  signal_variant = g_variant_new ("(u^as)",
                                  info->handle,
                                  info->uris);

  /* send the signal message over D-Bus */
  g_dbus_connection_emit_signal (info->service->connection,
                                 info->origin,
                                 THUMBNAILER_PATH,
                                 THUMBNAILER_IFACE,
                                 "Ready",
                                 signal_variant,
                                 NULL);

  scheduler_idle_info_free (info);

  return FALSE;
}



static void
tumbler_service_scheduler_ready (TumblerScheduler *scheduler,
                                 guint32 handle,
                                 const gchar *const *uris,
                                 const gchar *origin,
                                 TumblerService *service)
{
  SchedulerIdleInfo *info;

  g_return_if_fail (TUMBLER_IS_SCHEDULER (scheduler));
  g_return_if_fail (origin != NULL && *origin != '\0');
  g_return_if_fail (uris != NULL && uris[0] != NULL && *uris[0] != '\0');
  g_return_if_fail (TUMBLER_IS_SERVICE (service));

  g_debug ("Ready signal for job %d", handle);
  tumbler_util_dump_strv (G_LOG_DOMAIN, "URIs", uris);

  info = g_slice_new0 (SchedulerIdleInfo);

  info->scheduler = g_object_ref (scheduler);
  info->handle = handle;
  info->uris = g_strdupv ((gchar **) uris);
  info->origin = g_strdup (origin);
  info->service = g_object_ref (service);

  g_idle_add (tumbler_service_ready_idle, info);
}



static gboolean
tumbler_service_started_idle (gpointer user_data)
{
  SchedulerIdleInfo *info = user_data;
  GVariant *signal_variant;

  g_return_val_if_fail (info != NULL, FALSE);
  g_return_val_if_fail (TUMBLER_IS_SCHEDULER (info->scheduler), FALSE);
  g_return_val_if_fail (info->origin != NULL && *info->origin != '\0', FALSE);
  g_return_val_if_fail (TUMBLER_IS_SERVICE (info->service), FALSE);

  signal_variant = g_variant_new ("(u)", info->handle);

  /* send the signal message over D-Bus */
  g_dbus_connection_emit_signal (info->service->connection,
                                 info->origin,
                                 THUMBNAILER_PATH,
                                 THUMBNAILER_IFACE,
                                 "Started",
                                 signal_variant,
                                 NULL);

  scheduler_idle_info_free (info);

  return FALSE;
}



static void
tumbler_service_scheduler_started (TumblerScheduler *scheduler,
                                   guint32 handle,
                                   const gchar *origin,
                                   TumblerService *service)
{
  SchedulerIdleInfo *info;

  g_return_if_fail (TUMBLER_IS_SCHEDULER (scheduler));
  g_return_if_fail (origin != NULL && *origin != '\0');
  g_return_if_fail (TUMBLER_IS_SERVICE (service));

  g_debug ("Starting job %d", handle);

  info = g_slice_new0 (SchedulerIdleInfo);

  info->scheduler = g_object_ref (scheduler);
  info->handle = handle;
  info->origin = g_strdup (origin);
  info->service = g_object_ref (service);

  g_idle_add (tumbler_service_started_idle, info);
}



static void
tumbler_service_pre_unmount (TumblerService *service,
                             GMount *mount,
                             GVolumeMonitor *volume_monitor)
{
  GList *iter;

  g_return_if_fail (TUMBLER_IS_SERVICE (service));
  g_return_if_fail (G_IS_MOUNT (mount));
  g_return_if_fail (volume_monitor == service->volume_monitor);

  tumbler_mutex_lock (service->mutex);

  /* iterate over all schedulers, cancelling URIs belonging to the mount */
  for (iter = service->schedulers; iter != NULL; iter = iter->next)
    tumbler_scheduler_cancel_by_mount (iter->data, mount);

  tumbler_mutex_unlock (service->mutex);
}



static void
scheduler_idle_info_free (SchedulerIdleInfo *info)
{
  if (info == NULL)
    return;

  g_free (info->message);
  g_free (info->origin);
  g_strfreev (info->uris);

  g_object_unref (info->scheduler);
  g_object_unref (info->service);

  g_slice_free (SchedulerIdleInfo, info);
}



TumblerService *
tumbler_service_new (GDBusConnection *connection,
                     TumblerLifecycleManager *lifecycle_manager,
                     TumblerRegistry *registry)
{
  return g_object_new (TUMBLER_TYPE_SERVICE,
                       "connection", connection,
                       "lifecycle-manager", lifecycle_manager,
                       "registry", registry,
                       NULL);
}



static gboolean
tumbler_service_queue_cb (TumblerExportedService *skeleton,
                          GDBusMethodInvocation *invocation,
                          const gchar *const *uris,
                          const gchar *const *mime_hints,
                          const gchar *flavor_name,
                          const gchar *scheduler_name,
                          guint32 handle_to_dequeue,
                          TumblerService *service)
{
  TumblerSchedulerRequest *scheduler_request;
  TumblerThumbnailFlavor *flavor;
  TumblerScheduler *scheduler = NULL;
  TumblerFileInfo **infos;
  TumblerCache *cache;
  GList **thumbnailers;
  GList *iter;
  gchar *name;
  const gchar *origin;
  guint32 handle;
  guint length;

  g_dbus_async_return_val_if_fail (TUMBLER_IS_SERVICE (service), invocation, FALSE);
  g_dbus_async_return_val_if_fail (uris != NULL, invocation, FALSE);
  g_dbus_async_return_val_if_fail (mime_hints != NULL, invocation, FALSE);

  tumbler_mutex_lock (service->mutex);

  /* prevent the lifecycle manager to shut down the service as long
   * as the request is still being processed */
  tumbler_component_increment_use_count (TUMBLER_COMPONENT (service));

  /* if the scheduler is not defined, fall back to "default" */
  if (scheduler_name == NULL || *scheduler_name == '\0')
    scheduler_name = "default";

  cache = tumbler_cache_get_default ();
  flavor = tumbler_cache_get_flavor (cache, flavor_name);
  g_object_unref (cache);

  infos = tumbler_file_info_array_new_with_flavor (uris, mime_hints, flavor,
                                                   &length);

  /* get an array with one thumbnailer for each URI in the request */
  thumbnailers = tumbler_registry_get_thumbnailer_array (service->registry, infos,
                                                         length);

  origin = g_dbus_method_invocation_get_sender (invocation);

  /* allocate a scheduler request */
  scheduler_request = tumbler_scheduler_request_new (infos, thumbnailers, length, origin);

  /* release the file info array */
  tumbler_file_info_array_free (infos);


  /* get the request handle */
  handle = scheduler_request->handle;

  g_debug ("Handling request %d", handle);
  tumbler_util_dump_strvs_side_by_side (G_LOG_DOMAIN, "URIs", "Mime types", uris, mime_hints);

  /* iterate over all schedulers */
  for (iter = service->schedulers; iter != NULL; iter = iter->next)
    {
      /* dequeue the request with the given dequeue handle, in case this
       * scheduler is responsible for the given handle */
      if (handle_to_dequeue != 0)
        tumbler_scheduler_dequeue (TUMBLER_SCHEDULER (iter->data), handle_to_dequeue);

      /* determine the scheduler name */
      name = tumbler_scheduler_get_name (TUMBLER_SCHEDULER (iter->data));

      /* check if this is the scheduler we are looking for */
      if (g_strcmp0 (name, scheduler_name) == 0)
        scheduler = TUMBLER_SCHEDULER (iter->data);

      /* free the scheduler name */
      g_free (name);
    }

  /* default to the first scheduler in the list if we couldn't find
   * the scheduler with the desired name */
  if (scheduler == NULL && service->schedulers != NULL)
    scheduler = TUMBLER_SCHEDULER (service->schedulers->data);

  /* report unsupported flavors back to the client */
  if (flavor == NULL)
    {
      /* fake a started signal */
      tumbler_service_scheduler_started (scheduler, handle, scheduler_request->origin,
                                         service);

      /* emit an error signal */
      tumbler_service_scheduler_error (scheduler, handle, uris, TUMBLER_ERROR,
                                       TUMBLER_ERROR_UNSUPPORTED_FLAVOR,
                                       TUMBLER_ERROR_MESSAGE_UNSUPPORTED_FLAVOR,
                                       scheduler_request->origin,
                                       service);

      /* fake a finished signal */
      tumbler_service_scheduler_finished (scheduler, handle, scheduler_request->origin,
                                          service);

      /* release the request */
      tumbler_scheduler_request_free (scheduler_request);
    }
  else
    {
      /* let the scheduler take it from here */
      tumbler_scheduler_push (scheduler, scheduler_request);
      g_object_unref (flavor);
    }

  /* free the thumbnailer array */
  tumbler_thumbnailer_array_free (thumbnailers, length);

  tumbler_mutex_unlock (service->mutex);

  tumbler_exported_service_complete_queue (skeleton, invocation, handle);

  /* try to keep tumbler alive */
  tumbler_component_keep_alive (TUMBLER_COMPONENT (service), NULL);

  return TRUE;
}



static gboolean
tumbler_service_dequeue_cb (TumblerExportedService *skeleton,
                            GDBusMethodInvocation *invocation,
                            guint32 handle,
                            TumblerService *service)
{
  GList *iter;

  tumbler_mutex_lock (service->mutex);

  if (handle != 0)
    {
      g_debug ("Dequeuing files for job %d", handle);

      /* iterate over all available schedulers */
      for (iter = service->schedulers; iter != NULL; iter = iter->next)
        {
          /* dequeue the request with the given dequeue handle, in case this
           * scheduler is responsible for the given handle */
          tumbler_scheduler_dequeue (TUMBLER_SCHEDULER (iter->data), handle);
        }
    }

  tumbler_mutex_unlock (service->mutex);

  tumbler_exported_service_complete_dequeue (skeleton, invocation);

  /* keep tumbler alive */
  tumbler_component_keep_alive (TUMBLER_COMPONENT (service), NULL);

  return TRUE;
}



static gboolean
tumbler_service_get_schedulers_cb (TumblerExportedService *skeleton,
                                   GDBusMethodInvocation *invocation,
                                   TumblerService *service)
{
  gchar **supported_schedulers;
  GList *iter;
  guint n = 0;

  tumbler_mutex_lock (service->mutex);

  /* allocate an error for the schedulers */
  supported_schedulers = g_new0 (gchar *, g_list_length (service->schedulers) + 2);

  /* always prepend the "default" scheduler */
  supported_schedulers[n++] = g_strdup ("default");

  /* append all supported scheduler names */
  for (iter = service->schedulers; iter != NULL; iter = iter->next)
    {
      supported_schedulers[n++] =
        tumbler_scheduler_get_name (TUMBLER_SCHEDULER (iter->data));
    }

  tumbler_mutex_unlock (service->mutex);

  /* NULL-terminate the array */
  supported_schedulers[n] = NULL;

  /* return the scheduler array to the caller */
  tumbler_exported_service_complete_get_schedulers (skeleton,
                                                    invocation,
                                                    (const char *const *) supported_schedulers);

  /* free the array */
  g_strfreev (supported_schedulers);

  /* try to keep tumbler alive */
  tumbler_component_keep_alive (TUMBLER_COMPONENT (service), NULL);

  return TRUE;
}



static gboolean
tumbler_service_get_supported_cb (TumblerExportedService *skeleton,
                                  GDBusMethodInvocation *invocation,
                                  TumblerService *service)
{
  const gchar *const *mime_types;
  const gchar *const *uri_schemes;

  g_dbus_async_return_val_if_fail (TUMBLER_IS_SERVICE (service), invocation, FALSE);

  tumbler_mutex_lock (service->mutex);

  /* fetch all supported URI scheme / MIME type pairs from the registry */
  tumbler_registry_get_supported (service->registry, &uri_schemes, &mime_types);

  tumbler_mutex_unlock (service->mutex);

  /* return the arrays to the caller */
  tumbler_exported_service_complete_get_supported (skeleton, invocation, uri_schemes, mime_types);

  /* try to keep tumbler alive */
  tumbler_component_keep_alive (TUMBLER_COMPONENT (service), NULL);

  return TRUE;
}



static gboolean
tumbler_service_get_flavors_cb (TumblerExportedService *skeleton,
                                GDBusMethodInvocation *invocation,
                                TumblerService *service)
{
  TumblerCache *cache;
  const gchar **flavor_strings;
  GList *flavors;
  GList *iter;
  guint n;

  cache = tumbler_cache_get_default ();

  if (cache != NULL)
    {
      flavors = tumbler_cache_get_flavors (cache);
      flavor_strings = g_new0 (const gchar *, g_list_length (flavors) + 1);

      for (iter = flavors, n = 0; iter != NULL; iter = iter->next, ++n)
        flavor_strings[n] = tumbler_thumbnail_flavor_get_name (iter->data);
      flavor_strings[n] = NULL;

      tumbler_exported_service_complete_get_flavors (skeleton, invocation, flavor_strings);

      g_free (flavor_strings);
      g_list_free_full (flavors, g_object_unref);
      g_object_unref (cache);
    }
  else
    {
      flavor_strings = g_new0 (const gchar *, 1);
      flavor_strings[0] = NULL;

      tumbler_exported_service_complete_get_flavors (skeleton, invocation, flavor_strings);

      g_free (flavor_strings);
    }

  /* try to keep tumbler alive */
  tumbler_component_keep_alive (TUMBLER_COMPONENT (service), NULL);

  return TRUE;
}



gboolean
tumbler_service_is_exported (TumblerService *service)
{
  g_return_val_if_fail (TUMBLER_IS_SERVICE (service), FALSE);
  return service->dbus_interface_exported;
}
