/*
 * channel.c - proxy for a Telepathy channel (Group interface)
 *
 * Copyright (C) 2007-2008 Collabora Ltd. <http://www.collabora.co.uk/>
 * Copyright (C) 2007-2008 Nokia Corporation
 *
 * This library 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.
 *
 * This library 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 this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "telepathy-glib/channel-internal.h"

#include <telepathy-glib/dbus.h>
#include <telepathy-glib/gtypes.h>
#include <telepathy-glib/interfaces.h>
#include <telepathy-glib/proxy-subclass.h>
#include <telepathy-glib/util.h>

#define DEBUG_FLAG TP_DEBUG_GROUPS
#include "telepathy-glib/debug-internal.h"


/**
 * TP_ERRORS_REMOVED_FROM_GROUP:
 *
 * #GError domain representing the local user being removed from a channel
 * with the Group interface. The @code in a #GError with this domain must
 * be a member of #TpChannelGroupChangeReason.
 *
 * This error may be raised on non-Group channels with certain reason codes
 * if there's no better error code to use (mainly
 * %TP_CHANNEL_GROUP_CHANGE_REASON_NONE).
 *
 * This macro expands to a function call returning a #GQuark.
 *
 * Since: 0.7.1
 */
GQuark
tp_errors_removed_from_group_quark (void)
{
  static GQuark q = 0;

  if (q == 0)
    q = g_quark_from_static_string ("tp_errors_removed_from_group_quark");

  return q;
}


static void
local_pending_info_free (LocalPendingInfo *info)
{
  g_free (info->message);
  g_slice_free (LocalPendingInfo, info);
}


/**
 * tp_channel_group_get_self_handle:
 * @self: a channel
 *
 * Return the #TpChannel:group-self-handle property (see the description
 * of that property for notes on validity).
 *
 * Returns: the handle representing the user, or 0
 * Since: 0.7.12
 */
TpHandle
tp_channel_group_get_self_handle (TpChannel *self)
{
  g_return_val_if_fail (TP_IS_CHANNEL (self), 0);

  return self->priv->group_self_handle;
}


/**
 * tp_channel_group_get_flags:
 * @self: a channel
 *
 * Return the #TpChannel:group-flags property (see the description
 * of that property for notes on validity).
 *
 * Returns: the group flags, or 0
 * Since: 0.7.12
 */
TpChannelGroupFlags
tp_channel_group_get_flags (TpChannel *self)
{
  g_return_val_if_fail (TP_IS_CHANNEL (self), 0);

  return self->priv->group_flags;
}


/**
 * tp_channel_group_get_members:
 * @self: a channel
 *
 * If @self is ready and is a group, return a #TpIntSet containing
 * its members.
 *
 * If @self is a group but is not ready, the result may either be a set
 * of members, or %NULL.
 *
 * If @self is not a group, return %NULL.
 *
 * Returns: the members, or %NULL
 * Since: 0.7.12
 */
const TpIntSet *
tp_channel_group_get_members (TpChannel *self)
{
  g_return_val_if_fail (TP_IS_CHANNEL (self), NULL);

  return self->priv->group_members;
}


/**
 * tp_channel_group_get_local_pending:
 * @self: a channel
 *
 * If @self is ready and is a group, return a #TpIntSet containing
 * its local-pending members.
 *
 * If @self is a group but is not ready, the result may either be a set
 * of local-pending members, or %NULL.
 *
 * If @self is not a group, return %NULL.
 *
 * Returns: the local-pending members, or %NULL
 * Since: 0.7.12
 */
const TpIntSet *
tp_channel_group_get_local_pending (TpChannel *self)
{
  g_return_val_if_fail (TP_IS_CHANNEL (self), NULL);

  return self->priv->group_local_pending;
}


/**
 * tp_channel_group_get_remote_pending:
 * @self: a channel
 *
 * If @self is ready and is a group, return a #TpIntSet containing
 * its remote-pending members.
 *
 * If @self is a group but is not ready, the result may either be a set
 * of remote-pending members, or %NULL.
 *
 * If @self is not a group, return %NULL.
 *
 * Returns: the remote-pending members, or %NULL
 * Since: 0.7.12
  */
const TpIntSet *
tp_channel_group_get_remote_pending (TpChannel *self)
{
  g_return_val_if_fail (TP_IS_CHANNEL (self), NULL);

  return self->priv->group_remote_pending;
}


/**
 * tp_channel_group_get_local_pending_info:
 * @self: a channel
 * @local_pending: the handle of a local-pending contact about whom more
 *  information is needed
 * @actor: either %NULL or a location to return the contact who requested
 *  the change
 * @reason: either %NULL or a location to return the reason for the change
 * @message: either %NULL or a location to return the user-supplied message
 *
 * If @local_pending is actually the handle of a local-pending contact,
 * write additional information into @actor, @reason and @message and return
 * %TRUE. The handle and message are not referenced or copied, and can only be
 * assumed to remain valid until the main loop is re-entered.
 *
 * If @local_pending is not the handle of a local-pending contact,
 * write 0 into @actor, %TP_CHANNEL_GROUP_CHANGE_REASON_NONE into @reason
 * and "" into @message, and return %FALSE.
 *
 * Returns: %TRUE if the contact is in fact local-pending
 * Since: 0.7.12
 */
gboolean
tp_channel_group_get_local_pending_info (TpChannel *self,
                                         TpHandle local_pending,
                                         TpHandle *actor,
                                         TpChannelGroupChangeReason *reason,
                                         const gchar **message)
{
  gboolean ret = FALSE;
  TpHandle a = 0;
  TpChannelGroupChangeReason r = TP_CHANNEL_GROUP_CHANGE_REASON_NONE;
  const gchar *m = "";

  g_return_val_if_fail (TP_IS_CHANNEL (self), FALSE);

  if (self->priv->group_local_pending != NULL)
    {
      /* it could conceivably be someone who is local-pending */

      ret = tp_intset_is_member (self->priv->group_local_pending,
          local_pending);

      if (ret && self->priv->group_local_pending_info != NULL)
        {
          /* we might even have information about them */
          LocalPendingInfo *info = g_hash_table_lookup (
              self->priv->group_local_pending_info,
              GUINT_TO_POINTER (local_pending));

          if (info != NULL)
            {
              a = info->actor;
              r = info->reason;

              if (info->message != NULL)
                m = info->message;
            }
          /* else we have no info, which means (0, NONE, NULL) */
        }
    }

  if (actor != NULL)
    *actor = a;

  if (message != NULL)
    *message = m;

  if (reason != NULL)
    *reason = r;

  return ret;
}


/**
 * tp_channel_group_get_handle_owner:
 * @self: a channel
 * @handle: a handle which is a member of this channel
 *
 * Synopsis (see below for further explanation):
 *
 * - if @self is not a group or @handle is not a member of this channel,
 *   result is undefined;
 * - if @self does not have #TpChannel:ready = TRUE, result is undefined;
 * - if @self does not have flags that include %TP_CHANNEL_FLAG_PROPERTIES,
 *   result is undefined;
 * - if @handle is channel-specific and its globally valid "owner" is known,
 *   return that owner;
 * - if @handle is channel-specific and its globally valid "owner" is unknown,
 *   return zero;
 * - if @handle is globally valid, return @handle itself
 *
 * Some channels (those with flags that include
 * %TP_CHANNEL_FLAG_CHANNEL_SPECIFIC_HANDLES) have a concept of
 * "channel-specific handles". These are handles that only have meaning within
 * the context of the channel - for instance, in XMPP Multi-User Chat,
 * participants in a chatroom are identified by an in-room JID consisting
 * of the JID of the chatroom plus a local nickname.
 *
 * Depending on the protocol and configuration, it might be possible to find
 * out what globally valid handle (i.e. an identifier that you could add to
 * your contact list) "owns" a channel-specific handle. For instance, in
 * most XMPP MUC chatrooms, normal users cannot see what global JID
 * corresponds to an in-room JID, but moderators can.
 *
 * This is further complicated by the fact that channels with channel-specific
 * handles can sometimes have members with globally valid handles (for
 * instance, if you invite someone to an XMPP MUC using their globally valid
 * JID, you would expect to see the handle representing that JID in the
 * Group's remote-pending set).
 *
 * This function's result is undefined unless the channel is ready
 * and its flags include %TP_CHANNEL_FLAG_PROPERTIES (an implementation
 * without extra D-Bus round trips is not possible using the older API).
 *
 * Returns: the global handle that owns the given handle, or 0
 * Since: 0.7.12
 */
TpHandle
tp_channel_group_get_handle_owner (TpChannel *self,
                                   TpHandle handle)
{
  gpointer key, value;

  g_return_val_if_fail (TP_IS_CHANNEL (self), 0);

  if (self->priv->group_handle_owners == NULL)
    {
      /* undefined result - pretending it's global is probably as good as
       * any other behaviour, since we can't know either way */
      return handle;
    }

  if (g_hash_table_lookup_extended (self->priv->group_handle_owners,
        GUINT_TO_POINTER (handle), &key, &value))
    {
      /* channel-specific, value is either owner or 0 if unknown */
      return GPOINTER_TO_UINT (value);
    }
  else
    {
      /* either already globally valid, or not a member */
      return handle;
    }
}


/* This must be called before the local group members lists are created.  Until
 * this is called, the proxy is listening to both MembersChanged and
 * MembersChangedDetailed, but they are ignored until priv->group_members
 * exists.  If that list is created before one signal is disconnected, the
 * proxy will react to state changes twice and madness will ensue.
 */
static void
_got_initial_group_flags (TpChannel *self,
                          TpChannelGroupFlags flags)
{
  TpChannelPrivate *priv = self->priv;

  g_assert (priv->group_flags == 0);
  g_assert (self->priv->group_members == NULL);

  priv->group_flags = flags;
  DEBUG ("Initial GroupFlags: %u", flags);
  priv->have_group_flags = TRUE;

  if (flags != 0)
    g_object_notify ((GObject *) self, "group-flags");

  if (tp_proxy_get_invalidated (self) != NULL)
    {
      /* Because the proxy has been invalidated, it is not safe to call
       * tp_proxy_signal_connection_disconnect (below), so just return early */
      return;
    }

  /* If the channel claims to support MembersChangedDetailed, disconnect from
   * MembersChanged. Otherwise, disconnect from MembersChangedDetailed in case
   * it secretly emits it anyway, so we're only listening to one change
   * notification.
   */
  if (flags & TP_CHANNEL_GROUP_FLAG_MEMBERS_CHANGED_DETAILED)
    tp_proxy_signal_connection_disconnect (priv->members_changed_sig);
  else
    tp_proxy_signal_connection_disconnect (priv->members_changed_detailed_sig);

  priv->members_changed_sig = NULL;
  priv->members_changed_detailed_sig = NULL;
}


static void
tp_channel_got_group_flags_0_16_cb (TpChannel *self,
                                    guint flags,
                                    const GError *error,
                                    gpointer user_data G_GNUC_UNUSED,
                                    GObject *weak_object G_GNUC_UNUSED)
{
  g_assert (self->priv->group_flags == 0);

  if (error != NULL)
    {
      /* GetGroupFlags() has existed with its current signature since November
       * 2005. I think it's reasonable to say that if it doesn't work, the
       * channel is broken.
       */
      _tp_channel_abort_introspection (self, "GetGroupFlags() failed", error);
      return;
    }

  /* If we reach this point, GetAll has already failed... */
  if (flags & TP_CHANNEL_GROUP_FLAG_PROPERTIES)
    {
      DEBUG ("Treason uncloaked! The channel claims to support Group "
          "properties, but GetAll didn't work");
      flags &= ~TP_CHANNEL_GROUP_FLAG_PROPERTIES;
    }

  _got_initial_group_flags (self, flags);
  _tp_channel_continue_introspection (self);
}


static void
tp_channel_group_self_handle_changed_cb (TpChannel *self,
                                         guint self_handle,
                                         gpointer unused G_GNUC_UNUSED,
                                         GObject *unused_object G_GNUC_UNUSED)
{
  if (self_handle == self->priv->group_self_handle)
    return;

  DEBUG ("%p SelfHandle changed to %u", self, self_handle);

  self->priv->group_self_handle = self_handle;
  g_object_notify ((GObject *) self, "group-self-handle");
}


static void
tp_channel_got_self_handle_0_16_cb (TpChannel *self,
                                    guint self_handle,
                                    const GError *error,
                                    gpointer user_data G_GNUC_UNUSED,
                                    GObject *weak_object G_GNUC_UNUSED)
{
  if (error != NULL)
    {
      DEBUG ("%p Group.GetSelfHandle() failed, assuming 0: %s", self,
          error->message);
      tp_channel_group_self_handle_changed_cb (self, 0, NULL, NULL);
    }
  else
    {
      DEBUG ("Initial Group.SelfHandle: %u", self_handle);
      tp_channel_group_self_handle_changed_cb (self, self_handle, NULL, NULL);
    }

  _tp_channel_continue_introspection (self);
}


static void
_tp_channel_get_self_handle_0_16 (TpChannel *self)
{
  tp_cli_channel_interface_group_call_get_self_handle (self, -1,
      tp_channel_got_self_handle_0_16_cb, NULL, NULL, NULL);
}


static void
_tp_channel_get_group_flags_0_16 (TpChannel *self)
{
  tp_cli_channel_interface_group_call_get_group_flags (self, -1,
      tp_channel_got_group_flags_0_16_cb, NULL, NULL, NULL);
}


static void
_tp_channel_group_set_one_lp (TpChannel *self,
                              TpHandle handle,
                              TpHandle actor,
                              TpChannelGroupChangeReason reason,
                              const gchar *message)
{
  LocalPendingInfo *info = NULL;

  g_assert (self->priv->group_local_pending != NULL);

  tp_intset_add (self->priv->group_local_pending, handle);
  tp_intset_remove (self->priv->group_members, handle);
  tp_intset_remove (self->priv->group_remote_pending, handle);

  if (actor == 0 && reason == TP_CHANNEL_GROUP_CHANGE_REASON_NONE &&
      (message == NULL || message[0] == '\0'))
    {
      /* we just don't bother storing informationless local-pending */
      if (self->priv->group_local_pending_info != NULL)
        {
          g_hash_table_remove (self->priv->group_local_pending_info,
              GUINT_TO_POINTER (handle));
        }

      return;
    }

  if (self->priv->group_local_pending_info == NULL)
    {
      self->priv->group_local_pending_info = g_hash_table_new_full (
          g_direct_hash, g_direct_equal, NULL,
          (GDestroyNotify) local_pending_info_free);
    }
  else
    {
      info = g_hash_table_lookup (self->priv->group_local_pending_info,
          GUINT_TO_POINTER (handle));
    }

  if (info == NULL)
    {
      info = g_slice_new0 (LocalPendingInfo);
    }
  else
    {
      g_hash_table_steal (self->priv->group_local_pending_info,
          GUINT_TO_POINTER (handle));
    }

  info->actor = actor;
  info->reason = reason;
  g_free (info->message);

  if (message == NULL || message[0] == '\0')
    info->message = NULL;
  else
    info->message = g_strdup (message);

  g_hash_table_insert (self->priv->group_local_pending_info,
      GUINT_TO_POINTER (handle), info);
}


static void
_tp_channel_group_set_lp (TpChannel *self,
                          const GPtrArray *info)
{
  guint i;

  /* should only be called during initialization */
  g_assert (self->priv->group_local_pending != NULL);
  g_assert (self->priv->group_local_pending_info == NULL);

  tp_intset_clear (self->priv->group_local_pending);

  /* NULL-safe for ease of use with tp_asv_get_boxed */
  if (info == NULL)
    {
      return;
    }

  for (i = 0; i < info->len; i++)
    {
      GValueArray *item = g_ptr_array_index (info, i);
      TpHandle handle = g_value_get_uint (item->values + 0);
      TpHandle actor = g_value_get_uint (item->values + 1);
      TpChannelGroupChangeReason reason = g_value_get_uint (
          item->values + 2);
      const gchar *message = g_value_get_string (item->values + 3);

      if (handle == 0)
        {
          DEBUG ("Ignoring handle 0, claimed to be in local-pending");
          continue;
        }

      DEBUG ("+L %u, actor=%u, reason=%u, message=%s", handle,
          actor, reason, message);
      _tp_channel_group_set_one_lp (self, handle, actor,
          reason, message);
    }
}


static void
tp_channel_got_all_members_0_16_cb (TpChannel *self,
                                    const GArray *members,
                                    const GArray *local_pending,
                                    const GArray *remote_pending,
                                    const GError *error,
                                    gpointer user_data G_GNUC_UNUSED,
                                    GObject *weak_object G_GNUC_UNUSED)
{
  g_assert (self->priv->group_local_pending == NULL);
  g_assert (self->priv->group_local_pending_info == NULL);
  g_assert (self->priv->group_members == NULL);
  g_assert (self->priv->group_remote_pending == NULL);

  if (error == NULL)
    {
      DEBUG ("%p GetAllMembers returned %u members + %u LP + %u RP",
          self, members->len, local_pending->len, remote_pending->len);

      self->priv->group_local_pending = tp_intset_from_array (local_pending);
      self->priv->group_members = tp_intset_from_array (members);
      self->priv->group_remote_pending = tp_intset_from_array (remote_pending);

      if (tp_intset_remove (self->priv->group_members, 0))
        {
          DEBUG ("Ignoring handle 0, claimed to be in group");
        }

      if (tp_intset_remove (self->priv->group_local_pending, 0))
        {
          DEBUG ("Ignoring handle 0, claimed to be in local-pending");
        }

      if (tp_intset_remove (self->priv->group_remote_pending, 0))
        {
          DEBUG ("Ignoring handle 0, claimed to be in remote-pending");
        }

      /* the local-pending info will be filled in with the result of
       * GetLocalPendingMembersWithInfo, if it succeeds */
    }
  else
    {
      DEBUG ("%p GetAllMembers failed, assuming empty: %s", self,
          error->message);

      self->priv->group_local_pending = tp_intset_new ();
      self->priv->group_members = tp_intset_new ();
      self->priv->group_remote_pending = tp_intset_new ();
    }

  g_assert (self->priv->group_local_pending != NULL);
  g_assert (self->priv->group_members != NULL);
  g_assert (self->priv->group_remote_pending != NULL);

  _tp_channel_continue_introspection (self);
}


static void
_tp_channel_get_all_members_0_16 (TpChannel *self)
{
  tp_cli_channel_interface_group_call_get_all_members (self, -1,
      tp_channel_got_all_members_0_16_cb, NULL, NULL, NULL);
}


static void
tp_channel_glpmwi_0_16_cb (TpChannel *self,
                           const GPtrArray *info,
                           const GError *error,
                           gpointer user_data G_GNUC_UNUSED,
                           GObject *object G_GNUC_UNUSED)
{
  /* this should always run after tp_channel_got_all_members_0_16 */
  g_assert (self->priv->group_local_pending != NULL);
  g_assert (self->priv->group_local_pending_info == NULL);

  if (error == NULL)
    {
      DEBUG ("%p GetLocalPendingMembersWithInfo returned %u records",
          self, info->len);
      _tp_channel_group_set_lp (self, info);
    }
  else
    {
      DEBUG ("%p GetLocalPendingMembersWithInfo failed, keeping result of "
          "GetAllMembers instead: %s", self, error->message);
    }

  _tp_channel_continue_introspection (self);
}


static void
_tp_channel_glpmwi_0_16 (TpChannel *self)
{
  tp_cli_channel_interface_group_call_get_local_pending_members_with_info (
      self, -1, tp_channel_glpmwi_0_16_cb, NULL, NULL, NULL);
}

static void
_tp_channel_emit_initial_sets (TpChannel *self)
{
  GArray *added, *remote_pending;
  GArray empty_array = { NULL, 0 };
  TpIntSetIter iter = TP_INTSET_ITER_INIT (self->priv->group_local_pending);

  added = tp_intset_to_array (self->priv->group_members);
  remote_pending = tp_intset_to_array (self->priv->group_remote_pending);

  g_signal_emit_by_name (self, "group-members-changed", "",
      added, &empty_array, &empty_array, remote_pending, 0, 0);

  while (tp_intset_iter_next (&iter))
    {
      TpHandle handle;
      GArray local_pending = { (gchar *) &handle, 1 };
      TpHandle actor;
      TpChannelGroupChangeReason reason;
      const gchar *message;

      handle = iter.element;
      tp_channel_group_get_local_pending_info (self, handle, &actor, &reason,
          &message);

      g_signal_emit_by_name (self, "group-members-changed", message,
          &empty_array, &empty_array, &local_pending, &empty_array, actor,
          reason);
    }

  g_array_free (added, TRUE);
  g_array_free (remote_pending, TRUE);

  _tp_channel_continue_introspection (self);
}

static void
tp_channel_got_group_properties_cb (TpProxy *proxy,
                                    GHashTable *asv,
                                    const GError *error,
                                    gpointer unused G_GNUC_UNUSED,
                                    GObject *unused_object G_GNUC_UNUSED)
{
  TpChannel *self = TP_CHANNEL (proxy);
  static GType au_type = 0;

  if (G_UNLIKELY (au_type == 0))
    {
      au_type = dbus_g_type_get_collection ("GArray", G_TYPE_UINT);
    }

  if (error != NULL)
    {
      DEBUG ("Error getting group properties, falling back to 0.16 API: %s",
          error->message);
    }
  else if ((tp_asv_get_uint32 (asv, "GroupFlags", NULL)
      & TP_CHANNEL_GROUP_FLAG_PROPERTIES) == 0)
    {
      DEBUG ("Got group properties, but no Properties flag: assuming a "
          "broken implementation and falling back to 0.16 API");
    }
  else
    {
      GHashTable *handle_owners;
      GArray *arr;

      DEBUG ("Received %u group properties", g_hash_table_size (asv));

      _got_initial_group_flags (self,
          tp_asv_get_uint32 (asv, "GroupFlags", NULL));

      tp_channel_group_self_handle_changed_cb (self,
          tp_asv_get_uint32 (asv, "SelfHandle", NULL), NULL, NULL);

      g_assert (self->priv->group_members == NULL);
      g_assert (self->priv->group_remote_pending == NULL);

      arr = tp_asv_get_boxed (asv, "Members", au_type);

      if (arr == NULL)
        self->priv->group_members = tp_intset_new ();
      else
        self->priv->group_members = tp_intset_from_array (arr);

      if (tp_intset_remove (self->priv->group_members, 0))
        {
          DEBUG ("Ignoring handle 0, claimed to be in group");
        }

      arr = tp_asv_get_boxed (asv, "RemotePendingMembers", au_type);

      if (arr == NULL)
        self->priv->group_remote_pending = tp_intset_new ();
      else
        self->priv->group_remote_pending = tp_intset_from_array (arr);

      if (tp_intset_remove (self->priv->group_remote_pending, 0))
        {
          DEBUG ("Ignoring handle 0, claimed to be in remote-pending");
        }

      g_assert (self->priv->group_local_pending == NULL);
      g_assert (self->priv->group_local_pending_info == NULL);

      self->priv->group_local_pending = tp_intset_new ();

      /* this is NULL-safe with respect to the array */
      _tp_channel_group_set_lp (self,
          tp_asv_get_boxed (asv, "LocalPendingMembers",
              TP_ARRAY_TYPE_LOCAL_PENDING_INFO_LIST));

      handle_owners = tp_asv_get_boxed (asv, "HandleOwners",
          TP_HASH_TYPE_HANDLE_OWNER_MAP);

      self->priv->group_handle_owners = g_hash_table_new (g_direct_hash,
          g_direct_equal);

      if (handle_owners != NULL)
        tp_g_hash_table_update (self->priv->group_handle_owners,
            handle_owners, NULL, NULL);

      goto OUT;
    }

  /* Failure case: fall back. This is quite annoying, as we need to combine:
   *
   * - GetGroupFlags
   * - GetAllMembers
   * - GetLocalPendingMembersWithInfo
   *
   * Channel-specific handles can't really have a sane client API (without
   * lots of silly round-trips) unless the CM implements the HandleOwners
   * property, so I intend to ignore this in the fallback case.
   */

  g_queue_push_tail (self->priv->introspect_needed,
      _tp_channel_get_group_flags_0_16);

  g_queue_push_tail (self->priv->introspect_needed,
      _tp_channel_get_self_handle_0_16);

  g_queue_push_tail (self->priv->introspect_needed,
      _tp_channel_get_all_members_0_16);

  g_queue_push_tail (self->priv->introspect_needed,
      _tp_channel_glpmwi_0_16);

OUT:

  g_queue_push_tail (self->priv->introspect_needed,
      _tp_channel_emit_initial_sets);

  _tp_channel_continue_introspection (self);
}


static void
handle_members_changed (TpChannel *self,
                        const gchar *message,
                        const GArray *added,
                        const GArray *removed,
                        const GArray *local_pending,
                        const GArray *remote_pending,
                        guint actor,
                        guint reason,
                        GHashTable *details)
{
  guint i;

  if (self->priv->group_members == NULL)
    return;

  g_assert (self->priv->group_local_pending != NULL);
  g_assert (self->priv->group_remote_pending != NULL);

  for (i = 0; i < added->len; i++)
    {
      TpHandle handle = g_array_index (added, guint, i);

      DEBUG ("+++ contact#%u", handle);

      if (handle == 0)
        {
          DEBUG ("handle 0 shouldn't be in MembersChanged, ignoring");
          continue;
        }

      tp_intset_add (self->priv->group_members, handle);
      tp_intset_remove (self->priv->group_local_pending, handle);
      tp_intset_remove (self->priv->group_remote_pending, handle);
    }

  for (i = 0; i < local_pending->len; i++)
    {
      TpHandle handle = g_array_index (local_pending, guint, i);

      DEBUG ("+LP contact#%u", handle);

      if (handle == 0)
        {
          DEBUG ("handle 0 shouldn't be in MembersChanged, ignoring");
          continue;
        }

      /* Special-case renaming a local-pending contact, if the
       * signal is spec-compliant. Keep the old actor/reason/message in
       * this case */
      if (reason == TP_CHANNEL_GROUP_CHANGE_REASON_RENAMED &&
          added->len == 0 &&
          local_pending->len == 1 &&
          remote_pending->len == 0 &&
          removed->len == 1 &&
          self->priv->group_local_pending_info != NULL)
        {
          TpHandle old = g_array_index (removed, guint, 0);
          LocalPendingInfo *info = g_hash_table_lookup (
              self->priv->group_local_pending_info,
              GUINT_TO_POINTER (old));

          if (info != NULL)
            {
              _tp_channel_group_set_one_lp (self, handle,
                  info->actor, info->reason, info->message);
              continue;
            }
        }

      /* not reached if the Renamed special case occurred */
      _tp_channel_group_set_one_lp (self, handle, actor,
          reason, message);
    }

  for (i = 0; i < remote_pending->len; i++)
    {
      TpHandle handle = g_array_index (remote_pending, guint, i);

      DEBUG ("+RP contact#%u", handle);

      if (handle == 0)
        {
          DEBUG ("handle 0 shouldn't be in MembersChanged, ignoring");
          continue;
        }

      tp_intset_add (self->priv->group_remote_pending, handle);
      tp_intset_remove (self->priv->group_members, handle);
      tp_intset_remove (self->priv->group_local_pending, handle);
    }

  for (i = 0; i < removed->len; i++)
    {
      TpHandle handle = g_array_index (removed, guint, i);

      DEBUG ("--- contact#%u", handle);

      if (handle == 0)
        {
          DEBUG ("handle 0 shouldn't be in MembersChanged, ignoring");
          continue;
        }

      if (self->priv->group_local_pending_info != NULL)
        g_hash_table_remove (self->priv->group_local_pending_info,
            GUINT_TO_POINTER (handle));

      tp_intset_remove (self->priv->group_members, handle);
      tp_intset_remove (self->priv->group_local_pending, handle);
      tp_intset_remove (self->priv->group_remote_pending, handle);

      if (handle == self->priv->group_self_handle ||
          handle == tp_connection_get_self_handle (self->priv->connection))
        {
          const gchar *error_detail = tp_asv_get_string (details, "error");

          self->priv->group_remove_reason = reason;
          g_free (self->priv->group_remove_message);

          /* If there's an error detail and a message, "$error: $message", else
           * just the non-empty one (or indeed the empty string).
           */
          self->priv->group_remove_message = g_strdup_printf ("%s%s%s",
              (error_detail != NULL ? error_detail : ""),
              (error_detail != NULL && message[0] != '\0' ? ": " : ""),
              message);
        }

      /* FIXME: should check against the Connection's self-handle too,
       * after I add that API */
    }

  g_signal_emit_by_name (self, "group-members-changed", message,
      added, removed, local_pending, remote_pending, actor, reason);
  g_signal_emit_by_name (self, "group-members-changed-detailed", added,
      removed, local_pending, remote_pending, details);
}


static void
tp_channel_group_members_changed_cb (TpChannel *self,
                                     const gchar *message,
                                     const GArray *added,
                                     const GArray *removed,
                                     const GArray *local_pending,
                                     const GArray *remote_pending,
                                     guint actor,
                                     guint reason,
                                     gpointer unused G_GNUC_UNUSED,
                                     GObject *unused_object G_GNUC_UNUSED)
{
  GHashTable *details = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
      (GDestroyNotify) tp_g_value_slice_free);

  DEBUG ("%p MembersChanged: added %u, removed %u, "
      "moved %u to LP and %u to RP, actor %u, reason %u, message %s",
      self, added->len, removed->len, local_pending->len, remote_pending->len,
      actor, reason, message);

  if (actor != 0)
    {
      g_hash_table_insert (details, "actor",
          tp_g_value_slice_new_uint (actor));
    }

  if (reason != TP_CHANNEL_GROUP_CHANGE_REASON_NONE)
    {
      g_hash_table_insert (details, "change-reason",
          tp_g_value_slice_new_uint (reason));
    }

  if (*message != '\0')
    {
      g_hash_table_insert (details, "message",
          tp_g_value_slice_new_string (message));
    }

  handle_members_changed (self, message, added, removed, local_pending,
      remote_pending, actor, reason, details);

  g_hash_table_unref (details);
}


static void
tp_channel_group_members_changed_detailed_cb (TpChannel *self,
                                              const GArray *added,
                                              const GArray *removed,
                                              const GArray *local_pending,
                                              const GArray *remote_pending,
                                              GHashTable *details,
                                              gpointer unused G_GNUC_UNUSED,
                                              GObject *weak_obj G_GNUC_UNUSED)
{
  const gchar *message;
  guint actor;
  guint reason;

  DEBUG ("%p MembersChangedDetailed: added %u, removed %u, "
      "moved %u to LP and %u to RP",
      self, added->len, removed->len, local_pending->len, remote_pending->len);

  actor = tp_asv_get_uint32 (details, "actor", NULL);
  reason = tp_asv_get_uint32 (details, "change-reason", NULL);
  message = tp_asv_get_string (details, "message");

  if (message == NULL)
    message = "";

  handle_members_changed (self, message, added, removed, local_pending,
      remote_pending, actor, reason, details);
}


static void
tp_channel_handle_owners_changed_cb (TpChannel *self,
                                     GHashTable *added,
                                     const GArray *removed,
                                     gpointer unused G_GNUC_UNUSED,
                                     GObject *unused_object G_GNUC_UNUSED)
{
  guint i;

  /* ignore the signal if we don't have the initial set yet */
  if (self->priv->group_handle_owners == NULL)
    return;

  tp_g_hash_table_update (self->priv->group_handle_owners, added, NULL, NULL);

  for (i = 0; i < removed->len; i++)
    {
      g_hash_table_remove (self->priv->group_handle_owners,
          GUINT_TO_POINTER (g_array_index (removed, guint, i)));
    }
}

#define IMMUTABLE_FLAGS \
  (TP_CHANNEL_GROUP_FLAG_PROPERTIES | \
  TP_CHANNEL_GROUP_FLAG_MEMBERS_CHANGED_DETAILED)

static void
tp_channel_group_flags_changed_cb (TpChannel *self,
                                   guint added,
                                   guint removed,
                                   gpointer unused G_GNUC_UNUSED,
                                   GObject *unused_object G_GNUC_UNUSED)
{
  if (self->priv->have_group_flags)
    {
      DEBUG ("%p GroupFlagsChanged: +%u -%u", self, added, removed);

      added &= ~(self->priv->group_flags);
      removed &= self->priv->group_flags;

      DEBUG ("%p GroupFlagsChanged (after filtering): +%u -%u",
          self, added, removed);

      if ((added & IMMUTABLE_FLAGS) || (removed & IMMUTABLE_FLAGS))
        {
          GError *e = g_error_new (TP_DBUS_ERRORS, TP_DBUS_ERROR_INCONSISTENT,
              "CM is broken: it changed the Properties/"
              "Members_Changed_Detailed flags on an existing group channel "
              "(offending changes: added=%u, removed=%u)",
              added & IMMUTABLE_FLAGS, removed & IMMUTABLE_FLAGS);

          tp_proxy_invalidate ((TpProxy *) self, e);
          g_error_free (e);
          return;
        }

      self->priv->group_flags |= added;
      self->priv->group_flags &= ~removed;

      if (added != 0 || removed != 0)
        {
          g_object_notify ((GObject *) self, "group-flags");
          g_signal_emit_by_name (self, "group-flags-changed", added, removed);
        }
    }
}

#undef IMMUTABLE_FLAGS


void
_tp_channel_get_group_properties (TpChannel *self)
{
  TpChannelPrivate *priv = self->priv;
  TpProxySignalConnection *sc;
  GError *error = NULL;

  if (!tp_proxy_has_interface_by_id (self,
        TP_IFACE_QUARK_CHANNEL_INTERFACE_GROUP))
    {
      DEBUG ("%p: not a Group, continuing", self);
      _tp_channel_continue_introspection (self);
      return;
    }

  DEBUG ("%p", self);

  /* If this callback has been called, 'self' has not been invalidated. And we
   * just checked above that the proxy has the Group interface. So, connecting
   * to these signals must succeed. */
#define DIE(sig) \
  { \
    g_critical ("couldn't connect to " sig ": %s", error->message); \
    g_assert_not_reached (); \
    g_error_free (error); \
    return; \
  }

  priv->members_changed_sig =
      tp_cli_channel_interface_group_connect_to_members_changed (self,
          tp_channel_group_members_changed_cb, NULL, NULL, NULL, &error);

  if (priv->members_changed_sig == NULL)
    DIE ("MembersChanged");

  priv->members_changed_detailed_sig =
      tp_cli_channel_interface_group_connect_to_members_changed_detailed (self,
          tp_channel_group_members_changed_detailed_cb, NULL, NULL, NULL,
          &error);

  if (priv->members_changed_detailed_sig == NULL)
    DIE ("MembersChangedDetailed");

  sc = tp_cli_channel_interface_group_connect_to_group_flags_changed (self,
      tp_channel_group_flags_changed_cb, NULL, NULL, NULL, &error);

  if (sc == NULL)
    DIE ("GroupFlagsChanged");

  sc = tp_cli_channel_interface_group_connect_to_self_handle_changed (self,
      tp_channel_group_self_handle_changed_cb, NULL, NULL, NULL, &error);

  if (sc == NULL)
    DIE ("SelfHandleChanged");

  sc = tp_cli_channel_interface_group_connect_to_handle_owners_changed (self,
      tp_channel_handle_owners_changed_cb, NULL, NULL, NULL, &error);

  if (sc == NULL)
    DIE ("HandleOwnersChanged");

  /* First try the 0.17 API (properties). If this fails we'll fall back */
  tp_cli_dbus_properties_call_get_all (self, -1,
      TP_IFACE_CHANNEL_INTERFACE_GROUP, tp_channel_got_group_properties_cb,
      NULL, NULL, NULL);
}
