/*
 * Plug and Play support for hid devices found through SDL2
 *
 * Copyright 2017 CodeWeavers, Aric Stewart
 *
 * 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.1 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, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "config.h"
#include "wine/port.h"
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdlib.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
#ifdef HAVE_SDL_H
# include <SDL.h>
#endif

#include "ntstatus.h"
#define WIN32_NO_STATUS
#include "windef.h"
#include "winbase.h"
#include "winnls.h"
#include "winreg.h"
#include "winternl.h"
#include "ddk/wdm.h"
#include "ddk/hidtypes.h"
#include "wine/debug.h"
#include "wine/unicode.h"
#include "hidusage.h"

#ifdef WORDS_BIGENDIAN
# define LE_WORD(x) RtlUshortByteSwap(x)
#else
# define LE_WORD(x) (x)
#endif

#include "bus.h"
#include "unix_private.h"

WINE_DEFAULT_DEBUG_CHANNEL(plugplay);

#ifdef SONAME_LIBSDL2

WINE_DECLARE_DEBUG_CHANNEL(hid_report);

static const WCHAR sdl_busidW[] = {'S','D','L','J','O','Y',0};
static struct sdl_bus_options options;

static void *sdl_handle = NULL;
static UINT quit_event = -1;
static struct list event_queue = LIST_INIT(event_queue);

#define MAKE_FUNCPTR(f) static typeof(f) * p##f = NULL
MAKE_FUNCPTR(SDL_GetError);
MAKE_FUNCPTR(SDL_Init);
MAKE_FUNCPTR(SDL_JoystickClose);
MAKE_FUNCPTR(SDL_JoystickEventState);
MAKE_FUNCPTR(SDL_JoystickGetGUID);
MAKE_FUNCPTR(SDL_JoystickGetGUIDString);
MAKE_FUNCPTR(SDL_JoystickInstanceID);
MAKE_FUNCPTR(SDL_JoystickName);
MAKE_FUNCPTR(SDL_JoystickNumAxes);
MAKE_FUNCPTR(SDL_JoystickOpen);
MAKE_FUNCPTR(SDL_WaitEvent);
MAKE_FUNCPTR(SDL_JoystickNumButtons);
MAKE_FUNCPTR(SDL_JoystickNumBalls);
MAKE_FUNCPTR(SDL_JoystickNumHats);
MAKE_FUNCPTR(SDL_JoystickGetAxis);
MAKE_FUNCPTR(SDL_JoystickGetHat);
MAKE_FUNCPTR(SDL_IsGameController);
MAKE_FUNCPTR(SDL_GameControllerClose);
MAKE_FUNCPTR(SDL_GameControllerGetAxis);
MAKE_FUNCPTR(SDL_GameControllerGetButton);
MAKE_FUNCPTR(SDL_GameControllerName);
MAKE_FUNCPTR(SDL_GameControllerOpen);
MAKE_FUNCPTR(SDL_GameControllerEventState);
MAKE_FUNCPTR(SDL_HapticClose);
MAKE_FUNCPTR(SDL_HapticDestroyEffect);
MAKE_FUNCPTR(SDL_HapticNewEffect);
MAKE_FUNCPTR(SDL_HapticOpenFromJoystick);
MAKE_FUNCPTR(SDL_HapticQuery);
MAKE_FUNCPTR(SDL_HapticRumbleInit);
MAKE_FUNCPTR(SDL_HapticRumblePlay);
MAKE_FUNCPTR(SDL_HapticRumbleSupported);
MAKE_FUNCPTR(SDL_HapticRunEffect);
MAKE_FUNCPTR(SDL_HapticStopAll);
MAKE_FUNCPTR(SDL_JoystickIsHaptic);
MAKE_FUNCPTR(SDL_memset);
MAKE_FUNCPTR(SDL_GameControllerAddMapping);
MAKE_FUNCPTR(SDL_RegisterEvents);
MAKE_FUNCPTR(SDL_PushEvent);
static Uint16 (*pSDL_JoystickGetProduct)(SDL_Joystick * joystick);
static Uint16 (*pSDL_JoystickGetProductVersion)(SDL_Joystick * joystick);
static Uint16 (*pSDL_JoystickGetVendor)(SDL_Joystick * joystick);

struct platform_private
{
    struct unix_device unix_device;

    SDL_Joystick *sdl_joystick;
    SDL_GameController *sdl_controller;
    SDL_JoystickID id;

    int button_start;
    int axis_start;
    int ball_start;
    int hat_bit_offs; /* hatswitches are reported in the same bytes as buttons */

    struct hid_descriptor desc;

    int buffer_length;
    BYTE *report_buffer;

    SDL_Haptic *sdl_haptic;
    int haptic_effect_id;
};

static inline struct platform_private *impl_from_unix_device(struct unix_device *iface)
{
    return CONTAINING_RECORD(iface, struct platform_private, unix_device);
}

static inline struct platform_private *impl_from_DEVICE_OBJECT(DEVICE_OBJECT *device)
{
    return impl_from_unix_device(get_unix_device(device));
}

#define CONTROLLER_NUM_BUTTONS 11
#define CONTROLLER_NUM_AXES 6
#define CONTROLLER_NUM_HATSWITCHES 1

static void set_button_value(struct platform_private *ext, int index, int value)
{
    int byte_index = ext->button_start + index / 8;
    int bit_index = index % 8;
    BYTE mask = 1 << bit_index;

    if (value)
    {
        ext->report_buffer[byte_index] = ext->report_buffer[byte_index] | mask;
    }
    else
    {
        mask = ~mask;
        ext->report_buffer[byte_index] = ext->report_buffer[byte_index] & mask;
    }
}

static void set_axis_value(struct platform_private *ext, int index, short value, BOOL controller)
{
    WORD *report = (WORD *)(ext->report_buffer + ext->axis_start);

    if (controller && (index == SDL_CONTROLLER_AXIS_TRIGGERLEFT || index == SDL_CONTROLLER_AXIS_TRIGGERRIGHT))
        report[index] = LE_WORD(value);
    else
        report[index] = LE_WORD(value) + 32768;
}

static void set_ball_value(struct platform_private *ext, int index, int value1, int value2)
{
    int offset;
    offset = ext->ball_start + (index * sizeof(WORD));
    if (value1 > 127) value1 = 127;
    if (value1 < -127) value1 = -127;
    if (value2 > 127) value2 = 127;
    if (value2 < -127) value2 = -127;
    *((WORD*)&ext->report_buffer[offset]) = LE_WORD(value1);
    *((WORD*)&ext->report_buffer[offset + sizeof(WORD)]) = LE_WORD(value2);
}

static void set_hat_value(struct platform_private *ext, int index, int value)
{
    int byte = ext->button_start + (ext->hat_bit_offs + 4 * index) / 8;
    int bit_offs = (ext->hat_bit_offs + 4 * index) % 8;
    int num_low_bits, num_high_bits;
    unsigned char val, low_mask, high_mask;

    /* 4-bit hatswitch value is packed into button bytes */
    if (bit_offs <= 4)
    {
        num_low_bits = 4;
        num_high_bits = 0;
        low_mask = 0xf;
        high_mask = 0;
    }
    else
    {
        num_low_bits = 8 - bit_offs;
        num_high_bits = 4 - num_low_bits;
        low_mask = (1 << num_low_bits) - 1;
        high_mask = (1 << num_high_bits) - 1;
    }

    switch (value)
    {
        /* 8 1 2
         * 7 0 3
         * 6 5 4 */
        case SDL_HAT_CENTERED: val = 0; break;
        case SDL_HAT_UP: val = 1; break;
        case SDL_HAT_RIGHTUP: val = 2; break;
        case SDL_HAT_RIGHT: val = 3; break;
        case SDL_HAT_RIGHTDOWN: val = 4; break;
        case SDL_HAT_DOWN: val = 5; break;
        case SDL_HAT_LEFTDOWN: val = 6; break;
        case SDL_HAT_LEFT: val = 7; break;
        case SDL_HAT_LEFTUP: val = 8; break;
        default: return;
    }

    ext->report_buffer[byte] &= ~(low_mask << bit_offs);
    ext->report_buffer[byte] |= (val & low_mask) << bit_offs;
    if (high_mask)
    {
        ext->report_buffer[byte + 1] &= ~high_mask;
        ext->report_buffer[byte + 1] |= val & high_mask;
    }
}

static BOOL descriptor_add_haptic(struct platform_private *ext)
{
    if (pSDL_JoystickIsHaptic(ext->sdl_joystick))
    {
        ext->sdl_haptic = pSDL_HapticOpenFromJoystick(ext->sdl_joystick);
        if (ext->sdl_haptic &&
            ((pSDL_HapticQuery(ext->sdl_haptic) & SDL_HAPTIC_LEFTRIGHT) != 0 ||
             pSDL_HapticRumbleSupported(ext->sdl_haptic)))
        {
            pSDL_HapticStopAll(ext->sdl_haptic);
            pSDL_HapticRumbleInit(ext->sdl_haptic);
            if (!hid_descriptor_add_haptics(&ext->desc))
                return FALSE;
            ext->haptic_effect_id = -1;
        }
        else
        {
            pSDL_HapticClose(ext->sdl_haptic);
            ext->sdl_haptic = NULL;
        }
    }

    return TRUE;
}

static NTSTATUS build_report_descriptor(struct platform_private *ext)
{
    INT i;
    INT report_size;
    INT button_count, axis_count, ball_count, hat_count;
    static const USAGE device_usage[2] = {HID_USAGE_PAGE_GENERIC, HID_USAGE_GENERIC_GAMEPAD};
    static const USAGE controller_usages[] = {
        HID_USAGE_GENERIC_X,
        HID_USAGE_GENERIC_Y,
        HID_USAGE_GENERIC_Z,
        HID_USAGE_GENERIC_RX,
        HID_USAGE_GENERIC_RY,
        HID_USAGE_GENERIC_RZ,
        HID_USAGE_GENERIC_SLIDER,
        HID_USAGE_GENERIC_DIAL,
        HID_USAGE_GENERIC_WHEEL};
    static const USAGE joystick_usages[] = {
        HID_USAGE_GENERIC_X,
        HID_USAGE_GENERIC_Y,
        HID_USAGE_GENERIC_Z,
        HID_USAGE_GENERIC_RZ,
        HID_USAGE_GENERIC_RX,
        HID_USAGE_GENERIC_RY,
        HID_USAGE_GENERIC_SLIDER,
        HID_USAGE_GENERIC_DIAL,
        HID_USAGE_GENERIC_WHEEL};

    report_size = 0;

    axis_count = pSDL_JoystickNumAxes(ext->sdl_joystick);
    if (axis_count > 6)
    {
        FIXME("Clamping joystick to 6 axis\n");
        axis_count = 6;
    }

    ext->axis_start = report_size;
    if (axis_count)
    {
        report_size += (sizeof(WORD) * axis_count);
    }

    ball_count = pSDL_JoystickNumBalls(ext->sdl_joystick);
    ext->ball_start = report_size;
    if (ball_count)
    {
        if ((ball_count*2) + axis_count > 9)
        {
            FIXME("Capping ball + axis at 9\n");
            ball_count = (9-axis_count)/2;
        }
        report_size += (sizeof(WORD) * 2 * ball_count);
    }

    /* For now lump all buttons just into incremental usages, Ignore Keys */
    button_count = pSDL_JoystickNumButtons(ext->sdl_joystick);
    ext->button_start = report_size;

    hat_count = pSDL_JoystickNumHats(ext->sdl_joystick);
    ext->hat_bit_offs = button_count;

    report_size += (button_count + hat_count * 4 + 7) / 8;

    TRACE("Report will be %i bytes\n", report_size);

    if (!hid_descriptor_begin(&ext->desc, device_usage[0], device_usage[1]))
        return STATUS_NO_MEMORY;

    if (axis_count == 6 && button_count >= 14)
    {
        if (!hid_descriptor_add_axes(&ext->desc, axis_count, HID_USAGE_PAGE_GENERIC,
                                     controller_usages, FALSE, 16, 0, 0xffff))
            return STATUS_NO_MEMORY;
    }
    else if (axis_count)
    {
        if (!hid_descriptor_add_axes(&ext->desc, axis_count, HID_USAGE_PAGE_GENERIC,
                                     joystick_usages, FALSE, 16, 0, 0xffff))
            return STATUS_NO_MEMORY;
    }

    if (ball_count && !hid_descriptor_add_axes(&ext->desc, ball_count * 2, HID_USAGE_PAGE_GENERIC,
                                               &joystick_usages[axis_count], TRUE, 8, 0x81, 0x7f))
        return STATUS_NO_MEMORY;

    if (button_count && !hid_descriptor_add_buttons(&ext->desc, HID_USAGE_PAGE_BUTTON, 1, button_count))
        return STATUS_NO_MEMORY;

    if (hat_count && !hid_descriptor_add_hatswitch(&ext->desc, hat_count))
        return STATUS_NO_MEMORY;

    if (!descriptor_add_haptic(ext))
        return STATUS_NO_MEMORY;

    if (!hid_descriptor_end(&ext->desc))
        return STATUS_NO_MEMORY;

    ext->buffer_length = report_size;
    if (!(ext->report_buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, report_size)))
        goto failed;

    /* Initialize axis in the report */
    for (i = 0; i < axis_count; i++)
        set_axis_value(ext, i, pSDL_JoystickGetAxis(ext->sdl_joystick, i), FALSE);
    for (i = 0; i < hat_count; i++)
        set_hat_value(ext, i, pSDL_JoystickGetHat(ext->sdl_joystick, i));

    return STATUS_SUCCESS;

failed:
    HeapFree(GetProcessHeap(), 0, ext->report_buffer);
    hid_descriptor_free(&ext->desc);
    return STATUS_NO_MEMORY;
}

static SHORT compose_dpad_value(SDL_GameController *joystick)
{
    if (pSDL_GameControllerGetButton(joystick, SDL_CONTROLLER_BUTTON_DPAD_UP))
    {
        if (pSDL_GameControllerGetButton(joystick, SDL_CONTROLLER_BUTTON_DPAD_RIGHT))
            return SDL_HAT_RIGHTUP;
        if (pSDL_GameControllerGetButton(joystick, SDL_CONTROLLER_BUTTON_DPAD_LEFT))
            return SDL_HAT_LEFTUP;
        return SDL_HAT_UP;
    }

    if (pSDL_GameControllerGetButton(joystick, SDL_CONTROLLER_BUTTON_DPAD_DOWN))
    {
        if (pSDL_GameControllerGetButton(joystick, SDL_CONTROLLER_BUTTON_DPAD_RIGHT))
            return SDL_HAT_RIGHTDOWN;
        if (pSDL_GameControllerGetButton(joystick, SDL_CONTROLLER_BUTTON_DPAD_LEFT))
            return SDL_HAT_LEFTDOWN;
        return SDL_HAT_DOWN;
    }

    if (pSDL_GameControllerGetButton(joystick, SDL_CONTROLLER_BUTTON_DPAD_RIGHT))
        return SDL_HAT_RIGHT;
    if (pSDL_GameControllerGetButton(joystick, SDL_CONTROLLER_BUTTON_DPAD_LEFT))
        return SDL_HAT_LEFT;
    return SDL_HAT_CENTERED;
}

static NTSTATUS build_mapped_report_descriptor(struct platform_private *ext)
{
    static const USAGE left_axis_usages[] = {HID_USAGE_GENERIC_X, HID_USAGE_GENERIC_Y};
    static const USAGE right_axis_usages[] = {HID_USAGE_GENERIC_RX, HID_USAGE_GENERIC_RY};
    static const USAGE trigger_axis_usages[] = {HID_USAGE_GENERIC_Z, HID_USAGE_GENERIC_RZ};
    INT i;

    static const int BUTTON_BIT_COUNT = CONTROLLER_NUM_BUTTONS + CONTROLLER_NUM_HATSWITCHES * 4;

    ext->axis_start = 0;
    ext->button_start = CONTROLLER_NUM_AXES * sizeof(WORD);
    ext->hat_bit_offs = CONTROLLER_NUM_BUTTONS;

    ext->buffer_length = (BUTTON_BIT_COUNT + 7) / 8
        + CONTROLLER_NUM_AXES * sizeof(WORD)
        + 2/* unknown constant*/;

    TRACE("Report will be %i bytes\n", ext->buffer_length);

    if (!hid_descriptor_begin(&ext->desc, HID_USAGE_PAGE_GENERIC, HID_USAGE_GENERIC_GAMEPAD))
        return STATUS_NO_MEMORY;

    if (!hid_descriptor_add_axes(&ext->desc, 2, HID_USAGE_PAGE_GENERIC, left_axis_usages,
                                 FALSE, 16, 0, 0xffff))
        return STATUS_NO_MEMORY;

    if (!hid_descriptor_add_axes(&ext->desc, 2, HID_USAGE_PAGE_GENERIC, right_axis_usages,
                                 FALSE, 16, 0, 0xffff))
        return STATUS_NO_MEMORY;

    if (!hid_descriptor_add_axes(&ext->desc, 2, HID_USAGE_PAGE_GENERIC, trigger_axis_usages,
                                 FALSE, 16, 0, 0x7fff))
        return STATUS_NO_MEMORY;

    if (!hid_descriptor_add_buttons(&ext->desc, HID_USAGE_PAGE_BUTTON, 1, CONTROLLER_NUM_BUTTONS))
        return STATUS_NO_MEMORY;

    if (!hid_descriptor_add_hatswitch(&ext->desc, 1))
        return STATUS_NO_MEMORY;

    if (BUTTON_BIT_COUNT % 8 != 0)
    {
        /* unused bits between hatswitch and following constant */
        if (!hid_descriptor_add_padding(&ext->desc, 8 - (BUTTON_BIT_COUNT % 8)))
            return STATUS_NO_MEMORY;
    }

    /* unknown constant */
    if (!hid_descriptor_add_padding(&ext->desc, 16))
        return STATUS_NO_MEMORY;

    if (!descriptor_add_haptic(ext))
        return STATUS_NO_MEMORY;

    if (!hid_descriptor_end(&ext->desc))
        return STATUS_NO_MEMORY;

    if (!(ext->report_buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, ext->buffer_length)))
        goto failed;

    /* Initialize axis in the report */
    for (i = SDL_CONTROLLER_AXIS_LEFTX; i < SDL_CONTROLLER_AXIS_MAX; i++)
        set_axis_value(ext, i, pSDL_GameControllerGetAxis(ext->sdl_controller, i), TRUE);

    set_hat_value(ext, 0, compose_dpad_value(ext->sdl_controller));

    /* unknown constant */
    ext->report_buffer[14] = 0x89;
    ext->report_buffer[15] = 0xc5;

    return STATUS_SUCCESS;

failed:
    HeapFree(GetProcessHeap(), 0, ext->report_buffer);
    hid_descriptor_free(&ext->desc);
    return STATUS_NO_MEMORY;
}

static void sdl_device_destroy(struct unix_device *iface)
{
    struct platform_private *ext = impl_from_unix_device(iface);

    pSDL_JoystickClose(ext->sdl_joystick);
    if (ext->sdl_controller)
        pSDL_GameControllerClose(ext->sdl_controller);
    if (ext->sdl_haptic)
        pSDL_HapticClose(ext->sdl_haptic);

    HeapFree(GetProcessHeap(), 0, ext);
}

static int sdl_device_compare(struct unix_device *iface, void *context)
{
    return impl_from_unix_device(iface)->id - PtrToUlong(context);
}

static NTSTATUS sdl_device_start(struct unix_device *iface, DEVICE_OBJECT *device)
{
    struct platform_private *ext = impl_from_unix_device(iface);
    if (ext->sdl_controller) return build_mapped_report_descriptor(ext);
    return build_report_descriptor(ext);
}

static NTSTATUS sdl_device_get_reportdescriptor(struct unix_device *iface, BYTE *buffer,
                                                DWORD length, DWORD *out_length)
{
    struct platform_private *ext = impl_from_unix_device(iface);

    *out_length = ext->desc.size;
    if (length < ext->desc.size) return STATUS_BUFFER_TOO_SMALL;

    memcpy(buffer, ext->desc.data, ext->desc.size);
    return STATUS_SUCCESS;
}

static NTSTATUS sdl_device_get_string(struct unix_device *iface, DWORD index, WCHAR *buffer, DWORD length)
{
    struct platform_private *ext = impl_from_unix_device(iface);
    const char* str = NULL;

    switch (index)
    {
        case HID_STRING_ID_IPRODUCT:
            if (ext->sdl_controller)
                str = pSDL_GameControllerName(ext->sdl_controller);
            else
                str = pSDL_JoystickName(ext->sdl_joystick);
            break;
        case HID_STRING_ID_IMANUFACTURER:
            str = "SDL";
            break;
        case HID_STRING_ID_ISERIALNUMBER:
            str = "000000";
            break;
        default:
            ERR("Unhandled string index %i\n", index);
    }

    if (str && str[0])
        MultiByteToWideChar(CP_ACP, 0, str, -1, buffer, length);
    else
        buffer[0] = 0;

    return STATUS_SUCCESS;
}

static void sdl_device_set_output_report(struct unix_device *iface, HID_XFER_PACKET *packet, IO_STATUS_BLOCK *io)
{
    struct platform_private *ext = impl_from_unix_device(iface);

    if (ext->sdl_haptic && packet->reportId == 0)
    {
        WORD left = packet->reportBuffer[2] * 128;
        WORD right = packet->reportBuffer[3] * 128;

        if (ext->haptic_effect_id >= 0)
        {
            pSDL_HapticDestroyEffect(ext->sdl_haptic, ext->haptic_effect_id);
            ext->haptic_effect_id = -1;
        }
        pSDL_HapticStopAll(ext->sdl_haptic);
        if (left != 0 || right != 0)
        {
            SDL_HapticEffect effect;

            pSDL_memset( &effect, 0, sizeof(SDL_HapticEffect) );
            effect.type = SDL_HAPTIC_LEFTRIGHT;
            effect.leftright.length = -1;
            effect.leftright.large_magnitude = left;
            effect.leftright.small_magnitude = right;

            ext->haptic_effect_id = pSDL_HapticNewEffect(ext->sdl_haptic, &effect);
            if (ext->haptic_effect_id >= 0)
            {
                pSDL_HapticRunEffect(ext->sdl_haptic, ext->haptic_effect_id, 1);
            }
            else
            {
                float i = (float)((left + right)/2.0) / 32767.0;
                pSDL_HapticRumblePlay(ext->sdl_haptic, i, -1);
            }
        }

        io->Information = packet->reportBufferLen;
        io->Status = STATUS_SUCCESS;
    }
    else
    {
        io->Information = 0;
        io->Status = STATUS_NOT_IMPLEMENTED;
    }
}

static void sdl_device_get_feature_report(struct unix_device *iface, HID_XFER_PACKET *packet, IO_STATUS_BLOCK *io)
{
    io->Information = 0;
    io->Status = STATUS_NOT_IMPLEMENTED;
}

static void sdl_device_set_feature_report(struct unix_device *iface, HID_XFER_PACKET *packet, IO_STATUS_BLOCK *io)
{
    io->Information = 0;
    io->Status = STATUS_NOT_IMPLEMENTED;
}

static const struct unix_device_vtbl sdl_device_vtbl =
{
    sdl_device_destroy,
    sdl_device_compare,
    sdl_device_start,
    sdl_device_get_reportdescriptor,
    sdl_device_get_string,
    sdl_device_set_output_report,
    sdl_device_get_feature_report,
    sdl_device_set_feature_report,
};

static BOOL set_report_from_event(DEVICE_OBJECT *device, SDL_Event *event)
{
    struct platform_private *private;
    private = impl_from_DEVICE_OBJECT(device);
    if (private->sdl_controller)
    {
        /* We want mapped events */
        return TRUE;
    }

    switch(event->type)
    {
        case SDL_JOYBUTTONDOWN:
        case SDL_JOYBUTTONUP:
        {
            SDL_JoyButtonEvent *ie = &event->jbutton;

            set_button_value(private, ie->button, ie->state);

            process_hid_report(device, private->report_buffer, private->buffer_length);
            break;
        }
        case SDL_JOYAXISMOTION:
        {
            SDL_JoyAxisEvent *ie = &event->jaxis;

            if (ie->axis < 6)
            {
                set_axis_value(private, ie->axis, ie->value, FALSE);
                process_hid_report(device, private->report_buffer, private->buffer_length);
            }
            break;
        }
        case SDL_JOYBALLMOTION:
        {
            SDL_JoyBallEvent *ie = &event->jball;

            set_ball_value(private, ie->ball, ie->xrel, ie->yrel);
            process_hid_report(device, private->report_buffer, private->buffer_length);
            break;
        }
        case SDL_JOYHATMOTION:
        {
            SDL_JoyHatEvent *ie = &event->jhat;

            set_hat_value(private, ie->hat, ie->value);
            process_hid_report(device, private->report_buffer, private->buffer_length);
            break;
        }
        default:
            ERR("TODO: Process Report (0x%x)\n",event->type);
    }
    return FALSE;
}

static BOOL set_mapped_report_from_event(DEVICE_OBJECT *device, SDL_Event *event)
{
    struct platform_private *private;
    private = impl_from_DEVICE_OBJECT(device);

    switch(event->type)
    {
        case SDL_CONTROLLERBUTTONDOWN:
        case SDL_CONTROLLERBUTTONUP:
        {
            int usage = -1;
            SDL_ControllerButtonEvent *ie = &event->cbutton;

            switch (ie->button)
            {
                case SDL_CONTROLLER_BUTTON_A: usage = 0; break;
                case SDL_CONTROLLER_BUTTON_B: usage = 1; break;
                case SDL_CONTROLLER_BUTTON_X: usage = 2; break;
                case SDL_CONTROLLER_BUTTON_Y: usage = 3; break;
                case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: usage = 4; break;
                case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: usage = 5; break;
                case SDL_CONTROLLER_BUTTON_BACK: usage = 6; break;
                case SDL_CONTROLLER_BUTTON_START: usage = 7; break;
                case SDL_CONTROLLER_BUTTON_LEFTSTICK: usage = 8; break;
                case SDL_CONTROLLER_BUTTON_RIGHTSTICK: usage = 9; break;
                case SDL_CONTROLLER_BUTTON_GUIDE: usage = 10; break;

                case SDL_CONTROLLER_BUTTON_DPAD_UP:
                case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
                case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
                case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
                    set_hat_value(private, 0, compose_dpad_value(private->sdl_controller));
                    process_hid_report(device, private->report_buffer, private->buffer_length);
                    break;

                default:
                    ERR("Unknown Button %i\n",ie->button);
            }

            if (usage >= 0)
            {
                set_button_value(private, usage, ie->state);
                process_hid_report(device, private->report_buffer, private->buffer_length);
            }
            break;
        }
        case SDL_CONTROLLERAXISMOTION:
        {
            SDL_ControllerAxisEvent *ie = &event->caxis;

            set_axis_value(private, ie->axis, ie->value, TRUE);
            process_hid_report(device, private->report_buffer, private->buffer_length);
            break;
        }
        default:
            ERR("TODO: Process Report (%x)\n",event->type);
    }
    return FALSE;
}

static void sdl_add_device(unsigned int index)
{
    struct device_desc desc =
    {
        .busid = sdl_busidW,
        .input = -1,
        .serial = {'0','0','0','0',0},
    };
    struct platform_private *private;
    char guid_str[34];

    SDL_Joystick* joystick;
    SDL_JoystickID id;
    SDL_JoystickGUID guid;
    SDL_GameController *controller = NULL;

    if ((joystick = pSDL_JoystickOpen(index)) == NULL)
    {
        WARN("Unable to open sdl device %i: %s\n", index, pSDL_GetError());
        return;
    }

    if (options.map_controllers && pSDL_IsGameController(index))
        controller = pSDL_GameControllerOpen(index);

    id = pSDL_JoystickInstanceID(joystick);

    if (pSDL_JoystickGetProductVersion != NULL) {
        desc.vid = pSDL_JoystickGetVendor(joystick);
        desc.pid = pSDL_JoystickGetProduct(joystick);
        desc.version = pSDL_JoystickGetProductVersion(joystick);
    }
    else
    {
        desc.vid = 0x01;
        desc.pid = pSDL_JoystickInstanceID(joystick) + 1;
        desc.version = 0;
    }

    guid = pSDL_JoystickGetGUID(joystick);
    pSDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str));
    MultiByteToWideChar(CP_ACP, 0, guid_str, -1, desc.serial, sizeof(guid_str));

    if (controller) desc.is_gamepad = TRUE;
    else
    {
        int button_count, axis_count;

        axis_count = pSDL_JoystickNumAxes(joystick);
        button_count = pSDL_JoystickNumButtons(joystick);
        desc.is_gamepad = (axis_count == 6  && button_count >= 14);
    }

    TRACE("%s id %d, desc %s.\n", controller ? "controller" : "joystick", id, debugstr_device_desc(&desc));

    if (!(private = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*private))))
        return;
    private->unix_device.vtbl = &sdl_device_vtbl;
    private->sdl_joystick = joystick;
    private->sdl_controller = controller;
    private->id = id;

    bus_event_queue_device_created(&event_queue, &private->unix_device, &desc);
}

static void process_device_event(SDL_Event *event)
{
    DEVICE_OBJECT *device;
    SDL_JoystickID id;

    TRACE_(hid_report)("Received action %x\n", event->type);

    if (event->type == SDL_JOYDEVICEADDED)
        sdl_add_device(((SDL_JoyDeviceEvent *)event)->which);
    else if (event->type == SDL_JOYDEVICEREMOVED)
    {
        id = ((SDL_JoyDeviceEvent *)event)->which;
        bus_event_queue_device_removed(&event_queue, sdl_busidW, ULongToPtr(id));
    }
    else if (event->type >= SDL_JOYAXISMOTION && event->type <= SDL_JOYBUTTONUP)
    {
        id = ((SDL_JoyButtonEvent *)event)->which;
        device = bus_find_hid_device(sdl_busidW, ULongToPtr(id));
        if (device) set_report_from_event(device, event);
        else WARN("failed to find device with id %d\n", id);
    }
    else if (event->type >= SDL_CONTROLLERAXISMOTION && event->type <= SDL_CONTROLLERBUTTONUP)
    {
        id = ((SDL_ControllerButtonEvent *)event)->which;
        device = bus_find_hid_device(sdl_busidW, ULongToPtr(id));
        if (device) set_mapped_report_from_event(device, event);
        else WARN("failed to find device with id %d\n", id);
    }
}

static void sdl_load_mappings(void)
{
    HKEY key;
    static const WCHAR szPath[] = {'m','a','p',0};
    const char *mapping;

    if ((mapping = getenv("SDL_GAMECONTROLLERCONFIG")))
    {
        TRACE("Setting environment mapping %s\n", debugstr_a(mapping));
        if (pSDL_GameControllerAddMapping(mapping) < 0)
            WARN("Failed to add environment mapping %s\n", pSDL_GetError());
    }
    else if (!RegOpenKeyExW(driver_key, szPath, 0, KEY_QUERY_VALUE, &key))
    {
        DWORD index = 0;
        CHAR *buffer = NULL;
        DWORD buffer_len = 0;
        LSTATUS rc;

        do
        {
            CHAR name[255];
            DWORD name_len;
            DWORD type;
            DWORD data_len = buffer_len;

            name_len = sizeof(name);
            rc = RegEnumValueA(key, index, name, &name_len, NULL, &type, (LPBYTE)buffer, &data_len);
            if (rc == ERROR_MORE_DATA || buffer == NULL)
            {
                if (buffer) buffer = HeapReAlloc(GetProcessHeap(), 0, buffer, data_len);
                else buffer = HeapAlloc(GetProcessHeap(), 0, data_len);
                buffer_len = data_len;

                name_len = sizeof(name);
                rc = RegEnumValueA(key, index, name, &name_len, NULL, &type, (LPBYTE)buffer, &data_len);
            }

            if (rc == STATUS_SUCCESS)
            {
                TRACE("Setting registry mapping %s\n", debugstr_a(buffer));
                if (pSDL_GameControllerAddMapping(buffer) < 0)
                    WARN("Failed to add registry mapping %s\n", pSDL_GetError());
                index++;
            }
        } while (rc == STATUS_SUCCESS);
        HeapFree(GetProcessHeap(), 0, buffer);
        NtClose(key);
    }
}

NTSTATUS sdl_bus_init(void *args)
{
    TRACE("args %p\n", args);

    options = *(struct sdl_bus_options *)args;

    if (!(sdl_handle = dlopen(SONAME_LIBSDL2, RTLD_NOW)))
    {
        WARN("could not load %s\n", SONAME_LIBSDL2);
        return STATUS_UNSUCCESSFUL;
    }
#define LOAD_FUNCPTR(f)                          \
    if ((p##f = dlsym(sdl_handle, #f)) == NULL)  \
    {                                            \
        WARN("could not find symbol %s\n", #f);  \
        goto failed;                             \
    }
    LOAD_FUNCPTR(SDL_GetError);
    LOAD_FUNCPTR(SDL_Init);
    LOAD_FUNCPTR(SDL_JoystickClose);
    LOAD_FUNCPTR(SDL_JoystickEventState);
    LOAD_FUNCPTR(SDL_JoystickGetGUID);
    LOAD_FUNCPTR(SDL_JoystickGetGUIDString);
    LOAD_FUNCPTR(SDL_JoystickInstanceID);
    LOAD_FUNCPTR(SDL_JoystickName);
    LOAD_FUNCPTR(SDL_JoystickNumAxes);
    LOAD_FUNCPTR(SDL_JoystickOpen);
    LOAD_FUNCPTR(SDL_WaitEvent);
    LOAD_FUNCPTR(SDL_JoystickNumButtons);
    LOAD_FUNCPTR(SDL_JoystickNumBalls);
    LOAD_FUNCPTR(SDL_JoystickNumHats);
    LOAD_FUNCPTR(SDL_JoystickGetAxis);
    LOAD_FUNCPTR(SDL_JoystickGetHat);
    LOAD_FUNCPTR(SDL_IsGameController);
    LOAD_FUNCPTR(SDL_GameControllerClose);
    LOAD_FUNCPTR(SDL_GameControllerGetAxis);
    LOAD_FUNCPTR(SDL_GameControllerGetButton);
    LOAD_FUNCPTR(SDL_GameControllerName);
    LOAD_FUNCPTR(SDL_GameControllerOpen);
    LOAD_FUNCPTR(SDL_GameControllerEventState);
    LOAD_FUNCPTR(SDL_HapticClose);
    LOAD_FUNCPTR(SDL_HapticDestroyEffect);
    LOAD_FUNCPTR(SDL_HapticNewEffect);
    LOAD_FUNCPTR(SDL_HapticOpenFromJoystick);
    LOAD_FUNCPTR(SDL_HapticQuery);
    LOAD_FUNCPTR(SDL_HapticRumbleInit);
    LOAD_FUNCPTR(SDL_HapticRumblePlay);
    LOAD_FUNCPTR(SDL_HapticRumbleSupported);
    LOAD_FUNCPTR(SDL_HapticRunEffect);
    LOAD_FUNCPTR(SDL_HapticStopAll);
    LOAD_FUNCPTR(SDL_JoystickIsHaptic);
    LOAD_FUNCPTR(SDL_memset);
    LOAD_FUNCPTR(SDL_GameControllerAddMapping);
    LOAD_FUNCPTR(SDL_RegisterEvents);
    LOAD_FUNCPTR(SDL_PushEvent);
#undef LOAD_FUNCPTR
    pSDL_JoystickGetProduct = dlsym(sdl_handle, "SDL_JoystickGetProduct");
    pSDL_JoystickGetProductVersion = dlsym(sdl_handle, "SDL_JoystickGetProductVersion");
    pSDL_JoystickGetVendor = dlsym(sdl_handle, "SDL_JoystickGetVendor");

    if (pSDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0)
    {
        ERR("could not init SDL: %s\n", pSDL_GetError());
        goto failed;
    }

    if ((quit_event = pSDL_RegisterEvents(1)) == -1)
    {
        ERR("error registering quit event\n");
        goto failed;
    }

    pSDL_JoystickEventState(SDL_ENABLE);
    pSDL_GameControllerEventState(SDL_ENABLE);

    /* Process mappings */
    if (pSDL_GameControllerAddMapping != NULL) sdl_load_mappings();

    return STATUS_SUCCESS;

failed:
    dlclose(sdl_handle);
    sdl_handle = NULL;
    return STATUS_UNSUCCESSFUL;
}

NTSTATUS sdl_bus_wait(void *args)
{
    struct bus_event *result = args;
    SDL_Event event;

    do
    {
        if (bus_event_queue_pop(&event_queue, result)) return STATUS_PENDING;
        if (pSDL_WaitEvent(&event) != 0) process_device_event(&event);
        else WARN("SDL_WaitEvent failed: %s\n", pSDL_GetError());
    } while (event.type != quit_event);

    TRACE("SDL main loop exiting\n");
    bus_event_queue_destroy(&event_queue);
    dlclose(sdl_handle);
    sdl_handle = NULL;
    return STATUS_SUCCESS;
}

NTSTATUS sdl_bus_stop(void *args)
{
    SDL_Event event;

    if (!sdl_handle) return STATUS_SUCCESS;

    event.type = quit_event;
    if (pSDL_PushEvent(&event) != 1)
    {
        ERR("error pushing quit event\n");
        return STATUS_UNSUCCESSFUL;
    }

    return STATUS_SUCCESS;
}

#else

NTSTATUS sdl_bus_init(void *args)
{
    WARN("SDL support not compiled in!\n");
    return STATUS_NOT_IMPLEMENTED;
}

NTSTATUS sdl_bus_wait(void *args)
{
    WARN("SDL support not compiled in!\n");
    return STATUS_NOT_IMPLEMENTED;
}

NTSTATUS sdl_bus_stop(void *args)
{
    WARN("SDL support not compiled in!\n");
    return STATUS_NOT_IMPLEMENTED;
}

#endif /* SONAME_LIBSDL2 */
