/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2014 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "cockpitdbuscache.h"

#include "cockpitdbusrules.h"
#include "cockpitpaths.h"

#include <string.h>

#define DEBUG_BATCHES 0

/*
 * This is a cache of properties which tracks updates. The best way to do
 * this is via ObjectManager. But it also does introspection and uses that
 * to get both interface info, and information about which paths are present
 * and the interfaces they implement.
 *
 * One big complication is that all of this needs to have ordering guarantees,
 * including introspection. We keep track of which batch of properties we're
 * working on, and associate barrier callbacks which can only happen once
 * a given batch of properties has completed processing.
 *
 * Also information about an interface will be available before we notify
 * about properties on an interface. This is a further ordering guarantee.
 *
 * Since there are lots of strings, to help with allocation churn, we have our
 * own string intern table, where path, interface and property names are
 * stored while the cache is active. Each time we get a path etc. from an
 * external source source (such as GVariant) and we know we'll need it later
 * then we intern it, so it sticks around.
 */

struct _CockpitDBusCache {
  GObject parent;

  /* Cancelled on dispose */
  GCancellable *cancellable;

  /* The connection to talk on */
  GDBusConnection *connection;

  /* The readable DBus name, and actual unique owner */
  gchar *logname;
  gchar *name;
  gchar *name_owner;

  /* Introspection stuff */
  GHashTable *introspected;
  GQueue *introspects;
  GHashTable *introsent;
  GList *trash;

  /* The main data cache: paths > interfaces -> properties -> values */
  GHashTable *cache;

  /* The paths and interfaces we should watch */
  CockpitDBusRules *rules;

  /* Accumulated information about these various paths */
  GTree *managed;
  GTree *managed_not_ready;

  /* Signal Subscriptions */
  gboolean subscribed;
  guint subscribe_properties;
  guint subscribe_manager;

  /* Barrier related stuff */
  GQueue *batches;
  GQueue *barriers;
  guint number;
  gboolean barrier_progressing;
  guint barrier_progressing_rounds;
  GHashTable *update;

  /* Interned strings */
  GHashTable *interned;
};

enum {
  PROP_CONNECTION = 1,
  PROP_NAME,
  PROP_LOGNAME,
  PROP_INTERFACE_INFO
};

static guint signal_meta;
static guint signal_update;

G_DEFINE_TYPE (CockpitDBusCache, cockpit_dbus_cache, G_TYPE_OBJECT);

static void
hash_table_unref_or_null (gpointer data)
{
  if (data)
    g_hash_table_unref (data);
}

static const gchar *
intern_string (CockpitDBusCache *self,
               const gchar *string)
{
  const gchar * interned;
  gchar *copy;

  interned = g_hash_table_lookup (self->interned, string);
  if (!interned)
    {
      copy = g_strdup (string);
      g_hash_table_add (self->interned, copy);
      interned = copy;
    }

  return interned;
}

typedef struct {
  guint number;
  CockpitDBusBarrierFunc callback;
  gpointer user_data;
} BarrierData;

typedef struct {
  gint refs;
  guint number;
  gboolean orphan;
#if DEBUG_BATCHES
  GSList *debug;
#endif
} BatchData;

static void
barrier_progress (CockpitDBusCache *self)
{
  BarrierData *barrier;
  BatchData *batch;

  if (self->barrier_progressing)
    {
      /* It might happen that barrier_progress is called
       * recursively, while one of the barrier callbacks is
       * running.  We need to detect this and avoid calling the
       * next barrier callback while there is already one
       * running.  Allowing it might violate the ordering
       * guarantees if the running callback is doing more work
       * after causing the recursive call to barrier_progress.
       *
       * Concretely, process_interfaces below can cause
       * recursive calls in the middle of its work when it
       * processes multiple interfaces.
       */

      self->barrier_progressing_rounds += 1;
      return;
    }

  self->barrier_progressing = TRUE;
  self->barrier_progressing_rounds = 1;

  while (self->barrier_progressing_rounds > 0)
    {
      self->barrier_progressing_rounds -= 1;

      batch = g_queue_peek_head (self->batches);

      for (;;)
        {
          barrier = g_queue_peek_head (self->barriers);
          if (!barrier)
            break;

          /*
           * If there is a batch being processed, we must block
           * barriers that have an equal or later batch number.
           */
          if (batch && batch->number <= barrier->number)
            break;

          g_queue_pop_head (self->barriers);
          (barrier->callback) (self, barrier->user_data);
          g_slice_free (BarrierData, barrier);
        }
    }

  self->barrier_progressing = FALSE;
}

static void
barrier_flush (CockpitDBusCache *self)
{
  BarrierData *barrier;

  for (;;)
    {
      barrier = g_queue_pop_head (self->barriers);
      if (!barrier)
        return;
      (barrier->callback) (self, barrier->user_data);
      g_slice_free (BarrierData, barrier);
    }
}

#if DEBUG_BATCHES
static void
batch_dump (BatchData *batch)
{
  GSList *l;
  g_printerr ("BATCH %u (refs %d)\n", batch->number, batch->refs);
  batch->debug = g_slist_reverse (batch->debug);
  for (l = batch->debug; l != NULL; l = g_slist_next (l))
    g_printerr (" * %s\n", (gchar *)l->data);
  batch->debug = g_slist_reverse (batch->debug);
}
#endif /* DEBUG_BATCHES */

static void
batch_free (BatchData *batch)
{
#if DEBUG_BATCHES
  g_slist_foreach (batch->debug, (GFunc)g_free, NULL);
#endif
  g_slice_free (BatchData, batch);
}

static void
batch_progress (CockpitDBusCache *self)
{
  BatchData *batch;
  GHashTable *update;

  for (;;)
    {
      batch = g_queue_peek_head (self->batches);

      /*
       * Once a batch has completed it's refs field will be zero.
       * This means we can send notify of any property changes,
       * process any barriers waiting on this batch, and move on
       * to the next batch.
       */
      if (!batch || batch->refs > 0)
        return;

      g_queue_pop_head (self->batches);
      update = self->update;
      self->update = NULL;

      if (update)
        {
          g_signal_emit (self, signal_update, 0, update);
          g_hash_table_unref (update);
        }

      batch_free (batch);
      barrier_progress (self);
    }
}

static void
batch_flush (CockpitDBusCache *self)
{
  BatchData *batch;

  for (;;)
    {
      batch = g_queue_pop_head (self->batches);
      if (!batch)
        return;
      if (batch->refs == 0)
        batch_free (batch);
      else
        batch->orphan = TRUE;
    }
}

static BatchData *
batch_create (CockpitDBusCache *self)
{
  BatchData *batch = g_slice_new0 (BatchData);
  batch->refs = 1;
  self->number++;
  batch->number = self->number;
  g_queue_push_tail (self->batches, batch);
  return batch;
}

static BatchData *
_batch_ref (BatchData *batch,
            const gchar *function,
            gint line)
{
  g_assert (batch != NULL);
  batch->refs++;
#if DEBUG_BATCHES
  batch->debug = g_slist_prepend (batch->debug, g_strdup_printf (" * ref -> %d %s:%d",
                                                                 batch->refs, function, line));
#endif
  return batch;
}

#define batch_ref(batch) \
  (_batch_ref((batch), G_STRFUNC, __LINE__))

static void
_batch_unref (CockpitDBusCache *self,
              BatchData *batch,
              const gchar *function,
              gint line)
{
  g_assert (batch != NULL);
#if DEBUG_BATCHES
  if (!(batch->refs > 0))
      batch_dump (batch);
#endif
  g_assert (batch->refs > 0);
  batch->refs--;
#if DEBUG_BATCHES
  batch->debug = g_slist_prepend (batch->debug, g_strdup_printf (" * unref -> %d %s:%d",
                                                                 batch->refs, function, line));
#endif

  if (batch->refs == 0 && batch->orphan)
    batch_free (batch);
  else
    batch_progress (self);
}

#define batch_unref(self, batch) \
  (_batch_unref((self), (batch), G_STRFUNC, __LINE__))

static void
cockpit_dbus_cache_init (CockpitDBusCache *self)
{
  self->number = 1;

  self->cancellable = g_cancellable_new ();

  self->managed = cockpit_paths_new ();
  self->managed_not_ready = cockpit_paths_new ();

  /* Becomes a whole tree of hash tables */
  self->cache = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
                                       (GDestroyNotify)g_hash_table_unref);

  /* All of these are sets. ie: key and value identical */
  self->rules = cockpit_dbus_rules_new ();

  self->introspects = g_queue_new ();
  self->introsent = g_hash_table_new (g_str_hash, g_str_equal);

  self->batches = g_queue_new ();
  self->barriers = g_queue_new ();

  /* Put allocations we need to keep around, but can't handily track */
  self->trash = NULL;
  self->interned = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
}

typedef struct {
  const gchar *interface;
  const gchar *path;
  CockpitDBusIntrospectFunc callback;
  gpointer user_data;
  BatchData *batch;
  gboolean introspecting;
} IntrospectData;

static void
introspect_next (CockpitDBusCache *self);

static void
scrape_variant (CockpitDBusCache *self,
                BatchData *batch,
                GVariant *data);

static void
introspect_complete (CockpitDBusCache *self,
                     IntrospectData *id)
{
  GDBusInterfaceInfo *iface = NULL;

  if (id->interface)
    {
      iface = cockpit_dbus_interface_info_lookup (self->introspected, id->interface);
      if (!iface)
        {
          g_debug ("%s: introspect interface %s didn't work", self->logname, id->interface);

          /*
           * So we were expecting an interface that wasn't found at the expected
           * object. This means that something is wrong with the introspection
           * on the DBus service. We create a pretend empty interface, so that
           * the ordering guarantees are met.
           */

          iface = g_new0 (GDBusInterfaceInfo, 1);
          iface->ref_count = 1;
          iface->name = g_strdup (id->interface);

          cockpit_dbus_interface_info_push (self->introspected, iface);
          g_dbus_interface_info_unref (iface);
        }
    }

  if (id->callback)
    (id->callback) (self, iface, id->user_data);

  /* Mark as having called, as a double check */
  id->callback = NULL;

  batch_unref (self, id->batch);
  g_assert (id->callback == NULL);
  g_slice_free (IntrospectData, id);
}

static void
process_introspect_node (CockpitDBusCache *self,
                         BatchData *batch,
                         const gchar *path,
                         GDBusNodeInfo *node,
                         gboolean recursive);

static gboolean
dbus_error_matches_unknown (GError *error)
{
  gboolean ret = FALSE;

  if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD) ||
      g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED) ||
      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED))
    return TRUE;

#if GLIB_CHECK_VERSION(2,42,0)
  ret = g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_INTERFACE) ||
        g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_OBJECT) ||
        g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY);
#else

  gchar *remote = g_dbus_error_get_remote_error (error);
  if (remote)
    {
      /*
       * DBus used to only have the UnknownMethod error. It didn't have
       * specific errors for UnknownObject and UnknownInterface. So we're
       * pretty liberal on what we treat as an expected error here.
       *
       * HACK: GDBus also doesn't understand the newer error codes :S
       *
       * https://bugzilla.gnome.org/show_bug.cgi?id=727900
       */
      ret = (g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownMethod") ||
             g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownObject") ||
             g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownInterface") ||
             g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownProperty"));
      g_free (remote);
    }
#endif

  return ret;
}

static void
on_introspect_reply (GObject *source,
                     GAsyncResult *result,
                     gpointer user_data)
{
  CockpitDBusCache *self = user_data;
  IntrospectData *id;
  GDBusNodeInfo *node;
  GError *error = NULL;
  GVariant *retval;
  const gchar *xml;

  /* All done with this introspect */
  id = g_queue_pop_head (self->introspects);

  /* Introspects have been flushed */
  if (!id)
    {
      g_object_unref (self);
      return;
    }

  g_assert (id->introspecting);

  retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);

  if (retval)
    {
      g_debug ("%s: reply from Introspect() at %s", self->logname, id->path);

      g_variant_get (retval, "(&s)", &xml);

      node = g_dbus_node_info_new_for_xml (xml, &error);
      if (node)
        {
          process_introspect_node (self, id->batch, id->path, node, id->interface == NULL);
          g_dbus_node_info_unref (node);
        }
      g_variant_unref (retval);
    }

  if (error)
    {
      if (!dbus_error_matches_unknown (error))
        g_message ("%s: couldn't introspect %s: %s", self->logname, id->path, error->message);
      g_error_free (error);
    }

  introspect_complete (self, id);
  introspect_next (self);

  g_object_unref (self);
}

static void
introspect_next (CockpitDBusCache *self)
{
  IntrospectData *id;

  id = g_queue_peek_head (self->introspects);
  if (id && !id->introspecting)
    {
      if (g_cancellable_is_cancelled (self->cancellable))
        {
          g_queue_pop_head (self->introspects);
          introspect_complete (self, id);
        }
      else
        {
          GDBusInterfaceInfo *iface = NULL;
          if (id->interface)
            iface = cockpit_dbus_interface_info_lookup (self->introspected, id->interface);
          if (iface)
            {
              g_debug ("%s: not calling Introspect() on %s, already done",
                       self->logname, id->path);
              g_queue_pop_head (self->introspects);
              introspect_complete (self, id);
              introspect_next (self);
            }
          else
            {
              g_debug ("%s: calling Introspect() on %s", self->logname, id->path);

              id->introspecting = TRUE;
              g_dbus_connection_call (self->connection, self->name, id->path,
                                      "org.freedesktop.DBus.Introspectable", "Introspect",
                                      g_variant_new ("()"), G_VARIANT_TYPE ("(s)"),
                                      G_DBUS_CALL_FLAGS_NONE, -1,
                                      self->cancellable, on_introspect_reply,
                                      g_object_ref (self));
            }
        }
    }
}

static void
introspect_flush (CockpitDBusCache *self)
{
  gboolean note = FALSE;
  IntrospectData *id;
  GQueue *queue;

  queue = g_queue_new ();
  for (;;)
    {
      /* Steal everything, more could be added by callback */
      for (;;)
        {
          id = g_queue_pop_tail (self->introspects);
          if (!id)
            break;
          g_queue_push_head (queue, id);
        }

      id = g_queue_pop_head (queue);
      if (!id)
        {
          g_queue_free (queue);
          return;
        }

      if (!note)
        g_debug ("%s: flushing introspect queue", self->logname);
      note = TRUE;
      introspect_complete (self, id);
    }
}

static void
introspect_queue (CockpitDBusCache *self,
                  BatchData *batch,
                  const gchar *path,
                  const gchar *interface,
                  CockpitDBusIntrospectFunc callback,
                  gpointer user_data)
{
  IntrospectData *id;

  g_assert (path != NULL);
  g_assert (batch != NULL);

  id = g_slice_new0 (IntrospectData);
  id->interface = interface;
  id->batch = batch_ref (batch);
  id->path = path;
  id->callback = callback;
  id->user_data = user_data;

  g_debug ("%s: queueing introspect %s %s%s", self->logname, path,
           interface ? "for " : "", interface ? interface : "");
  g_queue_push_tail (self->introspects, id);

  introspect_next (self);
}

static void
introspect_maybe (CockpitDBusCache *self,
                  BatchData *batch,
                  const gchar *path,
                  const gchar *interface,
                  CockpitDBusIntrospectFunc callback,
                  gpointer user_data)
{
  GDBusInterfaceInfo *iface;

  g_assert (path);
  g_assert (interface);

  iface = cockpit_dbus_interface_info_lookup (self->introspected, interface);
  if (iface)
    {
      (callback) (self, iface, user_data);
    }
  else
    {
      if (batch == NULL)
        batch = batch_create (self);
      else
        batch = batch_ref (batch);

      introspect_queue (self, batch,
                        intern_string (self, path),
                        intern_string (self, interface),
                        callback, user_data);

      batch_unref (self, batch);
    }
}

void
cockpit_dbus_cache_introspect (CockpitDBusCache *self,
                               const gchar *path,
                               const gchar *interface,
                               CockpitDBusIntrospectFunc callback,
                               gpointer user_data)
{
  g_return_if_fail (self != NULL);
  g_return_if_fail (path != NULL);

  introspect_maybe (self, NULL, path, interface, callback, user_data);
}

static GHashTable *
emit_interfaces (CockpitDBusCache *self,
                 const gchar *path)
{
  GHashTable *interfaces;

  g_assert (path != NULL);

  if (!self->update)
    {
      self->update = g_hash_table_new_full (g_str_hash, g_str_equal,
                                            NULL, hash_table_unref_or_null);
    }

  interfaces = g_hash_table_lookup (self->update, path);
  if (!interfaces)
    {
      interfaces = g_hash_table_new_full (g_str_hash, g_str_equal,
                                          NULL, hash_table_unref_or_null);
      g_hash_table_replace (self->update, (gchar *)path, interfaces);
    }

  return interfaces;
}

static void
emit_remove (CockpitDBusCache *self,
             const gchar *path,
             const gchar *interface)
{
  GHashTable *interfaces = emit_interfaces (self, path);

  g_assert (interface != NULL);
  g_hash_table_replace (interfaces, (gchar *)interface, NULL);
}

static void
emit_change (CockpitDBusCache *self,
             const gchar *path,
             GDBusInterfaceInfo *iface,
             const gchar *property,
             GVariant *value)
{
  GHashTable *interfaces = emit_interfaces (self, path);
  GHashTable *properties;

  g_assert (iface != NULL);

  properties = g_hash_table_lookup (interfaces, iface->name);
  if (!properties)
    {
      properties = g_hash_table_new_full (g_str_hash, g_str_equal,
                                          NULL, (GDestroyNotify)g_variant_unref);
      g_hash_table_replace (interfaces, iface->name, properties);
    }

  if (property)
    {
      g_assert (value != NULL);
      g_hash_table_replace (properties, (gchar *)property, g_variant_ref (value));
    }
}

static gboolean
find_change (CockpitDBusCache *self,
             const gchar *path,
             GDBusInterfaceInfo *iface)
{
  GHashTable *interfaces;
  GHashTable *properties;

  if (self->update == NULL)
    return FALSE;

  interfaces = g_hash_table_lookup (self->update, path);
  if (interfaces == NULL)
    return FALSE;

  properties = g_hash_table_lookup (interfaces, iface->name);
  if (properties == NULL)
    return FALSE;

  return TRUE;
}

static GHashTable *
ensure_interfaces (CockpitDBusCache *self,
                   const gchar *path)
{
  GHashTable *interfaces;

  interfaces = g_hash_table_lookup (self->cache, path);
  if (!interfaces)
    {
      interfaces = g_hash_table_new_full (g_str_hash, g_str_equal,
                                          NULL, (GDestroyNotify)g_hash_table_unref);
      g_hash_table_replace (self->cache, (gchar *)path, interfaces);
    }

  return interfaces;
}

static GHashTable *
ensure_properties (CockpitDBusCache *self,
                   const gchar *path,
                   GDBusInterfaceInfo *iface)
{
  GHashTable *interfaces;
  GHashTable *properties;
  const gchar *name;

  interfaces = ensure_interfaces (self, path);
  properties = g_hash_table_lookup (interfaces, iface->name);
  if (!properties)
    {
      properties = g_hash_table_new_full (g_str_hash, g_str_equal,
                                          NULL, (GDestroyNotify)g_variant_unref);
      g_hash_table_replace (interfaces, iface->name, properties);

      g_debug ("%s: present %s at %s", self->logname, iface->name, path);
      emit_change (self, path, iface, NULL, NULL);
    }

  name = intern_string (self, iface->name);
  if (!g_hash_table_lookup (self->introsent, name))
    {
      g_hash_table_add (self->introsent, (gpointer)name);
      g_signal_emit (self, signal_meta, 0, iface);
    }

  return properties;
}

static void
process_value (CockpitDBusCache *self,
               GHashTable *properties,
               const gchar *path,
               GDBusInterfaceInfo *iface,
               const gchar *property,
               GVariant *variant)
{
  gpointer prev;
  GVariant *value;
  gpointer key;

  value = g_variant_get_variant (variant);

  if (g_hash_table_lookup_extended (properties, property, &key, &prev))
    {
      if (g_variant_equal (prev, value))
        {
          g_variant_unref (value);
          return;
        }

      g_hash_table_steal (properties, key);
      g_hash_table_replace (properties, key, value);
      g_variant_unref (prev);
    }
  else
    {
      g_hash_table_replace (properties, (gchar *)property, value);
    }

  g_debug ("%s: changed %s %s at %s", self->logname, iface->name, property, path);
  emit_change (self, path, iface, property, value);
}

typedef struct {
  CockpitDBusCache *self;
  const gchar *path;
  const gchar *property;
  BatchData *batch;
  GDBusInterfaceInfo *iface;
} GetData;

static void
process_get (CockpitDBusCache *self,
             BatchData *batch,
             const gchar *path,
             GDBusInterfaceInfo *iface,
             const gchar *property,
             GVariant *retval)
{
  GHashTable *properties;
  GVariant *variant;

  g_variant_get (retval, "(@v)", &variant);

  properties = ensure_properties (self, path, iface);
  process_value (self, properties, path, iface, property, variant);
  cockpit_dbus_cache_scrape (self, variant);
  g_variant_unref (variant);
}

static void
on_get_reply (GObject *source,
              GAsyncResult *result,
              gpointer user_data)
{
  GetData *gd = user_data;
  CockpitDBusCache *self = gd->self;
  GVariant *retval;
  GError *error = NULL;

  retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
  if (error)
    {
      if (!g_cancellable_is_cancelled (self->cancellable))
        {
          if (dbus_error_matches_unknown (error))
            {
              g_debug ("%s: couldn't get property %s %s at %s: %s", self->logname,
                       gd->iface->name, gd->property, gd->path, error->message);
            }
          else
            {
              g_message ("%s: couldn't get property %s %s at %s: %s", self->logname,
                         gd->iface->name, gd->property, gd->path, error->message);
            }
        }
      g_error_free (error);
    }

  if (retval)
    {
      g_debug ("%s: reply from Get() on %s", self->logname, gd->path);

      process_get (self, gd->batch, gd->path, gd->iface, gd->property, retval);
      g_variant_unref (retval);
    }

  batch_unref (self, gd->batch);

  g_object_unref (gd->self);
  g_slice_free (GetData, gd);
}

static void
process_properties (CockpitDBusCache *self,
                    BatchData *batch,
                    const gchar *path,
                    GDBusInterfaceInfo *iface,
                    GVariant *dict)
{
  GHashTable *properties;
  const gchar *property;
  GVariant *variant;
  GVariantIter iter;

  properties = ensure_properties (self, path, iface);

  g_variant_iter_init (&iter, dict);
  while (g_variant_iter_loop (&iter, "{s@v}", &property, &variant))
    {
      process_value (self, properties, path, iface,
                     intern_string (self, property), variant);
    }
}

typedef struct {
  const gchar *path;
  GVariant *body;
  BatchData *batch;
} PropertiesChangedData;

static void
process_properties_changed (CockpitDBusCache *self,
                            GDBusInterfaceInfo *iface,
                            gpointer user_data)
{
  PropertiesChangedData *pcd = user_data;
  GetData *gd;
  GVariantIter iter;
  const gchar *property;
  GVariant *changed;
  GVariant *invalidated;

  g_variant_get (pcd->body, "(@s@a{sv}@as)", NULL, &changed, &invalidated);

  process_properties (self, pcd->batch, pcd->path, iface, changed);

  /*
   * These are properties which the service didn't want to broadcast because
   * they're either calculated per-peer or expensive to calculate if nobody
   * is listening to them. We want them ... so get them and include them
   * in the current batch.
   */

  g_variant_iter_init (&iter, invalidated);
  while (g_variant_iter_loop (&iter, "&s", &property))
    {
      g_debug ("%s: calling Get() for %s %s at %s", self->logname, iface->name, property, pcd->path);

      gd = g_slice_new0 (GetData);
      gd->self = g_object_ref (self);
      gd->property = intern_string (self, property);
      gd->batch = batch_ref (pcd->batch);
      gd->path = pcd->path;
      gd->iface = iface;

      g_dbus_connection_call (self->connection, self->name, gd->path,
                              "org.freedesktop.DBus.Properties", "Get",
                              g_variant_new ("(ss)", iface->name, property),
                              G_VARIANT_TYPE ("(v)"),
                              G_DBUS_CALL_FLAGS_NONE, -1,
                              self->cancellable, on_get_reply, gd);
    }

  g_variant_unref (invalidated);
  g_variant_unref (changed);

  batch_unref (self, pcd->batch);

  g_variant_unref (pcd->body);
  g_slice_free (PropertiesChangedData, pcd);
}

static void
process_properties_barrier (CockpitDBusCache *self,
                            gpointer user_data)
{
  PropertiesChangedData *pcd = user_data;
  const gchar *interface;
  GVariant *changed;
  BatchData *batch;

  g_variant_get (pcd->body, "(&s@a{sv}@as)", &interface, &changed, NULL);

  batch = batch_create (self);

  pcd->batch = batch_ref (batch);
  introspect_maybe (self, pcd->batch, pcd->path, interface, process_properties_changed, pcd);

  scrape_variant (self, batch, changed);
  batch_unref (self, batch);

  g_variant_unref (changed);
}

static void
on_properties_signal (GDBusConnection *connection,
                      const gchar *sender,
                      const gchar *path,
                      const gchar *properties_iface,
                      const gchar *member,
                      GVariant *body,
                      gpointer user_data)
{
  CockpitDBusCache *self = user_data;
  PropertiesChangedData *pcd;
  const gchar *interface;

  if (!g_variant_is_of_type (body, G_VARIANT_TYPE ("(sa{sv}as)")))
    {
      g_debug ("%s: received PropertiesChanged with bad type", self->logname);
      return;
    }

  g_debug ("%s: signal PropertiesChanged at %s", self->logname, path);
  g_variant_get (body, "(&s@a{sv}@as)", &interface, NULL, NULL);

  /* We have to filter the sender ourselves; glib doesn't do that for
     well-known names.
   */
  if (self->name_owner && g_strcmp0 (sender, self->name_owner) != 0)
    return;

  if (!cockpit_dbus_rules_match (self->rules, path, interface, NULL, NULL))
    return;

  pcd = g_slice_new0 (PropertiesChangedData);
  pcd->body = g_variant_ref (body);
  pcd->path = intern_string (self, path);
  cockpit_dbus_cache_barrier (self, process_properties_barrier, pcd);
}

typedef struct {
  const gchar *path;
  GVariant *dict;
  BatchData *batch;
} ProcessInterfaceData;

static void
process_interface (CockpitDBusCache *self,
                   GDBusInterfaceInfo *iface,
                   gpointer user_data)
{
  ProcessInterfaceData *pid = user_data;

  process_properties (self, pid->batch, pid->path, iface, pid->dict);

  batch_unref (self, pid->batch);

  g_variant_unref (pid->dict);
  g_slice_free (ProcessInterfaceData, pid);
}

static void
process_interfaces (CockpitDBusCache *self,
                    BatchData *batch,
                    GHashTable *snapshot,
                    const gchar *path,
                    GVariant *dict)
{
  ProcessInterfaceData *pid;
  GVariant *inner;
  const gchar *interface;
  GVariantIter iter;

  if (batch)
    batch = batch_ref (batch);

  g_variant_iter_init (&iter, dict);
  while (g_variant_iter_loop (&iter, "{s@a{sv}}", &interface, &inner))
    {
      if (!cockpit_dbus_rules_match (self->rules, path, interface, NULL, NULL))
        continue;

      if (!batch)
        batch = batch_create (self);

      if (snapshot)
        g_hash_table_remove (snapshot, interface);

      pid = g_slice_new0 (ProcessInterfaceData);
      pid->batch = batch_ref (batch);
      pid->path = path;
      pid->dict = g_variant_ref (inner);
      cockpit_dbus_cache_introspect (self, path, interface, process_interface, pid);

      scrape_variant (self, batch, inner);
    }

  if (batch)
    batch_unref (self, batch);
}

typedef struct {
  GVariant *body;
  const gchar *manager_added;
} ProcessInterfacesData;

static void
retrieve_managed_objects (CockpitDBusCache *self,
                          const gchar *namespace_path,
                          BatchData *batch);

static void
process_interfaces_added (CockpitDBusCache *self,
                          gpointer user_data)
{
  ProcessInterfacesData *pis = user_data;
  BatchData *batch = NULL;
  GVariant *interfaces;
  const gchar *path;

  /*
   * We added a manager while processing this message, perform a full manager
   * load as part of the same batch.
   */
  if (pis->manager_added)
    {
      batch = batch_create (self);
      retrieve_managed_objects (self, pis->manager_added, batch);
    }

  g_variant_get (pis->body, "(&o@a{sa{sv}})", &path, &interfaces);
  process_interfaces (self, batch, NULL, intern_string (self, path), interfaces);
  g_variant_unref (interfaces);

  if (batch)
    batch_unref (self, batch);

  g_variant_unref (pis->body);
  g_slice_free (ProcessInterfacesData, pis);
}

static void
process_removed (CockpitDBusCache *self,
                 const gchar *path,
                 const gchar *interface)
{
  GHashTable *interfaces;
  GHashTable *properties;

  interfaces = g_hash_table_lookup (self->cache, path);
  if (!interfaces)
    return;

  properties = g_hash_table_lookup (interfaces, interface);
  if (!properties)
    return;

  g_hash_table_remove (interfaces, interface);

  g_debug ("%s: removed %s at %s", self->logname, interface, path);
  emit_remove (self, path, interface);
}

static void
process_interfaces_removed (CockpitDBusCache *self,
                            gpointer user_data)
{
  ProcessInterfacesData *pis = user_data;
  GVariant *array;
  const gchar *path;
  const gchar *interface;
  GVariantIter iter;
  BatchData *batch;

  batch = batch_create (self);

  /*
   * We added a manager while processing this message, perform a full manager
   * load as part of the same batch.
   */
  if (pis->manager_added)
    retrieve_managed_objects (self, pis->manager_added, batch);

  g_variant_get (pis->body, "(&o@as)", &path, &array);
  path = intern_string (self, path);

  g_variant_iter_init (&iter, array);
  while (g_variant_iter_loop (&iter, "&s", &interface))
    process_removed (self, path, intern_string (self, interface));

  batch_unref (self, batch);

  g_variant_unref (array);
  g_variant_unref (pis->body);
  g_slice_free (ProcessInterfacesData, pis);
}

static void
on_manager_signal (GDBusConnection *connection,
                   const gchar *sender,
                   const gchar *path,
                   const gchar *interface,
                   const gchar *member,
                   GVariant *body,
                   gpointer user_data)
{
  CockpitDBusCache *self = user_data;
  CockpitDBusBarrierFunc barrier_func = NULL;
  const gchar * manager_added;
  ProcessInterfacesData *pis;

  /* Note that this is an ObjectManager */
  manager_added = cockpit_paths_add (self->managed_not_ready, path);

  if (g_str_equal (member, "InterfacesAdded"))
    {
      if (g_variant_is_of_type (body, G_VARIANT_TYPE ("(oa{sa{sv}})")))
        {
          g_debug ("%s: signal InterfacesAdded at %s", self->logname, path);
          barrier_func = process_interfaces_added;
        }
      else
        {
          g_debug ("%s: received InterfacesAdded with bad type", self->logname);
        }
    }
  else if (g_str_equal (member, "InterfacesRemoved"))
    {
      if (g_variant_is_of_type (body, G_VARIANT_TYPE ("(oas)")))
        {
          g_debug ("%s: signal InterfacesRemoved at %s", self->logname, path);
          barrier_func = process_interfaces_removed;
        }
      else
        {
          g_debug ("%s: received InterfacesRemoved with bad type", self->logname);
        }
    }

  if (barrier_func)
    {
      pis = g_slice_new0 (ProcessInterfacesData);
      pis->body = g_variant_ref (body);
      pis->manager_added = manager_added;
      cockpit_dbus_cache_barrier (self, barrier_func, pis);
    }
}

static void
cockpit_dbus_cache_constructed (GObject *object)
{
  CockpitDBusCache *self = COCKPIT_DBUS_CACHE (object);

  g_return_if_fail (self->connection != NULL);

  if (!self->introspected)
    self->introspected = cockpit_dbus_interface_info_new ();

  self->subscribe_properties = g_dbus_connection_signal_subscribe (self->connection,
                                                                   self->name,
                                                                   "org.freedesktop.DBus.Properties",
                                                                   "PropertiesChanged",
                                                                   NULL, /* object_path */
                                                                   NULL, /* arg0 */
                                                                   G_DBUS_SIGNAL_FLAGS_NONE,
                                                                   on_properties_signal,
                                                                   self, NULL);

  self->subscribe_manager = g_dbus_connection_signal_subscribe (self->connection,
                                                                self->name,
                                                                "org.freedesktop.DBus.ObjectManager",
                                                                NULL, /* member */
                                                                NULL, /* object_path */
                                                                NULL, /* arg0 */
                                                                G_DBUS_SIGNAL_FLAGS_NONE,
                                                                on_manager_signal,
                                                                self, NULL);

  self->subscribed = TRUE;
}

static void
cockpit_dbus_cache_set_property (GObject *obj,
                                 guint prop_id,
                                 const GValue *value,
                                 GParamSpec *pspec)
{
  CockpitDBusCache *self = COCKPIT_DBUS_CACHE (obj);

  switch (prop_id)
    {
      case PROP_CONNECTION:
        self->connection = g_value_dup_object (value);
        break;
      case PROP_NAME:
        self->name = g_value_dup_string (value);
        break;
      case PROP_LOGNAME:
        self->logname = g_value_dup_string (value);
        break;
      case PROP_INTERFACE_INFO:
        self->introspected = g_value_dup_boxed (value);
        break;
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
        break;
    }
}

void
cockpit_dbus_cache_set_name_owner (CockpitDBusCache *self,
                                   const gchar *name_owner)
{
  g_free (self->name_owner);
  self->name_owner = g_strdup (name_owner);
}


static void
cockpit_dbus_cache_dispose (GObject *object)
{
  CockpitDBusCache *self = COCKPIT_DBUS_CACHE (object);

  g_cancellable_cancel (self->cancellable);

  if (self->subscribed)
    {
      g_dbus_connection_signal_unsubscribe (self->connection, self->subscribe_properties);
      g_dbus_connection_signal_unsubscribe (self->connection, self->subscribe_manager);
      self->subscribed = FALSE;
    }

  introspect_flush (self);
  batch_flush (self);
  barrier_flush (self);

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

static void
cockpit_dbus_cache_finalize (GObject *object)
{
  CockpitDBusCache *self = COCKPIT_DBUS_CACHE (object);

  g_clear_object (&self->connection);
  g_object_unref (self->cancellable);

  g_free (self->name_owner);
  g_free (self->name);
  g_free (self->logname);

  cockpit_dbus_rules_free (self->rules);
  g_tree_destroy (self->managed);
  g_tree_destroy (self->managed_not_ready);

  g_queue_free (self->batches);
  g_queue_free (self->barriers);

  g_assert (self->introspects->head == NULL);
  g_queue_free (self->introspects);

  g_hash_table_unref (self->introsent);
  g_hash_table_unref (self->introspected);
  g_hash_table_unref (self->cache);

  g_hash_table_destroy (self->interned);
  g_list_free_full (self->trash, g_free);

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

static void
cockpit_dbus_cache_class_init (CockpitDBusCacheClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->constructed = cockpit_dbus_cache_constructed;
  gobject_class->set_property = cockpit_dbus_cache_set_property;
  gobject_class->dispose = cockpit_dbus_cache_dispose;
  gobject_class->finalize = cockpit_dbus_cache_finalize;

  signal_meta = g_signal_new ("meta", COCKPIT_TYPE_DBUS_CACHE, G_SIGNAL_RUN_LAST,
                              G_STRUCT_OFFSET (CockpitDBusCacheClass, meta),
                              NULL, NULL, NULL, G_TYPE_NONE,
                              1, G_TYPE_DBUS_INTERFACE_INFO);

  signal_update = g_signal_new ("update", COCKPIT_TYPE_DBUS_CACHE, G_SIGNAL_RUN_LAST,
                                G_STRUCT_OFFSET (CockpitDBusCacheClass, update),
                                NULL, NULL, NULL, G_TYPE_NONE,
                                1, G_TYPE_HASH_TABLE);

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

  g_object_class_install_property (gobject_class, PROP_NAME,
       g_param_spec_string ("name", "name", "name", NULL,
                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_LOGNAME,
       g_param_spec_string ("logname", "logname", "logname", "internal",
                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
  g_object_class_install_property (gobject_class, PROP_INTERFACE_INFO,
       g_param_spec_boxed ("interface-info", NULL, NULL, G_TYPE_HASH_TABLE,
                           G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

static GHashTable *
snapshot_string_keys (GHashTable *table)
{
  GHashTable *set;
  GHashTableIter iter;
  gpointer key;

  set = g_hash_table_new (g_str_hash, g_str_equal);
  if (table)
    {
      g_hash_table_iter_init (&iter, table);
      while (g_hash_table_iter_next (&iter, &key, NULL))
        g_hash_table_add (set, key);
    }

  return set;
}

static void
process_get_all (CockpitDBusCache *self,
                 BatchData *batch,
                 const gchar *path,
                 GDBusInterfaceInfo *iface,
                 GVariant *retval)
{
  GVariant *dict;

  g_variant_get (retval, "(@a{sv})", &dict);

  process_properties (self, batch, path, iface, dict);
  scrape_variant (self, batch, dict);

  g_variant_unref (dict);
}

static void
process_removed_path (CockpitDBusCache *self,
                      const gchar *path)
{
  GHashTable *interfaces;
  GHashTableIter iter;
  gpointer interface;
  GHashTable *snapshot;

  interfaces = g_hash_table_lookup (self->cache, path);
  if (interfaces)
    {
      snapshot = snapshot_string_keys (interfaces);
      g_hash_table_iter_init (&iter, snapshot);
      while (g_hash_table_iter_next (&iter, &interface, NULL))
        process_removed (self, path, interface);
      g_hash_table_destroy (snapshot);
    }
}

static void
process_paths (CockpitDBusCache *self,
               BatchData *batch,
               GHashTable *snapshot,
               GVariant *dict)
{
  GVariant *inner;
  GHashTable *snap;
  const gchar *path;
  GVariantIter iter;
  GHashTableIter hter;
  gpointer key;

  g_variant_iter_init (&iter, dict);
  while (g_variant_iter_loop (&iter, "{o@a{sa{sv}}}", &path, &inner))
    {
      snap = NULL;
      if (snapshot)
        {
          g_hash_table_remove (snapshot, path);
          snap = snapshot_string_keys (g_hash_table_lookup (self->cache, path));
        }

      process_interfaces (self, batch, snap, intern_string (self, path), inner);

      if (snap)
        {
          g_hash_table_iter_init (&hter, snap);
          while (g_hash_table_iter_next (&hter, &key, NULL))
            process_removed (self, path, key);
          g_hash_table_destroy (snap);
        }
    }
}

static void
process_get_managed_objects (CockpitDBusCache *self,
                             BatchData *batch,
                             const gchar *manager_path,
                             GVariant *retval)
{
  /*
   * So here we handle things slightly differently than just pushing the
   * result through all the properties update mechanics. We get
   * indications of interfaces and entire paths disappearing here,
   * so we have to handle that.
   */

  GVariant *inner;
  GHashTableIter iter;
  GHashTable *snapshot;
  gpointer path;

  /* Snapshot everything under control of the path of the object manager */
  snapshot = g_hash_table_new (g_str_hash, g_str_equal);
  g_hash_table_iter_init (&iter, self->cache);
  while (g_hash_table_iter_next (&iter, &path, NULL))
    {
      if (cockpit_path_has_ancestor (path, manager_path))
        g_hash_table_add (snapshot, path);
    }

  g_variant_get (retval, "(@a{oa{sa{sv}}})", &inner);
  process_paths (self, batch, snapshot, inner);
  g_variant_unref (inner);

  g_hash_table_iter_init (&iter, snapshot);
  while (g_hash_table_iter_next (&iter, &path, NULL))
    process_removed_path (self, path);
  g_hash_table_unref (snapshot);
}

static void
process_introspect_children (CockpitDBusCache *self,
                             BatchData *batch,
                             const gchar *parent_path,
                             GDBusNodeInfo *node)
{
  GDBusNodeInfo *child;
  GHashTable *snapshot;
  GHashTableIter iter;
  gchar *child_path;
  gpointer path;
  guint i;

  /* Snapshot all direct children of path */
  snapshot = g_hash_table_new (g_str_hash, g_str_equal);
  g_hash_table_iter_init (&iter, self->cache);
  while (g_hash_table_iter_next (&iter, &path, NULL))
    {
      if (cockpit_path_has_parent (path, parent_path))
        g_hash_table_add (snapshot, path);
    }

  /* Poke any additional child nodes discovered */
  for (i = 0; node->nodes && node->nodes[i]; i++)
    {
      child = node->nodes[i];

      /* If the child has no path then it's useless */
      if (!child->path)
        continue;

      /* Figure out an object path for this node */
      if (g_str_has_prefix (child->path, "/"))
        child_path = g_strdup (child->path);
      else if (g_str_equal (parent_path, "/"))
        child_path = g_strdup_printf ("/%s", child->path);
      else
        child_path = g_strdup_printf ("%s/%s", parent_path, child->path);

      /* Remove everything in the snapshot related to this child */
      g_hash_table_remove (snapshot, child_path);

      if (cockpit_dbus_rules_match (self->rules, child_path, NULL, NULL, NULL) &&
          !cockpit_paths_contain_or_ancestor (self->managed, child_path))
        {
          /* Inline child interfaces are rare but possible */
          if (child->interfaces && child->interfaces[0])
            {
              process_introspect_node (self, batch,
                                       intern_string (self, child_path),
                                       child, TRUE);
            }

          /* If we have no knowledge of this child, then introspect it */
          else
            {
              introspect_queue (self, batch,
                                intern_string (self, child_path),
                                NULL, NULL, NULL);
            }
        }

      g_free (child_path);
    }

  /* Anything remaining in snapshot stays */
  g_hash_table_iter_init (&iter, snapshot);
  while (g_hash_table_iter_next (&iter, &path, NULL))
    process_removed_path (self, path);
  g_hash_table_unref (snapshot);
}

typedef struct {
  CockpitDBusCache *self;
  const gchar *path;
  GDBusInterfaceInfo *iface;
  BatchData *batch;
} GetAllData;

static void
on_get_all_reply (GObject *source,
                  GAsyncResult *result,
                  gpointer user_data)
{
  GetAllData *gad = user_data;
  CockpitDBusCache *self = gad->self;
  GError *error = NULL;
  GVariant *retval;

  retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
  if (error)
    {
      if (!g_cancellable_is_cancelled (self->cancellable))
        {
          if (dbus_error_matches_unknown (error))
            {
              g_debug ("%s: couldn't get all properties of %s at %s: %s", self->logname,
                       gad->iface->name, gad->path, error->message);
            }
          else
            {
              g_message ("%s: couldn't get all properties of %s at %s: %s", self->logname,
                         gad->iface->name, gad->path, error->message);
            }
        }
      g_error_free (error);
    }

  if (retval)
    {
      g_debug ("%s: reply to GetAll() for %s at %s", self->logname, gad->iface->name, gad->path);
      process_get_all (self, gad->batch, gad->path, gad->iface, retval);

      g_variant_unref (retval);
    }

  /* Whether or not this failed, we know the interface exists */
  ensure_properties (self, gad->path, gad->iface);
  emit_change (self, gad->path, gad->iface, NULL, NULL);

  batch_unref (self, gad->batch);

  g_object_unref (gad->self);
  g_slice_free (GetAllData, gad);
}

static void
retrieve_properties (CockpitDBusCache *self,
                     BatchData *batch,
                     const gchar *path,
                     GDBusInterfaceInfo *iface)
{
  GetAllData *gad;

  /* Don't bother getting properties for this well known interface
   * that doesn't have any.  Also, NetworkManager returns an error.
   */
  if (g_strcmp0 (iface->name, "org.freedesktop.DBus.Properties") == 0)
    return;

  /* Only get properties once per batch.
   */

  if (find_change (self, path, iface))
    {
      g_debug ("%s: skipping GetAll for %s at %s, already in batch", self->logname, iface->name, path);
      return;
    }

  emit_change (self, path, iface, NULL, NULL);

  g_debug ("%s: calling GetAll() for %s at %s", self->logname, iface->name, path);

  gad = g_slice_new0 (GetAllData);
  gad->self = g_object_ref (self);
  gad->batch = batch_ref (batch);
  gad->path = path;
  gad->iface = iface;

  g_dbus_connection_call (self->connection, self->name, path,
                          "org.freedesktop.DBus.Properties", "GetAll",
                          g_variant_new ("(s)", iface->name), G_VARIANT_TYPE ("(a{sv})"),
                          G_DBUS_CALL_FLAGS_NONE, -1,
                          self->cancellable, on_get_all_reply, gad);
}

static void
process_introspect_node (CockpitDBusCache *self,
                         BatchData *batch,
                         const gchar *path,
                         GDBusNodeInfo *node,
                         gboolean recursive)
{
  GDBusInterfaceInfo *iface;
  GDBusInterfaceInfo *prev;
  GHashTable *snapshot;
  GHashTableIter iter;
  gpointer interface;
  guint i;

  if (cockpit_paths_contain_or_ancestor (self->managed, path))
    recursive = FALSE;

  snapshot = snapshot_string_keys (g_hash_table_lookup (self->cache, path));

  for (i = 0; node->interfaces && node->interfaces[i] != NULL; i++)
    {
      iface = node->interfaces[i];
      if (!iface->name)
        {
          g_warning ("Received interface from %s at %s without name", self->logname, path);
          continue;
        }

      /* Cache this interface for later use elsewhere */
      prev = cockpit_dbus_interface_info_lookup (self->introspected, iface->name);
      if (prev)
        {
          iface = prev;
        }
      else
        {
          cockpit_dbus_interface_info_push (self->introspected, iface);
        }

      /* Skip these interfaces */
      if (g_str_has_prefix (iface->name, "org.freedesktop.DBus."))
        {
          /* But make sure we track the fact that something is here */
          ensure_interfaces (self, path);
          continue;
        }

      g_hash_table_remove (snapshot, iface->name);

      if (recursive && cockpit_dbus_rules_match (self->rules, path, iface->name, NULL, NULL))
        retrieve_properties (self, batch, path, iface);
    }

  /* Remove any interfaces not seen */
  g_hash_table_iter_init (&iter, snapshot);
  while (g_hash_table_iter_next (&iter, &interface, NULL))
    process_removed (self, path, interface);
  g_hash_table_destroy (snapshot);

  if (recursive)
    process_introspect_children (self, batch, path, node);
}

typedef struct {
  CockpitDBusCache *self;
  const gchar *path;
  BatchData *batch;
} GetManagedObjectsData;

static void
on_get_managed_objects_reply (GObject *source,
                              GAsyncResult *result,
                              gpointer user_data)
{
  GetManagedObjectsData *gmod = user_data;
  CockpitDBusCache *self = gmod->self;
  GError *error = NULL;
  GVariant *retval;

  retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
  if (error)
    {
      if (!g_cancellable_is_cancelled (self->cancellable))
        {
          /* Doesn't support ObjectManager? */
          if (dbus_error_matches_unknown (error))
            {
              g_debug ("%s: no ObjectManager at %s", self->logname, gmod->path);
            }
          else
            {
              g_message ("%s: couldn't get managed objects at %s: %s",
                         self->logname, gmod->path, error->message);
            }
        }
      g_error_free (error);
    }

  if (retval)
    {
      g_debug ("%s: reply from GetManagedObjects() on %s", self->logname, gmod->path);

      /* Note that this is indeed an object manager */
      cockpit_paths_add (self->managed, gmod->path);

      process_get_managed_objects (self, gmod->batch, gmod->path, retval);
      g_variant_unref (retval);
    }

  /*
   * The ObjectManager itself still needs introspecting ... since the
   * ObjectManager path itself cannot be included in the objects reported
   * by the ObjectManager ... dumb design decision in the dbus spec IMO.
   *
   * But we delay on this so that any children are treated as part of
   * object manager, and we don't go introspecting everything.
   */
  introspect_queue (self, gmod->batch, gmod->path, NULL, NULL, NULL);

  batch_unref (self, gmod->batch);

  g_object_unref (gmod->self);
  g_slice_free (GetManagedObjectsData, gmod);
}

static void
retrieve_managed_objects (CockpitDBusCache *self,
                          const gchar *namespace_path,
                          BatchData *batch)
{
  GetManagedObjectsData *gmod;

  g_assert (namespace_path != NULL);

  gmod = g_slice_new0 (GetManagedObjectsData);
  gmod->batch = batch_ref (batch);
  gmod->path = namespace_path;
  gmod->self = g_object_ref (self);

  g_debug ("%s: calling GetManagedObjects() on %s", self->logname, namespace_path);

  g_dbus_connection_call (self->connection, self->name, namespace_path,
                          "org.freedesktop.DBus.ObjectManager", "GetManagedObjects",
                          g_variant_new ("()"), G_VARIANT_TYPE ("(a{oa{sa{sv}}})"),
                          G_DBUS_CALL_FLAGS_NONE, -1, /* timeout */
                          self->cancellable, on_get_managed_objects_reply, gmod);
}


void
cockpit_dbus_cache_watch (CockpitDBusCache *self,
                          const gchar *path,
                          gboolean is_namespace,
                          const gchar *interface)
{
  const gchar *namespace_path;
  BatchData *batch;

  if (!cockpit_dbus_rules_add (self->rules, path, is_namespace, interface, NULL, NULL))
    return;

  if (!path)
    {
      path = "/";
      is_namespace = TRUE;
    }

  batch = batch_create (self);
  path = intern_string (self, path);

  namespace_path = is_namespace ? path : NULL;

  if (!namespace_path)
    namespace_path = cockpit_paths_contain_or_ancestor (self->managed, path);

  if (namespace_path)
    {
      retrieve_managed_objects (self, namespace_path, batch);
    }
  else
    {
      introspect_queue (self, batch, path, NULL, NULL, NULL);
    }

  batch_unref (self, batch);
}

gboolean
cockpit_dbus_cache_unwatch (CockpitDBusCache *self,
                            const gchar *path,
                            gboolean is_namespace,
                            const gchar *interface)
{
  return cockpit_dbus_rules_remove (self->rules, path, is_namespace, interface, NULL, NULL);
}

static void
scrape_variant_paths (GVariant *data,
                      GHashTable *paths)
{
  GVariantIter iter;
  GVariant *child;

  if (g_variant_is_of_type (data, G_VARIANT_TYPE_OBJECT_PATH))
    {
      g_hash_table_add (paths, (gchar *)g_variant_get_string (data, NULL));
    }
  else if (g_variant_is_container (data))
    {
      g_variant_iter_init (&iter, data);
      while ((child = g_variant_iter_next_value (&iter)) != NULL)
        {
          scrape_variant_paths (child, paths);
          g_variant_unref (child);
        }
    }
}

void
cockpit_dbus_cache_barrier (CockpitDBusCache *self,
                            CockpitDBusBarrierFunc callback,
                            gpointer user_data)
{
  BatchData *batch;
  BarrierData *barrier;

  g_return_if_fail (callback != NULL);

  batch = g_queue_peek_tail (self->batches);
  if (batch)
    {
      barrier = g_slice_new0 (BarrierData);
      barrier->number = batch->number;
      barrier->callback = callback;
      barrier->user_data = user_data;
      g_queue_push_tail (self->barriers, barrier);
    }
  else
    {
      (callback) (self, user_data);
    }
}

static void
scrape_variant (CockpitDBusCache *self,
                BatchData *batch,
                GVariant *data)
{
  GHashTable *paths;
  GHashTableIter iter;
  gpointer path;

  paths = g_hash_table_new (g_str_hash, g_str_equal);
  scrape_variant_paths (data, paths);

  if (batch)
    batch = batch_ref (batch);

  g_hash_table_iter_init (&iter, paths);
  while (g_hash_table_iter_next (&iter, &path, NULL))
    {
      /* Used as a NULL path, we never use it when scraped */
      if (g_str_equal (path, "/"))
        continue;

      /* Do we have it already? */
      if (g_hash_table_lookup (self->cache, path))
        continue;

      /* Is it a managed path? */
      if (cockpit_paths_contain_or_ancestor (self->managed, path))
        continue;

      /* Does it fit our rules */
      if (!cockpit_dbus_rules_match (self->rules, path, NULL, NULL, NULL))
        continue;

      if (!batch)
        batch = batch_create (self);

      introspect_queue (self, batch, intern_string (self, path), NULL, NULL, NULL);
    }

  if (batch)
    batch_unref (self, batch);

  g_hash_table_destroy (paths);
}

void
cockpit_dbus_cache_scrape (CockpitDBusCache *self,
                           GVariant *data)
{
  scrape_variant (self, NULL, data);
}

typedef struct {
  const gchar *path;
  BatchData *batch;
} PokeData;

static void
process_poke (CockpitDBusCache *self,
              GDBusInterfaceInfo *iface,
              gpointer user_data)
{
  PokeData *pd = user_data;

  retrieve_properties (self, pd->batch, pd->path, iface);

  batch_unref (self, pd->batch);
  g_slice_free (PokeData, pd);
}

void
cockpit_dbus_cache_poke (CockpitDBusCache *self,
                         const gchar *path,
                         const gchar *interface)
{
  GHashTable *interfaces;
  BatchData *batch;
  PokeData *pd;

  /* Check if we have this thing */
  interfaces = g_hash_table_lookup (self->cache, path);
  if (interfaces)
    {
      if (!interface)
        return;
      else if (g_hash_table_lookup (interfaces, interface))
        return;
    }

  /* Is it a managed path? */
  if (cockpit_paths_contain_or_ancestor (self->managed, path))
    return;

  /* Does it fit our rules */
  if (!cockpit_dbus_rules_match (self->rules, path, interface, NULL, NULL))
    return;

  batch = batch_create (self);
  path = intern_string (self, path);

  if (interface)
    {
      /*
       * A specific interface was poked. This means that we don't have to
       * go interspecting the entire path ... if we already have information
       * about the interface itself. So try that route.
       */

      pd = g_slice_new0 (PokeData);
      pd->path = path;
      pd->batch = batch_ref (batch);
      introspect_maybe (self, batch, path, interface, process_poke, pd);
    }
  else
    {
      /* The entire path was poked, must introspect to find out about it */
      introspect_queue (self, batch, path, NULL, NULL, NULL);
    }

  batch_unref (self, batch);
}

CockpitDBusCache *
cockpit_dbus_cache_new (GDBusConnection *connection,
                        const gchar *name,
                        const gchar *logname,
                        GHashTable *interface_info)
{
  return g_object_new (COCKPIT_TYPE_DBUS_CACHE,
                       "connection", connection,
                       "name", name,
                       "logname", logname,
                       "interface-info", interface_info,
                       NULL);
}

GHashTable *
cockpit_dbus_interface_info_new (void)
{
  /* The key is owned by the value */
  return g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
                                (GDestroyNotify)g_dbus_interface_info_unref);
}

GDBusInterfaceInfo *
cockpit_dbus_interface_info_lookup (GHashTable *interface_info,
                                    const gchar *interface_name)
{
  g_return_val_if_fail (interface_info != NULL, NULL);
  g_return_val_if_fail (interface_name != NULL, NULL);
  return g_hash_table_lookup (interface_info, interface_name);
}

void
cockpit_dbus_interface_info_push (GHashTable *interface_info,
                                  GDBusInterfaceInfo *iface)
{
  g_return_if_fail (interface_info != NULL);
  g_return_if_fail (iface != NULL);
  g_hash_table_replace (interface_info, iface->name,
                        g_dbus_interface_info_ref (iface));
}
