/*
  Simple DirectMedia Layer
  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
*/
#include "SDL_internal.h"

#ifdef SDL_VIDEO_DRIVER_X11

#include "SDL_x11pen.h"
#include "SDL_x11video.h"
#include "SDL_x11xinput2.h"
#include "../../events/SDL_events_c.h"
#include "../../events/SDL_mouse_c.h"
#include "../../events/SDL_pen_c.h"
#include "../../events/SDL_touch_c.h"

#define MAX_AXIS 16

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
static bool xinput2_initialized;
#endif
#if defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO) || defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH)
static bool xinput2_scrolling_supported;
static bool xinput2_multitouch_supported;
static bool xinput2_grabbed_touch_raised;
static int xinput2_active_touch_count;
#endif
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_GESTURE
static bool xinput2_gesture_supported;
#endif

/* Opcode returned X11_XQueryExtension
 * It will be used in event processing
 * to know that the event came from
 * this extension */
static int xinput2_opcode;

static Atom xinput2_rel_x_atom;
static Atom xinput2_rel_y_atom;
static Atom xinput2_abs_x_atom;
static Atom xinput2_abs_y_atom;

// Pointer button remapping table
static unsigned char *xinput2_pointer_button_map;
static int xinput2_pointer_button_map_size;

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
typedef struct
{
    int number;
    int scroll_type;
    double prev_value;
    double increment;
    bool prev_value_valid;
} SDL_XInput2ScrollInfo;

typedef struct
{
    int device_id;
    int scroll_info_count;
    SDL_XInput2ScrollInfo *scroll_info;
} SDL_XInput2ScrollableDevice;

static SDL_XInput2ScrollableDevice *scrollable_devices;
static int scrollable_device_count;
#endif

static void parse_relative_valuators(SDL_XInput2DeviceInfo *devinfo, const XIRawEvent *rawev)
{
    SDL_Mouse *mouse = SDL_GetMouse();
    double processed_coords[2] = { 0.0, 0.0 };
    int values_i = 0, found = 0;

    // Use the raw values if a custom transform function is set, or the relative system scale hint is unset.
    const bool use_raw_vals = mouse->InputTransform || !mouse->enable_relative_system_scale;

    for (int i = 0; i < rawev->valuators.mask_len * 8 && found < 2; ++i) {
        if (!XIMaskIsSet(rawev->valuators.mask, i)) {
            continue;
        }

        for (int j = 0; j < 2; ++j) {
            if (devinfo->number[j] == i) {
                const double current_val = use_raw_vals ? rawev->raw_values[values_i] : rawev->valuators.values[values_i];

                if (devinfo->relative[j]) {
                    processed_coords[j] = current_val;
                } else {
                    processed_coords[j] = (current_val - devinfo->prev_coords[j]); // convert absolute to relative
                    devinfo->prev_coords[j] = current_val;
                }
                ++found;

                break;
            }
        }

        ++values_i;
    }

    // Relative mouse motion is delivered to the window with keyboard focus
    if (mouse->relative_mode && SDL_GetKeyboardFocus()) {
        SDL_SendMouseMotion(rawev->time, mouse->focus, (SDL_MouseID)rawev->sourceid, true, (float)processed_coords[0], (float)processed_coords[1]);
    }
}

static int query_xinput2_version(Display *display, int major, int minor)
{
    // We don't care if this fails, so long as it sets major/minor on it's way out the door.
    X11_XIQueryVersion(display, &major, &minor);
    return (major * 1000) + minor;
}

static bool xinput2_version_atleast(const int version, const int wantmajor, const int wantminor)
{
    return version >= ((wantmajor * 1000) + wantminor);
}

static SDL_Window *xinput2_get_sdlwindow(SDL_VideoData *videodata, Window window)
{
    const SDL_WindowData *windowdata = X11_FindWindow(videodata, window);
    return windowdata ? windowdata->window : NULL;
}

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
static void xinput2_reset_scrollable_valuators()
{
    for (int i = 0; i < scrollable_device_count; ++i) {
        for (int j = 0; j < scrollable_devices[i].scroll_info_count; ++j) {
            scrollable_devices[i].scroll_info[j].prev_value_valid = false;
        }
    }
}

static void xinput2_parse_scrollable_valuators(const XIDeviceEvent *xev)
{
    for (int i = 0; i < scrollable_device_count; ++i) {
        const SDL_XInput2ScrollableDevice *sd = &scrollable_devices[i];
        if (xev->sourceid == sd->device_id) {
            int values_i = 0;
            for (int j = 0; j < xev->valuators.mask_len * 8; ++j) {
                if (!XIMaskIsSet(xev->valuators.mask, j)) {
                    continue;
                }

                for (int k = 0; k < sd->scroll_info_count; ++k) {
                    SDL_XInput2ScrollInfo *info = &sd->scroll_info[k];
                    if (info->number == j) {
                        const double current_val = xev->valuators.values[values_i];
                        const double delta = (info->prev_value - current_val) / info->increment;
                        /* Ignore very large jumps that can happen as a result of overflowing
                         * the maximum range, as the driver will reset the position to zero
                         * at "something that's close to 2^32".
                         *
                         * The first scroll event is meaningless by itself and must be discarded,
                         * as it is only useful for establishing a baseline for future deltas.
                         * This is a known deficiency of the XInput2 scroll protocol, and,
                         * unfortunately, there is nothing we can do about it.
                         *
                         * http://who-t.blogspot.com/2012/06/xi-21-protocol-design-issues.html
                         */
                        if (info->prev_value_valid && SDL_fabs(delta) < (double)SDL_MAX_SINT32 * 0.95) {
                            const double x = info->scroll_type == XIScrollTypeHorizontal ? delta : 0;
                            const double y = info->scroll_type == XIScrollTypeVertical ? delta : 0;

                            SDL_Mouse *mouse = SDL_GetMouse();
                            SDL_SendMouseWheel(xev->time, mouse->focus, (SDL_MouseID)xev->sourceid, (float)x, (float)y, SDL_MOUSEWHEEL_NORMAL);
                        }
                        info->prev_value = current_val;
                        info->prev_value_valid = true;
                    }
                }

                ++values_i;
            }
        }
    }
}
#endif // SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
static void xinput2_normalize_touch_coordinates(SDL_Window *window, double in_x, double in_y, float *out_x, float *out_y)
{
    if (window) {
        if (window->w == 1) {
            *out_x = 0.5f;
        } else {
            *out_x = (float)in_x / (window->w - 1);
        }
        if (window->h == 1) {
            *out_y = 0.5f;
        } else {
            *out_y = (float)in_y / (window->h - 1);
        }
    } else {
        // couldn't find the window...
        *out_x = (float)in_x;
        *out_y = (float)in_y;
    }
}
#endif // SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH

bool X11_InitXinput2(SDL_VideoDevice *_this)
{
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
    SDL_VideoData *data = _this->internal;

    int version = 0;
    XIEventMask eventmask;
    unsigned char mask[5] = { 0, 0, 0, 0, 0 };
    int event, err;

    /* XInput2 is required for relative mouse mode, so you probably want to leave this enabled */
    if (!SDL_GetHintBoolean("SDL_VIDEO_X11_XINPUT2", true)) {
        return false;
    }

    /*
     * Initialize XInput 2
     * According to http://who-t.blogspot.com/2009/05/xi2-recipes-part-1.html its better
     * to inform Xserver what version of Xinput we support.The server will store the version we support.
     * "As XI2 progresses it becomes important that you use this call as the server may treat the client
     * differently depending on the supported version".
     *
     * FIXME:event and err are not needed but if not passed X11_XQueryExtension returns SegmentationFault
     */
    if (!SDL_X11_HAVE_XINPUT2 ||
        !X11_XQueryExtension(data->display, "XInputExtension", &xinput2_opcode, &event, &err)) {
        return false; // X server does not have XInput at all
    }

    // We need at least 2.4 for Gesture, 2.2 for Multitouch, 2.0 otherwise.
    version = query_xinput2_version(data->display, 2, 4);
    if (!xinput2_version_atleast(version, 2, 0)) {
        return false; // X server does not support the version we want at all.
    }

    xinput2_initialized = true;

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO // Smooth scrolling needs XInput 2.1
    xinput2_scrolling_supported = xinput2_version_atleast(version, 2, 1);
#endif
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH // Multitouch needs XInput 2.2
    xinput2_multitouch_supported = xinput2_version_atleast(version, 2, 2);
#endif
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_GESTURE // Gesture needs XInput 2.4
    xinput2_gesture_supported = xinput2_version_atleast(version, 2, 4);
#endif

    // Populate the atoms for finding relative axes
    xinput2_rel_x_atom = X11_XInternAtom(data->display, "Rel X", False);
    xinput2_rel_y_atom = X11_XInternAtom(data->display, "Rel Y", False);
    xinput2_abs_x_atom = X11_XInternAtom(data->display, "Abs X", False);
    xinput2_abs_y_atom = X11_XInternAtom(data->display, "Abs Y", False);

    // Enable raw motion events for this display
    SDL_zero(eventmask);
    SDL_zeroa(mask);
    eventmask.deviceid = XIAllMasterDevices;
    eventmask.mask_len = sizeof(mask);
    eventmask.mask = mask;

    XISetMask(mask, XI_RawMotion);
    XISetMask(mask, XI_RawButtonPress);
    XISetMask(mask, XI_RawButtonRelease);

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
    if (xinput2_scrolling_supported) {
        XISetMask(mask, XI_Motion);
    }
#endif

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
    // Enable raw touch events if supported
    if (xinput2_multitouch_supported) {
        XISetMask(mask, XI_RawTouchBegin);
        XISetMask(mask, XI_RawTouchUpdate);
        XISetMask(mask, XI_RawTouchEnd);
    }
#endif

    X11_XISelectEvents(data->display, DefaultRootWindow(data->display), &eventmask, 1);

    SDL_zero(eventmask);
    SDL_zeroa(mask);
    eventmask.deviceid = XIAllDevices;
    eventmask.mask_len = sizeof(mask);
    eventmask.mask = mask;

    XISetMask(mask, XI_HierarchyChanged);
    X11_XISelectEvents(data->display, DefaultRootWindow(data->display), &eventmask, 1);

    X11_Xinput2UpdateDevices(_this);
    X11_Xinput2UpdatePointerMapping(_this);

    return true;
#else
    return false;
#endif
}

void X11_QuitXinput2(SDL_VideoDevice *_this)
{
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
    SDL_free(xinput2_pointer_button_map);
    xinput2_pointer_button_map = NULL;
    xinput2_pointer_button_map_size = 0;

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
    for (int i = 0; i < scrollable_device_count; ++i) {
        SDL_free(scrollable_devices[i].scroll_info);
    }
    SDL_free(scrollable_devices);
    scrollable_devices = NULL;
    scrollable_device_count = 0;
#endif
#endif
}

void X11_Xinput2UpdatePointerMapping(SDL_VideoDevice *_this)
{
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
    if (X11_Xinput2IsInitialized()) {
        SDL_VideoData *vid = _this->internal;

        SDL_free(xinput2_pointer_button_map);
        xinput2_pointer_button_map = NULL;
        xinput2_pointer_button_map_size = 0;

        xinput2_pointer_button_map_size = X11_XGetPointerMapping(vid->display, NULL, 0);
        if (xinput2_pointer_button_map_size) {
            xinput2_pointer_button_map = SDL_calloc(xinput2_pointer_button_map_size, sizeof(unsigned char));
            if (xinput2_pointer_button_map) {
                xinput2_pointer_button_map_size = X11_XGetPointerMapping(vid->display, xinput2_pointer_button_map, xinput2_pointer_button_map_size);
            } else {
                xinput2_pointer_button_map_size = 0;
            }
        }
    }
#endif
}

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
// xi2 device went away? take it out of the list.
static void xinput2_remove_device_info(SDL_VideoData *videodata, const int device_id)
{
    SDL_XInput2DeviceInfo *prev = NULL;
    SDL_XInput2DeviceInfo *devinfo;

    for (devinfo = videodata->mouse_device_info; devinfo; devinfo = devinfo->next) {
        if (devinfo->device_id == device_id) {
            SDL_assert((devinfo == videodata->mouse_device_info) == (prev == NULL));
            if (!prev) {
                videodata->mouse_device_info = devinfo->next;
            } else {
                prev->next = devinfo->next;
            }
            SDL_free(devinfo);
            return;
        }
        prev = devinfo;
    }
}

static SDL_XInput2DeviceInfo *xinput2_get_device_info(SDL_VideoData *videodata, const int device_id)
{
    // cache device info as we see new devices.
    SDL_XInput2DeviceInfo *prev = NULL;
    SDL_XInput2DeviceInfo *devinfo;
    XIDeviceInfo *xidevinfo;
    int i;

    for (devinfo = videodata->mouse_device_info; devinfo; devinfo = devinfo->next) {
        if (devinfo->device_id == device_id) {
            SDL_assert((devinfo == videodata->mouse_device_info) == (prev == NULL));
            if (prev) { // move this to the front of the list, assuming we'll get more from this one.
                prev->next = devinfo->next;
                devinfo->next = videodata->mouse_device_info;
                videodata->mouse_device_info = devinfo;
            }
            return devinfo;
        }
        prev = devinfo;
    }

    // don't know about this device yet, query and cache it.
    devinfo = (SDL_XInput2DeviceInfo *)SDL_calloc(1, sizeof(SDL_XInput2DeviceInfo));
    if (!devinfo) {
        return NULL;
    }

    xidevinfo = X11_XIQueryDevice(videodata->display, device_id, &i);
    if (!xidevinfo) {
        SDL_free(devinfo);
        return NULL;
    }

    devinfo->device_id = device_id;

    /* Search for relative axes with the following priority:
     *  - Labelled 'Rel X'/'Rel Y'
     *   - Labelled 'Abs X'/'Abs Y'
     *    - The first two axes found
     */
    bool have_rel_x = false, have_rel_y = false;
    bool have_abs_x = false, have_abs_y = false;
    int axis_index = 0;
    for (i = 0; i < xidevinfo->num_classes; i++) {
        const XIValuatorClassInfo *v = (const XIValuatorClassInfo *)xidevinfo->classes[i];
        if (v->type == XIValuatorClass) {
            if (v->label == xinput2_rel_x_atom || (v->label == xinput2_abs_x_atom && !have_rel_x) ||
                (axis_index == 0 && !have_rel_x && !have_abs_x)) {
                devinfo->number[0] = v->number;
                devinfo->relative[0] = (v->mode == XIModeRelative);
                devinfo->minval[0] = v->min;
                devinfo->maxval[0] = v->max;

                if (v->label == xinput2_rel_x_atom) {
                    have_rel_x = true;
                } else if (v->label == xinput2_abs_x_atom) {
                    have_abs_x = true;
                }
            } else if (v->label == xinput2_rel_y_atom || (v->label == xinput2_abs_y_atom && !have_rel_y) ||
                       (axis_index == 1 && !have_rel_y && !have_abs_y)) {
                devinfo->number[1] = v->number;
                devinfo->relative[1] = (v->mode == XIModeRelative);
                devinfo->minval[1] = v->min;
                devinfo->maxval[1] = v->max;

                if (v->label == xinput2_rel_y_atom) {
                    have_rel_y = true;
                } else if (v->label == xinput2_abs_y_atom) {
                    have_abs_y = true;
                }
            }

            // If two relative axes were found, nothing more to do.
            if (have_rel_x && have_rel_y) {
                break;
            }

            ++axis_index;
        }
    }

    X11_XIFreeDeviceInfo(xidevinfo);

    devinfo->next = videodata->mouse_device_info;
    videodata->mouse_device_info = devinfo;

    return devinfo;
}
#endif

void X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie)
{
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
    SDL_VideoData *videodata = _this->internal;

    if (cookie->extension != xinput2_opcode) {
        return;
    }

    switch (cookie->evtype) {
    case XI_HierarchyChanged:
    {
        const XIHierarchyEvent *hierev = (const XIHierarchyEvent *)cookie->data;
        int i;
        for (i = 0; i < hierev->num_info; i++) {
            // pen stuff...
            if ((hierev->info[i].flags & (XISlaveRemoved | XIDeviceDisabled)) != 0) {
                X11_RemovePenByDeviceID(hierev->info[i].deviceid);  // it's okay if this thing isn't actually a pen, it'll handle it.
            } else if ((hierev->info[i].flags & (XISlaveAdded | XIDeviceEnabled)) != 0) {
                X11_MaybeAddPenByDeviceID(_this, hierev->info[i].deviceid);  // this will do more checks to make sure this is valid.
            }

            // not pen stuff...
            if (hierev->info[i].flags & XISlaveRemoved) {
                xinput2_remove_device_info(videodata, hierev->info[i].deviceid);
            }
        }
        videodata->xinput_hierarchy_changed = true;
    } break;

    // !!! FIXME: the pen code used to rescan all devices here, but we can do this device-by-device with XI_HierarchyChanged. When do these events fire and why?
    //case XI_PropertyEvent:
    //case XI_DeviceChanged:

    case XI_PropertyEvent:
    {
        const XIPropertyEvent *proev = (const XIPropertyEvent *)cookie->data;
        // Handle pen proximity enter/leave
        if (proev->what == XIPropertyModified && proev->property == videodata->atoms.pen_atom_wacom_serial_ids) {
            const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
            SDL_WindowData *windowdata = X11_FindWindow(videodata, xev->event);
            X11_NotifyPenProximityChange(_this, windowdata ? windowdata->window : NULL, proev->deviceid);
        }
    } break;

    case XI_RawMotion:
    {
        const XIRawEvent *rawev = (const XIRawEvent *)cookie->data;
        const bool is_pen = X11_FindPenByDeviceID(rawev->sourceid) != NULL;

        videodata->global_mouse_changed = true;
        if (is_pen) {
            break; // Pens check for XI_Motion instead
        }

        SDL_XInput2DeviceInfo *devinfo = xinput2_get_device_info(videodata, rawev->deviceid);
        if (!devinfo) {
            break; // oh well.
        }

        parse_relative_valuators(devinfo, rawev);
    } break;

    case XI_KeyPress:
    case XI_KeyRelease:
    {
        const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
        SDL_WindowData *windowdata = X11_FindWindow(videodata, xev->event);
        XEvent xevent;

        if (xev->deviceid != xev->sourceid) {
            // Discard events from "Master" devices to avoid duplicates.
            break;
        }

        if (cookie->evtype == XI_KeyPress) {
            xevent.type = KeyPress;
        } else {
            xevent.type = KeyRelease;
        }
        xevent.xkey.serial = xev->serial;
        xevent.xkey.send_event = xev->send_event;
        xevent.xkey.display = xev->display;
        xevent.xkey.window = xev->event;
        xevent.xkey.root = xev->root;
        xevent.xkey.subwindow = xev->child;
        xevent.xkey.time = xev->time;
        xevent.xkey.x = (int)xev->event_x;
        xevent.xkey.y = (int)xev->event_y;
        xevent.xkey.x_root = (int)xev->root_x;
        xevent.xkey.y_root = (int)xev->root_y;
        xevent.xkey.state = xev->mods.effective;
        xevent.xkey.keycode = xev->detail;
        xevent.xkey.same_screen = 1;

        X11_HandleKeyEvent(_this, windowdata, (SDL_KeyboardID)xev->sourceid, &xevent);
    } break;

    case XI_RawButtonPress:
    case XI_RawButtonRelease:
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
    case XI_RawTouchBegin:
    case XI_RawTouchUpdate:
    case XI_RawTouchEnd:
#endif
    {
        videodata->global_mouse_changed = true;
    } break;

    case XI_ButtonPress:
    case XI_ButtonRelease:
    {
        const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
        X11_PenHandle *pen = X11_FindPenByDeviceID(xev->sourceid);
        int button = xev->detail;
        const bool down = (cookie->evtype == XI_ButtonPress);
#if defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO) || defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH)
        bool pointer_emulated = (xev->flags & XIPointerEmulated) != 0;
#else
        bool pointer_emulated = false;
#endif

        if (pen) {
            if (xev->deviceid != xev->sourceid) {
                // Discard events from "Master" devices to avoid duplicates.
                break;
            }
            // Only report button event; if there was also pen movement / pressure changes, we expect an XI_Motion event first anyway.
            SDL_Window *window = xinput2_get_sdlwindow(videodata, xev->event);
            if (button == 1) { // button 1 is the pen tip
                SDL_SendPenTouch(0, pen->pen, window, pen->is_eraser, down);
            } else {
                SDL_SendPenButton(0, pen->pen, window, button - 1, down);
            }
        } else if (!pointer_emulated) {
            // Otherwise assume a regular mouse
            SDL_WindowData *windowdata = X11_FindWindow(videodata, xev->event);
            int x_ticks = 0, y_ticks = 0;

            // Slave pointer devices don't have button remapping applied automatically, so do it manually.
            if (xev->deviceid != videodata->xinput_master_pointer_device) {
                if (button <= xinput2_pointer_button_map_size) {
                    button = xinput2_pointer_button_map[button - 1];
                }
            }

            /* Discard wheel events from "Master" devices to avoid duplicates,
             * as coarse wheel events are stateless and can't be deduplicated.
             */
            if (xev->deviceid != xev->sourceid && X11_IsWheelEvent(button, &x_ticks, &y_ticks)) {
                break;
            }

            if (down) {
                X11_HandleButtonPress(_this, windowdata, (SDL_MouseID)xev->sourceid, button,
                                      (float)xev->event_x, (float)xev->event_y, xev->time);
            } else {
                X11_HandleButtonRelease(_this, windowdata, (SDL_MouseID)xev->sourceid, button, xev->time);
            }
        }
    } break;

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
    case XI_Enter:
        xinput2_reset_scrollable_valuators();
        break;
#endif

    /* Register to receive XI_Motion (which deactivates MotionNotify), so that we can distinguish
       real mouse motions from synthetic ones, for multitouch and pen support. */
    case XI_Motion:
    {
        const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
#if defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO) || defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH)
        bool pointer_emulated = (xev->flags & XIPointerEmulated) != 0;
#else
        bool pointer_emulated = false;
#endif

        videodata->global_mouse_changed = true;

        X11_PenHandle *pen = X11_FindPenByDeviceID(xev->sourceid);

        if (pen) {
            if (xev->deviceid != xev->sourceid) {
                // Discard events from "Master" devices to avoid duplicates.
                break;
            }

            SDL_Window *window = xinput2_get_sdlwindow(videodata, xev->event);
            SDL_SendPenMotion(0, pen->pen, window, (float) xev->event_x, (float) xev->event_y);

            float axes[SDL_PEN_AXIS_COUNT];
            X11_PenAxesFromValuators(pen, xev->valuators.values, xev->valuators.mask, xev->valuators.mask_len, axes);

            for (int i = 0; i < SDL_arraysize(axes); i++) {
                if (pen->valuator_for_axis[i] != SDL_X11_PEN_AXIS_VALUATOR_MISSING) {
                    SDL_SendPenAxis(0, pen->pen, window, (SDL_PenAxis) i, axes[i]);
                }
            }
        } else if (!pointer_emulated) {
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
            if (xev->deviceid == xev->sourceid) {
                xinput2_parse_scrollable_valuators(xev);
            }
#endif

            /* Use the master device for non-relative motion, as the slave devices can seemingly lag behind,
             * except when the mouse is grabbed and touches are active, as core input events are used for
             * absolute motion while the mouse is grabbed, and core events don't have the XIPointerEmulated
             * flag to filter out pointer events emulated from touch events.
             */
            SDL_Window *window = xinput2_get_sdlwindow(videodata, xev->event);
            if (window && (xev->deviceid == videodata->xinput_master_pointer_device || (xinput2_active_touch_count && window->internal->mouse_grabbed))) {
                SDL_Mouse *mouse = SDL_GetMouse();
                if (!mouse->relative_mode) {
                        X11_ProcessHitTest(_this, window->internal, (float)xev->event_x, (float)xev->event_y, false);
                        SDL_SendMouseMotion(0, window, SDL_GLOBAL_MOUSE_ID, false, (float)xev->event_x, (float)xev->event_y);
                }
            }
        }
    } break;

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
    case XI_TouchBegin:
    {
        const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
        float x, y;
        ++xinput2_active_touch_count;
        SDL_Window *window = xinput2_get_sdlwindow(videodata, xev->event);
        xinput2_normalize_touch_coordinates(window, xev->event_x, xev->event_y, &x, &y);
        SDL_SendTouch(0, xev->sourceid, xev->detail, window, SDL_EVENT_FINGER_DOWN, x, y, 1.0);
    } break;

    case XI_TouchEnd:
    {
        const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
        float x, y;
        SDL_Window *window = xinput2_get_sdlwindow(videodata, xev->event);
        if (!--xinput2_active_touch_count && window && window->internal->mouse_grabbed) {
            xinput2_grabbed_touch_raised = true;
        }
        xinput2_normalize_touch_coordinates(window, xev->event_x, xev->event_y, &x, &y);
        SDL_SendTouch(0, xev->sourceid, xev->detail, window, SDL_EVENT_FINGER_UP, x, y, 1.0);
    } break;

    case XI_TouchUpdate:
    {
        const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
        float x, y;
        SDL_Window *window = xinput2_get_sdlwindow(videodata, xev->event);
        xinput2_normalize_touch_coordinates(window, xev->event_x, xev->event_y, &x, &y);
        SDL_SendTouchMotion(0, xev->sourceid, xev->detail, window, x, y, 1.0);
    } break;
#endif // SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_GESTURE
    case XI_GesturePinchBegin:
    case XI_GesturePinchUpdate:
    case XI_GesturePinchEnd:
    {
        const XIGesturePinchEvent *xev = (const XIGesturePinchEvent *)cookie->data;
        float x, y;
        SDL_Window *window = xinput2_get_sdlwindow(videodata, xev->event);
        xinput2_normalize_touch_coordinates(window, xev->event_x, xev->event_y, &x, &y);

        if (cookie->evtype == XI_GesturePinchBegin) {
            SDL_SendPinch(SDL_EVENT_PINCH_BEGIN, 0, window, 0);
        } else if (cookie->evtype == XI_GesturePinchUpdate) {
            SDL_SendPinch(SDL_EVENT_PINCH_UPDATE, 0, window, (float)xev->scale);
        } else {
            SDL_SendPinch(SDL_EVENT_PINCH_END, 0, window, 0);
        }
    } break;

#endif // SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_GESTURE

    }
#endif // SDL_VIDEO_DRIVER_X11_XINPUT2
}

void X11_InitXinput2Multitouch(SDL_VideoDevice *_this)
{
    xinput2_grabbed_touch_raised = false;
    xinput2_active_touch_count = 0;
}

bool X11_Xinput2HandlesMotionForWindow(SDL_WindowData *window_data)
{
    /* Send the active flag once more after the touch count is zero, to suppress the
     * emulated motion event when the last touch is raised.
     */
    const bool ret = window_data->xinput2_mouse_enabled &&
                     (!window_data->mouse_grabbed || xinput2_active_touch_count || xinput2_grabbed_touch_raised);
    xinput2_grabbed_touch_raised = false;

    return ret;
}

void X11_Xinput2Select(SDL_VideoDevice *_this, SDL_Window *window)
{
#if defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO) || defined(SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH)
    SDL_VideoData *data = _this->internal;
    SDL_WindowData *window_data = window->internal;
    XIEventMask eventmask;
    unsigned char mask[5] = { 0, 0, 0, 0, 0 };

    if (!xinput2_scrolling_supported && !xinput2_multitouch_supported) {
        return;
    }

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

    if (xinput2_scrolling_supported) {
        /* Track enter events that inform us that we need to update
         * the previous scroll coordinates since we cannot track
         * them outside our window.
         */
        XISetMask(mask, XI_Enter);
    }

    if (xinput2_multitouch_supported) {
        XISetMask(mask, XI_TouchBegin);
        XISetMask(mask, XI_TouchUpdate);
        XISetMask(mask, XI_TouchEnd);
        XISetMask(mask, XI_Motion);
    }

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_GESTURE
    if (xinput2_gesture_supported) {
        XISetMask(mask, XI_GesturePinchBegin);
        XISetMask(mask, XI_GesturePinchUpdate);
        XISetMask(mask, XI_GesturePinchEnd);
    }
#endif

    X11_XISelectEvents(data->display, window_data->xwindow, &eventmask, 1);
#endif
}

bool X11_Xinput2IsInitialized(void)
{
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
    return xinput2_initialized;
#else
    return false;
#endif
}

bool X11_Xinput2SelectMouseAndKeyboard(SDL_VideoDevice *_this, SDL_Window *window)
{
    SDL_WindowData *windowdata = window->internal;

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
    const SDL_VideoData *data = _this->internal;

    if (X11_Xinput2IsInitialized()) {
        XIEventMask eventmask;
        unsigned char mask[4] = { 0, 0, 0, 0 };

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

// This is not enabled by default because these events are only delivered to the window with mouse focus, not keyboard focus
#ifdef USE_XINPUT2_KEYBOARD
        XISetMask(mask, XI_KeyPress);
        XISetMask(mask, XI_KeyRelease);
        windowdata->xinput2_keyboard_enabled = true;
#endif

        XISetMask(mask, XI_ButtonPress);
        XISetMask(mask, XI_ButtonRelease);
        XISetMask(mask, XI_Motion);
        windowdata->xinput2_mouse_enabled = true;

        XISetMask(mask, XI_Enter);
        XISetMask(mask, XI_Leave);

        // Hotplugging:
        XISetMask(mask, XI_DeviceChanged);
        XISetMask(mask, XI_HierarchyChanged);
        XISetMask(mask, XI_PropertyEvent); // E.g., when swapping tablet pens

        if (X11_XISelectEvents(data->display, windowdata->xwindow, &eventmask, 1) != Success) {
            SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "Could not enable XInput2 event handling");
            windowdata->xinput2_keyboard_enabled = false;
            windowdata->xinput2_mouse_enabled = false;
        }
    }
#endif

    if (windowdata->xinput2_keyboard_enabled || windowdata->xinput2_mouse_enabled) {
        return true;
    }
    return false;
}

void X11_Xinput2GrabTouch(SDL_VideoDevice *_this, SDL_Window *window)
{
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
    SDL_WindowData *data = window->internal;
    Display *display = data->videodata->display;

    unsigned char mask[4] = { 0, 0, 0, 0 };
    XIGrabModifiers mods;
    XIEventMask eventmask;

    if (!xinput2_multitouch_supported) {
        return;
    }

    mods.modifiers = XIAnyModifier;
    mods.status = 0;

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

    XISetMask(eventmask.mask, XI_TouchBegin);
    XISetMask(eventmask.mask, XI_TouchUpdate);
    XISetMask(eventmask.mask, XI_TouchEnd);
    XISetMask(eventmask.mask, XI_Motion);

    X11_XIGrabTouchBegin(display, XIAllDevices, data->xwindow, True, &eventmask, 1, &mods);
#endif
}

void X11_Xinput2UngrabTouch(SDL_VideoDevice *_this, SDL_Window *window)
{
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
    SDL_WindowData *data = window->internal;
    Display *display = data->videodata->display;

    XIGrabModifiers mods;

    if (!xinput2_multitouch_supported) {
        return;
    }

    xinput2_grabbed_touch_raised = false;

    mods.modifiers = XIAnyModifier;
    mods.status = 0;

    X11_XIUngrabTouchBegin(display, XIAllDevices, data->xwindow, 1, &mods);
#endif
}

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2

static void AddDeviceID(Uint32 deviceID, Uint32 **list, int *count)
{
    int new_count = (*count + 1);
    Uint32 *new_list = (Uint32 *)SDL_realloc(*list, new_count * sizeof(*new_list));
    if (!new_list) {
        // Oh well, we'll drop this one
        return;
    }
    new_list[new_count - 1] = deviceID;

    *count = new_count;
    *list = new_list;
}

static bool HasDeviceID(Uint32 deviceID, const Uint32 *list, int count)
{
    for (int i = 0; i < count; ++i) {
        if (deviceID == list[i]) {
            return true;
        }
    }
    return false;
}

static void AddDeviceID64(Uint64 deviceID, Uint64 **list, int *count)
{
    int new_count = (*count + 1);
    Uint64 *new_list = (Uint64 *)SDL_realloc(*list, new_count * sizeof(*new_list));
    if (!new_list) {
        // Oh well, we'll drop this one
        return;
    }
    new_list[new_count - 1] = deviceID;

    *count = new_count;
    *list = new_list;
}

static bool HasDeviceID64(Uint64 deviceID, const Uint64 *list, int count)
{
    for (int i = 0; i < count; ++i) {
        if (deviceID == list[i]) {
            return true;
        }
    }
    return false;
}

#endif // SDL_VIDEO_DRIVER_X11_XINPUT2

void X11_Xinput2UpdateDevices(SDL_VideoDevice *_this)
{
#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
    SDL_VideoData *data = _this->internal;
    XIDeviceInfo *info;
    int ndevices;
    int old_keyboard_count = 0;
    SDL_KeyboardID *old_keyboards = NULL;
    int new_keyboard_count = 0;
    SDL_KeyboardID *new_keyboards = NULL;
    int old_mouse_count = 0;
    SDL_MouseID *old_mice = NULL;
    int new_mouse_count = 0;
    SDL_MouseID *new_mice = NULL;
    int old_touch_count = 0;
    Uint64 *old_touch_devices = NULL;
    int new_touch_count = 0;
    Uint64 *new_touch_devices = NULL;

    SDL_assert(X11_Xinput2IsInitialized());

    info = X11_XIQueryDevice(data->display, XIAllDevices, &ndevices);

    old_keyboards = SDL_GetKeyboards(&old_keyboard_count);
    old_mice = SDL_GetMice(&old_mouse_count);
    old_touch_devices = SDL_GetTouchDevices(&old_touch_count);

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
    // Scroll devices don't get add/remove events, so just rebuild the list.
    for (int i = 0; i < scrollable_device_count; ++i) {
        SDL_free(scrollable_devices[i].scroll_info);
    }
    SDL_free(scrollable_devices);
    scrollable_devices = NULL;
    scrollable_device_count = 0;
#endif

    for (int i = 0; i < ndevices; i++) {
        XIDeviceInfo *dev = &info[i];

        switch (dev->use) {
        case XIMasterKeyboard:
        case XISlaveKeyboard:
            {
                SDL_KeyboardID keyboardID = (SDL_KeyboardID)dev->deviceid;
                AddDeviceID(keyboardID, &new_keyboards, &new_keyboard_count);
                if (!HasDeviceID(keyboardID, old_keyboards, old_keyboard_count)) {
                    SDL_AddKeyboard(keyboardID, dev->name);
                }
            }
            break;
        case XIMasterPointer:
            data->xinput_master_pointer_device = dev->deviceid;
            SDL_FALLTHROUGH;
        case XISlavePointer:
            {
                SDL_MouseID mouseID = (SDL_MouseID)dev->deviceid;
                AddDeviceID(mouseID, &new_mice, &new_mouse_count);
                if (!HasDeviceID(mouseID, old_mice, old_mouse_count)) {
                    SDL_AddMouse(mouseID, dev->name);
                }
            }
            break;
        default:
            break;
        }

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO
        SDL_XInput2ScrollableDevice *sd = NULL;
        int allocated_scroll_info_count = 0;

        for (int j = 0; j < dev->num_classes; j++) {
            const XIAnyClassInfo *class = dev->classes[j];
            const XIScrollClassInfo *s = (XIScrollClassInfo *)class;

            if (class->type != XIScrollClass) {
                continue;
            }

            // Allocate a new scrollable device.
            if (!sd) {
                scrollable_devices = SDL_realloc(scrollable_devices, (scrollable_device_count + 1) * sizeof(SDL_XInput2ScrollableDevice));
                if (!scrollable_devices) {
                    // No memory; so just skip this.
                    break;
                }

                sd = &scrollable_devices[scrollable_device_count];
                ++scrollable_device_count;

                SDL_zerop(sd);
                sd->device_id = dev->deviceid;
            }

            // Allocate new scroll info entries two at a time, as they typically come in a horizontal/vertical pair.
            if (sd->scroll_info_count == allocated_scroll_info_count) {
                sd->scroll_info = SDL_realloc(sd->scroll_info, (allocated_scroll_info_count + 2) * sizeof(SDL_XInput2ScrollInfo));
                if (!sd->scroll_info) {
                    // No memory; just skip this.
                    break;
                }

                allocated_scroll_info_count += 2;
            }

            SDL_XInput2ScrollInfo *scroll_info = &sd->scroll_info[sd->scroll_info_count];
            ++sd->scroll_info_count;

            SDL_zerop(scroll_info);
            scroll_info->number = s->number;
            scroll_info->scroll_type = s->scroll_type;
            scroll_info->increment = s->increment;
        }
#endif

#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
        for (int j = 0; j < dev->num_classes; j++) {
            Uint64 touchID;
            SDL_TouchDeviceType touchType;
            XIAnyClassInfo *class = dev->classes[j];
            XITouchClassInfo *t = (XITouchClassInfo *)class;

            // Only touch devices
            if (class->type != XITouchClass) {
                continue;
            }

            touchID = (Uint64)t->sourceid;
            AddDeviceID64(touchID, &new_touch_devices, &new_touch_count);
            if (!HasDeviceID64(touchID, old_touch_devices, old_touch_count)) {
                if (t->mode == XIDependentTouch) {
                    touchType = SDL_TOUCH_DEVICE_INDIRECT_RELATIVE;
                } else { // XIDirectTouch
                    touchType = SDL_TOUCH_DEVICE_DIRECT;
                }
                SDL_AddTouch(touchID, touchType, dev->name);
            }
        }
#endif // SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
    }

    for (int i = old_keyboard_count; i--;) {
        if (!HasDeviceID(old_keyboards[i], new_keyboards, new_keyboard_count)) {
            SDL_RemoveKeyboard(old_keyboards[i]);
        }
    }

    for (int i = old_mouse_count; i--;) {
        if (!HasDeviceID(old_mice[i], new_mice, new_mouse_count)) {
            SDL_RemoveMouse(old_mice[i]);
        }
    }

    for (int i = old_touch_count; i--;) {
        if (!HasDeviceID64(old_touch_devices[i], new_touch_devices, new_touch_count)) {
            SDL_DelTouch(old_touch_devices[i]);
        }
    }

    SDL_free(old_keyboards);
    SDL_free(new_keyboards);
    SDL_free(old_mice);
    SDL_free(new_mice);
    SDL_free(old_touch_devices);
    SDL_free(new_touch_devices);

    X11_XIFreeDeviceInfo(info);

#endif // SDL_VIDEO_DRIVER_X11_XINPUT2
}

#endif // SDL_VIDEO_DRIVER_X11
