/*
 * Support for the X11 XInput extension.
 *
 * Please see the file LICENSE.txt in the source's root directory.
 *
 *  This file written by Ryan C. Gordon.
 */

#include "manymouse.h"

/* Try to use this on everything but Windows and Mac OS by default... */
#ifndef SUPPORT_XINPUT2
#if ( (defined(_WIN32) || defined(__CYGWIN__)) )
#define SUPPORT_XINPUT2 0
#elif ( (defined(__MACH__)) && (defined(__APPLE__)) )
#define SUPPORT_XINPUT2 0
#elif __has_include(<X11/extensions/XInput2.h>)
#include <X11/extensions/XInput2.h>
#define SUPPORT_XINPUT2 1
#else
#define SUPPORT_XINPUT2 0
#endif
#endif

#if SUPPORT_XINPUT2

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <sys/select.h>

/* 32 is good enough for now. */
#define MAX_MICE 32
#define MAX_AXIS 16
typedef struct
{
    int device_id;
    int connected;
    int relative[MAX_AXIS];
    int minval[MAX_AXIS];
    int maxval[MAX_AXIS];
    char name[64];
} MouseStruct;

static MouseStruct mice[MAX_MICE];
static unsigned int available_mice = 0;

static Display *display = NULL;
static int xi2_opcode = 0;


/* !!! FIXME: this is cut-and-paste between a few targets now. Move it to
 * !!! FIXME:  manymouse.c ...
 */
/*
 * Just trying to avoid malloc() here...we statically allocate a buffer
 *  for events and treat it as a ring buffer.
 */
/* !!! FIXME: tweak this? */
#define MAX_EVENTS 1024
static ManyMouseEvent input_events[MAX_EVENTS];
static volatile int input_events_read = 0;
static volatile int input_events_write = 0;

static void queue_event(const ManyMouseEvent *event)
{
    /* copy the event info. We'll process it in ManyMouse_PollEvent(). */
    memcpy(&input_events[input_events_write], event, sizeof (ManyMouseEvent));

    input_events_write = ((input_events_write + 1) % MAX_EVENTS);

    /* Ring buffer full? Lose oldest event. */
    if (input_events_write == input_events_read)
    {
        /* !!! FIXME: we need to not lose mouse buttons here. */
        input_events_read = ((input_events_read + 1) % MAX_EVENTS);
    } /* if */
} /* queue_event */


static int dequeue_event(ManyMouseEvent *event)
{
    if (input_events_read != input_events_write)  /* no events if equal. */
    {
        memcpy(event, &input_events[input_events_read], sizeof (*event));
        input_events_read = ((input_events_read + 1) % MAX_EVENTS);
        return 1;
    } /* if */
    return 0;  /* no event. */
} /* dequeue_event */


/*
 * You _probably_ have Xlib on your system if you're on a Unix box where you
 *  are planning to plug in multiple mice. That being said, we don't want
 *  to force a project to add Xlib to their builds, or force the end-user to
 *  have Xlib installed if they are otherwise running a console app that the
 *  evdev driver would handle.
 *
 * We load all Xlib symbols at runtime, and fail gracefully if they aren't
 *  available for some reason...ManyMouse might be able to use the evdev
 *  driver or at least return a zero.
 *
 * On Linux (and probably others), you'll need to add -ldl to your link line,
 *  but it's part of glibc, so this is pretty much going to be there.
 */

static void *libx11 = NULL;
static void *libxext = NULL;
static void *libxi = NULL;

typedef int (*XExtErrHandler)(Display *, _Xconst char *, _Xconst char *);

static XExtErrHandler (*pXSetExtensionErrorHandler)(XExtErrHandler h) = 0;
static Display* (*pXOpenDisplay)(_Xconst char*) = 0;
static int (*pXCloseDisplay)(Display*) = 0;
static int (*pXISelectEvents)(Display*,Window,XIEventMask*,int) = 0;
static Bool (*pXQueryExtension)(Display*,_Xconst char*,int*,int*,int*) = 0;
static Status (*pXIQueryVersion)(Display*,int*,int*) = 0;
static XIDeviceInfo* (*pXIQueryDevice)(Display*,int,int*) = 0;
static void (*pXIFreeDeviceInfo)(XIDeviceInfo*) = 0;
static Bool (*pXGetEventData)(Display*,XGenericEventCookie*) = 0;
static void (*pXFreeEventData)(Display*,XGenericEventCookie*) = 0;
static int (*pXNextEvent)(Display*,XEvent*) = 0;
static int (*pXPending)(Display*) = 0;
static int (*pXFlush)(Display*) = 0;
static int (*pXEventsQueued)(Display*,int) = 0;

static int symlookup(void *dll, void **addr, const char *sym)
{
    *addr = dlsym(dll, sym);
    if (*addr == NULL)
        return 0;

    return 1;
} /* symlookup */

static int find_api_symbols(void)
{
    void *dll = NULL;

    #define LOOKUP(x) { if (!symlookup(dll, (void **) &p##x, #x)) return 0; }
    dll = libx11 = dlopen("libX11.so.6", RTLD_GLOBAL | RTLD_LAZY);
    if (dll == NULL)
        return 0;

    LOOKUP(XOpenDisplay);
    LOOKUP(XCloseDisplay);
    LOOKUP(XGetEventData);
    LOOKUP(XFreeEventData);
    LOOKUP(XQueryExtension);
    LOOKUP(XNextEvent);
    LOOKUP(XPending);
    LOOKUP(XFlush);
    LOOKUP(XEventsQueued);

    dll = libxext = dlopen("libXext.so.6", RTLD_GLOBAL | RTLD_LAZY);
    if (dll == NULL)
        return 0;

    LOOKUP(XSetExtensionErrorHandler);

    dll = libxi = dlopen("libXi.so.6", RTLD_GLOBAL | RTLD_LAZY);
    if (dll == NULL)
        return 0;

    LOOKUP(XISelectEvents);
    LOOKUP(XIQueryVersion);
    LOOKUP(XIQueryDevice);
    LOOKUP(XIFreeDeviceInfo);

    #undef LOOKUP

    return 1;
} /* find_api_symbols */


static void xinput2_cleanup(void)
{
    if (display != NULL)
    {
        pXCloseDisplay(display);
        display = NULL;
    } /* if */

    memset(mice, '\0', sizeof (mice));
    available_mice = 0;

    #define LIBCLOSE(lib) { if (lib != NULL) { dlclose(lib); lib = NULL; } }
    LIBCLOSE(libxi);
    LIBCLOSE(libxext);
    LIBCLOSE(libx11);
    #undef LIBCLOSE

    memset(input_events, '\0', sizeof (input_events));
    input_events_read = input_events_write = 0;
} /* xinput2_cleanup */


static int init_mouse(MouseStruct *mouse, const XIDeviceInfo *devinfo)
{
    XIAnyClassInfo **classes = devinfo->classes;
    int axis = 0;

    /*
     * we only look at "slave" devices. "Master" pointers are the logical
     *  cursors, "slave" pointers are the hardware that back them.
     *  "Floating slaves" are hardware that don't back a cursor.
     */

    if ((devinfo->use != XISlavePointer) && (devinfo->use != XIFloatingSlave))
        return 0;  /* not a device we care about. */
    else if (strstr(devinfo->name, "XTEST pointer") != NULL)
        return 0;  /* skip this nonsense. It's for the XTEST extension. */

    mouse->device_id = devinfo->deviceid;
    mouse->connected = 1;

    for (int i = 0; i < devinfo->num_classes; i++)
    {
        if ((classes[i]->type == XIValuatorClass) && (axis < MAX_AXIS))
        {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-align"
            const XIValuatorClassInfo *v = (XIValuatorClassInfo*) classes[i];
#pragma GCC diagnostic pop
            mouse->relative[axis] = (v->mode == XIModeRelative);
            mouse->minval[axis] = (int) v->min;
            mouse->maxval[axis] = (int) v->max;
            axis++;
        } /* if */
    } /* for */

    strncpy(mouse->name, devinfo->name, sizeof (mouse->name));
    mouse->name[sizeof (mouse->name) - 1] = '\0';
    return 1;
} /* init_mouse */


static int (*Xext_handler)(Display *, _Xconst char *, _Xconst char *) = NULL;
static int xext_errhandler(Display *d, _Xconst char *ext, _Xconst char *reason)
{
    /* prevent Xlib spew to stderr for missing extensions. */
    return (strcmp(reason, "missing") == 0) ? 0 : Xext_handler(d, ext, reason);
} /* xext_errhandler */


static int register_for_events(Display *dpy)
{
    XIEventMask evmask;
    unsigned char mask[3] = { 0, 0, 0 };

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

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

    /* !!! FIXME: retval? */
    pXISelectEvents(dpy, DefaultRootWindow(dpy), &evmask, 1);
    return 1;
} /* register_for_events */


static int x11_xinput2_init_internal(unsigned char absOnly)
{
    const char *ext = "XInputExtension";
    XIDeviceInfo *device_list = NULL;
    int device_count = 0;
    int available = 0;
    int event = 0;
    int error = 0;
    int major = 2;
    int minor = 0;
    int i = 0;

    xinput2_cleanup();  /* just in case... */

    if (!find_api_symbols())
        return -1;  /* couldn't find all needed symbols. */

    display = pXOpenDisplay(NULL);
    if (display == NULL)
        return -1;  /* no X server at all */

    Xext_handler = pXSetExtensionErrorHandler(xext_errhandler);
    available = (pXQueryExtension(display, ext, &xi2_opcode, &event, &error) &&
                 (pXIQueryVersion(display, &major, &minor) != BadRequest));
    pXSetExtensionErrorHandler(Xext_handler);
    Xext_handler = NULL;

    if (!available || absOnly)
        return -1;  /* no XInput2 support. */

    /*
     * Register for events first, to prevent a race where we unplug a
     *  device between when we queried for the list and when we start
     *  listening for changes.
     */
    if (!register_for_events(display))
        return -1;

    device_list = pXIQueryDevice(display, XIAllDevices, &device_count);
    for (i = 0; i < device_count; i++)
    {
        MouseStruct *mouse = &mice[available_mice];
        if (init_mouse(mouse, &device_list[i]))
            available_mice++;
    } /* for */
    pXIFreeDeviceInfo(device_list);

    return (int)available_mice;
} /* x11_xinput2_init_internal */


static int x11_xinput2_init(const unsigned char filter)
{
    int retval = x11_xinput2_init_internal(filter);
    if (retval < 0)
        xinput2_cleanup();
    return retval;
} /* x11_xinput2_init */


static void x11_xinput2_quit(void)
{
    xinput2_cleanup();
} /* x11_xinput2_quit */


static const char *x11_xinput2_name(unsigned int index)
{
    return (index < available_mice) ? mice[index].name : NULL;
} /* x11_xinput2_name */


static int find_mouse_by_devid(const int devid)
{
    int i;
    const MouseStruct *mouse = mice;

    for (i = 0; i < (int)available_mice; i++, mouse++)
    {
        if (mouse->device_id == devid)
            return (mouse->connected) ? i : -1;
    } /* for */

    return -1;
} /* find_mouse_by_devid */


static int get_next_x11_event(XEvent *xev)
{
    int available = 0;

    pXFlush(display);
    if (pXEventsQueued(display, QueuedAlready))
        available = 1;
    else
    {
        /* XPending() blocks if there's no data, so select() first. */
        struct timeval nowait;
        const int fd = ConnectionNumber(display);
        fd_set fdset;
        FD_ZERO(&fdset);
        FD_SET(fd, &fdset);
        memset(&nowait, '\0', sizeof (nowait));
        if (select(fd+1, &fdset, NULL, NULL, &nowait) == 1)
            available = pXPending(display);
    } /* else */

    if (available)
    {
        memset(xev, '\0', sizeof (*xev));
        pXNextEvent(display, xev);
        return 1;
    } /* if */

    return 0;
} /* get_next_x11_event */


/* Everything else returns left (0), right (1), middle (2)...XI2 returns
   right and middle in reverse, so swap them ourselves. */
static inline int map_xi2_button(const int button)
{
    if (button == 2)
        return 3;
    else if (button == 3)
        return 2;
    return button;
} /* map_xi2_button */


static void pump_events(void)
{
    ManyMouseEvent event;
    const int opcode = xi2_opcode;
    const XIRawEvent *rawev = NULL;
    const XIHierarchyEvent *hierev = NULL;
    int mouse = 0;
    XEvent xev;
    int i = 0;

    while (get_next_x11_event(&xev))
    {
        /* All XI2 events are "cookie" events...which need extra tapdance. */
        if (xev.xcookie.type != GenericEvent)
            continue;
        else if (xev.xcookie.extension != opcode)
            continue;
        else if (!pXGetEventData(display, &xev.xcookie))
            continue;

        switch (xev.xcookie.evtype)
        {
            case XI_RawMotion:
                rawev = (const XIRawEvent *) xev.xcookie.data;
                mouse = find_mouse_by_devid(rawev->deviceid);
                if (mouse != -1)
                {
                    const double *values = rawev->raw_values;
                    int top = rawev->valuators.mask_len * 8;
                    if (top > MAX_AXIS)
                        top = MAX_AXIS;

                    for (i = 0; i < top; i++)
                    {
                        if (XIMaskIsSet(rawev->valuators.mask, i))
                        {
                            const int value = (int) *values;
                            if (mice[mouse].relative[i])
                                event.type = MANYMOUSE_EVENT_RELMOTION;
                            else
                                event.type = MANYMOUSE_EVENT_ABSMOTION;

                            event.device = (unsigned int)mouse;
                            event.item = (unsigned int)i;
                            event.value = value;
                            event.minval = mice[mouse].minval[i];
                            event.maxval = mice[mouse].maxval[i];

                            if ((!mice[mouse].relative[i]) || (value))
                                queue_event(&event);

                            values++;
                        } /* if */
                    } /* for */
                } /* if */
                break;

            case XI_RawButtonPress:
            case XI_RawButtonRelease:
                rawev = (const XIRawEvent *) xev.xcookie.data;
                mouse = find_mouse_by_devid(rawev->deviceid);
                if (mouse != -1)
                {
                    const int button = map_xi2_button(rawev->detail);
                    const int pressed = (xev.xcookie.evtype==XI_RawButtonPress);

                    /* gah, XInput2 still maps the wheel to buttons. */
                    if ((button >= 4) && (button <= 7))
                    {
                        if (pressed)  /* ignore "up" for these "buttons" */
                        {
                            event.type = MANYMOUSE_EVENT_SCROLL;
                            event.device = (unsigned int)mouse;

                            if ((button == 4) || (button == 5))
                                event.item = 0;
                            else
                                event.item = 1;

                            if ((button == 4) || (button == 6))
                                event.value = 1;
                            else
                                event.value = -1;

                            queue_event(&event);
                        } /* if */
                    } /* if */
                    else
                    {
                        event.type = MANYMOUSE_EVENT_BUTTON;
                        event.device = (unsigned int)mouse;
                        event.item = (unsigned int)button-1;
                        event.value = pressed;
                        queue_event(&event);
                    } /* else */
                } /* if */
                break;

            case XI_HierarchyChanged:
                hierev = (const XIHierarchyEvent *) xev.xcookie.data;
                for (i = 0; i < hierev->num_info; i++)
                {
                    if (hierev->info[i].flags & XISlaveRemoved)
                    {
                        mouse = find_mouse_by_devid(hierev->info[i].deviceid);
                        if (mouse != -1)
                        {
                            mice[mouse].connected = 0;
                            event.type = MANYMOUSE_EVENT_DISCONNECT;
                            event.device = (unsigned int)mouse;
                            queue_event(&event);
                        } /* if */
                    } /* if */
                } /* for */
                break;
        } /* switch */

        pXFreeEventData(display, &xev.xcookie);
    } /* while */
} /* pump_events */

static int x11_xinput2_poll(ManyMouseEvent *event)
{
    if (dequeue_event(event))  /* ...favor existing events in the queue... */
        return 1;

    pump_events();  /* pump runloop for new hardware events... */
    return dequeue_event(event);  /* see if anything had shown up... */
} /* x11_xinput2_poll */

static const ManyMouseDriver ManyMouseDriver_interface =
{
    "X11 XInput2 extension",
    x11_xinput2_init,
    x11_xinput2_quit,
    x11_xinput2_name,
    x11_xinput2_poll
};

const ManyMouseDriver *ManyMouseDriver_xinput2 = &ManyMouseDriver_interface;

#else
const ManyMouseDriver *ManyMouseDriver_xinput2 = 0;
#endif /* SUPPORT_XINPUT2 blocker */

/* end of x11_xinput2.c ... */

