/* GDK - The GIMP Drawing Kit
 * Copyright (C) 2009 Carlos Garnacho <carlosg@gnome.org>
 *
 * 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/>.
 */

#include "config.h"

#include <math.h>

#include "gdkdeviceprivate.h"
#include "gdkdevicetool.h"
#include "gdkdisplayprivate.h"
#include "gdkinternals.h"
#include "gdkintl.h"
#include "gdkkeysprivate.h"

/**
 * SECTION:gdkdevice
 * @Short_description: Object representing an input device
 * @Title: GdkDevice
 * @See_also: #GdkSeat
 *
 * The #GdkDevice object represents a single input device, such
 * as a keyboard, a mouse, a touchpad, etc.
 *
 * See the #GdkSeat documentation for more information about the
 * various kinds of devices, and their relationships.
 */

/**
 * GdkDevice:
 *
 * The GdkDevice struct contains only private fields and
 * should not be accessed directly.
 */

typedef struct _GdkAxisInfo GdkAxisInfo;

struct _GdkAxisInfo
{
  GdkAxisUse use;
  double min_axis;
  double max_axis;
  double min_value;
  double max_value;
  double resolution;
};

enum {
  CHANGED,
  TOOL_CHANGED,
  LAST_SIGNAL
};

static guint signals [LAST_SIGNAL] = { 0 };


static void gdk_device_finalize     (GObject      *object);
static void gdk_device_dispose      (GObject      *object);
static void gdk_device_set_property (GObject      *object,
                                     guint         prop_id,
                                     const GValue *value,
                                     GParamSpec   *pspec);
static void gdk_device_get_property (GObject      *object,
                                     guint         prop_id,
                                     GValue       *value,
                                     GParamSpec   *pspec);


G_DEFINE_ABSTRACT_TYPE (GdkDevice, gdk_device, G_TYPE_OBJECT)

enum {
  PROP_0,
  PROP_DISPLAY,
  PROP_NAME,
  PROP_SOURCE,
  PROP_HAS_CURSOR,
  PROP_N_AXES,
  PROP_VENDOR_ID,
  PROP_PRODUCT_ID,
  PROP_SEAT,
  PROP_NUM_TOUCHES,
  PROP_TOOL,
  PROP_DIRECTION,
  PROP_HAS_BIDI_LAYOUTS,
  PROP_CAPS_LOCK_STATE,
  PROP_NUM_LOCK_STATE,
  PROP_SCROLL_LOCK_STATE,
  PROP_MODIFIER_STATE,
  LAST_PROP
};

static GParamSpec *device_props[LAST_PROP] = { NULL, };

static void
gdk_device_class_init (GdkDeviceClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = gdk_device_finalize;
  object_class->dispose = gdk_device_dispose;
  object_class->set_property = gdk_device_set_property;
  object_class->get_property = gdk_device_get_property;

  /**
   * GdkDevice:display:
   *
   * The #GdkDisplay the #GdkDevice pertains to.
   */
  device_props[PROP_DISPLAY] =
      g_param_spec_object ("display",
                           P_("Device Display"),
                           P_("Display which the device belongs to"),
                           GDK_TYPE_DISPLAY,
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

  /**
   * GdkDevice:name:
   *
   * The device name.
   */
  device_props[PROP_NAME] =
      g_param_spec_string ("name",
                           P_("Device name"),
                           P_("Device name"),
                           NULL,
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * GdkDevice:source:
   *
   * Source type for the device.
   */
  device_props[PROP_SOURCE] =
      g_param_spec_enum ("source",
                         P_("Input source"),
                         P_("Source type for the device"),
                         GDK_TYPE_INPUT_SOURCE,
                         GDK_SOURCE_MOUSE,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                         G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GdkDevice:has-cursor:
   *
   * Whether the device is represented by a cursor on the screen.
   */
  device_props[PROP_HAS_CURSOR] =
      g_param_spec_boolean ("has-cursor",
                            P_("Whether the device has a cursor"),
                            P_("Whether there is a visible cursor following device motion"),
                            FALSE,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                            G_PARAM_STATIC_STRINGS);

  /**
   * GdkDevice:n-axes:
   *
   * Number of axes in the device.
   */
  device_props[PROP_N_AXES] =
      g_param_spec_uint ("n-axes",
                         P_("Number of axes in the device"),
                         P_("Number of axes in the device"),
                         0, G_MAXUINT,
                         0,
                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  /**
   * GdkDevice:vendor-id:
   *
   * Vendor ID of this device, see gdk_device_get_vendor_id().
   */
  device_props[PROP_VENDOR_ID] =
      g_param_spec_string ("vendor-id",
                           P_("Vendor ID"),
                           P_("Vendor ID"),
                           NULL,
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * GdkDevice:product-id:
   *
   * Product ID of this device, see gdk_device_get_product_id().
   */
  device_props[PROP_PRODUCT_ID] =
      g_param_spec_string ("product-id",
                           P_("Product ID"),
                           P_("Product ID"),
                           NULL,
                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * GdkDevice:seat:
   *
   * #GdkSeat of this device.
   */
  device_props[PROP_SEAT] =
      g_param_spec_object ("seat",
                           P_("Seat"),
                           P_("Seat"),
                           GDK_TYPE_SEAT,
                           G_PARAM_READWRITE |
                           G_PARAM_STATIC_STRINGS);

  /**
   * GdkDevice:num-touches:
   *
   * The maximal number of concurrent touches on a touch device.
   * Will be 0 if the device is not a touch device or if the number
   * of touches is unknown.
   */
  device_props[PROP_NUM_TOUCHES] =
      g_param_spec_uint ("num-touches",
                         P_("Number of concurrent touches"),
                         P_("Number of concurrent touches"),
                         0, G_MAXUINT,
                         0,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
                         G_PARAM_STATIC_STRINGS);

  device_props[PROP_TOOL] =
    g_param_spec_object ("tool",
                         P_("Tool"),
                         P_("The tool that is currently used with this device"),
                         GDK_TYPE_DEVICE_TOOL,
                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  device_props[PROP_DIRECTION] =
      g_param_spec_enum ("direction",
                         P_("Direction"),
                         P_("The direction of the current layout of the keyboard"),
                         PANGO_TYPE_DIRECTION, PANGO_DIRECTION_NEUTRAL,
                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  device_props[PROP_HAS_BIDI_LAYOUTS] =
      g_param_spec_boolean ("has-bidi-layouts",
                            P_("Has bidi layouts"),
                            P_("Whether the keyboard has bidi layouts"),
                            FALSE,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  device_props[PROP_CAPS_LOCK_STATE] =
      g_param_spec_boolean ("caps-lock-state",
                            P_("Caps lock state"),
                            P_("Whether the keyboard caps lock is on"),
                            FALSE,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  device_props[PROP_NUM_LOCK_STATE] =
      g_param_spec_boolean ("num-lock-state",
                            P_("Num lock state"),
                            P_("Whether the keyboard num lock is on"),
                            FALSE,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  device_props[PROP_SCROLL_LOCK_STATE] =
      g_param_spec_boolean ("scroll-lock-state",
                            P_("Scroll lock state"),
                            P_("Whether the keyboard scroll lock is on"),
                            FALSE,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  device_props[PROP_MODIFIER_STATE] =
      g_param_spec_flags ("modifier-state",
                          P_("Modifier state"),
                          P_("The modifier state of the keyboard"),
                          GDK_TYPE_MODIFIER_TYPE, 0,
                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, LAST_PROP, device_props);

  /**
   * GdkDevice::changed:
   * @device: the #GdkDevice that changed.
   *
   * The ::changed signal is emitted either when the #GdkDevice
   * has changed the number of either axes or keys. For example
   * on X11 this will normally happen when the physical device
   * routing events through the logical device changes (for
   * example, user switches from the USB mouse to a tablet); in
   * that case the logical device will change to reflect the axes
   * and keys on the new physical device.
   */
  signals[CHANGED] =
    g_signal_new (g_intern_static_string ("changed"),
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  0, NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 0);

  /**
   * GdkDevice::tool-changed:
   * @device: the #GdkDevice that changed.
   * @tool: The new current tool
   *
   * The ::tool-changed signal is emitted on pen/eraser
   * #GdkDevices whenever tools enter or leave proximity.
   */
  signals[TOOL_CHANGED] =
    g_signal_new (g_intern_static_string ("tool-changed"),
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST,
                  0, NULL, NULL,
                  NULL,
                  G_TYPE_NONE, 1, GDK_TYPE_DEVICE_TOOL);
}

static void
gdk_device_init (GdkDevice *device)
{
  device->axes = g_array_new (FALSE, TRUE, sizeof (GdkAxisInfo));
}

static void
gdk_device_finalize (GObject *object)
{
  GdkDevice *device = GDK_DEVICE (object);

  if (device->axes)
    {
      g_array_free (device->axes, TRUE);
      device->axes = NULL;
    }

  g_clear_pointer (&device->name, g_free);
  g_clear_pointer (&device->vendor_id, g_free);
  g_clear_pointer (&device->product_id, g_free);

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

static void
gdk_device_dispose (GObject *object)
{
  GdkDevice *device = GDK_DEVICE (object);
  GdkDevice *associated = device->associated;

  if (associated)
    _gdk_device_remove_physical_device (associated, device);

  if (associated)
    {
      device->associated = NULL;

      if (associated->associated == device)
        _gdk_device_set_associated_device (associated, NULL);

      g_object_unref (associated);
    }

  g_clear_object (&device->last_tool);

  G_OBJECT_CLASS (gdk_device_parent_class)->dispose (object);
}

static void
gdk_device_set_property (GObject      *object,
                         guint         prop_id,
                         const GValue *value,
                         GParamSpec   *pspec)
{
  GdkDevice *device = GDK_DEVICE (object);

  switch (prop_id)
    {
    case PROP_DISPLAY:
      device->display = g_value_get_object (value);
      break;
    case PROP_NAME:
      g_free (device->name);

      device->name = g_value_dup_string (value);
      break;
    case PROP_SOURCE:
      device->source = g_value_get_enum (value);
      break;
    case PROP_HAS_CURSOR:
      device->has_cursor = g_value_get_boolean (value);
      break;
    case PROP_VENDOR_ID:
      device->vendor_id = g_value_dup_string (value);
      break;
    case PROP_PRODUCT_ID:
      device->product_id = g_value_dup_string (value);
      break;
    case PROP_SEAT:
      device->seat = g_value_get_object (value);
      break;
    case PROP_NUM_TOUCHES:
      device->num_touches = g_value_get_uint (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
gdk_device_get_property (GObject    *object,
                         guint       prop_id,
                         GValue     *value,
                         GParamSpec *pspec)
{
  GdkDevice *device = GDK_DEVICE (object);

  switch (prop_id)
    {
    case PROP_DISPLAY:
      g_value_set_object (value, device->display);
      break;
    case PROP_NAME:
      g_value_set_string (value, device->name);
      break;
    case PROP_SOURCE:
      g_value_set_enum (value, device->source);
      break;
    case PROP_HAS_CURSOR:
      g_value_set_boolean (value, device->has_cursor);
      break;
    case PROP_N_AXES:
      g_value_set_uint (value, device->axes->len);
      break;
    case PROP_VENDOR_ID:
      g_value_set_string (value, device->vendor_id);
      break;
    case PROP_PRODUCT_ID:
      g_value_set_string (value, device->product_id);
      break;
    case PROP_SEAT:
      g_value_set_object (value, device->seat);
      break;
    case PROP_NUM_TOUCHES:
      g_value_set_uint (value, device->num_touches);
      break;
    case PROP_TOOL:
      g_value_set_object (value, device->last_tool);
      break;
    case PROP_DIRECTION:
      g_value_set_enum (value, gdk_device_get_direction (device));
      break;
    case PROP_HAS_BIDI_LAYOUTS:
      g_value_set_boolean (value, gdk_device_has_bidi_layouts (device));
      break;
    case PROP_CAPS_LOCK_STATE:
      g_value_set_boolean (value, gdk_device_get_caps_lock_state (device));
      break;
    case PROP_NUM_LOCK_STATE:
      g_value_set_boolean (value, gdk_device_get_num_lock_state (device));
      break;
    case PROP_SCROLL_LOCK_STATE:
      g_value_set_boolean (value, gdk_device_get_scroll_lock_state (device));
      break;
    case PROP_MODIFIER_STATE:
      g_value_set_flags (value, gdk_device_get_modifier_state (device));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

/**
 * gdk_device_get_surface_at_position:
 * @device: pointer #GdkDevice to query info to.
 * @win_x: (out) (allow-none): return location for the X coordinate of the device location,
 *         relative to the surface origin, or %NULL.
 * @win_y: (out) (allow-none): return location for the Y coordinate of the device location,
 *         relative to the surface origin, or %NULL.
 *
 * Obtains the surface underneath @device, returning the location of the device in @win_x and @win_y in
 * double precision. Returns %NULL if the surface tree under @device is not known to GDK (for example,
 * belongs to another application).
 *
 * Returns: (nullable) (transfer none): the #GdkSurface under the
 *   device position, or %NULL.
 **/
GdkSurface *
gdk_device_get_surface_at_position (GdkDevice *device,
                                    double    *win_x,
                                    double    *win_y)
{
  double tmp_x, tmp_y;
  GdkSurface *surface;

  g_return_val_if_fail (GDK_IS_DEVICE (device), NULL);
  g_return_val_if_fail (device->source != GDK_SOURCE_KEYBOARD, NULL);

  surface = _gdk_device_surface_at_position (device, &tmp_x, &tmp_y, NULL);

  if (win_x)
    *win_x = tmp_x;
  if (win_y)
    *win_y = tmp_y;

  return surface;
}

/**
 * gdk_device_get_name:
 * @device: a #GdkDevice
 *
 * Determines the name of the device, suitable
 * for showing in a user interface.
 *
 * Returns: a name
 **/
const char *
gdk_device_get_name (GdkDevice *device)
{
  g_return_val_if_fail (GDK_IS_DEVICE (device), NULL);

  return device->name;
}

/**
 * gdk_device_get_has_cursor:
 * @device: a #GdkDevice
 *
 * Determines whether the pointer follows device motion.
 * This is not meaningful for keyboard devices, which
 * don't have a pointer.
 *
 * Returns: %TRUE if the pointer follows device motion
 **/
gboolean
gdk_device_get_has_cursor (GdkDevice *device)
{
  g_return_val_if_fail (GDK_IS_DEVICE (device), FALSE);

  return device->has_cursor;
}

/**
 * gdk_device_get_source:
 * @device: a #GdkDevice
 *
 * Determines the type of the device.
 *
 * Returns: a #GdkInputSource
 **/
GdkInputSource
gdk_device_get_source (GdkDevice *device)
{
  g_return_val_if_fail (GDK_IS_DEVICE (device), 0);

  return device->source;
}

/**
 * gdk_device_get_axis_use:
 * @device: a pointer #GdkDevice.
 * @index_: the index of the axis.
 *
 * Returns the axis use for @index_.
 *
 * Returns: a #GdkAxisUse specifying how the axis is used.
 **/
GdkAxisUse
gdk_device_get_axis_use (GdkDevice *device,
                         guint      index_)
{
  GdkAxisInfo *info;

  g_return_val_if_fail (GDK_IS_DEVICE (device), GDK_AXIS_IGNORE);
  g_return_val_if_fail (device->source != GDK_SOURCE_KEYBOARD, GDK_AXIS_IGNORE);
  g_return_val_if_fail (index_ < device->axes->len, GDK_AXIS_IGNORE);

  info = &g_array_index (device->axes, GdkAxisInfo, index_);

  return info->use;
}

/**
 * gdk_device_get_display:
 * @device: a #GdkDevice
 *
 * Returns the #GdkDisplay to which @device pertains.
 *
 * Returns: (transfer none): a #GdkDisplay. This memory is owned
 *          by GTK, and must not be freed or unreffed.
 **/
GdkDisplay *
gdk_device_get_display (GdkDevice *device)
{
  g_return_val_if_fail (GDK_IS_DEVICE (device), NULL);

  return device->display;
}

void
_gdk_device_set_associated_device (GdkDevice *device,
                                   GdkDevice *associated)
{
  g_return_if_fail (GDK_IS_DEVICE (device));
  g_return_if_fail (associated == NULL || GDK_IS_DEVICE (associated));

  g_set_object (&device->associated, associated);
}

/*
 * gdk_device_list_physical_devices:
 * @device: a logical #GdkDevice
 *
 * Returns the list of physical devices attached to the given logical
 * #GdkDevice.
 *
 * Returns: (nullable) (transfer container) (element-type GdkDevice):
 *   the list of physical devices attached to a logical #GdkDevice
 */
GList *
gdk_device_list_physical_devices (GdkDevice *device)
{
  g_return_val_if_fail (GDK_IS_DEVICE (device), NULL);

  return g_list_copy (device->physical_devices);
}

void
_gdk_device_add_physical_device (GdkDevice *device,
                                 GdkDevice *physical)
{
  if (!g_list_find (device->physical_devices, physical))
    device->physical_devices = g_list_prepend (device->physical_devices, physical);
}

void
_gdk_device_remove_physical_device (GdkDevice *device,
                                    GdkDevice *physical)
{
  GList *elem;

  elem = g_list_find (device->physical_devices, physical);
  if (elem == NULL)
    return;

  device->physical_devices = g_list_delete_link (device->physical_devices, elem);
}

/*
 * gdk_device_get_n_axes:
 * @device: a pointer #GdkDevice
 *
 * Returns the number of axes the device currently has.
 *
 * Returns: the number of axes.
 **/
int
gdk_device_get_n_axes (GdkDevice *device)
{
  g_return_val_if_fail (GDK_IS_DEVICE (device), 0);
  g_return_val_if_fail (device->source != GDK_SOURCE_KEYBOARD, 0);

  return device->axes->len;
}

/*
 * gdk_device_get_axis: (skip)
 * @device: a #GdkDevice
 * @axes: (array): pointer to an array of axes
 * @use: the use to look for
 * @value: (out): location to store the found value.
 *
 * Interprets an array of double as axis values for a given device,
 * and locates the value in the array for a given axis use.
 *
 * Returns: %TRUE if the given axis use was found, otherwise %FALSE
 **/
gboolean
gdk_device_get_axis (GdkDevice  *device,
                     double     *axes,
                     GdkAxisUse  use,
                     double     *value)
{
  int i;

  g_return_val_if_fail (GDK_IS_DEVICE (device), FALSE);
  g_return_val_if_fail (device->source != GDK_SOURCE_KEYBOARD, FALSE);

  if (axes == NULL)
    return FALSE;

  g_return_val_if_fail (device->axes != NULL, FALSE);

  for (i = 0; i < device->axes->len; i++)
    {
      GdkAxisInfo axis_info;

      axis_info = g_array_index (device->axes, GdkAxisInfo, i);

      if (axis_info.use != use)
        continue;

      if (value)
        *value = axes[i];

      return TRUE;
    }

  return FALSE;
}

static GdkEventMask
get_native_grab_event_mask (GdkEventMask grab_mask)
{
  /* Similar to the above but for pointer events only */
  return
    GDK_POINTER_MOTION_MASK |
    GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
    GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
    GDK_SCROLL_MASK |
    (grab_mask &
     ~(GDK_BUTTON_MOTION_MASK |
       GDK_BUTTON1_MOTION_MASK |
       GDK_BUTTON2_MOTION_MASK |
       GDK_BUTTON3_MOTION_MASK));
}

GdkGrabStatus
gdk_device_grab (GdkDevice        *device,
                 GdkSurface       *surface,
                 gboolean          owner_events,
                 GdkEventMask      event_mask,
                 GdkCursor        *cursor,
                 guint32           time_)
{
  GdkGrabStatus res;

  g_return_val_if_fail (GDK_IS_DEVICE (device), GDK_GRAB_FAILED);
  g_return_val_if_fail (GDK_IS_SURFACE (surface), GDK_GRAB_FAILED);
  g_return_val_if_fail (gdk_surface_get_display (surface) == gdk_device_get_display (device), GDK_GRAB_FAILED);

  if (GDK_SURFACE_DESTROYED (surface))
    return GDK_GRAB_NOT_VIEWABLE;

  res = GDK_DEVICE_GET_CLASS (device)->grab (device,
                                             surface,
                                             owner_events,
                                             get_native_grab_event_mask (event_mask),
                                             NULL,
                                             cursor,
                                             time_);

  if (res == GDK_GRAB_SUCCESS)
    {
      GdkDisplay *display;
      gulong serial;

      display = gdk_surface_get_display (surface);
      serial = _gdk_display_get_next_serial (display);

      _gdk_display_add_device_grab (display,
                                    device,
                                    surface,
                                    owner_events,
                                    event_mask,
                                    serial,
                                    time_,
                                    FALSE);
    }

  return res;
}

void
gdk_device_ungrab (GdkDevice  *device,
                   guint32     time_)
{
  g_return_if_fail (GDK_IS_DEVICE (device));

  GDK_DEVICE_GET_CLASS (device)->ungrab (device, time_);
}

/* Private API */
void
_gdk_device_reset_axes (GdkDevice *device)
{
  int i;

  for (i = device->axes->len - 1; i >= 0; i--)
    g_array_remove_index (device->axes, i);

  g_object_notify_by_pspec (G_OBJECT (device), device_props[PROP_N_AXES]);
}

guint
_gdk_device_add_axis (GdkDevice   *device,
                      GdkAxisUse   use,
                      double       min_value,
                      double       max_value,
                      double       resolution)
{
  GdkAxisInfo axis_info;
  guint pos;

  axis_info.use = use;
  axis_info.min_value = min_value;
  axis_info.max_value = max_value;
  axis_info.resolution = resolution;

  switch ((guint) use)
    {
    case GDK_AXIS_X:
    case GDK_AXIS_Y:
      axis_info.min_axis = 0;
      axis_info.max_axis = 0;
      break;
    case GDK_AXIS_XTILT:
    case GDK_AXIS_YTILT:
      axis_info.min_axis = -1;
      axis_info.max_axis = 1;
      break;
    default:
      axis_info.min_axis = 0;
      axis_info.max_axis = 1;
      break;
    }

  device->axes = g_array_append_val (device->axes, axis_info);
  pos = device->axes->len - 1;

  g_object_notify_by_pspec (G_OBJECT (device), device_props[PROP_N_AXES]);

  return pos;
}

void
_gdk_device_get_axis_info (GdkDevice   *device,
                           guint        index_,
                           GdkAxisUse   *use,
                           double       *min_value,
                           double       *max_value,
                           double       *resolution)
{
  GdkAxisInfo *info;

  g_return_if_fail (GDK_IS_DEVICE (device));
  g_return_if_fail (index_ < device->axes->len);

  info = &g_array_index (device->axes, GdkAxisInfo, index_);

  *use = info->use;
  *min_value = info->min_value;
  *max_value = info->max_value;
  *resolution = info->resolution;
}

static GdkAxisInfo *
find_axis_info (GArray     *array,
                GdkAxisUse  use)
{
  GdkAxisInfo *info;
  int i;

  for (i = 0; i < GDK_AXIS_LAST; i++)
    {
      info = &g_array_index (array, GdkAxisInfo, i);

      if (info->use == use)
        return info;
    }

  return NULL;
}

gboolean
_gdk_device_translate_surface_coord (GdkDevice *device,
                                     GdkSurface *surface,
                                     guint      index_,
                                     double     value,
                                     double    *axis_value)
{
  GdkAxisInfo axis_info;
  GdkAxisInfo *axis_info_x, *axis_info_y;
  double device_width, device_height;
  double x_offset, y_offset;
  double x_scale, y_scale;
  double x_min, y_min;
  double x_resolution, y_resolution;
  double device_aspect;
  int surface_width, surface_height;

  if (index_ >= device->axes->len)
    return FALSE;

  axis_info = g_array_index (device->axes, GdkAxisInfo, index_);

  if (axis_info.use != GDK_AXIS_X &&
      axis_info.use != GDK_AXIS_Y)
    return FALSE;

  if (axis_info.use == GDK_AXIS_X)
    {
      axis_info_x = &axis_info;
      axis_info_y = find_axis_info (device->axes, GDK_AXIS_Y);
      if (axis_info_y == NULL)
        return FALSE;
    }
  else
    {
      axis_info_x = find_axis_info (device->axes, GDK_AXIS_X);
      axis_info_y = &axis_info;
      if (axis_info_x == NULL)
        return FALSE;
    }

  device_width = axis_info_x->max_value - axis_info_x->min_value;
  device_height = axis_info_y->max_value - axis_info_y->min_value;

  x_min = axis_info_x->min_value;
  y_min = axis_info_y->min_value;

  surface_width = gdk_surface_get_width (surface);
  surface_height = gdk_surface_get_height (surface);

  x_resolution = axis_info_x->resolution;
  y_resolution = axis_info_y->resolution;

  /*
   * Some drivers incorrectly report the resolution of the device
   * as zero (in partiular linuxwacom < 0.5.3 with usb tablets).
   * This causes the device_aspect to become NaN and totally
   * breaks windowed mode.  If this is the case, the best we can
   * do is to assume the resolution is non-zero is equal in both
   * directions (which is true for many devices).  The absolute
   * value of the resolution doesn't matter since we only use the
   * ratio.
   */
  if (x_resolution == 0 || y_resolution == 0)
    {
      x_resolution = 1;
      y_resolution = 1;
    }

  device_aspect = (device_height * y_resolution) /
    (device_width * x_resolution);

  if (device_aspect * surface_width >= surface_height)
    {
      /* device taller than surface */
      x_scale = surface_width / device_width;
      y_scale = (x_scale * x_resolution) / y_resolution;

      x_offset = 0;
      y_offset = - (device_height * y_scale - surface_height) / 2;
    }
  else
    {
      /* surface taller than device */
      y_scale = surface_height / device_height;
      x_scale = (y_scale * y_resolution) / x_resolution;

      y_offset = 0;
      x_offset = - (device_width * x_scale - surface_width) / 2;
    }

  if (axis_value)
    {
      if (axis_info.use == GDK_AXIS_X)
        *axis_value = x_offset + x_scale * (value - x_min);
      else
        *axis_value = y_offset + y_scale * (value - y_min);
    }

  return TRUE;
}

gboolean
_gdk_device_translate_screen_coord (GdkDevice *device,
                                    GdkSurface *surface,
                                    double     surface_root_x,
                                    double     surface_root_y,
                                    double     screen_width,
                                    double     screen_height,
                                    guint      index_,
                                    double     value,
                                    double    *axis_value)
{
  GdkAxisInfo axis_info;
  double axis_width, scale, offset;

  if (index_ >= device->axes->len)
    return FALSE;

  axis_info = g_array_index (device->axes, GdkAxisInfo, index_);

  if (axis_info.use != GDK_AXIS_X &&
      axis_info.use != GDK_AXIS_Y)
    return FALSE;

  axis_width = axis_info.max_value - axis_info.min_value;

  if (axis_info.use == GDK_AXIS_X)
    {
      if (axis_width > 0)
        scale = screen_width / axis_width;
      else
        scale = 1;

      offset = - surface_root_x;
    }
  else
    {
      if (axis_width > 0)
        scale = screen_height / axis_width;
      else
        scale = 1;

      offset = - surface_root_y;
    }

  if (axis_value)
    *axis_value = offset + scale * (value - axis_info.min_value);

  return TRUE;
}

gboolean
_gdk_device_translate_axis (GdkDevice *device,
                            guint      index_,
                            double     value,
                            double    *axis_value)
{
  GdkAxisInfo axis_info;
  double axis_width, out;

  if (index_ >= device->axes->len)
    return FALSE;

  axis_info = g_array_index (device->axes, GdkAxisInfo, index_);

  if (axis_info.use == GDK_AXIS_X ||
      axis_info.use == GDK_AXIS_Y)
    return FALSE;

  axis_width = axis_info.max_value - axis_info.min_value;
  out = (axis_info.max_axis * (value - axis_info.min_value) +
         axis_info.min_axis * (axis_info.max_value - value)) / axis_width;

  if (axis_value)
    *axis_value = out;

  return TRUE;
}

GdkSurface *
_gdk_device_surface_at_position (GdkDevice       *device,
                                 double          *win_x,
                                 double          *win_y,
                                 GdkModifierType *mask)
{
  return GDK_DEVICE_GET_CLASS (device)->surface_at_position (device,
                                                             win_x,
                                                             win_y,
                                                             mask);
}

/**
 * gdk_device_get_vendor_id:
 * @device: a physical #GdkDevice
 *
 * Returns the vendor ID of this device, or %NULL if this information couldn't
 * be obtained. This ID is retrieved from the device, and is thus constant for
 * it.
 *
 * This function, together with gdk_device_get_product_id(), can be used to eg.
 * compose #GSettings paths to store settings for this device.
 *
 * |[<!-- language="C" -->
 *  static GSettings *
 *  get_device_settings (GdkDevice *device)
 *  {
 *    const char *vendor, *product;
 *    GSettings *settings;
 *    GdkDevice *device;
 *    char *path;
 *
 *    vendor = gdk_device_get_vendor_id (device);
 *    product = gdk_device_get_product_id (device);
 *
 *    path = g_strdup_printf ("/org/example/app/devices/%s:%s/", vendor, product);
 *    settings = g_settings_new_with_path (DEVICE_SCHEMA, path);
 *    g_free (path);
 *
 *    return settings;
 *  }
 * ]|
 *
 * Returns: (nullable): the vendor ID, or %NULL
 */
const char *
gdk_device_get_vendor_id (GdkDevice *device)
{
  g_return_val_if_fail (GDK_IS_DEVICE (device), NULL);

  return device->vendor_id;
}

/**
 * gdk_device_get_product_id:
 * @device: a physical #GdkDevice
 *
 * Returns the product ID of this device, or %NULL if this information couldn't
 * be obtained. This ID is retrieved from the device, and is thus constant for
 * it. See gdk_device_get_vendor_id() for more information.
 *
 * Returns: (nullable): the product ID, or %NULL
 */
const char *
gdk_device_get_product_id (GdkDevice *device)
{
  g_return_val_if_fail (GDK_IS_DEVICE (device), NULL);

  return device->product_id;
}

void
gdk_device_set_seat (GdkDevice *device,
                     GdkSeat   *seat)
{
  g_return_if_fail (GDK_IS_DEVICE (device));
  g_return_if_fail (!seat || GDK_IS_SEAT (seat));

  if (device->seat == seat)
    return;

  device->seat = seat;
  g_object_notify (G_OBJECT (device), "seat");
}

/**
 * gdk_device_get_seat:
 * @device: A #GdkDevice
 *
 * Returns the #GdkSeat the device belongs to.
 *
 * Returns: (transfer none): a #GdkSeat
 **/
GdkSeat *
gdk_device_get_seat (GdkDevice *device)
{
  g_return_val_if_fail (GDK_IS_DEVICE (device), NULL);

  return device->seat;
}

void
gdk_device_update_tool (GdkDevice     *device,
                        GdkDeviceTool *tool)
{
  g_return_if_fail (GDK_IS_DEVICE (device));

  if (g_set_object (&device->last_tool, tool))
    {
      g_object_notify (G_OBJECT (device), "tool");
      g_signal_emit (device, signals[TOOL_CHANGED], 0, tool);
    }
}

/**
 * gdk_device_get_num_touches:
 * @device: a #GdkDevice
 *
 * Retrieves the number of touch points associated to @device.
 *
 * Returns: the number of touch points
 */
guint
gdk_device_get_num_touches (GdkDevice *device)
{
  g_return_val_if_fail (GDK_IS_DEVICE (device), 0);

  return device->num_touches;
}

/**
 * gdk_device_get_device_tool:
 * @device: a #GdkDevice
 *
 * Retrieves the #GdkDeviceTool associated to @device.
 *
 * Returns: (transfer none): the #GdkDeviceTool
 */
GdkDeviceTool *
gdk_device_get_device_tool (GdkDevice *device)
{
  g_return_val_if_fail (GDK_IS_DEVICE (device), NULL);

  return device->last_tool;
}

/**
 * gdk_device_get_caps_lock_state:
 * @device: a #GdkDevice
 *
 * Retrieves whether the Caps Lock modifier of the
 * keyboard is locked, if @device is a keyboard device.
 *
 * Returns: %TRUE if Caps Lock is on for @device
 */
gboolean
gdk_device_get_caps_lock_state (GdkDevice *device)
{
  GdkKeymap *keymap = gdk_display_get_keymap (device->display);

  if (device->source == GDK_SOURCE_KEYBOARD)
    return gdk_keymap_get_caps_lock_state (keymap);

  return FALSE;
}

/**
 * gdk_device_get_num_lock_state:
 * @device: a #GdkDevice
 *
 * Retrieves whether the Num Lock modifier of the
 * keyboard is locked, if @device is a keyboard device.
 *
 * Returns: %TRUE if Num Lock is on for @device
 */
gboolean
gdk_device_get_num_lock_state (GdkDevice *device)
{
  GdkKeymap *keymap = gdk_display_get_keymap (device->display);

  if (device->source == GDK_SOURCE_KEYBOARD)
    return gdk_keymap_get_num_lock_state (keymap);

  return FALSE;
}

/**
 * gdk_device_get_scroll_lock_state:
 * @device: a #GdkDevice
 *
 * Retrieves whether the Scroll Lock modifier of the
 * keyboard is locked, if @device is a keyboard device.
 *
 * Returns: %TRUE if Scroll Lock is on for @device
 */
gboolean
gdk_device_get_scroll_lock_state (GdkDevice *device)
{
  GdkKeymap *keymap = gdk_display_get_keymap (device->display);

  if (device->source == GDK_SOURCE_KEYBOARD)
    return gdk_keymap_get_scroll_lock_state (keymap);

  return FALSE;
}

/**
 * gdk_device_get_modifier_state:
 * @device: a #GdkDevice
 *
 * Retrieves the current modifier state of the keyboard,
 * if @device is a keyboard device.
 *
 * Returns: the current modifier state
 */
GdkModifierType
gdk_device_get_modifier_state (GdkDevice *device)
{
  GdkKeymap *keymap = gdk_display_get_keymap (device->display);

  if (device->source == GDK_SOURCE_KEYBOARD)
    return gdk_keymap_get_modifier_state (keymap);

  return 0;
}

/**
 * gdk_device_get_direction:
 * @device: a #GdkDevice
 *
 * Returns the direction of effective layout of the keyboard,
 * if @device is a keyboard device.
 *
 * The direction of a layout is the direction of the majority
 * of its symbols. See pango_unichar_direction().
 *
 * Returns: %PANGO_DIRECTION_LTR or %PANGO_DIRECTION_RTL
 *   if it can determine the direction. %PANGO_DIRECTION_NEUTRAL
 *   otherwise
 */
PangoDirection
gdk_device_get_direction (GdkDevice *device)
{
  GdkKeymap *keymap = gdk_display_get_keymap (device->display);

  if (device->source == GDK_SOURCE_KEYBOARD)
    return gdk_keymap_get_direction (keymap);

  return PANGO_DIRECTION_NEUTRAL;
}

/**
 * gdk_device_has_bidi_layouts:
 * @device: a #GdkDevice
 *
 * Determines if keyboard layouts for both right-to-left and
 * left-to-right languages are in use on the keyboard, if
 * @device is a keyboard device.
 *
 * Returns: %TRUE if there are layouts with both directions,
 *     %FALSE otherwise
 */
gboolean
gdk_device_has_bidi_layouts (GdkDevice *device)
{
  GdkKeymap *keymap = gdk_display_get_keymap (device->display);

  if (device->source == GDK_SOURCE_KEYBOARD)
    return gdk_keymap_have_bidi_layouts (keymap);

  return FALSE;
}
