/*
 * Copyright (C) 2022, Red Hat, Inc.
 *
 * This file 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, version 3.0 of the
 * License.
 *
 * This file 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 program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: LGPL-3.0-only
 */

#include "config.h"

#include <gio/gunixfdlist.h>
#include <stdlib.h>
#include <errno.h>

#include "inputcapture.h"
#include "inputcapture-private.h"
#include "portal-private.h"
#include "session-private.h"

/**
 * XdpInputCaptureSession
 *
 * A representation of a long-lived input capture portal interaction.
 *
 * The [class@InputCaptureSession] object is used to represent portal
 * interactions with the input capture desktop portal that extend over
 * multiple portal calls. Usually a caller creates an input capture session,
 * requests the available zones and sets up pointer barriers on those zones
 * before enabling the session.
 *
 * To find available zones, call [method@InputCaptureSession.get_zones].
 * These [class@InputCaptureZone] object represent the accessible desktop area
 * for input capturing. [class@InputCapturePointerBarrier] objects can be set
 * up on these zones to trigger input capture.
 *
 * The [class@InputCaptureSession] wraps a [class@Session] object.
 */

enum {
  SIGNAL_CLOSED,
  SIGNAL_ACTIVATED,
  SIGNAL_DEACTIVATED,
  SIGNAL_ZONES_CHANGED,
  SIGNAL_DISABLED,
  SIGNAL_LAST_SIGNAL
};

static guint signals[SIGNAL_LAST_SIGNAL];

struct _XdpInputCaptureSession
{
  GObject parent_instance;
  XdpSession *parent_session; /* strong ref */

  char *restore_token;
  XdpInputCaptureSessionPersistence persistence;

  GList *zones;

  guint signal_ids[SIGNAL_LAST_SIGNAL];
  guint zone_serial;
  guint zone_set;
};

G_DEFINE_TYPE (XdpInputCaptureSession, xdp_input_capture_session, G_TYPE_OBJECT)

static gboolean
_xdp_input_capture_session_is_valid (XdpInputCaptureSession *session)
{
  return XDP_IS_INPUT_CAPTURE_SESSION (session) && session->parent_session != NULL;
}

static void
parent_session_destroy (gpointer data, GObject *old_session)
{
  XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_SESSION (data);

  g_critical ("XdpSession destroyed before XdpInputCaptureSesssion, you lost count of your session refs");

  session->parent_session = NULL;
}

static void
xdp_input_capture_session_finalize (GObject *object)
{
  XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_SESSION (object);
  XdpSession *parent_session = session->parent_session;

  if (parent_session == NULL)
    {
      g_critical ("XdpSession destroyed before XdpInputCaptureSesssion, you lost count of your session refs");
    }
  else
    {
      for (guint i = 0; i < SIGNAL_LAST_SIGNAL; i++)
        {
          guint signal_id = session->signal_ids[i];
          if (signal_id > 0)
            g_dbus_connection_signal_unsubscribe (parent_session->portal->bus, signal_id);
        }

      g_object_weak_unref (G_OBJECT (parent_session), parent_session_destroy, session);
      session->parent_session->input_capture_session = NULL;
      g_clear_pointer (&session->parent_session, g_object_unref);
    }

  g_list_free_full (g_steal_pointer (&session->zones), g_object_unref);

  g_clear_pointer (&session->restore_token, g_free);

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

static void
xdp_input_capture_session_class_init (XdpInputCaptureSessionClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = xdp_input_capture_session_finalize;

  /**
   * XdpInputCaptureSession::zones-changed:
   * @session: the [class@InputCaptureSession]
   * @options: a GVariant with the signal options
   *
   * Emitted when an InputCapture session's zones have changed. When this
   * signal is emitted, all current zones will have their
   * [property@InputCaptureZone:is-valid] property set to %FALSE and all
   * internal references to those zones have been released. This signal is
   * sent after libportal has fetched the updated zones, a caller should call
   * xdp_input_capture_session_get_zones() to retrieve the new zones.
   */
  signals[SIGNAL_ZONES_CHANGED] =
    g_signal_new ("zones-changed",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                  0,
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 1,
                  G_TYPE_VARIANT);
  /**
   * XdpInputCaptureSession::activated:
   * @session: the [class@InputCaptureSession]
   * @activation_id: the unique activation_id to identify this input capture
   * @options: a GVariant with the signal options
   *
   * Emitted when an InputCapture session activates and sends events. When this
   * signal is emitted, events will appear on the transport layer.
   */
  signals[SIGNAL_ACTIVATED] =
    g_signal_new ("activated",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                  0,
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 2,
                  G_TYPE_UINT,
                  G_TYPE_VARIANT);
  /**
   * XdpInputCaptureSession::deactivated:
   * @session: the [class@InputCaptureSession]
   * @activation_id: the unique activation_id to identify this input capture
   * @options: a GVariant with the signal options
   *
   * Emitted when an InputCapture session deactivates and no longer sends
   * events.
   */
  signals[SIGNAL_DEACTIVATED] =
    g_signal_new ("deactivated",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                  0,
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 2,
                  G_TYPE_UINT,
                  G_TYPE_VARIANT);

  /**
   * XdpInputCaptureSession::disabled:
   * @session: the [class@InputCaptureSession]
   * @options: a GVariant with the signal options
   *
   * Emitted when an InputCapture session is disabled. This signal
   * is emitted when capturing was disabled by the server.
   */
  signals[SIGNAL_DISABLED] =
    g_signal_new ("disabled",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                  0,
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 1,
                  G_TYPE_VARIANT);
}

static void
xdp_input_capture_session_init (XdpInputCaptureSession *session)
{
  session->parent_session = NULL;
  session->zones = NULL;
  session->zone_set = 0;
  for (guint i = 0; i < SIGNAL_LAST_SIGNAL; i++)
    session->signal_ids[i] = 0;
}

/* A request-based method call */
typedef struct {
  XdpPortal *portal;
  char *session_path; /* object path for session */
  GTask *task;
  guint signal_id; /* Request::Response signal */
  char *request_path; /* object path for request */
  gulong cancelled_id; /* signal id for cancelled gobject signal */

  /* CreateSession or Start only */
  XdpParent *parent;
  char *parent_handle;
  XdpInputCapability capabilities;

  /* GetZones only */
  XdpInputCaptureSession *session;

  /* SetPointerBarrier only */
  GList *barriers;

} Call;

static void create_session (Call *call);
static void start_session (Call *call);
static void fetch_version (Call *call);
static void get_zones (Call *call);

static void
call_dispose (void *p)
{
  Call *call = p;

  /* CreateSesssion */
  if (call->parent)
    call->parent->parent_unexport (call->parent);

  g_clear_pointer (&call->parent, xdp_parent_free);
  g_clear_pointer (&call->parent_handle, g_free);

  /* Generic */
  if (call->signal_id)
    g_dbus_connection_signal_unsubscribe (call->portal->bus, g_steal_handle_id (&call->signal_id));

  if (call->task)
    g_clear_signal_handler (&call->cancelled_id, g_task_get_cancellable (call->task));

  g_clear_pointer (&call->request_path, g_free);

  g_clear_object (&call->portal);
  g_clear_object (&call->session);
  g_clear_pointer (&call->session_path, g_free);
  g_clear_object (&call->task);
}

static inline Call *
call_ref (Call *call)
{
  return g_rc_box_acquire (call);
}

static inline void
call_unref (void *call)
{
  g_rc_box_release_full (call, call_dispose);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (Call, call_unref)

static Call *
call_new (XdpPortal *portal,
          XdpInputCaptureSession *session,
          void *source_object,
          GCancellable *cancellable,
          GAsyncReadyCallback callback,
          void *callback_data)
{
  g_autoptr(Call) call = g_rc_box_new0 (Call);

  call->portal = g_object_ref (portal);

  if (session != NULL)
    call->session = g_object_ref (session);

  call->task = g_task_new (G_OBJECT (source_object), cancellable, callback, callback_data);

  /* The GTask holds a reference to the Call for as long as it's subscribed
   * to signals, etc. This is a circular reference which is released by
   * call_dispose(). */
  g_task_set_task_data (call->task, call_ref (call), call_unref);

  return g_steal_pointer (&call);
}

static void
call_returned (GObject *object,
               GAsyncResult *result,
               gpointer data)
{
  /* This releases the ref that was taken when we called a D-Bus method */
  g_autoptr(Call) call = data;
  GError *error = NULL;
  g_autoptr(GVariant) ret = NULL;

  /* If the Call's result has already been resolved, then the only reason
   * we have not yet freed the Call is that the D-Bus method call was still
   * holding a reference to it. Let the g_autoptr free it, but we don't need
   * to do anything else here. */
  if (call->task == NULL)
    return;

  ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
  if (error)
    {
      g_clear_signal_handler (&call->cancelled_id, g_task_get_cancellable (call->task));
      /* Now the Call failed, we can ignore any subsequent method replies */
      g_task_return_error (call->task, error);
      call_dispose (call);
    }
}

static gboolean
handle_matches_session (XdpInputCaptureSession *session, const char *id)
{
  const char *sid = session->parent_session->id;

  return g_str_equal (sid, id);
}

static void
set_zones (XdpInputCaptureSession *session, GVariant *zones, guint zone_set)
{
  GList *list = NULL;
  gsize nzones = g_variant_n_children (zones);

  for (gsize i = 0; i < nzones; i++)
    {
        guint width, height;
        gint x, y;
        XdpInputCaptureZone *z;

        g_variant_get_child (zones, i, "(uuii)", &width, &height, &x, &y);

        z = g_object_new (XDP_TYPE_INPUT_CAPTURE_ZONE,
                          "width", width,
                          "height", height,
                          "x", x,
                          "y", y,
                          "zone-set", zone_set,
                          "is-valid", TRUE,
                          NULL);
        list = g_list_append (list, z);
    }

  g_list_free_full (g_steal_pointer (&session->zones), (GDestroyNotify)_xdp_input_capture_zone_invalidate_and_free);
  session->zones = list;
  session->zone_set = zone_set;
}


static void
prep_call (Call *call, GDBusSignalCallback callback, GVariantBuilder *options)
{
  g_autofree char *token = NULL;

  g_return_if_fail (call->signal_id == 0);

  token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT));
  call->request_path = g_strconcat (REQUEST_PATH_PREFIX, call->portal->sender, "/", token, NULL);
  /* No free-function: the call owns the signal connection and will
   * unsubscribe when destroyed */
  call->signal_id = g_dbus_connection_signal_subscribe (call->portal->bus,
                                                        PORTAL_BUS_NAME,
                                                        REQUEST_INTERFACE,
                                                        "Response",
                                                        call->request_path,
                                                        NULL,
                                                        G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
                                                        callback,
                                                        call,
                                                        NULL);

  g_variant_builder_init (options, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add (options, "{sv}", "handle_token", g_variant_new_string (token));
}

static void
zones_changed_emit_signal (GObject *source_object,
                           GAsyncResult *res,
                           gpointer data)
{
  XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_SESSION (data);
  GVariantBuilder options;

  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add (&options, "{sv}", "zone_set", g_variant_new_uint32 (session->zone_set - 1));

  g_signal_emit (session, signals[SIGNAL_ZONES_CHANGED], 0, g_variant_new ("a{sv}", &options));
}

static void
zones_changed (GDBusConnection *bus,
               const char      *sender_name,
               const char      *object_path,
               const char      *interface_name,
               const char      *signal_name,
               GVariant        *parameters,
               gpointer         data)
{
  XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_SESSION (data);
  XdpPortal *portal = session->parent_session->portal;
  g_autoptr(GVariant) options = NULL;
  const char *handle = NULL;
  g_autoptr(Call) call = NULL;

  g_variant_get(parameters, "(o@a{sv})", &handle, &options);

  if (!handle_matches_session (session, handle))
    return;

  /* Zones have changed, but let's fetch the new zones before we notify the
   * caller so they're already available by the time they get notified */
  call = call_new (portal, session, portal, NULL, zones_changed_emit_signal, session);

  get_zones (call);
}

static void
activated (GDBusConnection *bus,
           const char      *sender_name,
           const char      *object_path,
           const char      *interface_name,
           const char      *signal_name,
           GVariant        *parameters,
           gpointer         data)
{
  XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_SESSION (data);
  g_autoptr(GVariant) options = NULL;
  guint32 activation_id = 0;
  const char *handle = NULL;

  g_variant_get (parameters, "(o@a{sv})", &handle, &options);

  /* FIXME: we should remove the activation_id from options, but ... meh? */
  if (!g_variant_lookup (options, "activation_id", "u", &activation_id))
    g_warning ("Portal bug: activation_id missing from Activated signal");

  if (!handle_matches_session (session, handle))
    return;

  g_signal_emit (session, signals[SIGNAL_ACTIVATED], 0, activation_id, options);
}

static void
deactivated (GDBusConnection *bus,
             const char        *sender_name,
             const char        *object_path,
             const char        *interface_name,
             const char        *signal_name,
             GVariant          *parameters,
             gpointer           data)
{
  XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_SESSION (data);
  g_autoptr(GVariant) options = NULL;
  guint32 activation_id = 0;
  const char *handle = NULL;

  g_variant_get(parameters, "(o@a{sv})", &handle, &options);

  /* FIXME: we should remove the activation_id from options, but ... meh? */
  if (!g_variant_lookup (options, "activation_id", "u", &activation_id))
    g_warning ("Portal bug: activation_id missing from Deactivated signal");

  if (!handle_matches_session (session, handle))
    return;

  g_signal_emit (session, signals[SIGNAL_DEACTIVATED], 0, activation_id, options);
}

static void
disabled (GDBusConnection *bus,
          const char      *sender_name,
          const char      *object_path,
          const char      *interface_name,
          const char      *signal_name,
          GVariant        *parameters,
          gpointer         data)
{
  XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_SESSION (data);
  g_autoptr(GVariant) options = NULL;
  const char *handle = NULL;

  g_variant_get(parameters, "(o@a{sv})", &handle, &options);

  if (!handle_matches_session (session, handle))
      return;

  g_signal_emit (session, signals[SIGNAL_DISABLED], 0, options);
}

static XdpInputCaptureSession *
_xdp_input_capture_session_new (XdpPortal *portal, const char *session_path)
{
  g_autoptr(XdpSession) parent_session = _xdp_session_new (portal, session_path, XDP_SESSION_INPUT_CAPTURE);
  g_autoptr(XdpInputCaptureSession) session = g_object_new (XDP_TYPE_INPUT_CAPTURE_SESSION, NULL);

  parent_session->input_capture_session = session; /* weak ref */
  g_object_weak_ref (G_OBJECT (parent_session), parent_session_destroy, session);
  session->parent_session = g_object_ref(parent_session); /* strong ref */

  session->signal_ids[SIGNAL_ZONES_CHANGED] =
    g_dbus_connection_signal_subscribe (portal->bus,
                                        PORTAL_BUS_NAME,
                                        "org.freedesktop.portal.InputCapture",
                                        "ZonesChanged",
                                        PORTAL_OBJECT_PATH,
                                        NULL,
                                        G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
                                        zones_changed,
                                        session,
                                        NULL);

  session->signal_ids[SIGNAL_ACTIVATED] =
    g_dbus_connection_signal_subscribe (portal->bus,
                                        PORTAL_BUS_NAME,
                                        "org.freedesktop.portal.InputCapture",
                                        "Activated",
                                        PORTAL_OBJECT_PATH,
                                        NULL,
                                        G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
                                        activated,
                                        session,
                                        NULL);

  session->signal_ids[SIGNAL_DEACTIVATED] =
    g_dbus_connection_signal_subscribe (portal->bus,
                                        PORTAL_BUS_NAME,
                                        "org.freedesktop.portal.InputCapture",
                                        "Deactivated",
                                        PORTAL_OBJECT_PATH,
                                        NULL,
                                        G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
                                        deactivated,
                                        session,
                                        NULL);

  session->signal_ids[SIGNAL_DISABLED] =
    g_dbus_connection_signal_subscribe (portal->bus,
                                        PORTAL_BUS_NAME,
                                        "org.freedesktop.portal.InputCapture",
                                        "Disabled",
                                        PORTAL_OBJECT_PATH,
                                        NULL,
                                        G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
                                        disabled,
                                        session,
                                        NULL);

  return g_object_ref(session);
}

static void
get_zones_done (GDBusConnection *bus,
                const char *sender_name,
                const char *object_path,
                const char *interface_name,
                const char *signal_name,
                GVariant *parameters,
                gpointer data)
{
  Call *call = data;
  guint32 response;
  g_autoptr(GVariant) ret = NULL;

  /* If the Call has already been disposed, we should have unsubscribed
   * from the Response signal at that time, so we shouldn't get here */
  g_return_if_fail (G_IS_TASK (call->task));

  g_variant_get (parameters, "(u@a{sv})", &response, &ret);

  if (response != 0)
    g_clear_signal_handler (&call->cancelled_id, g_task_get_cancellable (call->task));

  if (response == 0)
    {
      GVariant *zones = NULL;
      guint32 zone_set;
      XdpInputCaptureSession *session = call->session;

      g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id);
      call->signal_id = 0;

      if (session != NULL)
        g_object_ref (session);
      else
        session = _xdp_input_capture_session_new (call->portal, call->session_path);

      if (g_variant_lookup (ret, "zone_set", "u", &zone_set) &&
          g_variant_lookup (ret, "zones", "@a(uuii)", &zones))
        {
          set_zones (session, zones, zone_set);

          /* Only when zones are fetched for the legacy CreateSession() should we return
           * the new session. */
          if (call->session)
            g_task_return_boolean (call->task, TRUE);
          else
            g_task_return_pointer (call->task, g_steal_pointer (&session), g_object_unref);
        }
      else
        {
          g_warning("Faulty portal implementation, missing GetZone's zone_set or zones");
          response = 2;
        }
    }

  if (response == 1)
    g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "InputCapture GetZones() canceled");
  else if (response == 2)
    g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "InputCapture GetZones() failed");
  else if (response != 0)
    g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "InputCapture GetZones() unknown response code %d", response);

  /* If the Call succeeded or failed, we can ignore any subsequent method replies */
  if (g_task_get_completed (call->task))
    call_dispose (call);
}

static void
get_zones (Call *call)
{
  GVariantBuilder options;
  const char *session_id;

  /* May be called after CreateSession before we have an XdpInputCaptureSession, or by the
   * ZoneChanged signal when we do have a session */
  session_id = call->session ? call->session->parent_session->id : call->session_path;

  prep_call (call, get_zones_done, &options);
  g_dbus_connection_call (call->portal->bus,
                          PORTAL_BUS_NAME,
                          PORTAL_OBJECT_PATH,
                          "org.freedesktop.portal.InputCapture",
                          "GetZones",
                          g_variant_new ("(oa{sv})", session_id, &options),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          g_task_get_cancellable (call->task),
                          call_returned,
                          call_ref (call));
}

static void
session_created (GDBusConnection *bus,
                 const char *sender_name,
                 const char *object_path,
                 const char *interface_name,
                 const char *signal_name,
                 GVariant *parameters,
                 gpointer data)
{
  Call *call = data;
  guint32 response;
  g_autoptr(GVariant) ret = NULL;

  /* If the Call has already been disposed, we should have unsubscribed
   * from the Response signal at that time, so we shouldn't get here */
  g_return_if_fail (G_IS_TASK (call->task));

  g_variant_get (parameters, "(u@a{sv})", &response, &ret);

  if (response != 0)
    g_clear_signal_handler (&call->cancelled_id, g_task_get_cancellable (call->task));

  if (response == 0)
    {
      g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id);
      call->signal_id = 0;

      if (!g_variant_lookup (ret, "session_handle", "o", &call->session_path))
        {
          g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "CreateSession failed to return a session handle");
          response = 2;
        }
      else
        get_zones (call);
    }
  else if (response == 1)
    g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "CreateSession canceled");
  else if (response == 2)
    g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "CreateSession failed");
  else if (response != 0)
    g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "InputCapture CreateSession() unknown response code %d", response);

  /* If the Call failed, we can ignore any subsequent method replies */
  if (g_task_get_completed (call->task))
    call_dispose (call);
}

static void
call_cancelled_cb (GCancellable *cancellable,
                   gpointer data)
{
  Call *call = data;

  g_dbus_connection_call (call->portal->bus,
                          PORTAL_BUS_NAME,
                          call->request_path,
                          REQUEST_INTERFACE,
                          "Close",
                          NULL,
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          NULL, NULL, NULL);
}

static void
parent_exported (XdpParent *parent,
                 const char *handle,
                 gpointer data)
{
  Call *call = data;
  call->parent_handle = g_strdup (handle);

  if (call->session)
    start_session (call);
  else
    create_session (call);
}

static void
create_session2_returned (GObject *object,
                          GAsyncResult *result,
                          gpointer data)
{
  /* This releases the ref that was taken when we called a D-Bus method */
  g_autoptr(Call) call = data;
  GError *error = NULL;
  g_autoptr(GVariant) ret = NULL;
  g_autoptr(GVariant) results = NULL;
  g_autoptr(XdpInputCaptureSession) session = NULL;

  ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
  if (error)
    {
      g_clear_signal_handler (&call->cancelled_id, g_task_get_cancellable (call->task));
      /* Now the Call failed, we can ignore any subsequent method replies */
      g_task_return_error (call->task, error);
      call_dispose (call);
      return;
    }

  g_variant_get (ret, "(@a{sv})", &results);

  g_variant_lookup (results, "session_handle", "o", &call->session_path);
  if (!call->session_path)
    {
      g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED,
                               "Missing session handle");
      call_dispose (call);
      return;
    }

  session = _xdp_input_capture_session_new (call->portal, call->session_path);
  g_task_return_pointer (call->task, g_steal_pointer (&session), g_object_unref);
  call_dispose (call);
}

static void
create_session2 (Call *call)
{
  GVariantBuilder options;
  g_autofree char *session_token = NULL;
  GCancellable *cancellable;

  cancellable = g_task_get_cancellable (call->task);
  if (cancellable)
    call->cancelled_id = g_signal_connect (cancellable, "cancelled",
                                           G_CALLBACK (call_cancelled_cb), call);

  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);

  session_token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT));
  g_variant_builder_add (&options, "{sv}", "session_handle_token",
                         g_variant_new_string (session_token));

  g_dbus_connection_call (call->portal->bus,
                          PORTAL_BUS_NAME,
                          PORTAL_OBJECT_PATH,
                          "org.freedesktop.portal.InputCapture",
                          "CreateSession2",
                          g_variant_new ("(a{sv})", &options),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          cancellable,
                          create_session2_returned,
                          call_ref (call));
}

static void
create_session (Call *call)
{
  GVariantBuilder options;
  g_autofree char *session_token = NULL;
  GCancellable *cancellable;

  if (call->parent_handle == NULL)
    {
      call->parent->parent_export (call->parent, parent_exported, call);
      return;
    }

  cancellable = g_task_get_cancellable (call->task);
  if (cancellable)
    call->cancelled_id = g_signal_connect (cancellable, "cancelled", G_CALLBACK (call_cancelled_cb), call);

  session_token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT));

  prep_call (call, session_created, &options);
  g_variant_builder_add (&options, "{sv}", "session_handle_token", g_variant_new_string (session_token));
  g_variant_builder_add (&options, "{sv}", "capabilities", g_variant_new_uint32 (call->capabilities));

  g_dbus_connection_call (call->portal->bus,
                          PORTAL_BUS_NAME,
                          PORTAL_OBJECT_PATH,
                          "org.freedesktop.portal.InputCapture",
                          "CreateSession",
                          g_variant_new ("(sa{sv})", call->parent_handle, &options),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          cancellable,
                          call_returned,
                          call_ref (call));
}

/**
 * xdp_portal_create_input_capture_session2:
 * @portal: a [class@Portal]
 * @cancellable: (nullable): optional [class@Gio.Cancellable]
 * @callback: (scope async): a callback to call when the request is done
 * @data: data to pass to @callback
 *
 * Creates an inactive session for input capture.
 *
 * When the request is done, @callback will be called. You can then
 * call [method@Portal.create_input_capture_session2_finish] to get the results.
 */
void
xdp_portal_create_input_capture_session2 (XdpPortal *portal,
                                          GCancellable *cancellable,
                                          GAsyncReadyCallback callback,
                                          gpointer data)
{
  g_autoptr(Call) call = NULL;

  g_return_if_fail (XDP_IS_PORTAL (portal));

  call = call_new (portal, NULL, portal, cancellable, callback, data);

  create_session2 (call);
}

/**
 * xdp_portal_create_input_capture_session2_finish:
 * @portal: a [class@Portal]
 * @result: a [iface@Gio.AsyncResult]
 * @error: return location for an error
 *
 * Finishes the InputCapture CreateSession2 method call, and returns a
 * [class@InputCaptureSession]. To get to the [class@Session] within use
 * xdp_input_capture_session_get_session().
 *
 * The created session is inactive, and must be started with
 * [method.InputCaptureSession.start].
 *
 * Returns: (transfer full): a [class@InputCaptureSession]
 */
XdpInputCaptureSession *
xdp_portal_create_input_capture_session2_finish (XdpPortal *portal,
                                                 GAsyncResult *result,
                                                 GError **error)
{
  g_return_val_if_fail (XDP_IS_PORTAL (portal), NULL);
  g_return_val_if_fail (g_task_is_valid (result, portal), NULL);

  return g_task_propagate_pointer (G_TASK (result), error);
}

/**
 * xdp_portal_create_input_capture_session2_sync:
 * @portal: a [class@Portal]
 * @cancellable: (nullable): optional [class@Gio.Cancellable]
 * @error: return location for an error
 *
 * Creates an inactive session for input capture.
 *
 * Returns: (transfer full): a [class@InputCaptureSession]
 */
XdpInputCaptureSession *
xdp_portal_create_input_capture_session2_sync (XdpPortal     *portal,
                                               GCancellable  *cancellable,
                                               GError       **error)
{
  GVariantBuilder options;
  g_autofree char *session_token = NULL;
  g_autofree char *session_path = NULL;
  g_autoptr(GVariant) ret = NULL;
  g_autoptr(GVariant) results = NULL;

  session_token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT));

  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add (&options, "{sv}", "session_handle_token", g_variant_new_string (session_token));

  ret = g_dbus_connection_call_sync (portal->bus,
                                     PORTAL_BUS_NAME,
                                     PORTAL_OBJECT_PATH,
                                     "org.freedesktop.portal.InputCapture",
                                     "CreateSession2",
                                     g_variant_new ("(a{sv})", &options),
                                     NULL,
                                     G_DBUS_CALL_FLAGS_NONE,
                                     -1,
                                     cancellable,
                                     error);
  if (!ret)
    return NULL;

  g_variant_get (ret, "(@a{sv})", &results);
  g_variant_lookup (results, "session_handle", "o", &session_path);
  if (!session_path)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Missing session handle");
      return NULL;
    }

  return _xdp_input_capture_session_new (portal, session_path);
}

static void
session_started (GDBusConnection *bus,
                 const char *sender_name,
                 const char *object_path,
                 const char *interface_name,
                 const char *signal_name,
                 GVariant *parameters,
                 gpointer data)
{
  Call *call = data;
  guint32 response;
  g_autoptr(GVariant) ret = NULL;

  /* If the Call has already been disposed, we should have unsubscribed
   * from the Response signal at that time, so we shouldn't get here */
  g_return_if_fail (G_IS_TASK (call->task));

  g_variant_get (parameters, "(u@a{sv})", &response, &ret);

  if (response != 0)
    g_clear_signal_handler (&call->cancelled_id, g_task_get_cancellable (call->task));

  if (response == 0)
    {
      g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id);
      call->signal_id = 0;

      g_variant_lookup (ret, "clipboard_enabled", "b",
                        &call->session->parent_session->is_clipboard_enabled);
      g_clear_pointer (&call->session->restore_token, g_free);
      g_variant_lookup (ret, "restore_token", "s",
                        &call->session->restore_token);
      get_zones (call);
    }
  else if (response == 1)
    g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Start canceled");
  else if (response == 2)
    g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "Start failed");
  else
    g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "InputCapture Start() unknown response code %d", response);

  /* If the Call failed, we can ignore any subsequent method replies */
  if (g_task_get_completed (call->task))
    call_dispose (call);
}

static void
fetch_version_returned (GObject *object,
                        GAsyncResult *result,
                        gpointer data)
{
  g_autoptr(Call) call = data;
  g_autoptr(GVariant) version_variant = NULL;
  g_autoptr(GVariant) ret = NULL;
  g_autoptr(GError) error = NULL;

  ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
  if (error)
    {
      g_task_return_error (call->task, g_steal_pointer (&error));
      call_dispose (call);
      return;
    }

  g_variant_get_child (ret, 0, "v", &version_variant);
  call->portal->input_capture_interface_version = g_variant_get_uint32 (version_variant);

  start_session (call);
}

static void
fetch_version (Call *call)
{
  g_dbus_connection_call (call->portal->bus,
                          PORTAL_BUS_NAME,
                          PORTAL_OBJECT_PATH,
                          "org.freedesktop.DBus.Properties",
                          "Get",
                          g_variant_new ("(ss)", "org.freedesktop.portal.InputCapture", "version"),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          g_task_get_cancellable (call->task),
                          fetch_version_returned,
                          call_ref (call));
}

static void
start_session (Call *call)
{
  GVariantBuilder options;

  if (call->parent_handle == NULL)
    {
      call->parent->parent_export (call->parent, parent_exported, call);
      return;
    }

  prep_call (call, session_started, &options);

  g_variant_builder_add (&options, "{sv}", "capabilities",
                         g_variant_new_uint32 (call->capabilities));

  if (call->session->persistence != XDP_INPUT_CAPTURE_SESSION_PERSISTENCE_NONE)
    g_variant_builder_add (&options, "{sv}", "persist_mode",
                           g_variant_new_uint32 (call->session->persistence));

  if (call->portal->input_capture_interface_version >= 2 && call->session->restore_token)
    g_variant_builder_add (&options, "{sv}", "restore_token",
                           g_variant_new_string (call->session->restore_token));

  g_dbus_connection_call (call->portal->bus,
                          PORTAL_BUS_NAME,
                          PORTAL_OBJECT_PATH,
                          "org.freedesktop.portal.InputCapture",
                          "Start",
                          g_variant_new ("(osa{sv})",
                                         call->session->parent_session->id,
                                         call->parent_handle,
                                         &options),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          g_task_get_cancellable (call->task),
                          call_returned,
                          call_ref (call));
}

/**
 * xdp_input_capture_session_start:
 * @session: a [class@XdpInputCaptureSession]
 * @parent: (nullable): parent window information
 * @capabilities: which kinds of capabilities to request
 * @cancellable: (nullable): optional [class@Gio.Cancellable]
 * @callback: (scope async): a callback to call when the request is done
 * @data: data to pass to @callback
 *
 * Start the input capture session.
 *
 * When the request is done, @callback will be called. You can then
 * call [method@InputCaptureSession.start_finish] to get the results.
 */
void
xdp_input_capture_session_start (XdpInputCaptureSession *session,
                                 XdpParent              *parent,
                                 XdpInputCapability      capabilities,
                                 GCancellable           *cancellable,
                                 GAsyncReadyCallback     callback,
                                 gpointer                data)
{
  g_autoptr(Call) call = NULL;
  XdpPortal *portal;

  g_return_if_fail (_xdp_input_capture_session_is_valid (session));

  portal = session->parent_session->portal;

  call = call_new (portal, session, session, cancellable, callback, data);
  call->capabilities = capabilities;

  if (parent)
    call->parent = xdp_parent_copy (parent);
  else
    call->parent_handle = g_strdup ("");

  if (portal->input_capture_interface_version == 0)
    fetch_version (call);
  else
    start_session (call);
}

/**
 * xdp_input_capture_session_start_finish:
 * @session: a [class@InputCaptureSession]
 * @result: a [iface@Gio.AsyncResult]
 * @error: return location for an error
 *
 * Finishes the InputCapture Start request, and returns TRUE if it was
 * successful.
 *
 * Returns: TRUE on success.
 */
gboolean
xdp_input_capture_session_start_finish (XdpInputCaptureSession  *session,
                                        GAsyncResult            *result,
                                        GError                 **error)
{
  g_return_val_if_fail (XDP_IS_INPUT_CAPTURE_SESSION (session), FALSE);
  g_return_val_if_fail (g_task_is_valid (result, session), FALSE);

  return g_task_propagate_boolean (G_TASK (result), error);
}

/**
 * xdp_portal_create_input_capture_session:
 * @portal: a [class@Portal]
 * @parent: (nullable): parent window information
 * @capabilities: which kinds of capabilities to request
 * @cancellable: (nullable): optional [class@Gio.Cancellable]
 * @callback: (scope async): a callback to call when the request is done
 * @data: data to pass to @callback
 *
 * Creates a session for input capture
 *
 * When the request is done, @callback will be called. You can then
 * call [method@Portal.create_input_capture_session_finish] to get the results.
 */
void
xdp_portal_create_input_capture_session (XdpPortal *portal,
                                         XdpParent *parent,
                                         XdpInputCapability capabilities,
                                         GCancellable *cancellable,
                                         GAsyncReadyCallback callback,
                                         gpointer data)
{
  g_autoptr(Call) call = NULL;

  g_return_if_fail (XDP_IS_PORTAL (portal));

  call = call_new (portal, NULL, portal, cancellable, callback, data);

  if (parent)
    call->parent = xdp_parent_copy (parent);
  else
    call->parent_handle = g_strdup ("");

  call->capabilities = capabilities;

  create_session (call);
}

/**
 * xdp_portal_create_input_capture_session_finish:
 * @portal: a [class@Portal]
 * @result: a [iface@Gio.AsyncResult]
 * @error: return location for an error
 *
 * Finishes the InputCapture CreateSession request, and returns a
 * [class@InputCaptureSession]. To get to the [class@Session] within use
 * xdp_input_capture_session_get_session().
 *
 * Returns: (transfer full): a [class@InputCaptureSession]
 */
XdpInputCaptureSession *
xdp_portal_create_input_capture_session_finish (XdpPortal *portal,
                                                GAsyncResult *result,
                                                GError **error)
{
  XdpInputCaptureSession *session;

  g_return_val_if_fail (XDP_IS_PORTAL (portal), NULL);
  g_return_val_if_fail (g_task_is_valid (result, portal), NULL);

  session = g_task_propagate_pointer (G_TASK (result), error);

  if (session)
    return session;
  else
    return NULL;
}

/**
 * xdp_input_capture_session_get_session:
 * @session: a [class@XdpInputCaptureSession]
 *
 * Return the [class@XdpSession] for this InputCapture session.
 *
 * Returns: (transfer none): a [class@Session] object
 */
XdpSession *
xdp_input_capture_session_get_session (XdpInputCaptureSession *session)
{
  return session->parent_session;
}

/**
 * xdp_input_capture_session_get_zones:
 * @session: a [class@InputCaptureSession]
 *
 * Obtains the current set of [class@InputCaptureZone] objects.
 *
 * The returned object is valid until the zones are invalidated by the
 * [signal@InputCaptureSession::zones-changed] signal.
 *
 * Unless the session is active, this function returns `NULL`.
 *
 * Returns: (element-type XdpInputCaptureZone) (transfer none): the available
 * zones. The caller must keep a reference to the list or the elements if used
 * outside the immediate scope.
 */
GList *
xdp_input_capture_session_get_zones (XdpInputCaptureSession *session)
{
  g_return_val_if_fail (_xdp_input_capture_session_is_valid (session), NULL);

  return session->zones;
}

/**
 * xdp_input_capture_session_connect_to_eis:
 * @session: a [class@InputCaptureSession]
 * @error: return location for a #GError pointer
 *
 * Connect this session to an EIS implementation and return the fd.
 * This fd can be passed into ei_setup_backend_fd(). See the libei
 * documentation for details.
 *
 * This is a sync DBus invocation.
 *
 * Returns: a socket to the EIS implementation for this input capture
 * session or a negative errno on failure.
 */
int
xdp_input_capture_session_connect_to_eis (XdpInputCaptureSession  *session,
                                          GError                 **error)
{
  GVariantBuilder options;
  g_autoptr(GVariant) ret = NULL;
  g_autoptr(GUnixFDList) fd_list = NULL;
  int fd_out;
  XdpPortal *portal;
  XdpSession *parent_session = session->parent_session;

  if (!_xdp_input_capture_session_is_valid (session))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, "Session is not an InputCapture session");
      return -1;
    }

  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);

  portal = parent_session->portal;
  ret = g_dbus_connection_call_with_unix_fd_list_sync (portal->bus,
                                                       PORTAL_BUS_NAME,
                                                       PORTAL_OBJECT_PATH,
                                                       "org.freedesktop.portal.InputCapture",
                                                       "ConnectToEIS",
                                                       g_variant_new ("(oa{sv})",
                                                                      parent_session->id,
                                                                      &options),
                                                       NULL,
                                                       G_DBUS_CALL_FLAGS_NONE,
                                                       -1,
                                                       NULL,
                                                       &fd_list,
                                                       NULL,
                                                       error);

  if (!ret)
    return -1;

  g_variant_get (ret, "(h)", &fd_out);

  return g_unix_fd_list_get (fd_list, fd_out, NULL);
}

static void
free_barrier_list (GList *list)
{
  g_list_free_full (list, g_object_unref);
}

static void
set_pointer_barriers_done (GDBusConnection *bus,
                           const char *sender_name,
                           const char *object_path,
                           const char *interface_name,
                           const char *signal_name,
                           GVariant *parameters,
                           gpointer data)
{
  Call *call = data;
  guint32 response;
  g_autoptr(GVariant) ret = NULL;
  GVariant *failed = NULL;
  GList *failed_list = NULL;

  /* If the Call has already been disposed, we should have unsubscribed
   * from the Response signal at that time, so we shouldn't get here */
  g_return_if_fail (G_IS_TASK (call->task));

  g_variant_get (parameters, "(u@a{sv})", &response, &ret);

  if (g_variant_lookup (ret, "failed_barriers", "@au", &failed))
    {
      const guint *failed_barriers = NULL;
      gsize n_elements;
      GList *it = call->barriers;

      failed_barriers = g_variant_get_fixed_array (failed, &n_elements, sizeof (guint32));

      while (it)
        {
          XdpInputCapturePointerBarrier *b = it->data;
          gboolean is_failed = FALSE;

          for (gsize i = 0; !is_failed && i < n_elements; i++)
            is_failed = _xdp_input_capture_pointer_barrier_get_id (b) == failed_barriers[i];

          _xdp_input_capture_pointer_barrier_set_is_active (b, !is_failed);

          if (is_failed)
            failed_list = g_list_append (failed_list, g_object_ref(b));

          it = it->next;
        }
    }

  /* all failed barriers have an extra ref in failed_list, so we can unref all barriers
     in our original list */
  free_barrier_list (call->barriers);
  call->barriers = NULL;
  g_task_return_pointer (call->task, failed_list,  (GDestroyNotify)free_barrier_list);
  /* Now the Call succeeded, we can ignore any subsequent method replies */
  call_dispose (call);
}

static void
convert_barrier (gpointer data, gpointer user_data)
{
  XdpInputCapturePointerBarrier *barrier = data;
  GVariantBuilder *builder = user_data;
  GVariantBuilder dict;
  int id, x1, x2, y1, y2;

  g_object_get (barrier, "id", &id, "x1", &x1, "x2", &x2, "y1", &y1, "y2", &y2, NULL);

  g_variant_builder_init (&dict, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add (&dict, "{sv}", "barrier_id", g_variant_new_uint32 (id));
  g_variant_builder_add (&dict, "{sv}", "position",
                         g_variant_new("(iiii)", x1, y1, x2, y2));
  g_variant_builder_add (builder, "a{sv}", &dict);
}

static void
set_pointer_barriers (Call *call)
{
  GVariantBuilder options;
  GVariantBuilder barriers;
  g_autoptr(GVariantType) vtype = NULL;

  prep_call (call, set_pointer_barriers_done, &options);

  vtype = g_variant_type_new ("aa{sv}");

  g_variant_builder_init (&barriers, vtype);
  g_list_foreach (call->barriers, convert_barrier, &barriers);

  g_dbus_connection_call (call->portal->bus,
                          PORTAL_BUS_NAME,
                          PORTAL_OBJECT_PATH,
                          "org.freedesktop.portal.InputCapture",
                          "SetPointerBarriers",
                          g_variant_new ("(oa{sv}aa{sv}u)",
                                         call->session->parent_session->id,
                                         &options,
                                         &barriers,
                                         call->session->zone_set),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          g_task_get_cancellable (call->task),
                          call_returned,
                          call_ref (call));
}

static void
gobject_ref_wrapper (gpointer data,
                     gpointer user_data)
{
  g_object_ref (G_OBJECT (data));
}

/**
 * xdp_input_capture_session_set_pointer_barriers:
 * @session: a [class@InputCaptureSession]
 * @barriers: (element-type XdpInputCapturePointerBarrier) (transfer container): the pointer barriers to apply
 *
 * Sets the pointer barriers for this session. When the request is done,
 * @callback will be called. You can then call
 * [method@InputCaptureSession.set_pointer_barriers_finish] to
 * get the results. The result of this request is the list of pointer barriers
 * that failed to apply - barriers not present in the returned list are active.
 *
 * Once the pointer barrier is
 * applied (i.e. the reply to the DBus Request has been received), the
 * the [property@InputCapturePointerBarrier:is-active] property is changed on
 * that barrier. Failed barriers have the property set to a %FALSE value.
 */
void
xdp_input_capture_session_set_pointer_barriers (XdpInputCaptureSession *session,
                                                GList                  *barriers,
                                                GCancellable           *cancellable,
                                                GAsyncReadyCallback     callback,
                                                gpointer                data)
{
  g_autoptr(Call) call = NULL;
  XdpPortal *portal;

  g_return_if_fail (_xdp_input_capture_session_is_valid (session));
  g_return_if_fail (barriers != NULL);

  portal = session->parent_session->portal;

  /* The list is ours, but we ref each object so we can create the list for the
   * returned barriers during _finish*/
  g_list_foreach (barriers, gobject_ref_wrapper, NULL);

  call = call_new (portal, session, session, cancellable, callback, data);
  call->barriers = barriers;

  set_pointer_barriers (call);
}

/**
 * xdp_input_capture_session_set_pointer_barriers_finish:
 * @session: a [class@InputCaptureSession]
 * @result: a [iface@Gio.AsyncResult]
 * @error: return location for an error
 *
 * Finishes the set-pointer-barriers request, and returns a GList with the pointer
 * barriers that failed to apply and should be cleaned up by the caller.
 *
 * Returns: (element-type XdpInputCapturePointerBarrier) (transfer full): a list of failed pointer barriers
 */

GList *
xdp_input_capture_session_set_pointer_barriers_finish (XdpInputCaptureSession  *session,
                                                       GAsyncResult            *result,
                                                       GError                 **error)
{
  g_return_val_if_fail (_xdp_input_capture_session_is_valid (session), NULL);
  g_return_val_if_fail (g_task_is_valid (result, session), NULL);

  return g_task_propagate_pointer (G_TASK (result), error);
}

/**
 * xdp_input_capture_session_enable:
 * @session: a [class@InputCaptureSession]
 *
 * Enables this input capture session. In the future, this client may receive
 * input events.
 */
void
xdp_input_capture_session_enable (XdpInputCaptureSession *session)
{
  XdpPortal *portal;
  GVariantBuilder options;

  g_return_if_fail (_xdp_input_capture_session_is_valid (session));

  portal = session->parent_session->portal;

  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);

  g_dbus_connection_call  (portal->bus,
                           PORTAL_BUS_NAME,
                           PORTAL_OBJECT_PATH,
                           "org.freedesktop.portal.InputCapture",
                           "Enable",
                           g_variant_new ("(oa{sv})",
                                          session->parent_session->id,
                                          &options),
                           NULL,
                           G_DBUS_CALL_FLAGS_NONE,
                           1,
                           NULL,
                           NULL,
                           NULL);
}

/**
 * xdp_input_capture_session_disable:
 * @session: a [class@InputCaptureSession]
 *
 * Disables this input capture session.
 */
void
xdp_input_capture_session_disable (XdpInputCaptureSession *session)
{
  XdpPortal *portal;
  GVariantBuilder options;

  g_return_if_fail (_xdp_input_capture_session_is_valid (session));

  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);

  portal = session->parent_session->portal;
  g_dbus_connection_call (portal->bus,
                          PORTAL_BUS_NAME,
                          PORTAL_OBJECT_PATH,
                          "org.freedesktop.portal.InputCapture",
                          "Disable",
                          g_variant_new ("(oa{sv})",
                                         session->parent_session->id,
                                         &options),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          NULL,
                          NULL,
                          NULL);
}

static void
release_session (XdpInputCaptureSession *session,
                 guint                   activation_id,
                 gboolean                with_position,
                 gdouble                 x,
                 gdouble                 y)
{
  XdpPortal *portal;
  GVariantBuilder options;

  g_return_if_fail (_xdp_input_capture_session_is_valid (session));

  g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add (&options, "{sv}", "activation_id", g_variant_new_uint32 (activation_id));

  if (with_position)
    {
      g_variant_builder_add (&options,
                             "{sv}",
                             "cursor_position",
                             g_variant_new ("(dd)", x, y));
    }

  portal = session->parent_session->portal;
  g_dbus_connection_call (portal->bus,
                          PORTAL_BUS_NAME,
                          PORTAL_OBJECT_PATH,
                          "org.freedesktop.portal.InputCapture",
                          "Release",
                          g_variant_new ("(oa{sv})",
                                         session->parent_session->id,
                                        &options),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          NULL,
                          NULL,
                          NULL);
}

/**
 * xdp_input_capture_session_release:
 * @session: a [class@InputCaptureSession]
 *
 * Releases this input capture session without a suggested cursor position.
 */
void
xdp_input_capture_session_release (XdpInputCaptureSession *session,
                                   guint                   activation_id)
{
  g_return_if_fail (_xdp_input_capture_session_is_valid (session));

  release_session (session, activation_id, FALSE, 0, 0);
}

/**
 * xdp_input_capture_session_release_at:
 * @session: a [class@InputCaptureSession]
 * @cursor_x_position: the suggested cursor x position once capture has been released
 * @cursor_y_position: the suggested cursor y position once capture has been released
 *
 * Releases this input capture session with a suggested cursor position.
 * Note that the implementation is not required to honour this position.
 */
void
xdp_input_capture_session_release_at (XdpInputCaptureSession *session,
                                      guint                   activation_id,
                                      gdouble                 cursor_x_position,
                                      gdouble                 cursor_y_position)
{
  g_return_if_fail (_xdp_input_capture_session_is_valid (session));

  release_session (session, activation_id, TRUE, cursor_x_position, cursor_y_position);
}

static void
get_input_capture_interface_version_returned (GObject *object,
                                              GAsyncResult *result,
                                              gpointer data)
{
  g_autoptr(GTask) task = G_TASK (data);
  g_autoptr(GError) error = NULL;
  g_autoptr(GVariant) version_variant = NULL;
  g_autoptr(GVariant) ret = NULL;
  XdpPortal *portal;

  ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
  if (error)
    {
      g_task_return_error (task, g_steal_pointer (&error));
      return;
    }

  portal = g_task_get_source_object (task);

  g_variant_get_child (ret, 0, "v", &version_variant);
  portal->input_capture_interface_version = g_variant_get_uint32 (version_variant);

  g_task_return_int (task, portal->input_capture_interface_version);
}

/**
 * xdp_input_capture_session_get_restore_token:
 * @session: a [class@InputCaptureSession]
 *
 * Returns the restore token for this session or NULL if none exists. This token
 * can be passed to [method@InputCaptureSession.set_restore_token] for a future
 * session to restore this session, possibly skipping interactive permission
 * dialogs.
 *
 * This method only returns a token for a session created with
 * [method@Portal.create_input_capture_session2] and only once
 * [method@InputCaptureSession.start_finish] has completed.
 *
 * The token may change with every session.
 *
 * Returns: the restore token or NULL
 */
const char *
xdp_input_capture_session_get_restore_token (XdpInputCaptureSession *session)
{
  return session->restore_token;
}

/**
 * xdp_input_capture_session_set_restore_token:
 * @session: a [class@InputCaptureSession]
 * @restore_token: a restore token from a previous session
 *
 * Sets the restore token for the session about to be started. This instructs
 * the portal to restore the previous session identified by this token.
 *
 * This method can only be called for a session created with
 * [method@Portal.create_input_capture_session2] and only before
 * [method@InputCaptureSession.start] has been called. It has no effect
 * otherwise.
 *
 * The restore token for the current session can be obtained with
 * [method@InputCaptureSession.get_restore_token].
 */
void
xdp_input_capture_session_set_restore_token (XdpInputCaptureSession *session,
                                             const char             *restore_token)
{
  g_return_if_fail (restore_token != NULL);

  g_clear_pointer (&session->restore_token, g_free);
  session->restore_token = g_strdup (restore_token);
}

/**
 * xdp_input_capture_session_set_session_persistence:
 * @session: a [class@InputCaptureSession]
 * @persistence: the session persistence for this session
 *
 * Requests session persistence from the portal. A persistent session can
 * be restored using the restore token, see
 * [method@InputCaptureSession.set_restore_token].
 *
 * This method can only be called for a session created with
 * [method@Portal.create_input_capture_session2] and only before
 * [method@InputCaptureSession.start] has been called. It has no effect
 * otherwise.
 *
 * The default persistence is none.
 */
void
xdp_input_capture_session_set_session_persistence (XdpInputCaptureSession            *session,
                                                   XdpInputCaptureSessionPersistence  persistence)
{
  g_return_if_fail (persistence >= XDP_INPUT_CAPTURE_SESSION_PERSISTENCE_NONE &&
                    persistence <= XDP_INPUT_CAPTURE_SESSION_PERSISTENCE_PERSISTENT);
  session->persistence = persistence;
}

/**
 * xdp_portal_get_input_capture_version:
 * @portal: a [class@Portal]
 * @cancellable: (nullable): optional [class@Gio.Cancellable]
 * @callback: (scope async): a callback to call when the request is done
 * @callback_data: data to pass to @callback
 *
 * Retrieving the input portal API version.
 *
 * When the request is done, @callback will be called. You can then
 * call [method@Portal.get_input_capture_version_finish] to get the results.
 */
void
xdp_portal_get_input_capture_version (XdpPortal              *portal,
                                      GCancellable           *cancellable,
                                      GAsyncReadyCallback     callback,
                                      void                   *callback_data)
{
  g_autoptr(GTask) task = NULL;

  task = g_task_new (G_OBJECT (portal), cancellable, callback, callback_data);

  if (portal->input_capture_interface_version)
    {
      g_task_return_int (task, portal->input_capture_interface_version);
      return;
    }

  g_dbus_connection_call (portal->bus,
                          PORTAL_BUS_NAME,
                          PORTAL_OBJECT_PATH,
                          "org.freedesktop.DBus.Properties",
                          "Get",
                          g_variant_new ("(ss)", "org.freedesktop.portal.InputCapture", "version"),
                          NULL,
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          g_task_get_cancellable (task),
                          get_input_capture_interface_version_returned,
                          g_object_ref (task));
}

/**
 * xdp_portal_get_input_capture_version_finish:
 * @portal: a [class@Portal]
 * @result: a [iface@Gio.AsyncResult]
 * @error: return location for an error
 *
 * Finishes retrieving the input portal API version.
 *
 * Returns: the API version of the input capture portal, or -1 on error
 */
int
xdp_portal_get_input_capture_version_finish (XdpPortal     *portal,
                                             GAsyncResult  *result,
                                             GError       **error)
{
  g_return_val_if_fail (XDP_IS_PORTAL (portal), -1);
  g_return_val_if_fail (g_task_is_valid (result, portal), -1);

  return g_task_propagate_int (G_TASK (result), error);
}

/**
 * xdp_portal_get_input_capture_version_sync:
 * @portal: a [class@Portal]
 * @cancellable: (nullable): optional [class@Gio.Cancellable]
 * @error: return location for an error
 *
 * Retrieving the input portal API version.
 *
 * Returns: the API version of the input capture portal, or -1 on error
 */
int
xdp_portal_get_input_capture_version_sync (XdpPortal     *portal,
                                           GCancellable  *cancellable,
                                           GError       **error)
{
  g_autoptr(GVariant) ret = NULL;
  g_autoptr(GVariant) version_variant = NULL;

  if (portal->input_capture_interface_version)
    return portal->input_capture_interface_version;

  ret = g_dbus_connection_call_sync (portal->bus,
                                     PORTAL_BUS_NAME,
                                     PORTAL_OBJECT_PATH,
                                     "org.freedesktop.DBus.Properties",
                                     "Get",
                                     g_variant_new ("(ss)", "org.freedesktop.portal.InputCapture", "version"),
                                     NULL,
                                     G_DBUS_CALL_FLAGS_NONE,
                                     -1,
                                     cancellable,
                                     error);
  if (!ret)
    return -1;

  g_variant_get_child (ret, 0, "v", &version_variant);
  portal->input_capture_interface_version = g_variant_get_uint32 (version_variant);
  return portal->input_capture_interface_version;
}
