#include "../engine/dconf-engine.h"




typedef struct
{
  gpointer data; /* either GDBusConnection or GError */
  gboolean is_error;
  guint    waiting_for_serial;
  GQueue   queue;
} ConnectionState;

typedef struct
{
  guint32                serial;
  DConfEngineCallHandle *handle;
} DConfGDBusCall;

static ConnectionState connections[3];
static GMutex dconf_gdbus_lock;

static GBusType
connection_state_get_bus_type (ConnectionState *state)
{
  return state - connections;
}

static gboolean
connection_state_ensure_success (ConnectionState  *state,
                                 GError          **error)
{
  if (state->is_error)
    {
      if (error)
        *error = g_error_copy (state->data);

      return FALSE;
    }

  return TRUE;
}

static GDBusConnection *
connection_state_get_connection (ConnectionState *state)
{
  g_assert (!state->is_error);

  return state->data;
}

/* This function can be slow (as compared to the one below). */
static void
dconf_gdbus_handle_reply (ConnectionState *state,
                          GDBusMessage    *message)
{
  DConfEngineCallHandle *handle;
  GError *error = NULL;
  GVariant *body;

  g_mutex_lock (&dconf_gdbus_lock);
  {
    DConfGDBusCall *call;

    call = g_queue_pop_head (&state->queue);
    g_assert_cmpuint (g_dbus_message_get_reply_serial (message), ==, call->serial);
    handle = call->handle;

    g_slice_free (DConfGDBusCall, call);

    call = g_queue_peek_head (&state->queue);
    if (call)
      g_atomic_int_set (&state->waiting_for_serial, call->serial);
    else
      g_atomic_int_set (&state->waiting_for_serial, -1);
  }
  g_mutex_unlock (&dconf_gdbus_lock);

  body = g_dbus_message_get_body (message);

  if (g_dbus_message_get_message_type (message) == G_DBUS_MESSAGE_TYPE_ERROR)
    {
      const GVariantType *first_child_type;
      const gchar *error_message = NULL;

      first_child_type = g_variant_type_first (g_variant_get_type (body));

      if (g_variant_type_equal (first_child_type, G_VARIANT_TYPE_STRING))
        g_variant_get_child (body, 0, "&s", &error_message);

      error = g_dbus_error_new_for_dbus_error (g_dbus_message_get_error_name (message), error_message);
      body = NULL;
    }

  dconf_engine_call_handle_reply (handle, body, error);

  if (error)
    g_error_free (error);
}

/* We optimise for this function being super-efficient since it gets run
 * on every single D-Bus message in or out.
 *
 * We want to bail out as quickly as possible in the case that this
 * message does not interest us.  That means we should not hold locks or
 * anything like that.
 *
 * In the case that this message _does_ interest us (which should be
 * rare) we can take a lot more time.
 */
static GDBusMessage *
dconf_gdbus_filter_function (GDBusConnection *connection,
                             GDBusMessage    *message,
                             gboolean         incoming,
                             gpointer         user_data)
{
  ConnectionState *state = user_data;

  if (incoming)
    {
      switch (g_dbus_message_get_message_type (message))
        {
        case G_DBUS_MESSAGE_TYPE_SIGNAL:
          {
            const gchar *interface;

            interface = g_dbus_message_get_interface (message);
            if (interface && g_str_equal (interface, "ca.desrt.dconf.Writer"))
              dconf_engine_handle_dbus_signal (connection_state_get_bus_type (state),
                                               g_dbus_message_get_sender (message),
                                               g_dbus_message_get_path (message),
                                               g_dbus_message_get_member (message),
                                               g_dbus_message_get_body (message));

            /* Others could theoretically be interested in this... */
          }
          break;

        case G_DBUS_MESSAGE_TYPE_METHOD_RETURN:
        case G_DBUS_MESSAGE_TYPE_ERROR:
          if G_UNLIKELY (g_dbus_message_get_reply_serial (message) == g_atomic_int_get (&state->waiting_for_serial))
            {
              /* This is definitely for us. */
              dconf_gdbus_handle_reply (state, message);

              /* Nobody else should be interested in it. */
              g_clear_object (&message);
            }
          break;

        default:
          break;
        }
    }

  return message;
}

static void
dconf_gdbus_bus_connection_closed (GDBusConnection *connection,
                                   gboolean         remote_peer_vanished,
                                   GError          *error,
                                   gpointer         user_data)
{
  ConnectionState *state = user_data;

  dconf_engine_dbus_handle_connection_closed (connection, remote_peer_vanished, error,
                                              &dconf_gdbus_lock,
                                              &state->is_error,
                                              &state->data,
                                              G_CALLBACK (dconf_gdbus_bus_connection_closed),
                                              user_data);
}

static ConnectionState *
dconf_gdbus_get_connection_state (GBusType   bus_type,
                                  GError   **error)
{
  ConnectionState *state;

  g_assert (bus_type < G_N_ELEMENTS (connections));

  state = &connections[bus_type];

  if (!state->data)
    {
      GDBusConnection *connection;
      GError *error = NULL;
      gpointer result;

      /* This will only block the first time...
       *
       * Optimising this away is probably not worth the effort.
       */
      connection = g_bus_get_sync (bus_type, NULL, &error);

      if (connection)
        {
          g_signal_connect (connection, "closed",
                            G_CALLBACK (dconf_gdbus_bus_connection_closed),
                            state);
          g_dbus_connection_add_filter (connection, dconf_gdbus_filter_function, state, NULL);
          result = connection;
          state->is_error = FALSE;
        }
      else
        {
          result = error;
          state->is_error = TRUE;
        }

      state->data = result;
    }

  if (!connection_state_ensure_success (state, error))
    return NULL;

  return state;
}

gboolean
dconf_engine_dbus_call_async_func (GBusType                bus_type,
                                   const gchar            *bus_name,
                                   const gchar            *object_path,
                                   const gchar            *interface_name,
                                   const gchar            *method_name,
                                   GVariant               *parameters,
                                   DConfEngineCallHandle  *handle,
                                   GError                **error)
{
  ConnectionState *state;
  GDBusMessage *message;
  DConfGDBusCall *call;
  gboolean success;
  volatile guint *serial_ptr;
  guint my_serial;

  /* Hold the lock to make sure nothing else updates state (e.g. invalidates
   * connection) while we are processing here.
   */
  g_mutex_lock (&dconf_gdbus_lock);

  state = dconf_gdbus_get_connection_state (bus_type, error);

  if (state == NULL)
    {
      g_variant_unref (g_variant_ref_sink (parameters));

      g_mutex_unlock (&dconf_gdbus_lock);

      return FALSE;
    }

  message = g_dbus_message_new_method_call (bus_name, object_path, interface_name, method_name);
  g_dbus_message_set_body (message, parameters);

  /* We need to set the serial in call->serial.  Sometimes we also
   * need to set it in state->waiting_for_serial (in the case that no
   * other items are queued yet).
   *
   * g_dbus_connection_send_message() only has one out_serial parameter
   * so we can only set one of them atomically.  If needed, we elect
   * to set the waiting_for_serial because that is the one that is
   * accessed from the filter function without holding the lock.
   *
   * The serial number in the call structure is only accessed after the
   * lock is acquired which allows us to take our time setting it (for
   * as long as we're still holding the lock).
   *
   * In the case that waiting_for_serial should not be set we just use
   * a local variable and use that to fill call->serial.
   *
   * Also: the queue itself isn't accessed until after the lock is
   * taken, so we can delay adding the call to the queue until we know
   * that the sending of the message was successful.
   */

  if (g_queue_is_empty (&state->queue))
    serial_ptr = &state->waiting_for_serial;
  else
    serial_ptr = &my_serial;

  success = g_dbus_connection_send_message (connection_state_get_connection (state), message,
                                            G_DBUS_SEND_MESSAGE_FLAGS_NONE, serial_ptr, error);

  if (success)
    {
      call = g_slice_new (DConfGDBusCall);

      call->handle = handle;
      call->serial = *serial_ptr;

      g_queue_push_tail (&state->queue, call);
    }

  g_mutex_unlock (&dconf_gdbus_lock);

  g_object_unref (message);

  return success;
}

GVariant *
dconf_engine_dbus_call_sync_func (GBusType             bus_type,
                                  const gchar         *bus_name,
                                  const gchar         *object_path,
                                  const gchar         *interface_name,
                                  const gchar         *method_name,
                                  GVariant            *parameters,
                                  const GVariantType  *reply_type,
                                  GError             **error)
{
  g_autoptr(GDBusConnection) connection = NULL;
  ConnectionState *state;

  /* Hold the lock to make sure nothing else updates state (e.g. invalidates
   * connection) while we are processing here.
   */
  g_mutex_lock (&dconf_gdbus_lock);

  state = dconf_gdbus_get_connection_state (bus_type, error);

  if (state == NULL)
    {
      g_variant_unref (g_variant_ref_sink (parameters));

      g_mutex_unlock (&dconf_gdbus_lock);

      return NULL;
    }

  /* Hold a ref to the connection to make sure the object is still
   * valid even if we get a closed signal on it (in which case the
   * actual call would fail but the object would still be alive)
   * while processing here.
   */
  connection = g_object_ref (connection_state_get_connection (state));

  g_mutex_unlock (&dconf_gdbus_lock);

  return g_dbus_connection_call_sync (connection,
                                      bus_name, object_path, interface_name, method_name, parameters, reply_type,
                                      G_DBUS_CALL_FLAGS_NONE, -1, NULL, error);
}

#ifndef PIC
void
dconf_engine_dbus_init_for_testing (void)
{
}
#endif
