/*
 * Copyright (C) 2019 Red Hat Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Carlos Garnacho <carlosg@gnome.org>
 */
#include "config.h"

#include <linux/input-event-codes.h>
#include <X11/extensions/XInput2.h>
#include <X11/extensions/XKB.h>

#ifdef HAVE_LIBGUDEV
#include <gudev/gudev.h>
#endif

#include "backends/meta-input-settings-private.h"
#include "backends/x11/meta-backend-x11.h"
#include "backends/x11/meta-clutter-backend-x11.h"
#include "backends/x11/meta-event-x11.h"
#include "backends/x11/meta-input-device-tool-x11.h"
#include "backends/x11/meta-input-device-x11.h"
#include "backends/x11/meta-keymap-x11.h"
#include "backends/x11/meta-stage-x11.h"
#include "backends/x11/meta-virtual-input-device-x11.h"
#include "backends/x11/meta-xkb-a11y-x11.h"
#include "clutter/clutter-mutter.h"
#include "core/bell.h"
#include "meta-seat-x11.h"
#include "mtk/mtk-x11.h"

enum
{
  PROP_0,
  PROP_BACKEND,
  PROP_OPCODE,
  PROP_POINTER_ID,
  PROP_KEYBOARD_ID,
  N_PROPS,

  /* This property is overridden */
  PROP_TOUCH_MODE,
};

typedef struct _MetaTouchInfo MetaTouchInfo;

struct _MetaTouchInfo
{
  ClutterEventSequence *sequence;
  double x;
  double y;
};

struct _MetaSeatX11
{
  ClutterSeat parent_instance;

  MetaBackend *backend;

  ClutterInputDevice *core_pointer;
  ClutterInputDevice *core_keyboard;
  GList *devices;
  GHashTable *devices_by_id;
  GHashTable *tools_by_serial;
  GHashTable *touch_coords;
  MetaKeymapX11 *keymap;

#ifdef HAVE_LIBGUDEV
  GUdevClient *udev_client;
#endif

  int pointer_id;
  int keyboard_id;
  int opcode;
  ClutterGrabState grab_state;
  guint has_touchscreens : 1;
  guint touch_mode : 1;
  guint has_pointer_focus : 1;
};

static GParamSpec *props[N_PROPS] = { 0 };

G_DEFINE_TYPE (MetaSeatX11, meta_seat_x11, CLUTTER_TYPE_SEAT)

static const char *clutter_input_axis_atom_names[] = {
  "Abs X",              /* CLUTTER_INPUT_AXIS_X */
  "Abs Y",              /* CLUTTER_INPUT_AXIS_Y */
  "Abs Pressure",       /* CLUTTER_INPUT_AXIS_PRESSURE */
  "Abs Tilt X",         /* CLUTTER_INPUT_AXIS_XTILT */
  "Abs Tilt Y",         /* CLUTTER_INPUT_AXIS_YTILT */
  "Abs Wheel",          /* CLUTTER_INPUT_AXIS_WHEEL */
  "Abs Distance",       /* CLUTTER_INPUT_AXIS_DISTANCE */
};

static const char *wacom_type_atoms[] = {
    "STYLUS",
    "CURSOR",
    "ERASER",
    "PAD",
    "TOUCH"
};
#define N_WACOM_TYPE_ATOMS G_N_ELEMENTS (wacom_type_atoms)

enum
{
    WACOM_TYPE_STYLUS,
    WACOM_TYPE_CURSOR,
    WACOM_TYPE_ERASER,
    WACOM_TYPE_PAD,
    WACOM_TYPE_TOUCH,
};

enum
{
  PAD_AXIS_FIRST  = 3, /* First axes are always x/y/pressure, ignored in pads */
  PAD_AXIS_STRIP1 = PAD_AXIS_FIRST,
  PAD_AXIS_STRIP2,
  PAD_AXIS_RING1,
  PAD_AXIS_RING2,
};

#define N_AXIS_ATOMS    G_N_ELEMENTS (clutter_input_axis_atom_names)

static Atom clutter_input_axis_atoms[N_AXIS_ATOMS] = { 0, };

static Display *
xdisplay_from_seat (MetaSeatX11 *seat_x11)
{
  return meta_backend_x11_get_xdisplay (META_BACKEND_X11 (seat_x11->backend));
}

static Window
root_xwindow_from_seat (MetaSeatX11 *seat_x11)
{
  MetaBackendX11 *backend_x11 = META_BACKEND_X11 (seat_x11->backend);

  return meta_backend_x11_get_root_xwindow (backend_x11);
}

static void
translate_valuator_class (Display             *xdisplay,
                          ClutterInputDevice  *device,
                          XIValuatorClassInfo *class)
{
  static gboolean atoms_initialized = FALSE;
  ClutterInputAxis i, axis = CLUTTER_INPUT_AXIS_IGNORE;

  if (G_UNLIKELY (!atoms_initialized))
    {
      XInternAtoms (xdisplay,
                    (char **) clutter_input_axis_atom_names, N_AXIS_ATOMS,
                    False,
                    clutter_input_axis_atoms);

      atoms_initialized = TRUE;
    }

  for (i = 0;
       i < N_AXIS_ATOMS;
       i += 1)
    {
      if (clutter_input_axis_atoms[i] == class->label)
        {
          axis = i + 1;
          break;
        }
    }

  meta_input_device_x11_add_axis (device, axis,
                                  class->min,
                                  class->max,
                                  class->resolution);

  g_debug ("Added axis '%s' (min:%.2f, max:%.2fd, res:%d) of device %d",
           axis == CLUTTER_INPUT_AXIS_IGNORE ?
             "Ignored" :
             clutter_input_axis_atom_names[axis - 1],
           class->min,
           class->max,
           class->resolution,
           meta_input_device_x11_get_device_id (device));
}

static void
translate_device_classes (Display             *xdisplay,
                          ClutterInputDevice  *device,
                          XIAnyClassInfo     **classes,
                          int                  n_classes)
{
  int i;

  for (i = 0; i < n_classes; i++)
    {
      XIAnyClassInfo *class_info = classes[i];

      switch (class_info->type)
        {
        case XIValuatorClass:
          translate_valuator_class (xdisplay, device,
                                    (XIValuatorClassInfo *) class_info);
          break;

        case XIScrollClass:
          {
            XIScrollClassInfo *scroll_info = (XIScrollClassInfo *) class_info;
            ClutterScrollDirection direction;

            if (scroll_info->scroll_type == XIScrollTypeVertical)
              direction = CLUTTER_SCROLL_DOWN;
            else
              direction = CLUTTER_SCROLL_RIGHT;

            g_debug ("Scroll valuator %d: %s, increment: %f",
                     scroll_info->number,
                     scroll_info->scroll_type == XIScrollTypeVertical
                     ? "vertical"
                     : "horizontal",
                     scroll_info->increment);

            meta_input_device_x11_add_scroll_info (device,
                                                   scroll_info->number,
                                                   direction,
                                                   scroll_info->increment);
          }
          break;

        default:
          break;
        }
    }
}

static gboolean
is_touch_device (XIAnyClassInfo           **classes,
                 int                        n_classes,
                 ClutterInputDeviceType    *device_type,
                 ClutterInputCapabilities  *capabilities,
                 uint32_t                  *n_touch_points)
{
  int i;

  for (i = 0; i < n_classes; i++)
    {
      XITouchClassInfo *class = (XITouchClassInfo *) classes[i];

      if (class->type != XITouchClass)
        continue;

      if (class->num_touches > 0)
        {
          if (class->mode == XIDirectTouch)
            {
              *device_type = CLUTTER_TOUCHSCREEN_DEVICE;
              *capabilities = CLUTTER_INPUT_CAPABILITY_TOUCH;
            }
          else if (class->mode == XIDependentTouch)
            {
              *device_type = CLUTTER_TOUCHPAD_DEVICE;
              *capabilities =
                CLUTTER_INPUT_CAPABILITY_POINTER |
                CLUTTER_INPUT_CAPABILITY_TOUCHPAD;
            }
          else
            {
              continue;
            }

          *n_touch_points = class->num_touches;

          return TRUE;
        }
    }

  return FALSE;
}

static gboolean
is_touchpad_device (MetaSeatX11  *seat_x11,
                    XIDeviceInfo *info)
{
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  gulong nitems, bytes_after;
  uint32_t *data = NULL;
  int rc, format;
  Atom type;
  Atom prop;

  prop = XInternAtom (xdisplay,
                      "libinput Tapping Enabled", True);
  if (prop == None)
    return FALSE;

  mtk_x11_error_trap_push (xdisplay);
  rc = XIGetProperty (xdisplay,
                      info->deviceid,
                      prop,
                      0, 1, False, XA_INTEGER, &type, &format, &nitems, &bytes_after,
                      (guchar **) &data);
  mtk_x11_error_trap_pop (xdisplay);

  /* We don't care about the data */
  XFree (data);

  if (rc != Success || type != XA_INTEGER || format != 8 || nitems != 1)
    return FALSE;

  return TRUE;
}

static gboolean
get_device_ids (MetaSeatX11   *seat_x11,
                XIDeviceInfo  *info,
                char         **vendor_id,
                char         **product_id)
{
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  gulong nitems, bytes_after;
  uint32_t *data = NULL;
  int rc, format;
  Atom type;

  mtk_x11_error_trap_push (xdisplay);
  rc = XIGetProperty (xdisplay,
                      info->deviceid,
                      XInternAtom (xdisplay, "Device Product ID", False),
                      0, 2, False, XA_INTEGER, &type, &format, &nitems, &bytes_after,
                      (guchar **) &data);
  mtk_x11_error_trap_pop (xdisplay);

  if (rc != Success || type != XA_INTEGER || format != 32 || nitems != 2)
    {
      XFree (data);
      return FALSE;
    }

  if (vendor_id)
    *vendor_id = g_strdup_printf ("%.4x", data[0]);
  if (product_id)
    *product_id = g_strdup_printf ("%.4x", data[1]);

  XFree (data);

  return TRUE;
}

static char *
get_device_node_path (MetaSeatX11  *seat_x11,
                      XIDeviceInfo *info)
{
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  gulong nitems, bytes_after;
  guchar *data;
  int rc, format;
  Atom prop, type;
  char *node_path;

  prop = XInternAtom (xdisplay, "Device Node", False);
  if (prop == None)
    return NULL;

  mtk_x11_error_trap_push (xdisplay);

  rc = XIGetProperty (xdisplay,
                      info->deviceid, prop, 0, 1024, False,
                      XA_STRING, &type, &format, &nitems, &bytes_after,
                      (guchar **) &data);

  if (mtk_x11_error_trap_pop_with_return (xdisplay))
    return NULL;

  if (rc != Success || type != XA_STRING || format != 8)
    {
      XFree (data);
      return FALSE;
    }

  node_path = g_strdup ((char *) data);
  XFree (data);

  return node_path;
}

static void
get_pad_features (XIDeviceInfo *info,
                  uint32_t     *n_rings,
                  uint32_t     *n_strips)
{
  int i, rings = 0, strips = 0;

  for (i = PAD_AXIS_FIRST; i < info->num_classes; i++)
    {
      XIValuatorClassInfo *valuator = (XIValuatorClassInfo*) info->classes[i];
      int axis = valuator->number;

      if (valuator->type != XIValuatorClass)
        continue;
      if (valuator->max <= 1)
        continue;

      /* Ring/strip axes are fixed in pad devices as handled by the
       * wacom driver. Match those to detect pad features.
       */
      if (axis == PAD_AXIS_STRIP1 || axis == PAD_AXIS_STRIP2)
        strips++;
      else if (axis == PAD_AXIS_RING1 || axis == PAD_AXIS_RING2)
        rings++;
    }

  *n_rings = rings;
  *n_strips = strips;
}

/* The Wacom driver exports the tool type as property. Use that over
   guessing based on the device name */
static gboolean
guess_source_from_wacom_type (MetaSeatX11              *seat_x11,
                              XIDeviceInfo             *info,
                              ClutterInputDeviceType   *source_out,
                              ClutterInputCapabilities *capabilities_out)
{
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  gulong nitems, bytes_after;
  uint32_t *data = NULL;
  int rc, format;
  Atom type;
  Atom prop;
  Atom device_type;
  Atom types[N_WACOM_TYPE_ATOMS];

  prop = XInternAtom (xdisplay, "Wacom Tool Type", True);
  if (prop == None)
    return FALSE;

  mtk_x11_error_trap_push (xdisplay);
  rc = XIGetProperty (xdisplay,
                      info->deviceid,
                      prop,
                      0, 1, False, XA_ATOM, &type, &format, &nitems, &bytes_after,
                      (guchar **) &data);
  mtk_x11_error_trap_pop (xdisplay);

  if (rc != Success || type != XA_ATOM || format != 32 || nitems != 1)
    {
      XFree (data);
      return FALSE;
    }

  device_type = *data;
  XFree (data);

  if (device_type == 0)
      return FALSE;

  rc = XInternAtoms (xdisplay,
                     (char **)wacom_type_atoms,
                     N_WACOM_TYPE_ATOMS,
                     False,
                     types);
  if (rc == 0)
      return FALSE;

  if (device_type == types[WACOM_TYPE_STYLUS])
    {
      *source_out = CLUTTER_PEN_DEVICE;
      *capabilities_out = CLUTTER_INPUT_CAPABILITY_TABLET_TOOL;
    }
  else if (device_type == types[WACOM_TYPE_CURSOR])
    {
      *source_out = CLUTTER_CURSOR_DEVICE;
      *capabilities_out = CLUTTER_INPUT_CAPABILITY_TABLET_TOOL;
    }
  else if (device_type == types[WACOM_TYPE_ERASER])
    {
      *source_out = CLUTTER_ERASER_DEVICE;
      *capabilities_out = CLUTTER_INPUT_CAPABILITY_TABLET_TOOL;
    }
  else if (device_type == types[WACOM_TYPE_PAD])
    {
      *source_out = CLUTTER_PAD_DEVICE;
      *capabilities_out = CLUTTER_INPUT_CAPABILITY_TABLET_PAD;
    }
  else if (device_type == types[WACOM_TYPE_TOUCH])
    {
        uint32_t num_touches = 0;

        if (!is_touch_device (info->classes, info->num_classes,
                              source_out, capabilities_out, &num_touches))
          {
            *source_out = CLUTTER_TOUCHSCREEN_DEVICE;
            *capabilities_out = CLUTTER_INPUT_CAPABILITY_TOUCH;
          }
    }
  else
    {
      return FALSE;
    }

  return TRUE;
}

#ifdef HAVE_LIBGUDEV
static gboolean
has_udev_property (GUdevDevice *udev_device,
                   const char  *property_name)
{
  g_autoptr (GUdevDevice) parent_udev_device = NULL;

  if (NULL != g_udev_device_get_property (udev_device, property_name))
    return TRUE;

  parent_udev_device = g_udev_device_get_parent (udev_device);

  if (!parent_udev_device)
    return FALSE;

  return g_udev_device_get_property (parent_udev_device, property_name) != NULL;
}
#endif

static ClutterInputDevice *
create_device (MetaSeatX11    *seat_x11,
               ClutterBackend *clutter_backend,
               XIDeviceInfo   *info)
{
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  ClutterInputDeviceType source, touch_source;
  ClutterInputCapabilities capabilities = 0;
  ClutterInputDevice *retval;
  ClutterInputMode mode;
  uint32_t num_touches = 0, num_rings = 0, num_strips = 0;
  char *vendor_id = NULL, *product_id = NULL, *node_path = NULL;

  if (info->use == XIMasterKeyboard || info->use == XISlaveKeyboard)
    {
      source = CLUTTER_KEYBOARD_DEVICE;
      capabilities = CLUTTER_INPUT_CAPABILITY_KEYBOARD;
    }
  else if (is_touchpad_device (seat_x11, info))
    {
      source = CLUTTER_TOUCHPAD_DEVICE;
    }
  else if (info->use == XISlavePointer &&
           is_touch_device (info->classes, info->num_classes,
                            &touch_source, &capabilities,
                            &num_touches))
    {
      source = touch_source;
    }
  else if (!guess_source_from_wacom_type (seat_x11, info, &source, &capabilities))
    {
      char *name;

      name = g_ascii_strdown (info->name, -1);

      if (strstr (name, "eraser") != NULL)
        {
          source = CLUTTER_ERASER_DEVICE;
          capabilities = CLUTTER_INPUT_CAPABILITY_TABLET_TOOL;
        }
      else if (strstr (name, "cursor") != NULL)
        {
          source = CLUTTER_CURSOR_DEVICE;
          capabilities = CLUTTER_INPUT_CAPABILITY_TABLET_TOOL;
        }
      else if (strstr (name, " pad") != NULL)
        {
          source = CLUTTER_PAD_DEVICE;
          capabilities = CLUTTER_INPUT_CAPABILITY_TABLET_PAD;
        }
      else if (strstr (name, "wacom") != NULL || strstr (name, "pen") != NULL)
        {
          source = CLUTTER_PEN_DEVICE;
          capabilities = CLUTTER_INPUT_CAPABILITY_TABLET_TOOL;
        }
      else if (strstr (name, "touchpad") != NULL)
        {
          source = CLUTTER_TOUCHPAD_DEVICE;
          capabilities =
            CLUTTER_INPUT_CAPABILITY_POINTER |
            CLUTTER_INPUT_CAPABILITY_TOUCHPAD;
        }
      else
        {
          source = CLUTTER_POINTER_DEVICE;
          capabilities = CLUTTER_INPUT_CAPABILITY_POINTER;
        }

      g_free (name);
    }

  switch (info->use)
    {
    case XIMasterKeyboard:
    case XIMasterPointer:
      mode = CLUTTER_INPUT_MODE_LOGICAL;
      break;

    case XISlaveKeyboard:
    case XISlavePointer:
      mode = CLUTTER_INPUT_MODE_PHYSICAL;
      break;

    case XIFloatingSlave:
    default:
      mode = CLUTTER_INPUT_MODE_FLOATING;
      break;
    }

  if (info->use != XIMasterKeyboard &&
      info->use != XIMasterPointer)
    {
      get_device_ids (seat_x11, info, &vendor_id, &product_id);
      node_path = get_device_node_path (seat_x11, info);
    }

#ifdef HAVE_LIBGUDEV
  if (node_path)
    {
      g_autoptr (GUdevDevice) udev_device = NULL;

      udev_device = g_udev_client_query_by_device_file (seat_x11->udev_client,
                                                        node_path);
      if (udev_device)
        {
          if (has_udev_property (udev_device, "ID_INPUT_TRACKBALL"))
            capabilities |= CLUTTER_INPUT_CAPABILITY_TRACKBALL;
          if (has_udev_property (udev_device, "ID_INPUT_POINTINGSTICK"))
            capabilities |= CLUTTER_INPUT_CAPABILITY_TRACKPOINT;
        }
    }
#endif

  if (source == CLUTTER_PAD_DEVICE)
    get_pad_features (info, &num_rings, &num_strips);

  retval = g_object_new (META_TYPE_INPUT_DEVICE_X11,
                         "backend", seat_x11->backend,
                         "name", info->name,
                         "id", info->deviceid,
                         "has-cursor", (info->use == XIMasterPointer),
                         "device-type", source,
                         "capabilities", capabilities,
                         "device-mode", mode,
                         "vendor-id", vendor_id,
                         "product-id", product_id,
                         "device-node", node_path,
                         "n-rings", num_rings,
                         "n-strips", num_strips,
                         "n-mode-groups", MAX (num_rings, num_strips),
                         "seat", seat_x11,
                         NULL);

  translate_device_classes (xdisplay, retval,
                            info->classes,
                            info->num_classes);

  g_free (vendor_id);
  g_free (product_id);
  g_free (node_path);

  g_debug ("Created device '%s' (id: %d, has-cursor: %s)",
           info->name,
           info->deviceid,
           info->use == XIMasterPointer ? "yes" : "no");

  return retval;
}

static void
pad_passive_button_grab (MetaSeatX11        *seat_x11,
                         ClutterInputDevice *device)
{
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  Window root_xwindow = root_xwindow_from_seat (seat_x11);
  XIGrabModifiers xi_grab_mods = { XIAnyModifier, };
  XIEventMask xi_event_mask;
  int device_id, rc;

  device_id = meta_input_device_x11_get_device_id (device);

  xi_event_mask.deviceid = device_id;
  xi_event_mask.mask_len = XIMaskLen (XI_LASTEVENT);
  xi_event_mask.mask = g_new0 (unsigned char, xi_event_mask.mask_len);

  XISetMask (xi_event_mask.mask, XI_Motion);
  XISetMask (xi_event_mask.mask, XI_ButtonPress);
  XISetMask (xi_event_mask.mask, XI_ButtonRelease);

  mtk_x11_error_trap_push (xdisplay);
  rc = XIGrabButton (xdisplay,
                     device_id, XIAnyButton,
                     root_xwindow, None,
                     XIGrabModeSync, XIGrabModeSync,
                     True, &xi_event_mask, 1, &xi_grab_mods);
  if (rc != 0)
    {
      g_warning ("Could not passively grab pad device: %s",
                 clutter_input_device_get_device_name (device));
    }
  else
    {
      XIAllowEvents (xdisplay, device_id, XIAsyncDevice, CLUTTER_CURRENT_TIME);
    }

  mtk_x11_error_trap_pop (xdisplay);

  g_free (xi_event_mask.mask);
}

static void
update_touch_mode (MetaSeatX11 *seat_x11)
{
  gboolean touch_mode;

  touch_mode = seat_x11->has_touchscreens;

  if (seat_x11->touch_mode == touch_mode)
    return;

  seat_x11->touch_mode = touch_mode;
  g_object_notify (G_OBJECT (seat_x11), "touch-mode");
}

static ClutterInputDevice *
add_device (MetaSeatX11    *seat_x11,
            ClutterBackend *clutter_backend,
            XIDeviceInfo   *info)
{
  ClutterInputDevice *device;

  device = create_device (seat_x11, clutter_backend, info);

  g_hash_table_replace (seat_x11->devices_by_id,
                        GINT_TO_POINTER (info->deviceid),
                        device);

  if (info->use == XIMasterPointer &&
      info->deviceid == seat_x11->pointer_id)
    {
      seat_x11->core_pointer = device;
    }
  else if (info->use == XIMasterKeyboard &&
           info->deviceid == seat_x11->keyboard_id)
    {
      seat_x11->core_keyboard = device;
    }
  else if ((info->use == XISlavePointer &&
            info->attachment == seat_x11->pointer_id) ||
           (info->use == XISlaveKeyboard &&
            info->attachment == seat_x11->keyboard_id))
    {
      seat_x11->devices = g_list_prepend (seat_x11->devices, device);
    }
  else
    {
      g_warning ("Unhandled device: %s",
                 clutter_input_device_get_device_name (device));
    }

  if (clutter_input_device_get_device_type (device) == CLUTTER_PAD_DEVICE)
    pad_passive_button_grab (seat_x11, device);

  return device;
}

static gboolean
has_touchscreens (MetaSeatX11 *seat_x11)
{
  GList *l;

  for (l = seat_x11->devices; l; l = l->next)
    {
      if (clutter_input_device_get_device_type (l->data) == CLUTTER_TOUCHSCREEN_DEVICE)
        return TRUE;
    }

  return FALSE;
}

static void
remove_device (MetaSeatX11        *seat_x11,
               ClutterInputDevice *device)
{
  if (seat_x11->core_pointer == device)
    {
      seat_x11->core_pointer = NULL;
    }
  else if (seat_x11->core_keyboard == device)
    {
      seat_x11->core_keyboard = NULL;
    }
  else
    {
      seat_x11->devices = g_list_remove (seat_x11->devices, device);
    }
}

static gboolean
meta_seat_x11_handle_event_post (ClutterSeat        *seat,
                                 const ClutterEvent *event)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (seat);
  ClutterInputDevice *device;
  MetaInputSettings *input_settings;
  ClutterEventType event_type;
  gboolean is_touch;

  event_type = clutter_event_type (event);

  if (event_type != CLUTTER_DEVICE_ADDED &&
      event_type != CLUTTER_DEVICE_REMOVED)
    return TRUE;

  device = clutter_event_get_device (event);
  is_touch =
    clutter_input_device_get_device_type (device) == CLUTTER_TOUCHSCREEN_DEVICE;
  input_settings = meta_backend_get_input_settings (seat_x11->backend);

  switch (event_type)
    {
      case CLUTTER_DEVICE_ADDED:
        meta_input_settings_add_device (input_settings, device);
        seat_x11->has_touchscreens |= is_touch;
        break;
      case CLUTTER_DEVICE_REMOVED:
        if (is_touch)
          seat_x11->has_touchscreens = has_touchscreens (seat_x11);
        meta_input_settings_remove_device (input_settings, device);
        break;
      default:
        break;
    }

  if (is_touch)
    update_touch_mode (seat_x11);

  return TRUE;
}

static uint
device_get_tool_serial (MetaSeatX11        *seat_x11,
                        ClutterInputDevice *device)
{
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  gulong nitems, bytes_after;
  uint32_t *data = NULL;
  int serial_id = 0;
  int rc, format;
  Atom type;
  Atom prop;

  prop = XInternAtom (xdisplay, "Wacom Serial IDs", True);
  if (prop == None)
    return 0;

  mtk_x11_error_trap_push (xdisplay);
  rc = XIGetProperty (xdisplay,
                      meta_input_device_x11_get_device_id (device),
                      prop, 0, 4, FALSE, XA_INTEGER, &type, &format, &nitems, &bytes_after,
                      (guchar **) &data);
  mtk_x11_error_trap_pop (xdisplay);

  if (rc == Success && type == XA_INTEGER && format == 32 && nitems >= 4)
    serial_id = data[3];

  XFree (data);

  return serial_id;
}

static ClutterEvent *
translate_hierarchy_event (ClutterBackend   *clutter_backend,
                           MetaSeatX11      *seat_x11,
                           XIHierarchyEvent *ev)
{
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  int i;
  ClutterEvent *event = NULL;

  for (i = 0; i < ev->num_info; i++)
    {
      if (ev->info[i].flags & XIDeviceEnabled &&
          !g_hash_table_lookup (seat_x11->devices_by_id,
                                GINT_TO_POINTER (ev->info[i].deviceid)))
        {
          XIDeviceInfo *info;
          int n_devices;

          g_debug ("Hierarchy event: device enabled");

          mtk_x11_error_trap_push (xdisplay);
          info = XIQueryDevice (xdisplay,
                                ev->info[i].deviceid,
                                &n_devices);
          mtk_x11_error_trap_pop (xdisplay);
          if (info != NULL)
            {
              ClutterInputDevice *device;

              device = add_device (seat_x11, clutter_backend, &info[0]);

              event = clutter_event_device_notify_new (CLUTTER_DEVICE_ADDED,
                                                       CLUTTER_EVENT_NONE,
                                                       ms2us (ev->time),
                                                       device);
              XIFreeDeviceInfo (info);
            }
        }
      else if (ev->info[i].flags & XIDeviceDisabled)
        {
          g_autoptr (ClutterInputDevice) device = NULL;
          g_debug ("Hierarchy event: device disabled");

          g_hash_table_steal_extended (seat_x11->devices_by_id,
                                       GINT_TO_POINTER (ev->info[i].deviceid),
                                       NULL,
                                       (gpointer) &device);

          if (device != NULL)
            {
              remove_device (seat_x11, device);

              event = clutter_event_device_notify_new (CLUTTER_DEVICE_REMOVED,
                                                       CLUTTER_EVENT_NONE,
                                                       ms2us (ev->time),
                                                       device);
            }
        }
      else if ((ev->info[i].flags & XISlaveAttached) ||
               (ev->info[i].flags & XISlaveDetached))
        {
          g_debug ("Hierarchy event: physical device %s",
                   (ev->info[i].flags & XISlaveAttached)
                   ? "attached"
                   : "detached");
        }
    }

  return event;
}

static void
translate_property_event (MetaSeatX11 *seat_x11,
                          XIEvent     *event)
{
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  XIPropertyEvent *xev = (XIPropertyEvent *) event;
  Atom serial_ids_prop;
  ClutterInputDevice *device;

  serial_ids_prop = XInternAtom (xdisplay, "Wacom Serial IDs", True);
  if (serial_ids_prop == None)
    return;

  device = g_hash_table_lookup (seat_x11->devices_by_id,
                                GINT_TO_POINTER (xev->deviceid));
  if (!device)
    return;

  if (xev->property == serial_ids_prop)
    {
      ClutterInputDeviceTool *tool = NULL;
      ClutterInputDeviceToolType type;
      MetaInputSettings *input_settings;
      int serial_id;

      serial_id = device_get_tool_serial (seat_x11, device);

      if (serial_id != 0)
        {
          tool = g_hash_table_lookup (seat_x11->tools_by_serial,
                                      GUINT_TO_POINTER (serial_id));
          if (!tool)
            {
              type = clutter_input_device_get_device_type (device) == CLUTTER_ERASER_DEVICE ?
                CLUTTER_INPUT_DEVICE_TOOL_ERASER : CLUTTER_INPUT_DEVICE_TOOL_PEN;
              tool = meta_input_device_tool_x11_new (serial_id, type);
              g_hash_table_insert (seat_x11->tools_by_serial,
                                   GUINT_TO_POINTER (serial_id),
                                   tool);
            }
        }

      meta_input_device_x11_update_tool (device, tool);
      input_settings = meta_backend_get_input_settings (seat_x11->backend);
      meta_input_settings_notify_tool_change (input_settings, device, tool);
    }
}

static void
emulate_motion (MetaSeatX11 *seat_x11,
                double       x,
                double       y)
{
  ClutterInputDevice *pointer;
  ClutterEvent *event;

  pointer = clutter_seat_get_pointer (CLUTTER_SEAT (seat_x11));

  event = clutter_event_motion_new (CLUTTER_EVENT_FLAG_SYNTHETIC,
                                    CLUTTER_CURRENT_TIME,
                                    pointer,
                                    NULL, 0,
                                    GRAPHENE_POINT_INIT (x, y),
                                    GRAPHENE_POINT_INIT (0, 0),
                                    GRAPHENE_POINT_INIT (0, 0),
                                    GRAPHENE_POINT_INIT (0, 0),
                                    NULL);

  clutter_event_put (event);
  clutter_event_free (event);
}

static void
translate_raw_event (MetaSeatX11 *seat_x11,
                     XEvent      *xevent)
{
  ClutterInputDevice *device;
  XGenericEventCookie *cookie;
  XIEvent *xi_event;
  XIRawEvent *xev;
  float x,y;

  cookie = &xevent->xcookie;
  xi_event = (XIEvent *) cookie->data;
  xev = (XIRawEvent *) xi_event;

  device = g_hash_table_lookup (seat_x11->devices_by_id,
                                GINT_TO_POINTER (xev->deviceid));
  if (device == NULL)
    return;

  switch (cookie->evtype)
    {
    case XI_RawMotion:
      g_debug ("raw motion: device:%d '%s'",
               meta_input_device_x11_get_device_id (device),
               clutter_input_device_get_device_name (device));

      /* We don't get actual pointer location with raw events, and we cannot
       * rely on `clutter_input_device_get_coords()` either because of
       * unreparented toplevels (like all client-side decoration windows),
       * so we need to explicitly query the pointer here...
       */
      if (meta_input_device_x11_get_pointer_location (device, &x, &y))
        {
          if (_clutter_is_input_pointer_a11y_enabled (device))
            _clutter_input_pointer_a11y_on_motion_event (device, x, y);
          if (!seat_x11->has_pointer_focus)
            emulate_motion (seat_x11, x, y);
        }
      break;
    case XI_RawButtonPress:
    case XI_RawButtonRelease:
      g_debug ("raw button %s: device:%d '%s' button %i",
               cookie->evtype == XI_RawButtonPress
               ? "press  "
               : "release",
               meta_input_device_x11_get_device_id (device),
               clutter_input_device_get_device_name (device),
               xev->detail);
      if (_clutter_is_input_pointer_a11y_enabled (device))
        {
          _clutter_input_pointer_a11y_on_button_event (device,
                                                       xev->detail,
                                                       (cookie->evtype == XI_RawButtonPress));
        }
      break;
    }
}

static gboolean
translate_pad_axis (ClutterInputDevice *device,
                    XIValuatorState    *valuators,
                    ClutterEventType   *evtype,
                    uint32_t           *number,
                    double             *value)
{
  double *values;
  int i;

  values = valuators->values;

  for (i = PAD_AXIS_FIRST; i < valuators->mask_len * 8; i++)
    {
      double val;
      uint32_t axis_number = 0;

      if (!XIMaskIsSet (valuators->mask, i))
        continue;

      val = *values++;
      if (val <= 0)
        continue;

      meta_input_device_x11_translate_axis (device, i, val, value);

      if (i == PAD_AXIS_RING1 || i == PAD_AXIS_RING2)
        {
          *evtype = CLUTTER_PAD_RING;
          (*value) *= 360.0;
        }
      else if (i == PAD_AXIS_STRIP1 || i == PAD_AXIS_STRIP2)
        {
          *evtype = CLUTTER_PAD_STRIP;
        }
      else
        continue;

      if (i == PAD_AXIS_STRIP2 || i == PAD_AXIS_RING2)
        axis_number++;

      *number = axis_number;
      return TRUE;
    }

  return FALSE;
}

static ClutterEvent *
translate_pad_axis_event (XIDeviceEvent      *xev,
                          ClutterInputDevice *device)
{
  double value;
  uint32_t number, mode = 0;
  ClutterEventType evtype;
  ClutterEvent *event = NULL;

  if (!translate_pad_axis (device, &xev->valuators,
                           &evtype, &number, &value))
    return FALSE;

  /* When touching a ring/strip a first XI_Motion event
   * is generated. Use it to reset the pad state, so
   * later events actually have a directionality.
   */
  if (xev->evtype == XI_Motion)
    value = -1;

#ifdef HAVE_LIBWACOM
  mode = meta_input_device_x11_get_pad_group_mode (device, number);
#endif

  if (evtype == CLUTTER_PAD_RING)
    {
      event = clutter_event_pad_ring_new (CLUTTER_EVENT_NONE,
                                          ms2us (xev->time),
                                          device,
                                          CLUTTER_INPUT_DEVICE_PAD_SOURCE_UNKNOWN,
                                          number,
                                          0,
                                          value,
                                          mode);
    }
  else
    {
      event = clutter_event_pad_strip_new (CLUTTER_EVENT_NONE,
                                           ms2us (xev->time),
                                           device,
                                           CLUTTER_INPUT_DEVICE_PAD_SOURCE_UNKNOWN,
                                           number,
                                           0,
                                           value,
                                           mode);
    }

  g_debug ("%s: win:0x%x, device:%d '%s', time:%lu "
           "(value:%f)",
           evtype == CLUTTER_PAD_RING
           ? "pad ring  "
           : "pad strip",
           (unsigned int) xev->event,
           meta_input_device_x11_get_device_id (device),
           clutter_input_device_get_device_name (device),
           xev->time, value);

  return event;
}

static ClutterStage *
get_event_stage (MetaSeatX11 *seat_x11,
                 XIEvent     *xi_event)
{
  Window xwindow = None;

  switch (xi_event->evtype)
    {
    case XI_KeyPress:
    case XI_KeyRelease:
    case XI_ButtonPress:
    case XI_ButtonRelease:
    case XI_Motion:
    case XI_TouchBegin:
    case XI_TouchUpdate:
    case XI_TouchEnd:
      {
        XIDeviceEvent *xev = (XIDeviceEvent *) xi_event;

        xwindow = xev->event;
      }
      break;

    case XI_Enter:
    case XI_Leave:
    case XI_FocusIn:
    case XI_FocusOut:
      {
        XIEnterEvent *xev = (XIEnterEvent *) xi_event;

        xwindow = xev->event;
      }
      break;

    case XI_HierarchyChanged:
      return CLUTTER_STAGE (meta_backend_get_stage (seat_x11->backend));

    default:
      break;
    }

  if (xwindow == None)
    return NULL;

  return meta_x11_get_stage_from_window (xwindow);
}

/*
 * print_key_sym: Translate a symbol to its printable form if any
 * @symbol: the symbol to translate
 * @buffer: the buffer where to put the translated string
 * @len: size of the buffer
 *
 * Translates @symbol into a printable representation in @buffer, if possible.
 *
 * Return value: The number of bytes of the translated string, 0 if the
 *               symbol can't be printed
 *
 * Note: The code is derived from libX11's src/KeyBind.c
 *       Copyright 1985, 1987, 1998  The Open Group
 *
 * Note: This code works for Latin-1 symbols. clutter_keysym_to_unicode()
 *       does the work for the other keysyms.
 */
static int
print_keysym (uint32_t symbol,
              char    *buffer,
              int      len)
{
  unsigned long high_bytes;
  unsigned char c;

  high_bytes = symbol >> 8;
  if (!(len &&
        ((high_bytes == 0) ||
         ((high_bytes == 0xFF) &&
          (((symbol >= CLUTTER_KEY_BackSpace) &&
            (symbol <= CLUTTER_KEY_Clear)) ||
           (symbol == CLUTTER_KEY_Return) ||
           (symbol == CLUTTER_KEY_Escape) ||
           (symbol == CLUTTER_KEY_KP_Space) ||
           (symbol == CLUTTER_KEY_KP_Tab) ||
           (symbol == CLUTTER_KEY_KP_Enter) ||
           ((symbol >= CLUTTER_KEY_KP_Multiply) &&
            (symbol <= CLUTTER_KEY_KP_9)) ||
           (symbol == CLUTTER_KEY_KP_Equal) ||
           (symbol == CLUTTER_KEY_Delete))))))
    return 0;

  /* if X keysym, convert to ascii by grabbing low 7 bits */
  if (symbol == CLUTTER_KEY_KP_Space)
    c = CLUTTER_KEY_space & 0x7F; /* patch encoding botch */
  else if (high_bytes == 0xFF)
    c = symbol & 0x7F;
  else
    c = symbol & 0xFF;

  buffer[0] = c;
  return 1;
}

static double *
translate_axes (ClutterInputDevice *device,
                double              x,
                double              y,
                XIValuatorState    *valuators)
{
  uint32_t i;
  double *retval;
  double *values;

  retval = g_new0 (double, CLUTTER_INPUT_AXIS_LAST);
  values = valuators->values;

  for (i = 0; i < valuators->mask_len * 8; i++)
    {
      ClutterInputAxis axis;
      double val;

      if (!XIMaskIsSet (valuators->mask, i))
        continue;
      if (!meta_input_device_x11_get_axis (device, i, &axis))
        continue;

      val = *values++;

      switch (axis)
        {
        case CLUTTER_INPUT_AXIS_X:
          retval[axis] = x;
          break;

        case CLUTTER_INPUT_AXIS_Y:
          retval[axis] = y;
          break;

        default:
          meta_input_device_x11_translate_axis (device, i, val, &retval[axis]);
          break;
        }
    }

  return retval;
}

static double
scroll_valuators_changed (ClutterInputDevice *device,
                          XIValuatorState    *valuators,
                          double             *dx_p,
                          double             *dy_p)
{
  gboolean retval = FALSE;
  uint32_t n_axes, n_val, i;
  double *values;

  n_axes = meta_input_device_x11_get_n_axes (device);
  values = valuators->values;

  *dx_p = *dy_p = 0.0;

  n_val = 0;

  for (i = 0; i < MIN (valuators->mask_len * 8, n_axes); i++)
    {
      ClutterScrollDirection direction;
      double delta;

      if (!XIMaskIsSet (valuators->mask, i))
        continue;

      if (meta_input_device_x11_get_scroll_delta (device, i,
                                                  values[n_val],
                                                  &direction,
                                                  &delta))
        {
          retval = TRUE;

          if (direction == CLUTTER_SCROLL_UP ||
              direction == CLUTTER_SCROLL_DOWN)
            *dy_p = delta;
          else
            *dx_p = delta;
        }

      n_val += 1;
    }

  return retval;
}

static void
translate_coords (MetaStageX11 *stage_x11,
                  double        event_x,
                  double        event_y,
                  float        *x_out,
                  float        *y_out)
{
  MetaStageImpl *stage_impl = META_STAGE_IMPL (stage_x11);
  ClutterActor *stage = CLUTTER_ACTOR (stage_impl->wrapper);
  float stage_width;
  float stage_height;

  clutter_actor_get_size (stage, &stage_width, &stage_height);

  *x_out = CLAMP (event_x, 0, stage_width);
  *y_out = CLAMP (event_y, 0, stage_height);
}

static void
on_keymap_state_change (MetaKeymapX11 *keymap_x11,
                        gpointer       data)
{
  ClutterSeat *seat = data;
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (seat);
  MetaInputSettings *input_settings;
  MetaKbdA11ySettings kbd_a11y_settings;

  /* On keymaps state change, just reapply the current settings, it'll
   * take care of enabling/disabling mousekeys based on NumLock state.
   */
  input_settings = meta_backend_get_input_settings (seat_x11->backend);
  meta_input_settings_get_kbd_a11y_settings (input_settings, &kbd_a11y_settings);
  meta_seat_x11_apply_kbd_a11y_settings (seat, &kbd_a11y_settings);
}

static void
meta_seat_x11_set_property (GObject      *object,
                            guint         prop_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (object);

  switch (prop_id)
    {
    case PROP_BACKEND:
      seat_x11->backend = g_value_get_object (value);
      break;
    case PROP_OPCODE:
      seat_x11->opcode = g_value_get_int (value);
      break;
    case PROP_POINTER_ID:
      seat_x11->pointer_id = g_value_get_int (value);
      break;
    case PROP_KEYBOARD_ID:
      seat_x11->keyboard_id = g_value_get_int (value);
      break;
    case PROP_TOUCH_MODE:
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
meta_seat_x11_get_property (GObject    *object,
                            guint       prop_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (object);

  switch (prop_id)
    {
    case PROP_BACKEND:
      g_value_set_object (value, seat_x11->backend);
      break;
    case PROP_OPCODE:
      g_value_set_int (value, seat_x11->opcode);
      break;
    case PROP_POINTER_ID:
      g_value_set_int (value, seat_x11->pointer_id);
      break;
    case PROP_KEYBOARD_ID:
      g_value_set_int (value, seat_x11->keyboard_id);
      break;
    case PROP_TOUCH_MODE:
      g_value_set_boolean (value, seat_x11->touch_mode);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

void
meta_seat_x11_notify_devices (MetaSeatX11  *seat_x11,
			      ClutterStage *stage)
{
  GHashTableIter iter;
  ClutterInputDevice *device;

  g_hash_table_iter_init (&iter, seat_x11->devices_by_id);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &device))
    {
      ClutterEvent *event;

      event = clutter_event_device_notify_new (CLUTTER_DEVICE_ADDED,
                                               CLUTTER_EVENT_NONE,
                                               CLUTTER_CURRENT_TIME,
                                               device);
      clutter_event_put (event);
      clutter_event_free (event);
    }
}

static void
meta_seat_x11_constructed (GObject *object)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (object);
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  Window root_xwindow = root_xwindow_from_seat (seat_x11);
  ClutterBackend *clutter_backend =
    meta_backend_get_clutter_backend (seat_x11->backend);
  XIDeviceInfo *info;
  XIEventMask event_mask;
  unsigned char mask[XIMaskLen(XI_LASTEVENT)] = { 0, };
  int n_devices, i;
#ifdef HAVE_LIBGUDEV
  const char *udev_subsystems[] = { "input", NULL };

  seat_x11->udev_client = g_udev_client_new (udev_subsystems);
#endif

  info = XIQueryDevice (xdisplay, XIAllDevices, &n_devices);

  for (i = 0; i < n_devices; i++)
    {
      XIDeviceInfo *xi_device = &info[i];

      if (!xi_device->enabled)
        continue;

      add_device (seat_x11, clutter_backend, xi_device);
    }

  XIFreeDeviceInfo (info);

  XISetMask (mask, XI_HierarchyChanged);
  XISetMask (mask, XI_DeviceChanged);
  XISetMask (mask, XI_PropertyEvent);

  event_mask.deviceid = XIAllDevices;
  event_mask.mask_len = sizeof (mask);
  event_mask.mask = mask;

  XISelectEvents (xdisplay, root_xwindow,
                  &event_mask, 1);

  memset(mask, 0, sizeof (mask));
  XISetMask (mask, XI_RawMotion);
  XISetMask (mask, XI_RawButtonPress);
  XISetMask (mask, XI_RawButtonRelease);

  if (meta_backend_x11_get_barriers (META_BACKEND_X11 (seat_x11->backend)))
    {
      XISetMask (mask, XI_BarrierHit);
      XISetMask (mask, XI_BarrierLeave);
    }

  event_mask.deviceid = XIAllMasterDevices;
  event_mask.mask_len = sizeof (mask);
  event_mask.mask = mask;

  XISelectEvents (xdisplay, root_xwindow,
                  &event_mask, 1);

  XSync (xdisplay, False);

  seat_x11->keymap = g_object_new (META_TYPE_KEYMAP_X11,
                                   "backend", seat_x11->backend,
                                   NULL);
  g_signal_connect (seat_x11->keymap,
                    "state-changed",
                    G_CALLBACK (on_keymap_state_change),
                    seat_x11);

  meta_seat_x11_a11y_init (CLUTTER_SEAT (seat_x11));

  if (G_OBJECT_CLASS (meta_seat_x11_parent_class)->constructed)
    G_OBJECT_CLASS (meta_seat_x11_parent_class)->constructed (object);
}

static void
meta_seat_x11_finalize (GObject *object)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (object);

#ifdef HAVE_LIBGUDEV
  g_clear_object (&seat_x11->udev_client);
#endif

  g_hash_table_unref (seat_x11->devices_by_id);
  g_hash_table_unref (seat_x11->tools_by_serial);
  g_hash_table_unref (seat_x11->touch_coords);
  g_list_free (seat_x11->devices);

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

static ClutterInputDevice *
meta_seat_x11_get_pointer (ClutterSeat *seat)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (seat);

  return seat_x11->core_pointer;
}

static ClutterInputDevice *
meta_seat_x11_get_keyboard (ClutterSeat *seat)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (seat);

  return seat_x11->core_keyboard;
}

static const GList *
meta_seat_x11_peek_devices (ClutterSeat *seat)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (seat);

  return (const GList *) seat_x11->devices;
}

static void
meta_seat_x11_bell_notify (ClutterSeat *seat)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (seat);
  MetaContext *context = meta_backend_get_context (seat_x11->backend);
  MetaDisplay *display = meta_context_get_display (context);

  meta_bell_notify (display, NULL);
}

static ClutterKeymap *
meta_seat_x11_get_keymap (ClutterSeat *seat)
{
  return CLUTTER_KEYMAP (META_SEAT_X11 (seat)->keymap);
}

static ClutterVirtualInputDevice *
meta_seat_x11_create_virtual_device (ClutterSeat            *seat,
                                     ClutterInputDeviceType  device_type)
{
  return g_object_new (META_TYPE_VIRTUAL_INPUT_DEVICE_X11,
                       "seat", seat,
                       "device-type", device_type,
                       NULL);
}

static ClutterVirtualDeviceType
meta_seat_x11_get_supported_virtual_device_types (ClutterSeat *seat)
{
  return (CLUTTER_VIRTUAL_DEVICE_TYPE_KEYBOARD |
          CLUTTER_VIRTUAL_DEVICE_TYPE_POINTER);
}

static void
meta_seat_x11_warp_pointer (ClutterSeat *seat,
                            int          x,
                            int          y)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (seat);
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  Window root_xwindow = root_xwindow_from_seat (seat_x11);

  mtk_x11_error_trap_push (xdisplay);
  XIWarpPointer (xdisplay,
                 seat_x11->pointer_id,
                 None,
                 root_xwindow,
                 0, 0, 0, 0,
                 x, y);
  mtk_x11_error_trap_pop (xdisplay);
}

static void
meta_seat_x11_init_pointer_position (ClutterSeat *seat,
                                     float        x,
                                     float        y)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (seat);
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  Window root_xwindow = root_xwindow_from_seat (seat_x11);

  mtk_x11_error_trap_push (xdisplay);
  XIWarpPointer (xdisplay,
                 seat_x11->pointer_id,
                 None,
                 root_xwindow,
                 0, 0, 0, 0,
                 (int) x, (int) y);
  mtk_x11_error_trap_pop (xdisplay);
}

static uint32_t
translate_state (XIButtonState   *button_state,
                 XIModifierState *modifier_state,
                 XIGroupState    *group_state)
{
  uint32_t state = 0;
  int i;

  if (modifier_state)
    state |= modifier_state->effective;

  if (button_state)
    {
      for (i = 1; i < button_state->mask_len * 8; i++)
        {
          if (!XIMaskIsSet (button_state->mask, i))
            continue;

          switch (i)
            {
            case 1:
              state |= CLUTTER_BUTTON1_MASK;
              break;
            case 2:
              state |= CLUTTER_BUTTON2_MASK;
              break;
            case 3:
              state |= CLUTTER_BUTTON3_MASK;
              break;
            case 8:
              state |= CLUTTER_BUTTON4_MASK;
              break;
            case 9:
              state |= CLUTTER_BUTTON5_MASK;
              break;
            default:
              break;
            }
        }
    }

  if (group_state)
    state |= XkbBuildCoreState (0, group_state->effective);

  return state;
}

static gboolean
meta_seat_x11_query_state (ClutterSeat          *seat,
                           ClutterInputDevice   *device,
                           ClutterEventSequence *sequence,
                           graphene_point_t     *coords,
                           ClutterModifierType  *modifiers)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (seat);
  MetaBackendX11 *backend_x11 = META_BACKEND_X11 (seat_x11->backend);
  Display *xdisplay = xdisplay_from_seat (seat_x11);
  Window root_ret, child_ret;
  double root_x, root_y, win_x, win_y;
  XIButtonState button_state = { 0 };
  XIModifierState modifier_state;
  XIGroupState group_state;

  mtk_x11_error_trap_push (xdisplay);
  XIQueryPointer (xdisplay,
                  seat_x11->pointer_id,
                  meta_backend_x11_get_xwindow (backend_x11),
                  &root_ret, &child_ret,
                  &root_x, &root_y, &win_x, &win_y,
                  &button_state, &modifier_state, &group_state);
  if (mtk_x11_error_trap_pop_with_return (xdisplay))
    {
      g_free (button_state.mask);
      return FALSE;
    }

  if (sequence)
    {
      MetaTouchInfo *touch_info;

      touch_info = g_hash_table_lookup (seat_x11->touch_coords, sequence);
      if (!touch_info)
        {
          g_free (button_state.mask);
          return FALSE;
        }

      if (coords)
        {
          coords->x = touch_info->x;
          coords->y = touch_info->y;
        }
    }
  else
    {
      if (coords)
        {
          coords->x = win_x;
          coords->y = win_y;
        }
    }

  if (modifiers)
    *modifiers = translate_state (&button_state, &modifier_state, &group_state);

  g_free (button_state.mask);
  return TRUE;
}

static void
meta_seat_x11_update_touchpoint (MetaSeatX11          *seat,
                                 ClutterEventSequence *sequence,
                                 double                x,
                                 double                y)
{
  MetaTouchInfo *touch_info;

  touch_info = g_hash_table_lookup (seat->touch_coords, sequence);
  if (!touch_info)
    {
      touch_info = g_new0 (MetaTouchInfo, 1);
      touch_info->sequence = sequence;
      g_hash_table_insert (seat->touch_coords, sequence, touch_info);
    }

  touch_info->x = x;
  touch_info->y = y;
}

static void
meta_seat_x11_remove_touchpoint (MetaSeatX11          *seat,
                                 ClutterEventSequence *sequence)
{
  g_hash_table_remove (seat->touch_coords, sequence);
}

static void
meta_touch_info_free (MetaTouchInfo *touch_info)
{
  g_free (touch_info);
}

static ClutterGrabState
meta_seat_x11_grab (ClutterSeat *seat,
                    uint32_t     time)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (seat);
  MetaBackend *backend = seat_x11->backend;
  ClutterGrabState state = CLUTTER_GRAB_STATE_NONE;

  g_return_val_if_fail (seat_x11->grab_state == CLUTTER_GRAB_STATE_NONE,
                        seat_x11->grab_state);

  if (meta_backend_grab_device (backend,
                                META_VIRTUAL_CORE_POINTER_ID,
                                time))
    state |= CLUTTER_GRAB_STATE_POINTER;

  if (meta_backend_grab_device (backend,
                                META_VIRTUAL_CORE_KEYBOARD_ID,
                                time))
    state |= CLUTTER_GRAB_STATE_KEYBOARD;

  seat_x11->grab_state = state;

  return state;
}

static void
meta_seat_x11_ungrab (ClutterSeat *seat,
                      uint32_t     time)
{
  MetaSeatX11 *seat_x11 = META_SEAT_X11 (seat);
  MetaBackend *backend = seat_x11->backend;

  if ((seat_x11->grab_state & CLUTTER_GRAB_STATE_POINTER) != 0)
    {
      meta_backend_ungrab_device (backend,
                                  META_VIRTUAL_CORE_POINTER_ID,
                                  time);
    }

  if ((seat_x11->grab_state & CLUTTER_GRAB_STATE_KEYBOARD) != 0)
    {
      meta_backend_ungrab_device (backend,
                                  META_VIRTUAL_CORE_KEYBOARD_ID,
                                  time);
    }

  seat_x11->grab_state = CLUTTER_GRAB_STATE_NONE;
}

static void
meta_seat_x11_class_init (MetaSeatX11Class *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  ClutterSeatClass *seat_class = CLUTTER_SEAT_CLASS (klass);

  object_class->set_property = meta_seat_x11_set_property;
  object_class->get_property = meta_seat_x11_get_property;
  object_class->constructed = meta_seat_x11_constructed;
  object_class->finalize = meta_seat_x11_finalize;

  seat_class->get_pointer = meta_seat_x11_get_pointer;
  seat_class->get_keyboard = meta_seat_x11_get_keyboard;
  seat_class->peek_devices = meta_seat_x11_peek_devices;
  seat_class->bell_notify = meta_seat_x11_bell_notify;
  seat_class->get_keymap = meta_seat_x11_get_keymap;
  seat_class->create_virtual_device = meta_seat_x11_create_virtual_device;
  seat_class->get_supported_virtual_device_types = meta_seat_x11_get_supported_virtual_device_types;
  seat_class->warp_pointer = meta_seat_x11_warp_pointer;
  seat_class->init_pointer_position = meta_seat_x11_init_pointer_position;
  seat_class->handle_event_post = meta_seat_x11_handle_event_post;
  seat_class->query_state = meta_seat_x11_query_state;
  seat_class->grab = meta_seat_x11_grab;
  seat_class->ungrab = meta_seat_x11_ungrab;

  props[PROP_BACKEND] =
    g_param_spec_object ("backend", NULL, NULL,
                         META_TYPE_BACKEND,
                         G_PARAM_READWRITE |
                         G_PARAM_CONSTRUCT_ONLY);
  props[PROP_OPCODE] =
    g_param_spec_int ("opcode", NULL, NULL,
                      0, G_MAXINT, 0,
                      G_PARAM_READWRITE |
                      G_PARAM_CONSTRUCT_ONLY);
  props[PROP_POINTER_ID] =
    g_param_spec_int ("pointer-id", NULL, NULL,
                      2, G_MAXINT, 2,
                      G_PARAM_READWRITE |
                      G_PARAM_CONSTRUCT_ONLY);
  props[PROP_KEYBOARD_ID] =
    g_param_spec_int ("keyboard-id", NULL, NULL,
                      2, G_MAXINT, 2,
                      G_PARAM_READWRITE |
                      G_PARAM_CONSTRUCT_ONLY);

  g_object_class_install_properties (object_class, N_PROPS, props);

  g_object_class_override_property (object_class, PROP_TOUCH_MODE,
                                    "touch-mode");
}

static void
meta_seat_x11_init (MetaSeatX11 *seat)
{
  seat->devices_by_id = g_hash_table_new_full (NULL, NULL,
                                               NULL,
                                               (GDestroyNotify) g_object_unref);
  seat->tools_by_serial = g_hash_table_new_full (NULL, NULL, NULL,
                                                 (GDestroyNotify) g_object_unref);
  seat->touch_coords = g_hash_table_new_full (NULL, NULL, NULL,
                                              (GDestroyNotify) meta_touch_info_free);
}

MetaSeatX11 *
meta_seat_x11_new (MetaBackend *backend,
                   int          opcode,
                   int          logical_pointer,
                   int          logical_keyboard)
{
  MetaSeatX11 *seat_x11;

  seat_x11 = g_object_new (META_TYPE_SEAT_X11,
                           "backend", backend,
                           "opcode", opcode,
                           "pointer-id", logical_pointer,
                           "keyboard-id", logical_keyboard,
                           NULL);

  return seat_x11;
}

MetaBackend *
meta_seat_x11_get_backend (MetaSeatX11 *seat_x11)
{
  return seat_x11->backend;
}

static ClutterInputDevice *
get_source_device_checked (MetaSeatX11   *seat,
                           XIDeviceEvent *xev)
{
  ClutterInputDevice *source_device;

  source_device = g_hash_table_lookup (seat->devices_by_id,
                                       GINT_TO_POINTER (xev->sourceid));

  if (!source_device)
    g_warning ("Impossible to get the source device with id %d for event of "
               "type %d", xev->sourceid, xev->evtype);

  return source_device;
}

static uint32_t
evdev_button_code (uint32_t x_button)
{
  uint32_t button;

  switch (x_button)
    {
    case 1:
      button = BTN_LEFT;
      break;

      /* The evdev input right and middle button numbers are swapped
         relative to how Clutter numbers them */
    case 2:
      button = BTN_MIDDLE;
      break;

    case 3:
      button = BTN_RIGHT;
      break;

    default:
      button = x_button + (BTN_LEFT - 1) + 4;
      break;
    }

  return button;
}

static ClutterModifierType
get_modifier_for_button (int i)
{
  switch (i)
    {
    case 1:
      return CLUTTER_BUTTON1_MASK;
    case 2:
      return CLUTTER_BUTTON2_MASK;
    case 3:
      return CLUTTER_BUTTON3_MASK;
    case 4:
      return CLUTTER_BUTTON4_MASK;
    case 5:
      return CLUTTER_BUTTON5_MASK;
    default:
      return 0;
    }
}

ClutterEvent *
meta_seat_x11_translate_event (MetaSeatX11  *seat,
                               XEvent       *xevent)
{
  Display *xdisplay = xdisplay_from_seat (seat);
  ClutterBackend *clutter_backend =
    meta_backend_get_clutter_backend (seat->backend);
  ClutterStage *stage = NULL;
  MetaStageX11 *stage_x11 = NULL;
  ClutterInputDevice *device, *source_device;
  XGenericEventCookie *cookie;
  ClutterEvent *event = NULL;
  XIEvent *xi_event;

  if (meta_keymap_x11_handle_event (seat->keymap, xevent))
    return NULL;

  cookie = &xevent->xcookie;

  if (cookie->type != GenericEvent ||
      cookie->extension != seat->opcode)
    return NULL;

  xi_event = (XIEvent *) cookie->data;

  if (!xi_event)
    return NULL;

  if (cookie->evtype == XI_RawMotion ||
      cookie->evtype == XI_RawButtonPress ||
      cookie->evtype == XI_RawButtonRelease)
    {
      translate_raw_event (seat, xevent);
      return NULL;
    }

  if (!(xi_event->evtype == XI_DeviceChanged ||
        xi_event->evtype == XI_PropertyEvent))
    {
      stage = get_event_stage (seat, xi_event);
      if (stage == NULL || CLUTTER_ACTOR_IN_DESTRUCTION (stage))
        return NULL;
      else
        stage_x11 = META_STAGE_X11 (_clutter_stage_get_window (stage));
    }

  switch (xi_event->evtype)
    {
    case XI_HierarchyChanged:
      {
        XIHierarchyEvent *xev = (XIHierarchyEvent *) xi_event;

        event = translate_hierarchy_event (clutter_backend, seat, xev);
      }
      break;

    case XI_DeviceChanged:
      {
        XIDeviceChangedEvent *xev = (XIDeviceChangedEvent *) xi_event;

        device = g_hash_table_lookup (seat->devices_by_id,
                                      GINT_TO_POINTER (xev->deviceid));
        source_device = g_hash_table_lookup (seat->devices_by_id,
                                             GINT_TO_POINTER (xev->sourceid));
        if (device)
          {
            meta_input_device_x11_reset_axes (device);
            translate_device_classes (xdisplay,
                                      device,
                                      xev->classes,
                                      xev->num_classes);
          }

        if (source_device)
          meta_input_device_x11_reset_scroll_info (source_device);
      }
      break;
    case XI_KeyPress:
    case XI_KeyRelease:
      {
        XIDeviceEvent *xev = (XIDeviceEvent *) xi_event;
        MetaKeymapX11 *keymap_x11 = seat->keymap;
        char buffer[7] = { 0, };
        uint32_t keyval, evcode, keycode;
        ClutterModifierType state;
        int len;
        gunichar unicode_value;

        source_device = get_source_device_checked (seat, xev);
        if (!source_device)
          return NULL;

        state = translate_state (&xev->buttons, &xev->mods, &xev->group);

        keycode = xev->detail;

        /* clutter-xkb-utils.c adds a fixed offset of 8 to go into XKB's
         * range, so we do the reverse here. */
        evcode = keycode - 8;

        /* keyval is the key ignoring all modifiers ('1' vs. '!') */
        keyval = meta_keymap_x11_translate_key_state (keymap_x11,
                                                      keycode,
                                                      &state,
                                                      NULL);

        /* XXX keep this in sync with the evdev device manager */
        len = print_keysym (keyval, buffer, sizeof (buffer));
        if (len == 0)
          {
            /* not printable */
            unicode_value = (gunichar) '\0';
          }
        else
          {
            unicode_value = g_utf8_get_char_validated (buffer, len);
            if (unicode_value == -1 ||
                unicode_value == -2)
              unicode_value = (gunichar) '\0';
          }

        event = clutter_event_key_new ((xev->evtype == XI_KeyPress) ?
                                       CLUTTER_KEY_PRESS : CLUTTER_KEY_RELEASE,
                                       (xev->evtype == XI_KeyPress &&
                                        xev->flags & XIKeyRepeat) ?
                                       CLUTTER_EVENT_FLAG_REPEATED :
                                       CLUTTER_EVENT_NONE,
                                       ms2us (xev->time),
                                       source_device,
                                       state,
                                       keyval,
                                       evcode,
                                       keycode,
                                       unicode_value);

        g_debug ("%s: win:0x%x device:%d source:%d, key: %12s (%d)",
                 clutter_event_type (event) == CLUTTER_KEY_PRESS
                 ? "key press  "
                 : "key release",
                 (unsigned int) stage_x11->xwin,
                 xev->deviceid,
                 xev->sourceid,
                 keyval ? buffer : "(none)",
                 keyval);

        if (xi_event->evtype == XI_KeyPress)
          meta_stage_x11_set_user_time (stage_x11, xev->time);
      }
      break;

    case XI_ButtonPress:
    case XI_ButtonRelease:
      {
        XIDeviceEvent *xev = (XIDeviceEvent *) xi_event;
        ClutterScrollDirection scroll_direction;
        ClutterModifierType state;
        ClutterInputDeviceTool *tool;
        float x, y;
        int button;
        uint32_t evdev_code;
        double *axes;

        source_device = get_source_device_checked (seat, xev);
	if (!source_device)
          return NULL;

        device = g_hash_table_lookup (seat->devices_by_id,
                                      GINT_TO_POINTER (xev->deviceid));

	if (clutter_input_device_get_device_type (source_device) == CLUTTER_PAD_DEVICE)
          {
            uint32_t button, group, mode;

            /* We got these events because of the passive button grab */
            XIAllowEvents (xdisplay, xev->sourceid, XIAsyncDevice, xev->time);

            if (xev->detail >= 4 && xev->detail <= 7)
              {
                if (xi_event->evtype == XI_ButtonPress)
                  event = translate_pad_axis_event (xev, source_device);
                break;
              }

            /* The 4-7 button range is taken as non-existent on pad devices,
             * let the buttons above that take over this range.
             */
            if (xev->detail > 7)
              xev->detail -= 4;

            /* Pad buttons are 0-indexed */
            button = xev->detail - 1;

#ifdef HAVE_LIBWACOM
            meta_input_device_x11_update_pad_state (device,
                                                    button,
                                                    (xi_event->evtype == XI_ButtonPress),
                                                    &group,
                                                    &mode);
#endif

            event = clutter_event_pad_button_new ((xi_event->evtype == XI_ButtonPress) ?
                                                  CLUTTER_PAD_BUTTON_PRESS :
                                                  CLUTTER_PAD_BUTTON_RELEASE,
                                                  CLUTTER_EVENT_NONE,
                                                  us2ms (xev->time),
                                                  source_device,
                                                  button,
                                                  group,
                                                  mode);

            g_debug ("%s: win:0x%x, device:%d '%s', time:%lu "
                     "(button:%d)",
                     (xi_event->evtype == XI_ButtonPress)
                     ? "pad button press  "
                     : "pad button release",
                     (unsigned int) stage_x11->xwin,
                     meta_input_device_x11_get_device_id (device),
                     clutter_input_device_get_device_name (device),
                     xev->time,
                     button);
            break;
          }

        switch (xev->detail)
          {
          case 4:
          case 5:
          case 6:
          case 7:
            /* we only generate Scroll events on ButtonPress */
            if (xi_event->evtype == XI_ButtonRelease)
              return NULL;

            if (xev->detail == 4)
              scroll_direction = CLUTTER_SCROLL_UP;
            else if (xev->detail == 5)
              scroll_direction = CLUTTER_SCROLL_DOWN;
            else if (xev->detail == 6)
              scroll_direction = CLUTTER_SCROLL_LEFT;
            else
              scroll_direction = CLUTTER_SCROLL_RIGHT;

            translate_coords (stage_x11, xev->event_x, xev->event_y, &x, &y);
            state = translate_state (&xev->buttons, &xev->mods, &xev->group);
            tool = meta_input_device_x11_get_current_tool (source_device);

            event = clutter_event_scroll_discrete_new (CLUTTER_EVENT_NONE,
                                                       ms2us (xev->time),
                                                       source_device,
                                                       tool,
                                                       state,
                                                       GRAPHENE_POINT_INIT (x, y),
                                                       scroll_direction);

            g_debug ("scroll: win:0x%x, device:%d '%s', time:%d "
                     "(direction:%s, "
                     "x:%.2f, y:%.2f, "
                     "emulated:%s)",
                     (unsigned int) stage_x11->xwin,
                     meta_input_device_x11_get_device_id (device),
                     clutter_input_device_get_device_name (device),
                     clutter_event_get_time (event),
                     scroll_direction == CLUTTER_SCROLL_UP ? "up" :
                     scroll_direction == CLUTTER_SCROLL_DOWN ? "down" :
                     scroll_direction == CLUTTER_SCROLL_LEFT ? "left" :
                     scroll_direction == CLUTTER_SCROLL_RIGHT ? "right" :
                     "invalid",
                     x, y,
                     (xev->flags & XIPointerEmulated) ? "yes" : "no");
            break;

          default:
            translate_coords (stage_x11, xev->event_x, xev->event_y, &x, &y);
            button = xev->detail;
            evdev_code = evdev_button_code (xev->detail);
            state = translate_state (&xev->buttons, &xev->mods, &xev->group);
            tool = meta_input_device_x11_get_current_tool (source_device);
            axes = translate_axes (device, x, y, &xev->valuators);

            /* The XIButtonState sent in the event specifies the
             * state of the buttons before the event. In order to
             * get the current state of the buttons, we need to
             * filter out the current button.
             */
            switch (xi_event->evtype)
              {
              case XI_ButtonPress:
                state |= (get_modifier_for_button (button));
                break;
              case XI_ButtonRelease:
                state &= ~(get_modifier_for_button (button));
                break;
              default:
                break;
              }

            event = clutter_event_button_new ((xi_event->evtype == XI_ButtonPress) ?
                                              CLUTTER_BUTTON_PRESS :
                                              CLUTTER_BUTTON_RELEASE,
                                              (xev->flags & XIPointerEmulated) ?
                                              CLUTTER_EVENT_FLAG_POINTER_EMULATED :
                                              CLUTTER_EVENT_NONE,
                                              ms2us (xev->time),
                                              source_device,
                                              tool,
                                              state,
                                              GRAPHENE_POINT_INIT (x, y),
                                              button,
                                              evdev_code,
                                              axes);

            g_debug ("%s: win:0x%x, device:%d '%s', time:%lu "
                     "(button:%d, "
                     "x:%.2f, y:%.2f, "
                     "axes:%s, "
                     "emulated:%s)",
                     (xi_event->evtype == XI_ButtonPress)
                     ? "button press  "
                     : "button release",
                     (unsigned int) stage_x11->xwin,
                     meta_input_device_x11_get_device_id (device),
                     clutter_input_device_get_device_name (device),
                     xev->time,
                     xev->detail,
                     x, y,
                     axes != NULL ? "yes" : "no",
                     (xev->flags & XIPointerEmulated) ? "yes" : "no");
            break;
          }

        if (xi_event->evtype == XI_ButtonPress)
          meta_stage_x11_set_user_time (stage_x11, xev->time);
      }
      break;

    case XI_Motion:
      {
        XIDeviceEvent *xev = (XIDeviceEvent *) xi_event;
        double delta_x, delta_y;
        ClutterModifierType state;
        ClutterInputDeviceTool *tool;
        float x, y;
        double *axes;

        source_device = get_source_device_checked (seat, xev);
        if (!source_device)
          break;

        device = g_hash_table_lookup (seat->devices_by_id,
                                      GINT_TO_POINTER (xev->deviceid));

        if (clutter_input_device_get_device_type (source_device) == CLUTTER_PAD_DEVICE)
          {
            event = translate_pad_axis_event (xev, source_device);
            break;
          }

        translate_coords (stage_x11, xev->event_x, xev->event_y, &x, &y);
        state = translate_state (&xev->buttons, &xev->mods, &xev->group);
        tool = meta_input_device_x11_get_current_tool (source_device);

        if (scroll_valuators_changed (source_device,
                                      &xev->valuators,
                                      &delta_x, &delta_y))
          {
            event = clutter_event_scroll_smooth_new (CLUTTER_EVENT_NONE,
                                                     ms2us (xev->time),
                                                     source_device,
                                                     tool,
                                                     state,
                                                     GRAPHENE_POINT_INIT (x, y),
                                                     GRAPHENE_POINT_INIT (delta_x, delta_y),
                                                     CLUTTER_SCROLL_SOURCE_UNKNOWN,
                                                     CLUTTER_SCROLL_FINISHED_NONE);

            g_debug ("smooth scroll: win:0x%x device:%d '%s' (x:%.2f, y:%.2f, delta:%f, %f)",
                     (unsigned int) stage_x11->xwin,
                     meta_input_device_x11_get_device_id (source_device),
                     clutter_input_device_get_device_name (source_device),
                     x, y,
                     delta_x, delta_y);
            break;
          }

        axes = translate_axes (device, x, y, &xev->valuators);
        event = clutter_event_motion_new ((xev->flags & XIPointerEmulated) ?
                                          CLUTTER_EVENT_FLAG_POINTER_EMULATED :
                                          CLUTTER_EVENT_NONE,
                                          ms2us (xev->time),
                                          source_device,
                                          tool,
                                          state,
                                          GRAPHENE_POINT_INIT (x, y),
                                          GRAPHENE_POINT_INIT (0, 0),
                                          GRAPHENE_POINT_INIT (0, 0),
                                          GRAPHENE_POINT_INIT (0, 0),
                                          axes);

        g_debug ("motion: win:0x%x device:%d '%s' (x:%.2f, y:%.2f, axes:%s)",
                 (unsigned int) stage_x11->xwin,
                 meta_input_device_x11_get_device_id (source_device),
                 clutter_input_device_get_device_name (source_device),
                 x, y,
                 axes != NULL ? "yes" : "no");
      }
      break;

    case XI_TouchBegin:
    case XI_TouchEnd:
      {
        XIDeviceEvent *xev = (XIDeviceEvent *) xi_event;
        ClutterEventType evtype;
        ClutterModifierType state;
        ClutterEventSequence *sequence;
        float x, y;

        device = g_hash_table_lookup (seat->devices_by_id,
                                      GINT_TO_POINTER (xev->deviceid));
        source_device = g_hash_table_lookup (seat->devices_by_id,
                                             GINT_TO_POINTER (xev->sourceid));

        if (xi_event->evtype == XI_TouchBegin)
          evtype = CLUTTER_TOUCH_BEGIN;
        else
          evtype = CLUTTER_TOUCH_END;

        translate_coords (stage_x11, xev->event_x, xev->event_y, &x, &y);
        state = translate_state (&xev->buttons, &xev->mods, &xev->group);
        /* "NULL" sequences are special cased in clutter */
        sequence = GINT_TO_POINTER (MAX (1, xev->detail + 1));

        if (xi_event->evtype == XI_TouchBegin)
          {
            state |= CLUTTER_BUTTON1_MASK;

            meta_stage_x11_set_user_time (stage_x11, xev->time);
            meta_seat_x11_update_touchpoint (seat,
                                             sequence,
                                             xev->root_x,
                                             xev->root_y);
          }
        else if (xi_event->evtype == XI_TouchEnd)
          {
            meta_seat_x11_remove_touchpoint (seat, sequence);
          }

        event = clutter_event_touch_new (evtype,
                                         (xev->flags & XITouchEmulatingPointer) ?
                                         CLUTTER_EVENT_FLAG_POINTER_EMULATED :
                                         CLUTTER_EVENT_NONE,
                                         ms2us (xev->time),
                                         source_device,
                                         sequence,
                                         state,
                                         GRAPHENE_POINT_INIT (x, y));

        g_debug ("touch %s: win:0x%x device:%d '%s' (seq:%d, x:%.2f, y:%.2f)",
                 evtype == CLUTTER_TOUCH_BEGIN ? "begin" : "end",
                 (unsigned int) stage_x11->xwin,
                 meta_input_device_x11_get_device_id (device),
                 clutter_input_device_get_device_name (device),
                 GPOINTER_TO_UINT (sequence),
                 x, y);
      }
      break;

    case XI_TouchUpdate:
      {
        XIDeviceEvent *xev = (XIDeviceEvent *) xi_event;
        ClutterEventSequence *sequence;
        ClutterModifierType state;
        float x, y;

        device = g_hash_table_lookup (seat->devices_by_id,
                                      GINT_TO_POINTER (xev->deviceid));
        source_device = g_hash_table_lookup (seat->devices_by_id,
                                             GINT_TO_POINTER (xev->sourceid));

        /* "NULL" sequences are special cased in clutter */
        sequence = GINT_TO_POINTER (MAX (1, xev->detail + 1));
        translate_coords (stage_x11, xev->event_x, xev->event_y, &x, &y);
        state = translate_state (&xev->buttons, &xev->mods, &xev->group);
        state |= CLUTTER_BUTTON1_MASK;

        meta_seat_x11_update_touchpoint (seat,
                                         sequence,
                                         xev->root_x,
                                         xev->root_y);

        event = clutter_event_touch_new (CLUTTER_TOUCH_UPDATE,
                                         (xev->flags & XITouchEmulatingPointer) ?
                                         CLUTTER_EVENT_FLAG_POINTER_EMULATED :
                                         CLUTTER_EVENT_NONE,
                                         ms2us (xev->time),
                                         source_device,
                                         sequence,
                                         state,
                                         GRAPHENE_POINT_INIT (x, y));

        g_debug ("touch update: win:0x%x device:%d '%s' (seq:%d, x:%.2f, y:%.2f)",
                 (unsigned int) stage_x11->xwin,
                 meta_input_device_x11_get_device_id (device),
                 clutter_input_device_get_device_name (device),
                 GPOINTER_TO_UINT (sequence),
                 x, y);
      }
      break;

    case XI_Enter:
    case XI_Leave:
      {
        XIEnterEvent *xev = (XIEnterEvent *) xi_event;
        float x, y;

        device = g_hash_table_lookup (seat->devices_by_id,
                                      GINT_TO_POINTER (xev->deviceid));

        source_device = g_hash_table_lookup (seat->devices_by_id,
                                             GINT_TO_POINTER (xev->sourceid));

        translate_coords (stage_x11, xev->event_x, xev->event_y, &x, &y);

        event = clutter_event_crossing_new ((xi_event->evtype == XI_Enter) ?
                                            CLUTTER_ENTER : CLUTTER_LEAVE,
                                            CLUTTER_EVENT_NONE,
                                            ms2us (xev->time),
                                            device,
                                            NULL,
                                            GRAPHENE_POINT_INIT (x, y),
                                            CLUTTER_ACTOR (stage), NULL);

        if (xev->deviceid == seat->pointer_id)
          seat->has_pointer_focus = (xi_event->evtype == XI_Enter);

        meta_input_device_x11_reset_scroll_info (source_device);
      }
      break;

    case XI_FocusIn:
    case XI_FocusOut:
      break;
    case XI_PropertyEvent:
      translate_property_event (seat, xi_event);
      break;
    }

  return event;
}

void
meta_seat_x11_select_stage_events (MetaSeatX11  *seat,
                                   ClutterStage *stage)
{
  Display *xdisplay = xdisplay_from_seat (seat);
  MetaStageX11 *stage_x11;
  XIEventMask xi_event_mask;
  unsigned char *mask;
  int len;

  stage_x11 = META_STAGE_X11 (_clutter_stage_get_window (stage));

  len = XIMaskLen (XI_LASTEVENT);
  mask = g_new0 (unsigned char, len);

  XISetMask (mask, XI_Motion);
  XISetMask (mask, XI_ButtonPress);
  XISetMask (mask, XI_ButtonRelease);
  XISetMask (mask, XI_KeyPress);
  XISetMask (mask, XI_KeyRelease);
  XISetMask (mask, XI_Enter);
  XISetMask (mask, XI_Leave);

  XISetMask (mask, XI_TouchBegin);
  XISetMask (mask, XI_TouchUpdate);
  XISetMask (mask, XI_TouchEnd);

  xi_event_mask.deviceid = XIAllMasterDevices;
  xi_event_mask.mask = mask;
  xi_event_mask.mask_len = len;

  XISelectEvents (xdisplay, stage_x11->xwin, &xi_event_mask, 1);

  g_free (mask);
}
