/*
 * Copyright (C) 2015 Red Hat Inc.
 * Copyright (C) 2020-2021 Pascal Nowack
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 * Written by:
 *     Jonas Ådahl <jadahl@gmail.com>
 */

#include "config.h"

#include "grd-session.h"

#include <gio/gunixfdlist.h>
#include <glib-object.h>
#include <glib-unix.h>
#include <libei.h>
#include <linux/input-event-codes.h>
#include <sys/mman.h>
#include <xkbcommon/xkbcommon.h>

#include "grd-clipboard.h"
#include "grd-dbus-mutter-remote-desktop.h"
#include "grd-context.h"
#include "grd-private.h"
#include "grd-stream.h"
#include "grd-utils.h"

enum
{
  PROP_0,

  PROP_CONTEXT,
};

enum
{
  READY,
  STARTED,
  STOPPED,
  TOUCH_DEVICE_ADDED,
  TOUCH_DEVICE_REMOVED,

  N_SIGNALS
};

static guint signals[N_SIGNALS];

typedef enum _EvdevButtonType
{
  EVDEV_BUTTON_TYPE_NONE,
  EVDEV_BUTTON_TYPE_KEY,
  EVDEV_BUTTON_TYPE_BUTTON,
} EvdevButtonType;

typedef enum _ScreenCastType
{
  SCREEN_CAST_TYPE_NONE,
  SCREEN_CAST_TYPE_MONITOR,
  SCREEN_CAST_TYPE_WINDOW,
  SCREEN_CAST_TYPE_AREA,
  SCREEN_CAST_TYPE_VIRTUAL,
} ScreenCastType;

typedef struct _AsyncDBusRecordCallContext
{
  GrdSession *session;
  uint32_t stream_id;
  ScreenCastType screen_cast_type;
} AsyncDBusRecordCallContext;

typedef struct _GrdRegion
{
  struct ei_device *ei_device;
  struct ei_region *ei_region;
} GrdRegion;

struct _GrdTouchContact
{
  struct ei_touch *ei_touch_contact;
};

typedef struct _GrdEiPing
{
  GTask *task;
  struct ei_ping *ping;
} GrdEiPing;

typedef struct _GrdSessionPrivate
{
  GrdContext *context;

  GrdDBusMutterRemoteDesktopSession *remote_desktop_session;
  GrdDBusMutterScreenCastSession *screen_cast_session;

  gboolean is_ready;

  GrdClipboard *clipboard;

  struct ei *ei;
  struct ei_seat *ei_seat;
  struct ei_device *ei_pointer;
  struct ei_device *ei_abs_pointer;
  struct ei_device *ei_keyboard;
  struct ei_device *ei_touch;
  uint32_t ei_sequence;
  GSource *ei_source;

  GHashTable *abs_pointer_regions;
  GHashTable *touch_regions;

  struct xkb_context *xkb_context;
  struct xkb_keymap *xkb_keymap;
  struct xkb_state *xkb_state;

  GCancellable *cancellable;

  gboolean started;

  gboolean locked_modifier_valid;
  gboolean caps_lock_state;
  gboolean num_lock_state;
  gulong caps_lock_state_changed_id;
  gulong num_lock_state_changed_id;

  GHashTable *pings;
} GrdSessionPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (GrdSession, grd_session, G_TYPE_OBJECT)

static void
finish_and_free_ping (GrdEiPing *ping);

GrdContext *
grd_session_get_context (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  return priv->context;
}

static void
clear_ei (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  g_clear_pointer (&priv->xkb_state, xkb_state_unref);
  g_clear_pointer (&priv->xkb_keymap, xkb_keymap_unref);
  g_clear_pointer (&priv->xkb_context, xkb_context_unref);

  g_clear_pointer (&priv->touch_regions, g_hash_table_unref);
  g_clear_pointer (&priv->abs_pointer_regions, g_hash_table_unref);

  g_clear_pointer (&priv->ei_touch, ei_device_unref);
  g_clear_pointer (&priv->ei_keyboard, ei_device_unref);
  g_clear_pointer (&priv->ei_abs_pointer, ei_device_unref);
  g_clear_pointer (&priv->ei_pointer, ei_device_unref);
  g_clear_pointer (&priv->ei_seat, ei_seat_unref);
  g_clear_pointer (&priv->ei_source, g_source_destroy);
  g_clear_pointer (&priv->ei, ei_unref);
}

static void
clear_session (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  clear_ei (session);

  g_clear_signal_handler (&priv->caps_lock_state_changed_id,
                          priv->remote_desktop_session);
  g_clear_signal_handler (&priv->num_lock_state_changed_id,
                          priv->remote_desktop_session);

  g_clear_object (&priv->remote_desktop_session);
  g_clear_object (&priv->screen_cast_session);
}

void
grd_session_stop (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  if (priv->cancellable && g_cancellable_is_cancelled (priv->cancellable))
    return;

  GRD_SESSION_GET_CLASS (session)->stop (session);

  clear_ei (session);

  if (priv->remote_desktop_session && priv->started)
    {
      GrdDBusMutterRemoteDesktopSession *proxy = priv->remote_desktop_session;
      g_autoptr (GError) error = NULL;

      if (!grd_dbus_mutter_remote_desktop_session_call_stop_sync (proxy, NULL, &error))
        {
          if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED) &&
              !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT))
            g_warning ("Failed to stop: %s", error->message);
        }
    }

  if (priv->cancellable)
    g_cancellable_cancel (priv->cancellable);

  clear_session (session);

  g_signal_emit (session, signals[STOPPED], 0);
}

gboolean
grd_session_flush_input_finish (GrdSession    *session,
                                GAsyncResult  *result,
                                GError       **error)
{
  return g_task_propagate_boolean (G_TASK (result), error);
}

void
grd_session_flush_input_async (GrdSession          *session,
                               GCancellable        *cancellable,
                               GAsyncReadyCallback  callback,
                               gpointer             user_data)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  GrdEiPing *ping;

  g_assert (priv->is_ready);

  ping = g_new0 (GrdEiPing, 1);
  ping->task = g_task_new (session,
                           cancellable,
                           callback,
                           user_data);
  ping->ping = ei_new_ping (priv->ei);
  ei_ping (ping->ping);

  g_hash_table_insert (priv->pings, ping->ping, ping);
}

static void
on_screen_cast_stream_start_finished (GObject      *object,
                                      GAsyncResult *result,
                                      gpointer      user_data)
{
  GrdDBusMutterScreenCastStream *stream_proxy = GRD_DBUS_MUTTER_SCREEN_CAST_STREAM (object);
  g_autoptr (GError) error = NULL;

  if (!grd_dbus_mutter_screen_cast_stream_call_start_finish (stream_proxy,
                                                             result,
                                                             &error))
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      g_warning ("Failed to start screen cast stream: %s", error->message);
      grd_session_stop (GRD_SESSION (user_data));
      return;
    }
}

static void
on_screen_cast_stream_proxy_acquired (GObject      *object,
                                      GAsyncResult *result,
                                      gpointer      user_data)
{
  g_autofree AsyncDBusRecordCallContext *async_context = user_data;
  g_autoptr (GrdDBusMutterScreenCastStream) stream_proxy = NULL;
  GrdSession *session;
  GrdSessionPrivate *priv;
  GrdSessionClass *klass;
  g_autoptr (GError) error = NULL;
  GrdStream *stream;

  stream_proxy = grd_dbus_mutter_screen_cast_stream_proxy_new_finish (result, &error);
  if (!stream_proxy)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      g_warning ("Failed to acquire stream proxy: %s", error->message);
      grd_session_stop (async_context->session);
      return;
    }

  session = async_context->session;
  priv = grd_session_get_instance_private (session);
  klass = GRD_SESSION_GET_CLASS (session);

  stream = grd_stream_new (async_context->stream_id, stream_proxy,
                           priv->cancellable, &error);
  if (!stream)
    {
      g_warning ("Failed to create stream: %s", error->message);
      grd_session_stop (async_context->session);
      return;
    }

  klass->on_stream_created (session, async_context->stream_id, stream);

  grd_dbus_mutter_screen_cast_stream_call_start (stream_proxy,
                                                 priv->cancellable,
                                                 on_screen_cast_stream_start_finished,
                                                 session);
}

static const char *
screen_cast_type_to_string (ScreenCastType type)
{
  switch (type)
    {
    case SCREEN_CAST_TYPE_NONE:
      return "none";
    case SCREEN_CAST_TYPE_MONITOR:
      return "monitor";
    case SCREEN_CAST_TYPE_WINDOW:
      return "window";
    case SCREEN_CAST_TYPE_AREA:
      return "area";
    case SCREEN_CAST_TYPE_VIRTUAL:
      return "virtual";
    }

  g_assert_not_reached ();
}

static void
on_record_finished (GObject      *object,
                    GAsyncResult *result,
                    gpointer      user_data)
{
  GrdDBusMutterScreenCastSession *proxy = GRD_DBUS_MUTTER_SCREEN_CAST_SESSION (object);
  g_autofree AsyncDBusRecordCallContext *async_context = user_data;
  ScreenCastType screen_cast_type = async_context->screen_cast_type;
  GrdSession *session;
  GrdSessionPrivate *priv;
  GDBusConnection *connection;
  g_autofree char *stream_path = NULL;
  g_autoptr (GError) error = NULL;
  gboolean retval = FALSE;

  switch (screen_cast_type)
    {
    case SCREEN_CAST_TYPE_NONE:
      g_assert_not_reached ();
      break;
    case SCREEN_CAST_TYPE_MONITOR:
      retval = grd_dbus_mutter_screen_cast_session_call_record_monitor_finish (
        proxy, &stream_path, result, &error);
      break;
    case SCREEN_CAST_TYPE_WINDOW:
    case SCREEN_CAST_TYPE_AREA:
      g_assert_not_reached ();
      break;
    case SCREEN_CAST_TYPE_VIRTUAL:
      retval = grd_dbus_mutter_screen_cast_session_call_record_virtual_finish (
        proxy, &stream_path, result, &error);
      break;
    }

  if (!retval)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      g_warning ("Failed to record %s: %s",
                 screen_cast_type_to_string (screen_cast_type), error->message);
      grd_session_stop (async_context->session);
      return;
    }

  session = async_context->session;
  priv = grd_session_get_instance_private (session);
  connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (proxy));

  grd_dbus_mutter_screen_cast_stream_proxy_new (connection,
                                                G_DBUS_PROXY_FLAGS_NONE,
                                                MUTTER_SCREEN_CAST_BUS_NAME,
                                                stream_path,
                                                priv->cancellable,
                                                on_screen_cast_stream_proxy_acquired,
                                                g_steal_pointer (&async_context));
}

void
grd_session_record_monitor (GrdSession              *session,
                            uint32_t                 stream_id,
                            const char              *connector,
                            GrdScreenCastCursorMode  cursor_mode)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  GrdSessionClass *klass = GRD_SESSION_GET_CLASS (session);
  GVariantBuilder properties_builder;
  AsyncDBusRecordCallContext *async_context;

  g_assert (klass->on_stream_created);

  async_context = g_malloc0 (sizeof (AsyncDBusRecordCallContext));
  async_context->session = session;
  async_context->stream_id = stream_id;
  async_context->screen_cast_type = SCREEN_CAST_TYPE_MONITOR;

  g_variant_builder_init (&properties_builder, G_VARIANT_TYPE ("a{sv}"));
  g_variant_builder_add (&properties_builder, "{sv}",
                         "cursor-mode", g_variant_new_uint32 (cursor_mode));

  grd_dbus_mutter_screen_cast_session_call_record_monitor (priv->screen_cast_session,
                                                           connector ? connector : "",
                                                           g_variant_builder_end (&properties_builder),
                                                           priv->cancellable,
                                                           on_record_finished,
                                                           async_context);
}

void
grd_session_record_virtual (GrdSession              *session,
                            uint32_t                 stream_id,
                            GrdScreenCastCursorMode  cursor_mode,
                            gboolean                 is_platform)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  GrdSessionClass *klass = GRD_SESSION_GET_CLASS (session);
  GVariantBuilder properties_builder;
  AsyncDBusRecordCallContext *async_context;

  g_assert (klass->on_stream_created);

  async_context = g_malloc0 (sizeof (AsyncDBusRecordCallContext));
  async_context->session = session;
  async_context->stream_id = stream_id;
  async_context->screen_cast_type = SCREEN_CAST_TYPE_VIRTUAL;

  g_variant_builder_init (&properties_builder, G_VARIANT_TYPE ("a{sv}"));
  g_variant_builder_add (&properties_builder, "{sv}",
                         "cursor-mode", g_variant_new_uint32 (cursor_mode));
  g_variant_builder_add (&properties_builder, "{sv}",
                         "is-platform", g_variant_new_boolean (is_platform));

  grd_dbus_mutter_screen_cast_session_call_record_virtual (priv->screen_cast_session,
                                                           g_variant_builder_end (&properties_builder),
                                                           priv->cancellable,
                                                           on_record_finished,
                                                           async_context);
}

void
grd_session_notify_keyboard_keycode (GrdSession  *session,
                                     uint32_t     keycode,
                                     GrdKeyState  state)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  if (!priv->ei_keyboard)
    return;

  ei_device_keyboard_key (priv->ei_keyboard, keycode, state);
  ei_device_frame (priv->ei_keyboard, g_get_monotonic_time ());
}

static gboolean
pick_keycode_for_keysym_in_current_group (GrdSession *session,
                                          uint32_t    keysym,
                                          uint32_t   *keycode_out,
                                          uint32_t   *level_out)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  struct xkb_keymap *xkb_keymap;
  struct xkb_state  *xkb_state;
  uint32_t keycode, layout;
  xkb_keycode_t min_keycode, max_keycode;

  g_assert (keycode_out);

  xkb_keymap = priv->xkb_keymap;
  xkb_state = priv->xkb_state;

  layout = xkb_state_serialize_layout (xkb_state, XKB_STATE_LAYOUT_EFFECTIVE);
  min_keycode = xkb_keymap_min_keycode (xkb_keymap);
  max_keycode = xkb_keymap_max_keycode (xkb_keymap);
  for (keycode = min_keycode; keycode < max_keycode; keycode++)
    {
      int num_levels, level;

      num_levels = xkb_keymap_num_levels_for_key (xkb_keymap, keycode, layout);
      for (level = 0; level < num_levels; level++)
        {
          const xkb_keysym_t *syms;
          int num_syms, sym;

          num_syms = xkb_keymap_key_get_syms_by_level (xkb_keymap, keycode,
                                                       layout, level, &syms);
          for (sym = 0; sym < num_syms; sym++)
            {
              if (syms[sym] == keysym)
                {
                  *keycode_out = keycode;
                  if (level_out)
                    *level_out = level;
                  return TRUE;
                }
            }
        }
    }

  return FALSE;
}

static uint32_t
xkb_keycode_to_evdev (uint32_t xkb_keycode)
{
  return xkb_keycode - 8;
}

static void
apply_level_modifiers (GrdSession *session,
                       uint64_t    time_us,
                       uint32_t    level,
                       uint32_t    key_state)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  uint32_t keysym, keycode, evcode;

  if (level == 0)
    return;

  if (level == 1)
    {
      keysym = XKB_KEY_Shift_L;
    }
  else if (level == 2)
    {
      keysym = XKB_KEY_ISO_Level3_Shift;
    }
  else
    {
      g_warning ("Unhandled level: %d", level);
      return;
    }

  if (!pick_keycode_for_keysym_in_current_group (session, keysym,
                                                 &keycode, NULL))
    return;

  evcode = xkb_keycode_to_evdev (keycode);

  ei_device_keyboard_key (priv->ei_keyboard, evcode, key_state);
  ei_device_frame (priv->ei_keyboard, time_us);
}

static EvdevButtonType
get_button_type (uint16_t code)
{
  switch (code)
    {
    case BTN_TOOL_PEN:
    case BTN_TOOL_RUBBER:
    case BTN_TOOL_BRUSH:
    case BTN_TOOL_PENCIL:
    case BTN_TOOL_AIRBRUSH:
    case BTN_TOOL_MOUSE:
    case BTN_TOOL_LENS:
    case BTN_TOOL_QUINTTAP:
    case BTN_TOOL_DOUBLETAP:
    case BTN_TOOL_TRIPLETAP:
    case BTN_TOOL_QUADTAP:
    case BTN_TOOL_FINGER:
    case BTN_TOUCH:
      return EVDEV_BUTTON_TYPE_NONE;
    }

  if (code >= KEY_ESC && code <= KEY_MICMUTE)
    return EVDEV_BUTTON_TYPE_KEY;
  if (code >= BTN_MISC && code <= BTN_GEAR_UP)
    return EVDEV_BUTTON_TYPE_BUTTON;
  if (code >= KEY_OK && code <= KEY_LIGHTS_TOGGLE)
    return EVDEV_BUTTON_TYPE_KEY;
  if (code >= BTN_DPAD_UP && code <= BTN_DPAD_RIGHT)
    return EVDEV_BUTTON_TYPE_BUTTON;
  if (code >= KEY_ALS_TOGGLE && code <= KEY_KBDINPUTASSIST_CANCEL)
    return EVDEV_BUTTON_TYPE_KEY;
  if (code >= BTN_TRIGGER_HAPPY && code <= BTN_TRIGGER_HAPPY40)
    return EVDEV_BUTTON_TYPE_BUTTON;
  return EVDEV_BUTTON_TYPE_NONE;
}

void
grd_session_notify_keyboard_keysym (GrdSession *session,
                                    uint32_t    keysym,
                                    GrdKeyState state)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  int64_t now_us;
  uint32_t keycode = 0, level = 0, evcode = 0;

  if (!priv->xkb_state || !priv->ei_keyboard)
    return;

  now_us = g_get_monotonic_time ();

  if (!pick_keycode_for_keysym_in_current_group (session,
                                                 keysym,
                                                 &keycode, &level))
    {
      g_warning ("No keycode found for keyval %x in current group", keysym);
      return;
    }

  evcode = xkb_keycode_to_evdev (keycode);
  if (get_button_type (evcode) != EVDEV_BUTTON_TYPE_KEY)
    {
      g_warning ("Unknown/invalid key 0x%x pressed", evcode);
      return;
    }

  if (state)
    apply_level_modifiers (session, now_us, level, state);

  ei_device_keyboard_key (priv->ei_keyboard, evcode, state);
  ei_device_frame (priv->ei_keyboard, now_us);

  if (!state)
    apply_level_modifiers (session, now_us, level, state);
}

void
grd_session_notify_pointer_button (GrdSession     *session,
                                   int32_t         button,
                                   GrdButtonState  state)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  if (!priv->ei_abs_pointer)
    return;

  ei_device_button_button (priv->ei_abs_pointer, button, state);
  ei_device_frame (priv->ei_abs_pointer, g_get_monotonic_time ());
}

void
grd_session_notify_pointer_axis (GrdSession          *session,
                                 double               dx,
                                 double               dy,
                                 GrdPointerAxisFlags  flags)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  if (!priv->ei_abs_pointer)
    return;

  if (flags & GRD_POINTER_AXIS_FLAGS_FINISH)
    ei_device_scroll_stop (priv->ei_abs_pointer, true, true);
  else
    ei_device_scroll_delta (priv->ei_abs_pointer, dx, dy);

  ei_device_frame (priv->ei_abs_pointer, g_get_monotonic_time ());
}

void
grd_session_notify_pointer_axis_discrete (GrdSession    *session,
                                          GrdPointerAxis axis,
                                          int            steps)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  if (!priv->ei_abs_pointer)
    return;

  ei_device_scroll_discrete (priv->ei_abs_pointer,
                             axis == GRD_POINTER_AXIS_HORIZONTAL ? steps * 120 : 0,
                             axis == GRD_POINTER_AXIS_VERTICAL ? steps * 120 : 0);
  ei_device_frame (priv->ei_abs_pointer, g_get_monotonic_time ());
}

void
grd_session_notify_pointer_motion (GrdSession *session,
                                   double      dx,
                                   double      dy)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  if (!priv->ei_pointer)
    return;

  ei_device_pointer_motion (priv->ei_pointer, dx, dy);
  ei_device_frame (priv->ei_pointer, g_get_monotonic_time ());
}

static gboolean
transform_position (GrdSession               *session,
                    GHashTable               *regions,
                    GrdStream                *stream,
                    const GrdEventMotionAbs  *motion_abs,
                    struct ei_device        **ei_device,
                    double                   *x,
                    double                   *y)
{
  GrdRegion *region;
  double scale_x;
  double scale_y;
  double scaled_x;
  double scaled_y;

  g_assert (motion_abs->input_rect_width > 0);
  g_assert (motion_abs->input_rect_height > 0);

  region = g_hash_table_lookup (regions, grd_stream_get_mapping_id (stream));
  if (!region)
    return FALSE;

  scale_x = ((double) motion_abs->input_rect_width) /
            ei_region_get_width (region->ei_region);
  scale_y = ((double) motion_abs->input_rect_height) /
            ei_region_get_height (region->ei_region);
  scaled_x = motion_abs->x / scale_x;
  scaled_y = motion_abs->y / scale_y;

  *ei_device = region->ei_device;
  *x = ei_region_get_x (region->ei_region) + scaled_x;
  *y = ei_region_get_y (region->ei_region) + scaled_y;

  return TRUE;
}

void
grd_session_notify_pointer_motion_absolute (GrdSession              *session,
                                            GrdStream               *stream,
                                            const GrdEventMotionAbs *motion_abs)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  struct ei_device *ei_device = NULL;
  double x = 0;
  double y = 0;

  if (!transform_position (session, priv->abs_pointer_regions,
                           stream, motion_abs, &ei_device, &x, &y))
    return;

  ei_device_pointer_motion_absolute (ei_device, x, y);
  ei_device_frame (ei_device, g_get_monotonic_time ());
}

gboolean
grd_session_has_touch_device (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  return !!priv->ei_touch;
}

GrdTouchContact *
grd_session_acquire_touch_contact (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  GrdTouchContact *touch_contact;

  g_assert (priv->ei_touch);

  touch_contact = g_new0 (GrdTouchContact, 1);
  touch_contact->ei_touch_contact = ei_device_touch_new (priv->ei_touch);

  return touch_contact;
}

void
grd_session_release_touch_contact (GrdSession      *session,
                                   GrdTouchContact *touch_contact)
{
  g_clear_pointer (&touch_contact->ei_touch_contact, ei_touch_unref);

  g_free (touch_contact);
}

void
grd_session_notify_touch_down (GrdSession              *session,
                               const GrdTouchContact   *touch_contact,
                               GrdStream               *stream,
                               const GrdEventMotionAbs *motion_abs)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  struct ei_device *ei_device = NULL;
  double x = 0;
  double y = 0;

  if (!transform_position (session, priv->touch_regions, stream, motion_abs,
                           &ei_device, &x, &y))
    return;

  ei_touch_down (touch_contact->ei_touch_contact, x, y);
}

void
grd_session_notify_touch_motion (GrdSession              *session,
                                 const GrdTouchContact   *touch_contact,
                                 GrdStream               *stream,
                                 const GrdEventMotionAbs *motion_abs)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  struct ei_device *ei_device = NULL;
  double x = 0;
  double y = 0;

  if (!transform_position (session, priv->touch_regions, stream, motion_abs,
                           &ei_device, &x, &y))
    return;

  ei_touch_motion (touch_contact->ei_touch_contact, x, y);
}

void
grd_session_notify_touch_up (GrdSession      *session,
                             GrdTouchContact *touch_contact)
{
  ei_touch_up (touch_contact->ei_touch_contact);
}

void
grd_session_notify_touch_cancel (GrdSession      *session,
                                 GrdTouchContact *touch_contact)
{
  ei_touch_cancel (touch_contact->ei_touch_contact);
}

void
grd_session_notify_touch_device_frame (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  g_assert (priv->ei_touch);

  ei_device_frame (priv->ei_touch, g_get_monotonic_time ());
}

static GVariant *
serialize_mime_type_tables (GList *mime_type_tables)
{
  GVariantBuilder builder;
  GList *l;

  g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
  for (l = mime_type_tables; l; l = l->next)
    {
      GrdMimeTypeTable *mime_type_table = l->data;
      GrdMimeType mime_type;
      const char *mime_type_string;

      mime_type = mime_type_table->mime_type;
      mime_type_string = grd_mime_type_to_string (mime_type);
      g_variant_builder_add (&builder, "s", mime_type_string);
    }

  return g_variant_builder_end (&builder);
}

static GVariant *
serialize_clipboard_options (GList *mime_type_tables)
{
  GVariantBuilder builder;

  g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));
  if (mime_type_tables)
    {
      g_variant_builder_add (&builder, "{sv}", "mime-types",
                             serialize_mime_type_tables (mime_type_tables));
    }

  return g_variant_builder_end (&builder);
}

gboolean
grd_session_enable_clipboard (GrdSession   *session,
                              GrdClipboard *clipboard,
                              GList        *mime_type_tables)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  GVariant *options_variant;
  g_autoptr (GError) error = NULL;

  if (!priv->remote_desktop_session)
    return FALSE;

  options_variant = serialize_clipboard_options (mime_type_tables);
  if (!grd_dbus_mutter_remote_desktop_session_call_enable_clipboard_sync (
         priv->remote_desktop_session, options_variant, NULL, &error))
    {
      g_warning ("Failed to enable clipboard: %s", error->message);
      return FALSE;
    }
  priv->clipboard = clipboard;

  return TRUE;
}

void
grd_session_disable_clipboard (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  priv->clipboard = NULL;
  if (!priv->remote_desktop_session)
    return;

  grd_dbus_mutter_remote_desktop_session_call_disable_clipboard (
    priv->remote_desktop_session, NULL, NULL, NULL);
}

void
grd_session_set_selection (GrdSession *session,
                           GList      *mime_type_tables)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  GVariant *options_variant;
  g_autoptr (GError) error = NULL;

  options_variant = serialize_clipboard_options (mime_type_tables);

  if (!grd_dbus_mutter_remote_desktop_session_call_set_selection_sync (
         priv->remote_desktop_session, options_variant, NULL, &error))
    g_warning ("Failed to set selection: %s", error->message);
}

void
grd_session_selection_write (GrdSession    *session,
                             unsigned int   serial,
                             const uint8_t *data,
                             uint32_t       size)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  g_autoptr (GError) error = NULL;
  g_autoptr (GVariant) fd_variant = NULL;
  g_autoptr (GUnixFDList) fd_list = NULL;
  int fd_idx = -1;
  int fd;

  if (!data || !size)
    {
      grd_dbus_mutter_remote_desktop_session_call_selection_write_done (
        priv->remote_desktop_session, serial, FALSE, NULL, NULL, NULL);
      return;
    }

  if (!grd_dbus_mutter_remote_desktop_session_call_selection_write_sync (
         priv->remote_desktop_session, serial, NULL, &fd_variant, &fd_list,
         NULL, &error))
    {
      g_warning ("Failed to write selection for serial %u: %s",
                 serial, error->message);
      return;
    }

  g_variant_get (fd_variant, "h", &fd_idx);
  if (!G_IS_UNIX_FD_LIST (fd_list) ||
      fd_idx < 0 || fd_idx >= g_unix_fd_list_get_length (fd_list))
    {
      g_warning ("Failed to acquire file descriptor for serial %u: Invalid "
                 "file descriptor list sent by display server", serial);
      return;
    }

  fd = g_unix_fd_list_get (fd_list, fd_idx, &error);
  if (fd == -1)
    {
      g_warning ("Failed to acquire file descriptor for serial %u: %s",
                 serial, error->message);
      return;
    }

  if (write (fd, data, size) < 0)
    {
      grd_dbus_mutter_remote_desktop_session_call_selection_write_done (
        priv->remote_desktop_session, serial, FALSE, NULL, NULL, NULL);

      close (fd);
      return;
    }

  grd_dbus_mutter_remote_desktop_session_call_selection_write_done (
    priv->remote_desktop_session, serial, TRUE, NULL, NULL, NULL);

  close (fd);
}

int
grd_session_selection_read (GrdSession  *session,
                            GrdMimeType  mime_type)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  g_autoptr (GError) error = NULL;
  g_autoptr (GVariant) fd_variant = NULL;
  g_autoptr (GUnixFDList) fd_list = NULL;
  int fd_idx = -1;
  int fd;
  const char *mime_type_string;

  mime_type_string = grd_mime_type_to_string (mime_type);
  if (!grd_dbus_mutter_remote_desktop_session_call_selection_read_sync (
         priv->remote_desktop_session, mime_type_string, NULL, &fd_variant,
         &fd_list, NULL, &error))
    {
      g_warning ("Failed to read selection: %s", error->message);
      return -1;
    }

  g_variant_get (fd_variant, "h", &fd_idx);
  if (!G_IS_UNIX_FD_LIST (fd_list) ||
      fd_idx < 0 || fd_idx >= g_unix_fd_list_get_length (fd_list))
    {
      g_warning ("Failed to acquire file descriptor: Invalid file descriptor "
                 "list sent by display server");
      return -1;
    }

  fd = g_unix_fd_list_get (fd_list, fd_idx, &error);
  if (fd == -1)
    {
      g_warning ("Failed to acquire file descriptor: %s", error->message);
      return -1;
    }

  return fd;
}

static void
maybe_emit_started_signal (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  if (!priv->is_ready ||
      !priv->started)
    return;

  g_signal_emit (session, signals[STARTED], 0);
}

static void
on_session_start_finished (GObject      *object,
                           GAsyncResult *result,
                           gpointer      user_data)
{
  GrdDBusMutterRemoteDesktopSession *proxy;
  GrdSession *session;
  GrdSessionPrivate *priv;
  g_autoptr (GError) error = NULL;

  proxy = GRD_DBUS_MUTTER_REMOTE_DESKTOP_SESSION (object);
  if (!grd_dbus_mutter_remote_desktop_session_call_start_finish (proxy,
                                                                 result,
                                                                 &error))
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      g_warning ("Failed to start session: %s", error->message);
      grd_session_stop (GRD_SESSION (user_data));
      return;
    }

  session = GRD_SESSION (user_data);
  priv = grd_session_get_instance_private (session);

  priv->started = TRUE;
  maybe_emit_started_signal (session);
}

static void
start_session (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  GrdDBusMutterRemoteDesktopSession *proxy = priv->remote_desktop_session;

  grd_dbus_mutter_remote_desktop_session_call_start (proxy,
                                                     priv->cancellable,
                                                     on_session_start_finished,
                                                     session);
}

static void
on_screen_cast_session_proxy_acquired (GObject      *object,
                                       GAsyncResult *result,
                                       gpointer      user_data)
{
  GrdDBusMutterScreenCastSession *session_proxy;
  GrdSession *session;
  GrdSessionPrivate *priv;
  g_autoptr (GError) error = NULL;

  session_proxy =
    grd_dbus_mutter_screen_cast_session_proxy_new_finish (result, &error);
  if (!session_proxy)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      g_warning ("Failed to acquire screen cast session proxy: %s\n",
                 error->message);
      grd_session_stop (GRD_SESSION (user_data));
      return;
    }

  session = GRD_SESSION (user_data);
  priv = grd_session_get_instance_private (session);

  priv->screen_cast_session = session_proxy;

  start_session (session);
}

static void
on_screen_cast_session_created (GObject      *source_object,
                                GAsyncResult *res,
                                gpointer      user_data)
{
  GrdDBusMutterScreenCast *screen_cast_proxy;
  GrdSession *session;
  GrdSessionPrivate *priv;
  GDBusConnection *connection;
  g_autofree char *session_path = NULL;
  g_autoptr (GError) error = NULL;

  screen_cast_proxy = GRD_DBUS_MUTTER_SCREEN_CAST (source_object);
  if (!grd_dbus_mutter_screen_cast_call_create_session_finish (screen_cast_proxy,
                                                               &session_path,
                                                               res,
                                                               &error))
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      g_warning ("Failed to start screen cast session: %s\n", error->message);
      grd_session_stop (GRD_SESSION (user_data));
      return;
    }

  session = GRD_SESSION (user_data);
  priv = grd_session_get_instance_private (session);
  connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (screen_cast_proxy));

  grd_dbus_mutter_screen_cast_session_proxy_new (connection,
                                                 G_DBUS_PROXY_FLAGS_NONE,
                                                 MUTTER_SCREEN_CAST_BUS_NAME,
                                                 session_path,
                                                 priv->cancellable,
                                                 on_screen_cast_session_proxy_acquired,
                                                 session);
}

static void
on_remote_desktop_session_closed (GrdDBusMutterRemoteDesktopSession *session_proxy,
                                  GrdSession                        *session)
{
  g_debug ("Remote desktop session closed, stopping session");

  clear_session (session);
  grd_session_stop (session);
}

static void
on_remote_desktop_session_selection_owner_changed (GrdDBusMutterRemoteDesktopSession *session_proxy,
                                                   GVariant                          *options_variant,
                                                   GrdSession                        *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  GVariant *is_owner_variant, *mime_types_variant;
  GVariantIter iter;
  const char *mime_string;
  GrdMimeType mime_type;
  GList *mime_type_list = NULL;

  if (!priv->clipboard)
    return;

  is_owner_variant = g_variant_lookup_value (options_variant, "session-is-owner",
                                             G_VARIANT_TYPE ("b"));
  if (is_owner_variant && g_variant_get_boolean (is_owner_variant))
    return;

  mime_types_variant = g_variant_lookup_value (options_variant, "mime-types",
                                               G_VARIANT_TYPE ("(as)"));
  if (!mime_types_variant)
    return;

  g_variant_iter_init (&iter, g_variant_get_child_value (mime_types_variant, 0));
  while (g_variant_iter_loop (&iter, "s", &mime_string))
    {
      mime_type = grd_mime_type_from_string (mime_string);
      if (mime_type != GRD_MIME_TYPE_NONE)
        {
          mime_type_list = g_list_append (mime_type_list,
                                          GUINT_TO_POINTER (mime_type));
          g_debug ("Clipboard[SelectionOwnerChanged]: Server advertises mime "
                   "type %s", mime_string);
        }
      else
        {
          g_debug ("Clipboard[SelectionOwnerChanged]: Server advertised unknown "
                   "mime type: %s", mime_string);
        }
    }

  if (mime_type_list)
    grd_clipboard_update_client_mime_type_list (priv->clipboard, mime_type_list);
}

static void
on_remote_desktop_session_selection_transfer (GrdDBusMutterRemoteDesktopSession *session_proxy,
                                              char                              *mime_type_string,
                                              unsigned int                       serial,
                                              GrdSession                        *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  GrdMimeType mime_type;

  if (!priv->clipboard)
    return;

  mime_type = grd_mime_type_from_string (mime_type_string);
  if (mime_type == GRD_MIME_TYPE_NONE)
    {
      grd_dbus_mutter_remote_desktop_session_call_selection_write_done (
        priv->remote_desktop_session, serial, FALSE, NULL, NULL, NULL);
      return;
    }

  grd_clipboard_request_client_content_for_mime_type (priv->clipboard,
                                                      mime_type, serial);
}

static gboolean
grd_ei_source_prepare (gpointer user_data)
{
  GrdSession *session = GRD_SESSION (user_data);
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  return !!ei_peek_event (priv->ei);
}

static gboolean
setup_xkb_keymap (GrdSession        *session,
                  struct ei_keymap  *keymap,
                  GError           **error)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  struct xkb_context *xkb_context = NULL;
  struct xkb_keymap *xkb_keymap = NULL;
  struct xkb_state *xkb_state = NULL;
  size_t keymap_size;
  g_autofree char *buf = NULL;

  g_clear_pointer (&priv->xkb_state, xkb_state_unref);
  g_clear_pointer (&priv->xkb_keymap, xkb_keymap_unref);
  g_clear_pointer (&priv->xkb_context, xkb_context_unref);

  xkb_context = xkb_context_new (XKB_CONTEXT_NO_FLAGS);
  if (!xkb_context)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Failed to create XKB context");
      goto err;
    }

  keymap_size = ei_keymap_get_size (keymap);
  buf = g_malloc0 (keymap_size + 1);
  while (TRUE)
    {
      int ret;

      ret = read (ei_keymap_get_fd (keymap), buf, keymap_size);
      if (ret > 0)
        {
          break;
        }
      else if (ret == 0)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                       "Keyboard layout was empty");
          goto err;
        }
      else if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
        {
          continue;
        }
      else
        {
          g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno),
                       "Failed to read layout: %s", g_strerror (errno));
          goto err;
        }
    }

  xkb_keymap = xkb_keymap_new_from_string (xkb_context, buf,
                                           XKB_KEYMAP_FORMAT_TEXT_V1,
                                           XKB_KEYMAP_COMPILE_NO_FLAGS);
  if (!xkb_keymap)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Failed to create XKB keymap");
      goto err;
    }

  xkb_state = xkb_state_new (xkb_keymap);
  if (!xkb_state)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Failed to create XKB state");
      goto err;
    }

  priv->xkb_context = xkb_context;
  priv->xkb_keymap = xkb_keymap;
  priv->xkb_state = xkb_state;
  return TRUE;

err:
  g_clear_pointer (&xkb_state, xkb_state_unref);
  g_clear_pointer (&xkb_keymap, xkb_keymap_unref);
  g_clear_pointer (&xkb_context, xkb_context_unref);
  return FALSE;
}

static gboolean
process_keymap (GrdSession        *session,
                struct ei_device  *device,
                GError           **error)
{
  struct ei_keymap *keymap;
  enum ei_keymap_type type;

  keymap = ei_device_keyboard_get_keymap (device);
  if (!keymap)
    return TRUE;

  type = ei_keymap_get_type (keymap);
  switch (type)
    {
    case EI_KEYMAP_TYPE_XKB:
      return setup_xkb_keymap (session, keymap, error);
    default:
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Unknown keyboard layout type");
      return FALSE;
    }

  return TRUE;
}

static void
grd_region_free (GrdRegion *region)
{
  ei_region_unref (region->ei_region);
  ei_device_unref (region->ei_device);
  g_free (region);
}

static void
maybe_dispose_ei_abs_pointer (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  g_hash_table_remove_all (priv->abs_pointer_regions);
  g_clear_pointer (&priv->ei_abs_pointer, ei_device_unref);
}

static void
maybe_dispose_ei_touch (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  g_signal_emit (session, signals[TOUCH_DEVICE_REMOVED], 0);
  g_hash_table_remove_all (priv->touch_regions);
  g_clear_pointer (&priv->ei_touch, ei_device_unref);
}

static void
process_regions (GrdSession       *session,
                 struct ei_device *ei_device,
                 GHashTable       *regions)
{
  size_t i = 0;
  struct ei_region *ei_region;

  while ((ei_region = ei_device_get_region (ei_device, i++)))
    {
      const char *mapping_id;
      GrdRegion *region;

      mapping_id = ei_region_get_mapping_id (ei_region);
      if (!mapping_id)
        continue;

      g_debug ("ei: New region: mapping-id: %s, %ux%u (%u, %u)",
               mapping_id,
               ei_region_get_width (ei_region), ei_region_get_height (ei_region),
               ei_region_get_x (ei_region), ei_region_get_y (ei_region));

      g_assert (ei_region_get_width (ei_region) > 0);
      g_assert (ei_region_get_height (ei_region) > 0);

      region = g_new0 (GrdRegion, 1);
      region->ei_device = ei_device_ref (ei_device);
      region->ei_region = ei_region_ref (ei_region);
      g_hash_table_insert (regions, g_strdup (mapping_id), region);
    }
}

static gboolean
grd_ei_source_dispatch (gpointer user_data)
{
  GrdSession *session = GRD_SESSION (user_data);
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  struct ei_event *event;

  ei_dispatch (priv->ei);

  while ((event = ei_get_event (priv->ei)))
    {
      enum ei_event_type ei_event_type = ei_event_get_type (event);
      gboolean handled = TRUE;

      switch (ei_event_type)
        {
        case EI_EVENT_CONNECT:
          if (!priv->is_ready)
            {
              priv->is_ready = TRUE;
              g_signal_emit (session, signals[READY], 0);
              maybe_emit_started_signal (session);
            }
          break;
        case EI_EVENT_DISCONNECT:
          break;
        case EI_EVENT_SEAT_ADDED:
          if (priv->ei_seat)
            break;

          priv->ei_seat = ei_seat_ref (ei_event_get_seat (event));
          ei_seat_bind_capabilities (priv->ei_seat,
                                     EI_DEVICE_CAP_POINTER,
                                     EI_DEVICE_CAP_KEYBOARD,
                                     EI_DEVICE_CAP_POINTER_ABSOLUTE,
                                     EI_DEVICE_CAP_BUTTON,
                                     EI_DEVICE_CAP_SCROLL,
                                     EI_DEVICE_CAP_TOUCH,
                                     NULL);
          break;
        case EI_EVENT_SEAT_REMOVED:
          if (ei_event_get_seat (event) == priv->ei_seat)
            g_clear_pointer (&priv->ei_seat, ei_seat_unref);
          break;
        case EI_EVENT_DEVICE_ADDED:
          {
            struct ei_device *device = ei_event_get_device (event);

            if (ei_device_has_capability (device, EI_DEVICE_CAP_KEYBOARD))
              {
                g_autoptr (GError) error = NULL;

                g_clear_pointer (&priv->ei_keyboard, ei_device_unref);
                priv->ei_keyboard = ei_device_ref (device);
                if (!process_keymap (session, priv->ei_keyboard, &error))
                  {
                    g_warning ("Failed to handle keyboard layout: %s",
                               error->message);
                    ei_event_unref (event);
                    grd_session_stop (session);
                    return G_SOURCE_REMOVE;
                  }
              }
            if (ei_device_has_capability (device, EI_DEVICE_CAP_POINTER))
              {
                g_clear_pointer (&priv->ei_pointer, ei_device_unref);
                priv->ei_pointer = ei_device_ref (device);
              }
            if (ei_device_has_capability (device, EI_DEVICE_CAP_POINTER_ABSOLUTE))
              {
                maybe_dispose_ei_abs_pointer (session);
                priv->ei_abs_pointer = ei_device_ref (device);
                process_regions (session, device, priv->abs_pointer_regions);
              }
            if (ei_device_has_capability (device, EI_DEVICE_CAP_TOUCH))
              {
                maybe_dispose_ei_touch (session);
                priv->ei_touch = ei_device_ref (device);
                process_regions (session, device, priv->touch_regions);
              }
            break;
          }
        case EI_EVENT_DEVICE_RESUMED:
          if (ei_event_get_device (event) == priv->ei_pointer)
            ei_device_start_emulating (priv->ei_pointer, ++priv->ei_sequence);
          if (ei_event_get_device (event) == priv->ei_abs_pointer)
            ei_device_start_emulating (priv->ei_abs_pointer, ++priv->ei_sequence);
          if (ei_event_get_device (event) == priv->ei_keyboard)
            ei_device_start_emulating (priv->ei_keyboard, ++priv->ei_sequence);
          if (ei_event_get_device (event) == priv->ei_touch)
            {
              ei_device_start_emulating (priv->ei_touch, ++priv->ei_sequence);
              g_signal_emit (session, signals[TOUCH_DEVICE_ADDED], 0);
            }
          break;
        case EI_EVENT_DEVICE_PAUSED:
          break;
        case EI_EVENT_DEVICE_REMOVED:
          if (ei_event_get_device (event) == priv->ei_pointer)
            g_clear_pointer (&priv->ei_pointer, ei_device_unref);
          if (ei_event_get_device (event) == priv->ei_abs_pointer)
            maybe_dispose_ei_abs_pointer (session);
          if (ei_event_get_device (event) == priv->ei_keyboard)
            g_clear_pointer (&priv->ei_keyboard, ei_device_unref);
          if (ei_event_get_device (event) == priv->ei_touch)
            maybe_dispose_ei_touch (session);
          break;
        case EI_EVENT_KEYBOARD_MODIFIERS:
          if (priv->xkb_keymap)
            {
              GrdSessionClass *klass = GRD_SESSION_GET_CLASS (session);
              uint32_t latched_mods =
                ei_event_keyboard_get_xkb_mods_latched (event);
              uint32_t locked_mods =
                ei_event_keyboard_get_xkb_mods_locked (event);
              gboolean caps_lock_state;
              gboolean num_lock_state;

              caps_lock_state =
                !!((latched_mods | locked_mods) &
                   (1 << xkb_keymap_mod_get_index (priv->xkb_keymap,
                                                   XKB_MOD_NAME_CAPS)));
              num_lock_state =
                !!((latched_mods | locked_mods) &
                   (1 << xkb_keymap_mod_get_index (priv->xkb_keymap,
                                                   XKB_MOD_NAME_NUM)));

              if (!priv->locked_modifier_valid ||
                  caps_lock_state != priv->caps_lock_state)
                {
                  g_debug ("Caps lock state: %s",
                           caps_lock_state ? "locked" : "unlocked");
                  priv->caps_lock_state = caps_lock_state;

                  if (klass->on_caps_lock_state_changed)
                    {
                      klass->on_caps_lock_state_changed (session,
                                                         caps_lock_state);
                    }
                }

              if (!priv->locked_modifier_valid ||
                  num_lock_state != priv->num_lock_state)
                {
                  g_debug ("Num lock state: %s",
                           num_lock_state ? "locked" : "unlocked");
                  priv->num_lock_state = num_lock_state;

                  if (klass->on_num_lock_state_changed)
                    {
                      klass->on_num_lock_state_changed (session,
                                                        num_lock_state);
                    }
                }

              priv->locked_modifier_valid = TRUE;
            }
          else
            {
              g_warning ("Couldn't update caps lock / num lock state, "
                         "no keymap");
            }
          break;
        case EI_EVENT_PONG:
          {
            GrdEiPing *ping = NULL;

            if (g_hash_table_steal_extended (priv->pings,
                                             ei_event_pong_get_ping (event),
                                             NULL,
                                             (gpointer *) &ping))
              finish_and_free_ping (ping);
            break;
          }
        default:
          handled = FALSE;
          break;
        }

      if (handled)
        {
          g_debug ("ei: Handled event type %s",
                   ei_event_type_to_string (ei_event_type));
        }
      else
        {
          g_debug ("ei: Didn't handle event type %s",
                   ei_event_type_to_string (ei_event_type));
        }
      ei_event_unref (event);
    }

  return G_SOURCE_CONTINUE;
}

static void
on_eis_connected (GObject      *object,
                  GAsyncResult *result,
                  gpointer      user_data)
{
  GrdDBusMutterRemoteDesktopSession *proxy =
    GRD_DBUS_MUTTER_REMOTE_DESKTOP_SESSION (object);
  g_autoptr (GVariant) fd_variant = NULL;
  g_autoptr (GUnixFDList) fd_list = NULL;
  GrdSession *session;
  GrdSessionPrivate *priv;
  int fd_idx = -1;
  int fd;
  g_autoptr (GError) error = NULL;
  const char *remote_desktop_session_id;
  GrdDBusMutterScreenCast *screen_cast_proxy;
  GVariantBuilder properties_builder;
  GVariant *properties_variant;
  int ret;

  if (!grd_dbus_mutter_remote_desktop_session_call_connect_to_eis_finish (proxy,
                                                                          &fd_variant,
                                                                          &fd_list,
                                                                          result,
                                                                          &error))
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      g_warning ("Failed to connect to EIS: %s",
                 error->message);
      grd_session_stop (GRD_SESSION (user_data));
      return;
    }

  session = GRD_SESSION (user_data);
  priv = grd_session_get_instance_private (session);

  fd_idx = g_variant_get_handle (fd_variant);
  if (!G_IS_UNIX_FD_LIST (fd_list) ||
      fd_idx < 0 || fd_idx >= g_unix_fd_list_get_length (fd_list))
    {
      g_warning ("Failed to acquire file descriptor for EI backend: Invalid "
                 "file descriptor list sent by display server");
      grd_session_stop (GRD_SESSION (user_data));
      return;
    }

  fd = g_unix_fd_list_get (fd_list, fd_idx, &error);

  priv->ei = ei_new_sender (session);
  ei_configure_name (priv->ei, "gnome-remote-desktop");

  ret = ei_setup_backend_fd (priv->ei, fd);
  if (ret < 0)
    {
      g_warning ("Failed to setup libei backend: %s", g_strerror (-ret));

      grd_session_stop (GRD_SESSION (user_data));
      return;
    }

  priv->ei_source = grd_create_fd_source (ei_get_fd (priv->ei),
                                          "libei",
                                          grd_ei_source_prepare,
                                          grd_ei_source_dispatch,
                                          session, NULL);
  g_source_attach (priv->ei_source, NULL);
  g_source_unref (priv->ei_source);

  g_signal_connect (priv->remote_desktop_session, "closed",
                    G_CALLBACK (on_remote_desktop_session_closed),
                    session);
  g_signal_connect (priv->remote_desktop_session, "selection-owner-changed",
                    G_CALLBACK (on_remote_desktop_session_selection_owner_changed),
                    session);
  g_signal_connect (priv->remote_desktop_session, "selection-transfer",
                    G_CALLBACK (on_remote_desktop_session_selection_transfer),
                    session);

  remote_desktop_session_id =
    grd_dbus_mutter_remote_desktop_session_get_session_id (priv->remote_desktop_session);

  g_variant_builder_init (&properties_builder, G_VARIANT_TYPE ("a{sv}"));
  g_variant_builder_add (&properties_builder, "{sv}",
                         "remote-desktop-session-id",
                         g_variant_new_string (remote_desktop_session_id));
  g_variant_builder_add (&properties_builder, "{sv}",
                         "disable-animations",
                         g_variant_new_boolean (TRUE));
  properties_variant = g_variant_builder_end (&properties_builder);

  screen_cast_proxy = grd_context_get_mutter_screen_cast_proxy (priv->context);
  grd_dbus_mutter_screen_cast_call_create_session (screen_cast_proxy,
                                                   properties_variant,
                                                   priv->cancellable,
                                                   on_screen_cast_session_created,
                                                   session);
}

static void
on_remote_desktop_session_proxy_acquired (GObject      *object,
                                          GAsyncResult *result,
                                          gpointer      user_data)
{
  GrdDBusMutterRemoteDesktopSession *session_proxy;
  GrdSession *session;
  GrdSessionPrivate *priv;
  g_autoptr (GError) error = NULL;
  GVariantBuilder options_builder;

  session_proxy =
    grd_dbus_mutter_remote_desktop_session_proxy_new_finish (result, &error);
  if (!session_proxy)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      g_warning ("Failed to acquire remote desktop session proxy: %s",
                 error->message);
      grd_session_stop (GRD_SESSION (user_data));
      return;
    }

  session = GRD_SESSION (user_data);
  priv = grd_session_get_instance_private (session);

  priv->remote_desktop_session = session_proxy;

  g_variant_builder_init (&options_builder, G_VARIANT_TYPE ("a{sv}"));

  grd_dbus_mutter_remote_desktop_session_call_connect_to_eis (
    priv->remote_desktop_session,
    g_variant_builder_end (&options_builder),
    NULL,
    priv->cancellable,
    on_eis_connected,
    session);
}

static void
on_remote_desktop_session_created (GObject      *source_object,
                                   GAsyncResult *res,
                                   gpointer      user_data)
{
  GrdDBusMutterRemoteDesktop *remote_desktop_proxy;
  GrdSession *session;
  GrdSessionPrivate *priv;
  GDBusConnection *connection;
  g_autofree char *session_path = NULL;
  g_autoptr (GError) error = NULL;

  remote_desktop_proxy = GRD_DBUS_MUTTER_REMOTE_DESKTOP (source_object);
  if (!grd_dbus_mutter_remote_desktop_call_create_session_finish (remote_desktop_proxy,
                                                                  &session_path,
                                                                  res,
                                                                  &error))
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      g_warning ("Failed to start remote desktop session: %s\n", error->message);
      grd_session_stop (GRD_SESSION (user_data));
      return;
    }

  session = GRD_SESSION (user_data);
  priv = grd_session_get_instance_private (session);
  connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (remote_desktop_proxy));

  grd_dbus_mutter_remote_desktop_session_proxy_new (connection,
                                                    G_DBUS_PROXY_FLAGS_NONE,
                                                    MUTTER_REMOTE_DESKTOP_BUS_NAME,
                                                    session_path,
                                                    priv->cancellable,
                                                    on_remote_desktop_session_proxy_acquired,
                                                    session);
}

void
grd_session_start (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);
  GrdDBusMutterRemoteDesktop *remote_desktop_proxy;

  priv->cancellable = g_cancellable_new ();

  remote_desktop_proxy = grd_context_get_mutter_remote_desktop_proxy (priv->context);

  g_assert (remote_desktop_proxy);

  grd_dbus_mutter_remote_desktop_call_create_session (remote_desktop_proxy,
                                                      priv->cancellable,
                                                      on_remote_desktop_session_created,
                                                      session);
}

gboolean
grd_session_is_ready (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  return priv->is_ready;
}

static void
grd_session_finalize (GObject *object)
{
  GrdSession *session = GRD_SESSION (object);
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  if (priv->cancellable)
    g_assert (g_cancellable_is_cancelled (priv->cancellable));
  g_clear_object (&priv->cancellable);

  g_assert (!priv->ei_source);

  g_clear_pointer (&priv->pings, g_hash_table_unref);
  g_clear_pointer (&priv->touch_regions, g_hash_table_unref);
  g_clear_pointer (&priv->abs_pointer_regions, g_hash_table_unref);

  g_assert (!priv->xkb_state);
  g_assert (!priv->xkb_keymap);
  g_assert (!priv->xkb_context);

  g_assert (!priv->ei_touch);
  g_assert (!priv->ei_keyboard);
  g_assert (!priv->ei_abs_pointer);
  g_assert (!priv->ei_pointer);
  g_assert (!priv->ei_seat);
  g_assert (!priv->ei);

  g_assert (!priv->remote_desktop_session);
  g_assert (!priv->screen_cast_session);

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

static void
grd_session_set_property (GObject      *object,
                          guint         prop_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
  GrdSession *session = GRD_SESSION (object);
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  switch (prop_id)
    {
    case PROP_CONTEXT:
      priv->context = g_value_get_object (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
grd_session_get_property (GObject    *object,
                          guint       prop_id,
                          GValue     *value,
                          GParamSpec *pspec)
{
  GrdSession *session = GRD_SESSION (object);
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  switch (prop_id)
    {
    case PROP_CONTEXT:
      g_value_set_object (value, priv->context);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
grd_ei_ping_free (GrdEiPing *ping)
{
  g_object_unref (ping->task);
  g_clear_pointer (&ping->ping, ei_ping_unref);
  g_free (ping);
}

static void
cancel_and_free_ping (gpointer user_data)
{
  GrdEiPing *ping = user_data;
  GCancellable *cancellable;

  cancellable = g_task_get_cancellable (ping->task);
  if (!g_cancellable_is_cancelled (cancellable))
    g_cancellable_cancel (cancellable);

  g_task_return_error_if_cancelled (ping->task);
  grd_ei_ping_free (ping);
}

static void
finish_and_free_ping (GrdEiPing *ping)
{
  if (!g_task_return_error_if_cancelled (ping->task))
    g_task_return_boolean (ping->task, TRUE);
  grd_ei_ping_free (ping);
}

static void
grd_session_init (GrdSession *session)
{
  GrdSessionPrivate *priv = grd_session_get_instance_private (session);

  priv->abs_pointer_regions =
    g_hash_table_new_full (g_str_hash, g_str_equal,
                           g_free,
                           (GDestroyNotify) grd_region_free);
  priv->touch_regions =
    g_hash_table_new_full (g_str_hash, g_str_equal,
                           g_free,
                           (GDestroyNotify) grd_region_free);

  priv->pings = g_hash_table_new_full (NULL, NULL,
                                       NULL, cancel_and_free_ping);
}

static void
grd_session_class_init (GrdSessionClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = grd_session_finalize;
  object_class->set_property = grd_session_set_property;
  object_class->get_property = grd_session_get_property;

  g_object_class_install_property (object_class,
                                   PROP_CONTEXT,
                                   g_param_spec_object ("context",
                                                        "GrdContext",
                                                        "The GrdContext instance",
                                                        GRD_TYPE_CONTEXT,
                                                        G_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY |
                                                        G_PARAM_STATIC_STRINGS));

  signals[READY] = g_signal_new ("ready",
                                 G_TYPE_FROM_CLASS (klass),
                                 G_SIGNAL_RUN_LAST,
                                 0,
                                 NULL, NULL, NULL,
                                 G_TYPE_NONE, 0);
  signals[STARTED] = g_signal_new ("started",
                                   G_TYPE_FROM_CLASS (klass),
                                   G_SIGNAL_RUN_LAST,
                                   0,
                                   NULL, NULL, NULL,
                                   G_TYPE_NONE, 0);
  signals[STOPPED] = g_signal_new ("stopped",
                                   G_TYPE_FROM_CLASS (klass),
                                   G_SIGNAL_RUN_LAST,
                                   0,
                                   NULL, NULL, NULL,
                                   G_TYPE_NONE, 0);
  signals[TOUCH_DEVICE_ADDED] = g_signal_new ("touch-device-added",
                                              G_TYPE_FROM_CLASS (klass),
                                              G_SIGNAL_RUN_LAST,
                                              0,
                                              NULL, NULL, NULL,
                                              G_TYPE_NONE, 0);
  signals[TOUCH_DEVICE_REMOVED] = g_signal_new ("touch-device-removed",
                                                G_TYPE_FROM_CLASS (klass),
                                                G_SIGNAL_RUN_LAST,
                                                0,
                                                NULL, NULL, NULL,
                                                G_TYPE_NONE, 0);
}
