/*
  Simple DirectMedia Layer
  Copyright (C) 1997-2023 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_JOYSTICK_HIDAPI

#include "SDL_hints.h"
#include "SDL_events.h"
#include "SDL_timer.h"
#include "SDL_joystick.h"
#include "SDL_gamecontroller.h"
#include "../../SDL_hints_c.h"
#include "../SDL_sysjoystick.h"
#include "SDL_hidapijoystick_c.h"
#include "SDL_hidapi_rumble.h"
#include "SDL_hidapi_nintendo.h"

#ifdef SDL_JOYSTICK_HIDAPI_WII

/* Define this if you want to log all packets from the controller */
/*#define DEBUG_WII_PROTOCOL*/

#define ENABLE_CONTINUOUS_REPORTING SDL_TRUE

#define INPUT_WAIT_TIMEOUT_MS      (3 * 1000)
#define MOTION_PLUS_UPDATE_TIME_MS (8 * 1000)
#define STATUS_UPDATE_TIME_MS      (15 * 60 * 1000)

#define WII_EXTENSION_NONE            0x2E2E
#define WII_EXTENSION_UNINITIALIZED   0xFFFF
#define WII_EXTENSION_NUNCHUK         0x0000
#define WII_EXTENSION_GAMEPAD         0x0101
#define WII_EXTENSION_WIIUPRO         0x0120
#define WII_EXTENSION_MOTIONPLUS_MASK 0xF0FF
#define WII_EXTENSION_MOTIONPLUS_ID   0x0005

#define WII_MOTIONPLUS_MODE_NONE     0x00
#define WII_MOTIONPLUS_MODE_STANDARD 0x04
#define WII_MOTIONPLUS_MODE_NUNCHUK  0x05
#define WII_MOTIONPLUS_MODE_GAMEPAD  0x07

typedef enum
{
    k_eWiiInputReportIDs_Status = 0x20,
    k_eWiiInputReportIDs_ReadMemory = 0x21,
    k_eWiiInputReportIDs_Acknowledge = 0x22,
    k_eWiiInputReportIDs_ButtonData0 = 0x30,
    k_eWiiInputReportIDs_ButtonData1 = 0x31,
    k_eWiiInputReportIDs_ButtonData2 = 0x32,
    k_eWiiInputReportIDs_ButtonData3 = 0x33,
    k_eWiiInputReportIDs_ButtonData4 = 0x34,
    k_eWiiInputReportIDs_ButtonData5 = 0x35,
    k_eWiiInputReportIDs_ButtonData6 = 0x36,
    k_eWiiInputReportIDs_ButtonData7 = 0x37,
    k_eWiiInputReportIDs_ButtonDataD = 0x3D,
    k_eWiiInputReportIDs_ButtonDataE = 0x3E,
    k_eWiiInputReportIDs_ButtonDataF = 0x3F,
} EWiiInputReportIDs;

typedef enum
{
    k_eWiiOutputReportIDs_Rumble = 0x10,
    k_eWiiOutputReportIDs_LEDs = 0x11,
    k_eWiiOutputReportIDs_DataReportingMode = 0x12,
    k_eWiiOutputReportIDs_IRCameraEnable = 0x13,
    k_eWiiOutputReportIDs_SpeakerEnable = 0x14,
    k_eWiiOutputReportIDs_StatusRequest = 0x15,
    k_eWiiOutputReportIDs_WriteMemory = 0x16,
    k_eWiiOutputReportIDs_ReadMemory = 0x17,
    k_eWiiOutputReportIDs_SpeakerData = 0x18,
    k_eWiiOutputReportIDs_SpeakerMute = 0x19,
    k_eWiiOutputReportIDs_IRCameraEnable2 = 0x1a,
} EWiiOutputReportIDs;

typedef enum
{
    k_eWiiPlayerLEDs_P1 = 0x10,
    k_eWiiPlayerLEDs_P2 = 0x20,
    k_eWiiPlayerLEDs_P3 = 0x40,
    k_eWiiPlayerLEDs_P4 = 0x80,
} EWiiPlayerLEDs;

typedef enum
{
    k_eWiiCommunicationState_None,                  /* No special communications happening */
    k_eWiiCommunicationState_CheckMotionPlusStage1, /* Sent standard extension identify request */
    k_eWiiCommunicationState_CheckMotionPlusStage2, /* Sent Motion Plus extension identify request */
} EWiiCommunicationState;

typedef enum
{
    k_eWiiButtons_A = SDL_CONTROLLER_BUTTON_MISC1,
    k_eWiiButtons_B,
    k_eWiiButtons_One,
    k_eWiiButtons_Two,
    k_eWiiButtons_Plus,
    k_eWiiButtons_Minus,
    k_eWiiButtons_Home,
    k_eWiiButtons_DPad_Up,
    k_eWiiButtons_DPad_Down,
    k_eWiiButtons_DPad_Left,
    k_eWiiButtons_DPad_Right,
    k_eWiiButtons_Max
} EWiiButtons;

#define k_unWiiPacketDataLength 22

typedef struct
{
    Uint8 rgucBaseButtons[2];
    Uint8 rgucAccelerometer[3];
    Uint8 rgucExtension[21];
    SDL_bool hasBaseButtons;
    SDL_bool hasAccelerometer;
    Uint8 ucNExtensionBytes;
} WiiButtonData;

typedef struct
{
    SDL_HIDAPI_Device *device;
    SDL_Joystick *joystick;
    EWiiCommunicationState m_eCommState;
    EWiiExtensionControllerType m_eExtensionControllerType;
    SDL_bool m_bUseButtonLabels;
    SDL_bool m_bPlayerLights;
    int m_nPlayerIndex;
    SDL_bool m_bRumbleActive;
    SDL_bool m_bMotionPlusPresent;
    Uint8 m_ucMotionPlusMode;
    SDL_bool m_bReportSensors;
    Uint8 m_rgucReadBuffer[k_unWiiPacketDataLength];
    Uint32 m_unLastInput;
    Uint32 m_unLastStatus;
    Uint32 m_unNextMotionPlusCheck;
    SDL_bool m_bDisconnected;

    struct StickCalibrationData
    {
        Uint16 min;
        Uint16 max;
        Uint16 center;
        Uint16 deadzone;
    } m_StickCalibrationData[6];
} SDL_DriverWii_Context;

static void HIDAPI_DriverWii_RegisterHints(SDL_HintCallback callback, void *userdata)
{
    SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_WII, callback, userdata);
}

static void HIDAPI_DriverWii_UnregisterHints(SDL_HintCallback callback, void *userdata)
{
    SDL_DelHintCallback(SDL_HINT_JOYSTICK_HIDAPI_WII, callback, userdata);
}

static SDL_bool HIDAPI_DriverWii_IsEnabled(void)
{
#if 1 /* This doesn't work with the dolphinbar, so don't enable by default right now */
    return SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_WII, SDL_FALSE);
#else
    return SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_WII,
                              SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI,
                                                 SDL_HIDAPI_DEFAULT));
#endif
}

static SDL_bool HIDAPI_DriverWii_IsSupportedDevice(SDL_HIDAPI_Device *device, const char *name, SDL_GameControllerType type, Uint16 vendor_id, Uint16 product_id, Uint16 version, int interface_number, int interface_class, int interface_subclass, int interface_protocol)
{
    if (vendor_id == USB_VENDOR_NINTENDO &&
        (product_id == USB_PRODUCT_NINTENDO_WII_REMOTE ||
         product_id == USB_PRODUCT_NINTENDO_WII_REMOTE2)) {
        return SDL_TRUE;
    }
    return SDL_FALSE;
}

static int ReadInput(SDL_DriverWii_Context *ctx)
{
    int size;

    /* Make sure we don't try to read at the same time a write is happening */
    if (SDL_AtomicGet(&ctx->device->rumble_pending) > 0) {
        return 0;
    }

    size = SDL_hid_read_timeout(ctx->device->dev, ctx->m_rgucReadBuffer, sizeof(ctx->m_rgucReadBuffer), 0);
#ifdef DEBUG_WII_PROTOCOL
    if (size > 0) {
        HIDAPI_DumpPacket("Wii packet: size = %d", ctx->m_rgucReadBuffer, size);
    }
#endif
    return size;
}

static SDL_bool WriteOutput(SDL_DriverWii_Context *ctx, const Uint8 *data, int size, SDL_bool sync)
{
#ifdef DEBUG_WII_PROTOCOL
    if (size > 0) {
        HIDAPI_DumpPacket("Wii write packet: size = %d", data, size);
    }
#endif
    if (sync) {
        return SDL_hid_write(ctx->device->dev, data, size) >= 0;
    } else {
        /* Use the rumble thread for general asynchronous writes */
        if (SDL_HIDAPI_LockRumble() != 0) {
            return SDL_FALSE;
        }
        return SDL_HIDAPI_SendRumbleAndUnlock(ctx->device, data, size) >= 0;
    }
}

static SDL_bool ReadInputSync(SDL_DriverWii_Context *ctx, EWiiInputReportIDs expectedID, SDL_bool (*isMine)(const Uint8 *))
{
    Uint32 TimeoutMs = 250; /* Seeing successful reads after about 200 ms */
    Uint32 startTicks = SDL_GetTicks();

    int nRead = 0;
    while ((nRead = ReadInput(ctx)) != -1) {
        if (nRead > 0) {
            if (ctx->m_rgucReadBuffer[0] == expectedID && (isMine == NULL || isMine(ctx->m_rgucReadBuffer))) {
                return SDL_TRUE;
            }
        } else {
            if (SDL_TICKS_PASSED(SDL_GetTicks(), startTicks + TimeoutMs)) {
                break;
            }
            SDL_Delay(1);
        }
    }
    SDL_SetError("Read timed out");
    return SDL_FALSE;
}

static SDL_bool IsWriteMemoryResponse(const Uint8 *data)
{
    return data[3] == k_eWiiOutputReportIDs_WriteMemory;
}

static SDL_bool WriteRegister(SDL_DriverWii_Context *ctx, Uint32 address, const Uint8 *data, int size, SDL_bool sync)
{
    Uint8 writeRequest[k_unWiiPacketDataLength];

    SDL_zeroa(writeRequest);
    writeRequest[0] = k_eWiiOutputReportIDs_WriteMemory;
    writeRequest[1] = 0x04 | ctx->m_bRumbleActive;
    writeRequest[2] = (address >> 16) & 0xff;
    writeRequest[3] = (address >> 8) & 0xff;
    writeRequest[4] = address & 0xff;
    writeRequest[5] = size;
    SDL_assert(size > 0 && size <= 16);
    SDL_memcpy(writeRequest + 6, data, size);

    if (!WriteOutput(ctx, writeRequest, sizeof(writeRequest), sync)) {
        return SDL_FALSE;
    }
    if (sync) {
        /* Wait for response */
        if (!ReadInputSync(ctx, k_eWiiInputReportIDs_Acknowledge, IsWriteMemoryResponse)) {
            return SDL_FALSE;
        }
        if (ctx->m_rgucReadBuffer[4]) {
            SDL_SetError("Write memory failed: %d", ctx->m_rgucReadBuffer[4]);
            return SDL_FALSE;
        }
    }
    return SDL_TRUE;
}

static SDL_bool ReadRegister(SDL_DriverWii_Context *ctx, Uint32 address, int size, SDL_bool sync)
{
    Uint8 readRequest[7];

    readRequest[0] = k_eWiiOutputReportIDs_ReadMemory;
    readRequest[1] = 0x04 | ctx->m_bRumbleActive;
    readRequest[2] = (address >> 16) & 0xff;
    readRequest[3] = (address >> 8) & 0xff;
    readRequest[4] = address & 0xff;
    readRequest[5] = (size >> 8) & 0xff;
    readRequest[6] = size & 0xff;

    SDL_assert(size > 0 && size <= 0xffff);

    if (!WriteOutput(ctx, readRequest, sizeof(readRequest), sync)) {
        return SDL_FALSE;
    }
    if (sync) {
        SDL_assert(size <= 16); /* Only waiting for one packet is supported right now */
        /* Wait for response */
        if (!ReadInputSync(ctx, k_eWiiInputReportIDs_ReadMemory, NULL)) {
            return SDL_FALSE;
        }
    }
    return SDL_TRUE;
}

static SDL_bool SendExtensionIdentify(SDL_DriverWii_Context *ctx, SDL_bool sync)
{
    return ReadRegister(ctx, 0xA400FE, 2, sync);
}

static SDL_bool ParseExtensionIdentifyResponse(SDL_DriverWii_Context *ctx, Uint16 *extension)
{
    int i;

    if (ctx->m_rgucReadBuffer[0] != k_eWiiInputReportIDs_ReadMemory) {
        SDL_SetError("Unexpected extension response type");
        return SDL_FALSE;
    }

    if (ctx->m_rgucReadBuffer[4] != 0x00 || ctx->m_rgucReadBuffer[5] != 0xFE) {
        SDL_SetError("Unexpected extension response address");
        return SDL_FALSE;
    }

    if (ctx->m_rgucReadBuffer[3] != 0x10) {
        Uint8 error = (ctx->m_rgucReadBuffer[3] & 0xF);

        if (error == 7) {
            /* The extension memory isn't mapped */
            *extension = WII_EXTENSION_NONE;
            return SDL_TRUE;
        }

        if (error) {
            SDL_SetError("Failed to read extension type: %d", error);
        } else {
            SDL_SetError("Unexpected read length when reading extension type: %d", (ctx->m_rgucReadBuffer[3] >> 4) + 1);
        }
        return SDL_FALSE;
    }

    *extension = 0;
    for (i = 6; i < 8; i++) {
        *extension = *extension << 8 | ctx->m_rgucReadBuffer[i];
    }
    return SDL_TRUE;
}

static EWiiExtensionControllerType GetExtensionType(Uint16 extension_id)
{
    switch (extension_id) {
    case WII_EXTENSION_NONE:
        return k_eWiiExtensionControllerType_None;
    case WII_EXTENSION_NUNCHUK:
        return k_eWiiExtensionControllerType_Nunchuk;
    case WII_EXTENSION_GAMEPAD:
        return k_eWiiExtensionControllerType_Gamepad;
    case WII_EXTENSION_WIIUPRO:
        return k_eWiiExtensionControllerType_WiiUPro;
    default:
        return k_eWiiExtensionControllerType_Unknown;
    }
}

static SDL_bool SendExtensionReset(SDL_DriverWii_Context *ctx, SDL_bool sync)
{
    SDL_bool result = SDL_TRUE;
    {
        Uint8 data = 0x55;
        result = result && WriteRegister(ctx, 0xA400F0, &data, sizeof(data), sync);
    }
    /* This write will fail if there is no extension connected, that's fine */
    {
        Uint8 data = 0x00;
        (void)WriteRegister(ctx, 0xA400FB, &data, sizeof(data), sync);
    }
    return result;
}

static SDL_bool GetMotionPlusState(SDL_DriverWii_Context *ctx, SDL_bool *connected, Uint8 *mode)
{
    Uint16 extension;

    if (connected) {
        *connected = SDL_FALSE;
    }
    if (mode) {
        *mode = 0;
    }

    if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_WiiUPro) {
        /* The Wii U Pro controller never has the Motion Plus extension */
        return SDL_TRUE;
    }

    if (SendExtensionIdentify(ctx, SDL_TRUE) &&
        ParseExtensionIdentifyResponse(ctx, &extension)) {
        if ((extension & WII_EXTENSION_MOTIONPLUS_MASK) == WII_EXTENSION_MOTIONPLUS_ID) {
            /* Motion Plus is currently active */
            if (connected) {
                *connected = SDL_TRUE;
            }
            if (mode) {
                *mode = (extension >> 8);
            }
            return SDL_TRUE;
        }
    }

    if (ReadRegister(ctx, 0xA600FE, 2, SDL_TRUE) &&
        ParseExtensionIdentifyResponse(ctx, &extension)) {
        if ((extension & WII_EXTENSION_MOTIONPLUS_MASK) == WII_EXTENSION_MOTIONPLUS_ID) {
            /* Motion Plus is currently connected */
            if (connected) {
                *connected = SDL_TRUE;
            }
        }
        return SDL_TRUE;
    }

    /* Failed to read the register or parse the response */
    return SDL_FALSE;
}

static SDL_bool NeedsPeriodicMotionPlusCheck(SDL_DriverWii_Context *ctx, SDL_bool status_update)
{
    if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_WiiUPro) {
        /* The Wii U Pro controller never has the Motion Plus extension */
        return SDL_FALSE;
    }

    if (ctx->m_ucMotionPlusMode != WII_MOTIONPLUS_MODE_NONE && !status_update) {
        /* We'll get a status update when Motion Plus is disconnected */
        return SDL_FALSE;
    }

    return SDL_TRUE;
}

static void SchedulePeriodicMotionPlusCheck(SDL_DriverWii_Context *ctx)
{
    ctx->m_unNextMotionPlusCheck = SDL_GetTicks() + MOTION_PLUS_UPDATE_TIME_MS;
    if (!ctx->m_unNextMotionPlusCheck) {
        ctx->m_unNextMotionPlusCheck = 1;
    }
}

static void CheckMotionPlusConnection(SDL_DriverWii_Context *ctx)
{
    SendExtensionIdentify(ctx, SDL_FALSE);

    ctx->m_eCommState = k_eWiiCommunicationState_CheckMotionPlusStage1;
}

static void ActivateMotionPlusWithMode(SDL_DriverWii_Context *ctx, Uint8 mode)
{
#if defined(__LINUX__)
    /* Linux drivers maintain a lot of state around the Motion Plus
     * extension, so don't mess with it here.
     */
#else
    WriteRegister(ctx, 0xA600FE, &mode, sizeof(mode), SDL_TRUE);

    ctx->m_ucMotionPlusMode = mode;
#endif /* LINUX */
}

static void ActivateMotionPlus(SDL_DriverWii_Context *ctx)
{
    Uint8 mode = WII_MOTIONPLUS_MODE_STANDARD;

    /* Pick the pass-through mode based on the connected controller */
    if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_Nunchuk) {
        mode = WII_MOTIONPLUS_MODE_NUNCHUK;
    } else if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_Gamepad) {
        mode = WII_MOTIONPLUS_MODE_GAMEPAD;
    }
    ActivateMotionPlusWithMode(ctx, mode);
}

static void DeactivateMotionPlus(SDL_DriverWii_Context *ctx)
{
    Uint8 data = 0x55;
    WriteRegister(ctx, 0xA400F0, &data, sizeof(data), SDL_TRUE);

    /* Wait for the deactivation status message */
    ReadInputSync(ctx, k_eWiiInputReportIDs_Status, NULL);

    ctx->m_ucMotionPlusMode = WII_MOTIONPLUS_MODE_NONE;
}

static void UpdatePowerLevelWii(SDL_Joystick *joystick, Uint8 batteryLevelByte)
{
    if (batteryLevelByte > 178) {
        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_FULL);
    } else if (batteryLevelByte > 51) {
        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_MEDIUM);
    } else if (batteryLevelByte > 13) {
        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_LOW);
    } else {
        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_EMPTY);
    }
}

static void UpdatePowerLevelWiiU(SDL_Joystick *joystick, Uint8 extensionBatteryByte)
{
    SDL_bool charging = extensionBatteryByte & 0x08 ? SDL_FALSE : SDL_TRUE;
    SDL_bool pluggedIn = extensionBatteryByte & 0x04 ? SDL_FALSE : SDL_TRUE;
    Uint8 batteryLevel = extensionBatteryByte >> 4;

    /* Not sure if all Wii U Pro controllers act like this, but on mine
     * 4, 3, and 2 are held for about 20 hours each
     * 1 is held for about 6 hours
     * 0 is held for about 2 hours
     * No value above 4 has been observed.
     */
    if (pluggedIn && !charging) {
        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_WIRED);
    } else if (batteryLevel >= 4) {
        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_FULL);
    } else if (batteryLevel > 1) {
        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_MEDIUM);
    } else if (batteryLevel == 1) {
        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_LOW);
    } else {
        SDL_PrivateJoystickBatteryLevel(joystick, SDL_JOYSTICK_POWER_EMPTY);
    }
}

static EWiiInputReportIDs GetButtonPacketType(SDL_DriverWii_Context *ctx)
{
    switch (ctx->m_eExtensionControllerType) {
    case k_eWiiExtensionControllerType_WiiUPro:
        return k_eWiiInputReportIDs_ButtonDataD;
    case k_eWiiExtensionControllerType_Nunchuk:
    case k_eWiiExtensionControllerType_Gamepad:
        if (ctx->m_bReportSensors) {
            return k_eWiiInputReportIDs_ButtonData5;
        } else {
            return k_eWiiInputReportIDs_ButtonData2;
        }
    default:
        if (ctx->m_bReportSensors) {
            return k_eWiiInputReportIDs_ButtonData5;
        } else {
            return k_eWiiInputReportIDs_ButtonData0;
        }
    }
}

static SDL_bool RequestButtonPacketType(SDL_DriverWii_Context *ctx, EWiiInputReportIDs type)
{
    Uint8 data[3];
    Uint8 tt = ctx->m_bRumbleActive;

    /* Continuous reporting off, tt & 4 == 0 */
    if (ENABLE_CONTINUOUS_REPORTING) {
        tt |= 4;
    }

    data[0] = k_eWiiOutputReportIDs_DataReportingMode;
    data[1] = tt;
    data[2] = type;
    return WriteOutput(ctx, data, sizeof(data), SDL_FALSE);
}

static void ResetButtonPacketType(SDL_DriverWii_Context *ctx)
{
    RequestButtonPacketType(ctx, GetButtonPacketType(ctx));
}

static void InitStickCalibrationData(SDL_DriverWii_Context *ctx)
{
    int i;
    switch (ctx->m_eExtensionControllerType) {
    case k_eWiiExtensionControllerType_WiiUPro:
        for (i = 0; i < 4; i++) {
            ctx->m_StickCalibrationData[i].min = 1000;
            ctx->m_StickCalibrationData[i].max = 3000;
            ctx->m_StickCalibrationData[i].center = 0;
            ctx->m_StickCalibrationData[i].deadzone = 100;
        }
        break;
    case k_eWiiExtensionControllerType_Gamepad:
        for (i = 0; i < 4; i++) {
            ctx->m_StickCalibrationData[i].min = i < 2 ? 9 : 5;
            ctx->m_StickCalibrationData[i].max = i < 2 ? 54 : 26;
            ctx->m_StickCalibrationData[i].center = 0;
            ctx->m_StickCalibrationData[i].deadzone = i < 2 ? 4 : 2;
        }
        break;
    case k_eWiiExtensionControllerType_Nunchuk:
        for (i = 0; i < 2; i++) {
            ctx->m_StickCalibrationData[i].min = 40;
            ctx->m_StickCalibrationData[i].max = 215;
            ctx->m_StickCalibrationData[i].center = 0;
            ctx->m_StickCalibrationData[i].deadzone = 10;
        }
        break;
    default:
        break;
    }
}

static void InitializeExtension(SDL_DriverWii_Context *ctx)
{
    SendExtensionReset(ctx, SDL_TRUE);
    InitStickCalibrationData(ctx);
    ResetButtonPacketType(ctx);
}

static void SDLCALL SDL_GameControllerButtonReportingHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
{
    SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)userdata;
    ctx->m_bUseButtonLabels = SDL_GetStringBoolean(hint, SDL_TRUE);
}

static void UpdateSlotLED(SDL_DriverWii_Context *ctx)
{
    Uint8 leds;
    Uint8 data[2];

    /* The lowest bit needs to have the rumble status */
    leds = ctx->m_bRumbleActive;

    if (ctx->m_bPlayerLights) {
        /* Use the same LED codes as Smash 8-player for 5-7 */
        if (ctx->m_nPlayerIndex == 0 || ctx->m_nPlayerIndex > 3) {
            leds |= k_eWiiPlayerLEDs_P1;
        }
        if (ctx->m_nPlayerIndex == 1 || ctx->m_nPlayerIndex == 4) {
            leds |= k_eWiiPlayerLEDs_P2;
        }
        if (ctx->m_nPlayerIndex == 2 || ctx->m_nPlayerIndex == 5) {
            leds |= k_eWiiPlayerLEDs_P3;
        }
        if (ctx->m_nPlayerIndex == 3 || ctx->m_nPlayerIndex == 6) {
            leds |= k_eWiiPlayerLEDs_P4;
        }
        /* Turn on all lights for other player indexes */
        if (ctx->m_nPlayerIndex < 0 || ctx->m_nPlayerIndex > 6) {
            leds |= k_eWiiPlayerLEDs_P1 | k_eWiiPlayerLEDs_P2 | k_eWiiPlayerLEDs_P3 | k_eWiiPlayerLEDs_P4;
        }
    }

    data[0] = k_eWiiOutputReportIDs_LEDs;
    data[1] = leds;
    WriteOutput(ctx, data, sizeof(data), SDL_FALSE);
}

static void SDLCALL SDL_PlayerLEDHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
{
    SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)userdata;
    SDL_bool bPlayerLights = SDL_GetStringBoolean(hint, SDL_TRUE);

    if (bPlayerLights != ctx->m_bPlayerLights) {
        ctx->m_bPlayerLights = bPlayerLights;

        UpdateSlotLED(ctx);
    }
}

static EWiiExtensionControllerType ReadExtensionControllerType(SDL_HIDAPI_Device *device)
{
    SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;
    EWiiExtensionControllerType eExtensionControllerType = k_eWiiExtensionControllerType_Unknown;
    const int MAX_ATTEMPTS = 20;
    int attempts = 0;

    /* Create enough of a context to read the controller type from the device */
    for (attempts = 0; attempts < MAX_ATTEMPTS; ++attempts) {
        Uint16 extension;
        if (SendExtensionIdentify(ctx, SDL_TRUE) &&
            ParseExtensionIdentifyResponse(ctx, &extension)) {
            Uint8 motion_plus_mode = 0;
            if ((extension & WII_EXTENSION_MOTIONPLUS_MASK) == WII_EXTENSION_MOTIONPLUS_ID) {
                motion_plus_mode = (Uint8)(extension >> 8);
            }
            if (motion_plus_mode || extension == WII_EXTENSION_UNINITIALIZED) {
                SendExtensionReset(ctx, SDL_TRUE);
                if (SendExtensionIdentify(ctx, SDL_TRUE)) {
                    ParseExtensionIdentifyResponse(ctx, &extension);
                }
            }

            eExtensionControllerType = GetExtensionType(extension);

            /* Reset the Motion Plus controller if needed */
            if (motion_plus_mode) {
                ActivateMotionPlusWithMode(ctx, motion_plus_mode);
            }
            break;
        }
    }
    return eExtensionControllerType;
}

static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
{
    SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;

    switch (ctx->m_eExtensionControllerType) {
    case k_eWiiExtensionControllerType_None:
        HIDAPI_SetDeviceName(device, "Nintendo Wii Remote");
        break;
    case k_eWiiExtensionControllerType_Nunchuk:
        HIDAPI_SetDeviceName(device, "Nintendo Wii Remote with Nunchuk");
        break;
    case k_eWiiExtensionControllerType_Gamepad:
        HIDAPI_SetDeviceName(device, "Nintendo Wii Remote with Classic Controller");
        break;
    case k_eWiiExtensionControllerType_WiiUPro:
        HIDAPI_SetDeviceName(device, "Nintendo Wii U Pro Controller");
        break;
    default:
        HIDAPI_SetDeviceName(device, "Nintendo Wii Remote with Unknown Extension");
        break;
    }
    device->guid.data[15] = ctx->m_eExtensionControllerType;
}

static SDL_bool HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
{
    SDL_DriverWii_Context *ctx;

    ctx = (SDL_DriverWii_Context *)SDL_calloc(1, sizeof(*ctx));
    if (ctx == NULL) {
        SDL_OutOfMemory();
        return SDL_FALSE;
    }
    ctx->device = device;
    device->context = ctx;

    if (device->vendor_id == USB_VENDOR_NINTENDO) {
        ctx->m_eExtensionControllerType = ReadExtensionControllerType(device);

        UpdateDeviceIdentity(device);
    }
    return HIDAPI_JoystickConnected(device, NULL);
}

static int HIDAPI_DriverWii_GetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id)
{
    return -1;
}

static void HIDAPI_DriverWii_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id, int player_index)
{
    SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;

    if (!ctx->joystick) {
        return;
    }

    ctx->m_nPlayerIndex = player_index;

    UpdateSlotLED(ctx);
}

static SDL_bool HIDAPI_DriverWii_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
{
    SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;

    SDL_AssertJoysticksLocked();

    ctx->joystick = joystick;

    InitializeExtension(ctx);

    GetMotionPlusState(ctx, &ctx->m_bMotionPlusPresent, &ctx->m_ucMotionPlusMode);

    if (NeedsPeriodicMotionPlusCheck(ctx, SDL_FALSE)) {
        SchedulePeriodicMotionPlusCheck(ctx);
    }

    if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_None ||
        ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_Nunchuk) {
        SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 100.0f);
        if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_Nunchuk) {
            SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL_L, 100.0f);
        }

        if (ctx->m_bMotionPlusPresent) {
            SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 100.0f);
        }
    }

    SDL_AddHintCallback(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS,
                        SDL_GameControllerButtonReportingHintChanged, ctx);

    /* Initialize player index (needed for setting LEDs) */
    ctx->m_nPlayerIndex = SDL_JoystickGetPlayerIndex(joystick);
    ctx->m_bPlayerLights = SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED, SDL_TRUE);
    UpdateSlotLED(ctx);

    SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED,
                        SDL_PlayerLEDHintChanged, ctx);

    /* Initialize the joystick capabilities */
    if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_WiiUPro) {
        joystick->nbuttons = 15;
    } else {
        /* Maximum is Classic Controller + Wiimote */
        joystick->nbuttons = k_eWiiButtons_Max;
    }
    joystick->naxes = SDL_CONTROLLER_AXIS_MAX;

    ctx->m_unLastInput = SDL_GetTicks();

    return SDL_TRUE;
}

static int HIDAPI_DriverWii_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
{
    SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;
    SDL_bool active = (low_frequency_rumble || high_frequency_rumble) ? SDL_TRUE : SDL_FALSE;

    if (active != ctx->m_bRumbleActive) {
        Uint8 data[2];

        data[0] = k_eWiiOutputReportIDs_Rumble;
        data[1] = active;
        WriteOutput(ctx, data, sizeof(data), SDL_FALSE);

        ctx->m_bRumbleActive = active;
    }
    return 0;
}

static int HIDAPI_DriverWii_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
{
    return SDL_Unsupported();
}

static Uint32 HIDAPI_DriverWii_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
{
    return SDL_JOYCAP_RUMBLE;
}

static int HIDAPI_DriverWii_SetJoystickLED(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)
{
    return SDL_Unsupported();
}

static int HIDAPI_DriverWii_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, const void *data, int size)
{
    return SDL_Unsupported();
}

static int HIDAPI_DriverWii_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, SDL_bool enabled)
{
    SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;

    if (enabled != ctx->m_bReportSensors) {
        ctx->m_bReportSensors = enabled;

        if (ctx->m_bMotionPlusPresent) {
            if (enabled) {
                ActivateMotionPlus(ctx);
            } else {
                DeactivateMotionPlus(ctx);
            }
        }

        ResetButtonPacketType(ctx);
    }
    return 0;
}

static void PostStickCalibrated(SDL_Joystick *joystick, struct StickCalibrationData *calibration, Uint8 axis, Uint16 data)
{
    Sint16 value = 0;
    if (!calibration->center) {
        /* Center on first read */
        calibration->center = data;
        return;
    }
    if (data < calibration->min) {
        calibration->min = data;
    }
    if (data > calibration->max) {
        calibration->max = data;
    }
    if (data < calibration->center - calibration->deadzone) {
        Uint16 zero = calibration->center - calibration->deadzone;
        Uint16 range = zero - calibration->min;
        Uint16 distance = zero - data;
        float fvalue = (float)distance / (float)range;
        value = (Sint16)(fvalue * SDL_JOYSTICK_AXIS_MIN);
    } else if (data > calibration->center + calibration->deadzone) {
        Uint16 zero = calibration->center + calibration->deadzone;
        Uint16 range = calibration->max - zero;
        Uint16 distance = data - zero;
        float fvalue = (float)distance / (float)range;
        value = (Sint16)(fvalue * SDL_JOYSTICK_AXIS_MAX);
    }
    if (axis == SDL_CONTROLLER_AXIS_LEFTY || axis == SDL_CONTROLLER_AXIS_RIGHTY) {
        if (value) {
            value = ~value;
        }
    }
    SDL_PrivateJoystickAxis(joystick, axis, value);
}

/* Send button data to SDL
 *`defs` is a mapping for each bit to which button it represents.  0xFF indicates an unused bit
 *`data` is the button data from the controller
 *`size` is the number of bytes in `data` and the number of arrays of 8 mappings in `defs`
 *`on` is the joystick value to be sent if a bit is on
 *`off` is the joystick value to be sent if a bit is off
 */
static void PostPackedButtonData(SDL_Joystick *joystick, const Uint8 defs[][8], const Uint8 *data, int size, Uint8 on, Uint8 off)
{
    int i, j;

    for (i = 0; i < size; i++) {
        for (j = 0; j < 8; j++) {
            Uint8 button = defs[i][j];
            if (button != 0xFF) {
                Uint8 state = (data[i] >> j) & 1 ? on : off;
                SDL_PrivateJoystickButton(joystick, button, state);
            }
        }
    }
}

static const Uint8 GAMEPAD_BUTTON_DEFS[3][8] = {
    {
        0xFF /* Unused */,
        SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
        SDL_CONTROLLER_BUTTON_START,
        SDL_CONTROLLER_BUTTON_GUIDE,
        SDL_CONTROLLER_BUTTON_BACK,
        SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
        SDL_CONTROLLER_BUTTON_DPAD_DOWN,
        SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
    },
    {
        SDL_CONTROLLER_BUTTON_DPAD_UP,
        SDL_CONTROLLER_BUTTON_DPAD_LEFT,
        0xFF /* ZR */,
        SDL_CONTROLLER_BUTTON_X,
        SDL_CONTROLLER_BUTTON_A,
        SDL_CONTROLLER_BUTTON_Y,
        SDL_CONTROLLER_BUTTON_B,
        0xFF /*ZL*/,
    },
    {
        SDL_CONTROLLER_BUTTON_RIGHTSTICK,
        SDL_CONTROLLER_BUTTON_LEFTSTICK,
        0xFF /* Charging */,
        0xFF /* Plugged In */,
        0xFF /* Unused */,
        0xFF /* Unused */,
        0xFF /* Unused */,
        0xFF /* Unused */,
    }
};

static const Uint8 GAMEPAD_BUTTON_DEFS_POSITIONAL[3][8] = {
    {
        0xFF /* Unused */,
        SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
        SDL_CONTROLLER_BUTTON_START,
        SDL_CONTROLLER_BUTTON_GUIDE,
        SDL_CONTROLLER_BUTTON_BACK,
        SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
        SDL_CONTROLLER_BUTTON_DPAD_DOWN,
        SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
    },
    {
        SDL_CONTROLLER_BUTTON_DPAD_UP,
        SDL_CONTROLLER_BUTTON_DPAD_LEFT,
        0xFF /* ZR */,
        SDL_CONTROLLER_BUTTON_Y,
        SDL_CONTROLLER_BUTTON_B,
        SDL_CONTROLLER_BUTTON_X,
        SDL_CONTROLLER_BUTTON_A,
        0xFF /*ZL*/,
    },
    {
        SDL_CONTROLLER_BUTTON_RIGHTSTICK,
        SDL_CONTROLLER_BUTTON_LEFTSTICK,
        0xFF /* Charging */,
        0xFF /* Plugged In */,
        0xFF /* Unused */,
        0xFF /* Unused */,
        0xFF /* Unused */,
        0xFF /* Unused */,
    }
};

static const Uint8 MP_GAMEPAD_BUTTON_DEFS[3][8] = {
    {
        0xFF /* Unused */,
        SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
        SDL_CONTROLLER_BUTTON_START,
        SDL_CONTROLLER_BUTTON_GUIDE,
        SDL_CONTROLLER_BUTTON_BACK,
        SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
        SDL_CONTROLLER_BUTTON_DPAD_DOWN,
        SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
    },
    {
        0xFF /* Motion Plus data */,
        0xFF /* Motion Plus data */,
        0xFF /* ZR */,
        SDL_CONTROLLER_BUTTON_X,
        SDL_CONTROLLER_BUTTON_A,
        SDL_CONTROLLER_BUTTON_Y,
        SDL_CONTROLLER_BUTTON_B,
        0xFF /*ZL*/,
    },
    {
        SDL_CONTROLLER_BUTTON_RIGHTSTICK,
        SDL_CONTROLLER_BUTTON_LEFTSTICK,
        0xFF /* Charging */,
        0xFF /* Plugged In */,
        0xFF /* Unused */,
        0xFF /* Unused */,
        0xFF /* Unused */,
        0xFF /* Unused */,
    }
};

static const Uint8 MP_GAMEPAD_BUTTON_DEFS_POSITIONAL[3][8] = {
    {
        0xFF /* Unused */,
        SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
        SDL_CONTROLLER_BUTTON_START,
        SDL_CONTROLLER_BUTTON_GUIDE,
        SDL_CONTROLLER_BUTTON_BACK,
        SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
        SDL_CONTROLLER_BUTTON_DPAD_DOWN,
        SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
    },
    {
        0xFF /* Motion Plus data */,
        0xFF /* Motion Plus data */,
        0xFF /* ZR */,
        SDL_CONTROLLER_BUTTON_Y,
        SDL_CONTROLLER_BUTTON_B,
        SDL_CONTROLLER_BUTTON_X,
        SDL_CONTROLLER_BUTTON_A,
        0xFF /*ZL*/,
    },
    {
        SDL_CONTROLLER_BUTTON_RIGHTSTICK,
        SDL_CONTROLLER_BUTTON_LEFTSTICK,
        0xFF /* Charging */,
        0xFF /* Plugged In */,
        0xFF /* Unused */,
        0xFF /* Unused */,
        0xFF /* Unused */,
        0xFF /* Unused */,
    }
};

static const Uint8 MP_FIXUP_DPAD_BUTTON_DEFS[2][8] = {
    {
        SDL_CONTROLLER_BUTTON_DPAD_UP,
        0xFF,
        0xFF,
        0xFF,
        0xFF,
        0xFF,
        0xFF,
        0xFF,
    },
    {
        SDL_CONTROLLER_BUTTON_DPAD_LEFT,
        0xFF,
        0xFF,
        0xFF,
        0xFF,
        0xFF,
        0xFF,
        0xFF,
    }
};

static void HandleWiiUProButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
    static const Uint8 axes[] = { SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_LEFTY, SDL_CONTROLLER_AXIS_RIGHTY };
    const Uint8(*buttons)[8] = ctx->m_bUseButtonLabels ? GAMEPAD_BUTTON_DEFS : GAMEPAD_BUTTON_DEFS_POSITIONAL;
    Uint8 zl, zr;
    int i;

    if (data->ucNExtensionBytes < 11) {
        return;
    }

    /* Buttons */
    PostPackedButtonData(joystick, buttons, data->rgucExtension + 8, 3, SDL_RELEASED, SDL_PRESSED);

    /* Triggers */
    zl = data->rgucExtension[9] & 0x80;
    zr = data->rgucExtension[9] & 0x04;
    SDL_PrivateJoystickAxis(joystick, SDL_CONTROLLER_AXIS_TRIGGERLEFT, zl ? SDL_JOYSTICK_AXIS_MIN : SDL_JOYSTICK_AXIS_MAX);
    SDL_PrivateJoystickAxis(joystick, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, zr ? SDL_JOYSTICK_AXIS_MIN : SDL_JOYSTICK_AXIS_MAX);

    /* Sticks */
    for (i = 0; i < 4; i++) {
        Uint16 value = data->rgucExtension[i * 2] | (data->rgucExtension[i * 2 + 1] << 8);
        PostStickCalibrated(joystick, &ctx->m_StickCalibrationData[i], axes[i], value);
    }

    /* Power */
    UpdatePowerLevelWiiU(joystick, data->rgucExtension[10]);
}

static void HandleGamepadControllerButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
    const Uint8(*buttons)[8] = ctx->m_bUseButtonLabels ? ((ctx->m_ucMotionPlusMode == WII_MOTIONPLUS_MODE_GAMEPAD) ? MP_GAMEPAD_BUTTON_DEFS : GAMEPAD_BUTTON_DEFS) : ((ctx->m_ucMotionPlusMode == WII_MOTIONPLUS_MODE_GAMEPAD) ? MP_GAMEPAD_BUTTON_DEFS_POSITIONAL : GAMEPAD_BUTTON_DEFS_POSITIONAL);
    Uint8 lx, ly, rx, ry, zl, zr;

    if (data->ucNExtensionBytes < 6) {
        return;
    }

    /* Buttons */
    PostPackedButtonData(joystick, buttons, data->rgucExtension + 4, 2, SDL_RELEASED, SDL_PRESSED);
    if (ctx->m_ucMotionPlusMode == WII_MOTIONPLUS_MODE_GAMEPAD) {
        PostPackedButtonData(joystick, MP_FIXUP_DPAD_BUTTON_DEFS, data->rgucExtension, 2, SDL_RELEASED, SDL_PRESSED);
    }

    /* Triggers */
    zl = data->rgucExtension[5] & 0x80;
    zr = data->rgucExtension[5] & 0x04;
    SDL_PrivateJoystickAxis(joystick, SDL_CONTROLLER_AXIS_TRIGGERLEFT, zl ? SDL_JOYSTICK_AXIS_MIN : SDL_JOYSTICK_AXIS_MAX);
    SDL_PrivateJoystickAxis(joystick, SDL_CONTROLLER_AXIS_TRIGGERRIGHT, zr ? SDL_JOYSTICK_AXIS_MIN : SDL_JOYSTICK_AXIS_MAX);

    /* Sticks */
    if (ctx->m_ucMotionPlusMode == WII_MOTIONPLUS_MODE_GAMEPAD) {
        lx = data->rgucExtension[0] & 0x3E;
        ly = data->rgucExtension[1] & 0x3E;
    } else {
        lx = data->rgucExtension[0] & 0x3F;
        ly = data->rgucExtension[1] & 0x3F;
    }
    rx = (data->rgucExtension[2] >> 7) | ((data->rgucExtension[1] >> 5) & 0x06) | ((data->rgucExtension[0] >> 3) & 0x18);
    ry = data->rgucExtension[2] & 0x1F;
    PostStickCalibrated(joystick, &ctx->m_StickCalibrationData[0], SDL_CONTROLLER_AXIS_LEFTX, lx);
    PostStickCalibrated(joystick, &ctx->m_StickCalibrationData[1], SDL_CONTROLLER_AXIS_LEFTY, ly);
    PostStickCalibrated(joystick, &ctx->m_StickCalibrationData[2], SDL_CONTROLLER_AXIS_RIGHTX, rx);
    PostStickCalibrated(joystick, &ctx->m_StickCalibrationData[3], SDL_CONTROLLER_AXIS_RIGHTY, ry);
}

static void HandleWiiRemoteButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
    static const Uint8 buttons[2][8] = {
        {
            k_eWiiButtons_DPad_Left,
            k_eWiiButtons_DPad_Right,
            k_eWiiButtons_DPad_Down,
            k_eWiiButtons_DPad_Up,
            k_eWiiButtons_Plus,
            0xFF /* Unused */,
            0xFF /* Unused */,
            0xFF /* Unused */,
        },
        {
            k_eWiiButtons_Two,
            k_eWiiButtons_One,
            k_eWiiButtons_B,
            k_eWiiButtons_A,
            k_eWiiButtons_Minus,
            0xFF /* Unused */,
            0xFF /* Unused */,
            k_eWiiButtons_Home,
        }
    };
    if (data->hasBaseButtons) {
        PostPackedButtonData(joystick, buttons, data->rgucBaseButtons, 2, SDL_PRESSED, SDL_RELEASED);
    }
}

static void HandleWiiRemoteButtonDataAsMainController(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
    /* Wii remote maps really badly to a normal controller
     * Mapped 1 and 2 as X and Y
     * Not going to attempt positional mapping
     */
    static const Uint8 buttons[2][8] = {
        {
            SDL_CONTROLLER_BUTTON_DPAD_LEFT,
            SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
            SDL_CONTROLLER_BUTTON_DPAD_DOWN,
            SDL_CONTROLLER_BUTTON_DPAD_UP,
            SDL_CONTROLLER_BUTTON_START,
            0xFF /* Unused */,
            0xFF /* Unused */,
            0xFF /* Unused */,
        },
        {
            SDL_CONTROLLER_BUTTON_Y,
            SDL_CONTROLLER_BUTTON_X,
            SDL_CONTROLLER_BUTTON_A,
            SDL_CONTROLLER_BUTTON_B,
            SDL_CONTROLLER_BUTTON_BACK,
            0xFF /* Unused */,
            0xFF /* Unused */,
            SDL_CONTROLLER_BUTTON_GUIDE,
        }
    };
    if (data->hasBaseButtons) {
        PostPackedButtonData(joystick, buttons, data->rgucBaseButtons, 2, SDL_PRESSED, SDL_RELEASED);
    }
}

static void HandleNunchuckButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
    Uint8 c_button, z_button;

    if (data->ucNExtensionBytes < 6) {
        return;
    }

    if (ctx->m_ucMotionPlusMode == WII_MOTIONPLUS_MODE_NUNCHUK) {
        c_button = (data->rgucExtension[5] & 0x08) ? SDL_RELEASED : SDL_PRESSED;
        z_button = (data->rgucExtension[5] & 0x04) ? SDL_RELEASED : SDL_PRESSED;
    } else {
        c_button = (data->rgucExtension[5] & 0x02) ? SDL_RELEASED : SDL_PRESSED;
        z_button = (data->rgucExtension[5] & 0x01) ? SDL_RELEASED : SDL_PRESSED;
    }
    SDL_PrivateJoystickButton(joystick, SDL_CONTROLLER_BUTTON_LEFTSHOULDER, c_button);
    SDL_PrivateJoystickAxis(joystick, SDL_CONTROLLER_AXIS_TRIGGERLEFT, z_button ? SDL_JOYSTICK_AXIS_MAX : SDL_JOYSTICK_AXIS_MIN);
    PostStickCalibrated(joystick, &ctx->m_StickCalibrationData[0], SDL_CONTROLLER_AXIS_LEFTX, data->rgucExtension[0]);
    PostStickCalibrated(joystick, &ctx->m_StickCalibrationData[1], SDL_CONTROLLER_AXIS_LEFTY, data->rgucExtension[1]);

    if (ctx->m_bReportSensors) {
        const float ACCEL_RES_PER_G = 200.0f;
        Sint16 x, y, z;
        float values[3];

        x = (data->rgucExtension[2] << 2);
        y = (data->rgucExtension[3] << 2);
        z = (data->rgucExtension[4] << 2);

        if (ctx->m_ucMotionPlusMode == WII_MOTIONPLUS_MODE_NUNCHUK) {
            x |= ((data->rgucExtension[5] >> 3) & 0x02);
            y |= ((data->rgucExtension[5] >> 4) & 0x02);
            z &= ~0x04;
            z |= ((data->rgucExtension[5] >> 5) & 0x06);
        } else {
            x |= ((data->rgucExtension[5] >> 2) & 0x03);
            y |= ((data->rgucExtension[5] >> 4) & 0x03);
            z |= ((data->rgucExtension[5] >> 6) & 0x03);
        }

        x -= 0x200;
        y -= 0x200;
        z -= 0x200;

        values[0] = -((float)x / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
        values[1] = ((float)z / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
        values[2] = ((float)y / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
        SDL_PrivateJoystickSensor(joystick, SDL_SENSOR_ACCEL_L, 0, values, 3);
    }
}

static void HandleMotionPlusData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
    if (ctx->m_bReportSensors) {
        const float GYRO_RES_PER_DEGREE = 8192.0f;
        int x, y, z;
        float values[3];

        x = (data->rgucExtension[0] | ((data->rgucExtension[3] << 6) & 0xFF00)) - 8192;
        y = (data->rgucExtension[1] | ((data->rgucExtension[4] << 6) & 0xFF00)) - 8192;
        z = (data->rgucExtension[2] | ((data->rgucExtension[5] << 6) & 0xFF00)) - 8192;

        if (data->rgucExtension[3] & 0x02) {
            /* Slow rotation rate: 8192/440 units per deg/s */
            x *= 440;
        } else {
            /* Fast rotation rate: 8192/2000 units per deg/s */
            x *= 2000;
        }
        if (data->rgucExtension[4] & 0x02) {
            /* Slow rotation rate: 8192/440 units per deg/s */
            y *= 440;
        } else {
            /* Fast rotation rate: 8192/2000 units per deg/s */
            y *= 2000;
        }
        if (data->rgucExtension[3] & 0x01) {
            /* Slow rotation rate: 8192/440 units per deg/s */
            z *= 440;
        } else {
            /* Fast rotation rate: 8192/2000 units per deg/s */
            z *= 2000;
        }

        values[0] = -((float)z / GYRO_RES_PER_DEGREE) * (float)M_PI / 180.0f;
        values[1] = ((float)x / GYRO_RES_PER_DEGREE) * (float)M_PI / 180.0f;
        values[2] = ((float)y / GYRO_RES_PER_DEGREE) * (float)M_PI / 180.0f;
        SDL_PrivateJoystickSensor(joystick, SDL_SENSOR_GYRO, 0, values, 3);
    }
}

static void HandleWiiRemoteAccelData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
    const float ACCEL_RES_PER_G = 100.0f;
    Sint16 x, y, z;
    float values[3];

    if (!ctx->m_bReportSensors) {
        return;
    }

    x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03)) - 0x200;
    y = ((data->rgucAccelerometer[1] << 2) | ((data->rgucBaseButtons[1] >> 4) & 0x02)) - 0x200;
    z = ((data->rgucAccelerometer[2] << 2) | ((data->rgucBaseButtons[1] >> 5) & 0x02)) - 0x200;

    values[0] = -((float)x / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
    values[1] = ((float)z / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
    values[2] = ((float)y / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
    SDL_PrivateJoystickSensor(joystick, SDL_SENSOR_ACCEL, 0, values, 3);
}

static void HandleButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, WiiButtonData *data)
{
    if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_WiiUPro) {
        HandleWiiUProButtonData(ctx, joystick, data);
        return;
    }

    if (ctx->m_ucMotionPlusMode != WII_MOTIONPLUS_MODE_NONE &&
        data->ucNExtensionBytes > 5) {
        if (data->rgucExtension[5] & 0x01) {
            /* The data is invalid, possibly during a hotplug */
            return;
        }

        if (data->rgucExtension[4] & 0x01) {
            if (ctx->m_eExtensionControllerType == k_eWiiExtensionControllerType_None) {
                /* Something was plugged into the extension port, reinitialize to get new state */
                ctx->m_bDisconnected = SDL_TRUE;
            }
        } else {
            if (ctx->m_eExtensionControllerType != k_eWiiExtensionControllerType_None) {
                /* Something was removed from the extension port, reinitialize to get new state */
                ctx->m_bDisconnected = SDL_TRUE;
            }
        }

        if (data->rgucExtension[5] & 0x02) {
            HandleMotionPlusData(ctx, joystick, data);

            /* The extension data is consumed */
            data->ucNExtensionBytes = 0;
        }
    }

    HandleWiiRemoteButtonData(ctx, joystick, data);
    switch (ctx->m_eExtensionControllerType) {
    case k_eWiiExtensionControllerType_Nunchuk:
        HandleNunchuckButtonData(ctx, joystick, data);
        SDL_FALLTHROUGH;
    case k_eWiiExtensionControllerType_None:
        HandleWiiRemoteButtonDataAsMainController(ctx, joystick, data);
        break;
    case k_eWiiExtensionControllerType_Gamepad:
        HandleGamepadControllerButtonData(ctx, joystick, data);
        break;
    default:
        break;
    }
    HandleWiiRemoteAccelData(ctx, joystick, data);
}

static void GetBaseButtons(WiiButtonData *dst, const Uint8 *src)
{
    SDL_memcpy(dst->rgucBaseButtons, src, 2);
    dst->hasBaseButtons = SDL_TRUE;
}

static void GetAccelerometer(WiiButtonData *dst, const Uint8 *src)
{
    SDL_memcpy(dst->rgucAccelerometer, src, 3);
    dst->hasAccelerometer = SDL_TRUE;
}

static void GetExtensionData(WiiButtonData *dst, const Uint8 *src, int size)
{
    SDL_bool valid_data = SDL_FALSE;
    int i;

    if (size > sizeof(dst->rgucExtension)) {
        size = sizeof(dst->rgucExtension);
    }

    for (i = 0; i < size; ++i) {
        if (src[i] != 0xFF) {
            valid_data = SDL_TRUE;
            break;
        }
    }
    if (valid_data) {
        SDL_memcpy(dst->rgucExtension, src, size);
        dst->ucNExtensionBytes = size;
    }
}

static void HandleStatus(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick)
{
    SDL_bool hadExtension = ctx->m_eExtensionControllerType != k_eWiiExtensionControllerType_None;
    SDL_bool hasExtension = ctx->m_rgucReadBuffer[3] & 2 ? SDL_TRUE : SDL_FALSE;
    WiiButtonData data;
    SDL_zero(data);
    GetBaseButtons(&data, ctx->m_rgucReadBuffer + 1);
    HandleButtonData(ctx, joystick, &data);

    if (ctx->m_eExtensionControllerType != k_eWiiExtensionControllerType_WiiUPro) {
        /* Wii U has separate battery level tracking */
        UpdatePowerLevelWii(joystick, ctx->m_rgucReadBuffer[6]);
    }

    /* The report data format has been reset, need to update it */
    ResetButtonPacketType(ctx);

    SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Status update, extension %s\n", hasExtension ? "CONNECTED" : "DISCONNECTED");

    /* When Motion Plus is active, we get extension connect/disconnect status
     * through the Motion Plus packets. Otherwise we can use the status here.
     */
    if (ctx->m_ucMotionPlusMode != WII_MOTIONPLUS_MODE_NONE) {
        /* Check to make sure the Motion Plus extension state hasn't changed,
         * otherwise we'll get extension connect/disconnect status through
         * Motion Plus packets.
         */
        if (NeedsPeriodicMotionPlusCheck(ctx, SDL_TRUE)) {
            ctx->m_unNextMotionPlusCheck = SDL_GetTicks();
        }

    } else if (hadExtension != hasExtension) {
        /* Reinitialize to get new state */
        ctx->m_bDisconnected = SDL_TRUE;
    }
}

static void HandleResponse(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick)
{
    EWiiInputReportIDs type = ctx->m_rgucReadBuffer[0];
    WiiButtonData data;
    SDL_assert(type == k_eWiiInputReportIDs_Acknowledge || type == k_eWiiInputReportIDs_ReadMemory);
    SDL_zero(data);
    GetBaseButtons(&data, ctx->m_rgucReadBuffer + 1);
    HandleButtonData(ctx, joystick, &data);

    switch (ctx->m_eCommState) {
    case k_eWiiCommunicationState_None:
        break;

    case k_eWiiCommunicationState_CheckMotionPlusStage1:
    case k_eWiiCommunicationState_CheckMotionPlusStage2:
    {
        Uint16 extension = 0;
        if (ParseExtensionIdentifyResponse(ctx, &extension)) {
            if ((extension & WII_EXTENSION_MOTIONPLUS_MASK) == WII_EXTENSION_MOTIONPLUS_ID) {
                /* Motion Plus is currently active */
                SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Motion Plus CONNECTED (stage %d)\n", ctx->m_eCommState == k_eWiiCommunicationState_CheckMotionPlusStage1 ? 1 : 2);

                if (!ctx->m_bMotionPlusPresent) {
                    /* Reinitialize to get new sensor availability */
                    ctx->m_bDisconnected = SDL_TRUE;
                }
                ctx->m_eCommState = k_eWiiCommunicationState_None;

            } else if (ctx->m_eCommState == k_eWiiCommunicationState_CheckMotionPlusStage1) {
                /* Check to see if Motion Plus is present */
                ReadRegister(ctx, 0xA600FE, 2, SDL_FALSE);

                ctx->m_eCommState = k_eWiiCommunicationState_CheckMotionPlusStage2;

            } else {
                /* Motion Plus is not present */
                SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Motion Plus DISCONNECTED (stage %d)\n", ctx->m_eCommState == k_eWiiCommunicationState_CheckMotionPlusStage1 ? 1 : 2);

                if (ctx->m_bMotionPlusPresent) {
                    /* Reinitialize to get new sensor availability */
                    ctx->m_bDisconnected = SDL_TRUE;
                }
                ctx->m_eCommState = k_eWiiCommunicationState_None;
            }
        }
    } break;
    default:
        /* Should never happen */
        break;
    }
}

static void HandleButtonPacket(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick)
{
    EWiiInputReportIDs eExpectedReport = GetButtonPacketType(ctx);
    WiiButtonData data;

    /* FIXME: This should see if the data format is compatible rather than equal */
    if (eExpectedReport != ctx->m_rgucReadBuffer[0]) {
        SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Resetting report mode to %d\n", eExpectedReport);
        RequestButtonPacketType(ctx, eExpectedReport);
    }

    /* IR camera data is not supported */
    SDL_zero(data);
    switch (ctx->m_rgucReadBuffer[0]) {
    case k_eWiiInputReportIDs_ButtonData0: /* 30 BB BB */
        GetBaseButtons(&data, ctx->m_rgucReadBuffer + 1);
        break;
    case k_eWiiInputReportIDs_ButtonData1: /* 31 BB BB AA AA AA */
    case k_eWiiInputReportIDs_ButtonData3: /* 33 BB BB AA AA AA II II II II II II II II II II II II */
        GetBaseButtons(&data, ctx->m_rgucReadBuffer + 1);
        GetAccelerometer(&data, ctx->m_rgucReadBuffer + 3);
        break;
    case k_eWiiInputReportIDs_ButtonData2: /* 32 BB BB EE EE EE EE EE EE EE EE */
        GetBaseButtons(&data, ctx->m_rgucReadBuffer + 1);
        GetExtensionData(&data, ctx->m_rgucReadBuffer + 3, 8);
        break;
    case k_eWiiInputReportIDs_ButtonData4: /* 34 BB BB EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE */
        GetBaseButtons(&data, ctx->m_rgucReadBuffer + 1);
        GetExtensionData(&data, ctx->m_rgucReadBuffer + 3, 19);
        break;
    case k_eWiiInputReportIDs_ButtonData5: /* 35 BB BB AA AA AA EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE */
        GetBaseButtons(&data, ctx->m_rgucReadBuffer + 1);
        GetAccelerometer(&data, ctx->m_rgucReadBuffer + 3);
        GetExtensionData(&data, ctx->m_rgucReadBuffer + 6, 16);
        break;
    case k_eWiiInputReportIDs_ButtonData6: /* 36 BB BB II II II II II II II II II II EE EE EE EE EE EE EE EE EE */
        GetBaseButtons(&data, ctx->m_rgucReadBuffer + 1);
        GetExtensionData(&data, ctx->m_rgucReadBuffer + 13, 9);
        break;
    case k_eWiiInputReportIDs_ButtonData7: /* 37 BB BB AA AA AA II II II II II II II II II II EE EE EE EE EE EE */
        GetBaseButtons(&data, ctx->m_rgucReadBuffer + 1);
        GetExtensionData(&data, ctx->m_rgucReadBuffer + 16, 6);
        break;
    case k_eWiiInputReportIDs_ButtonDataD: /* 3d EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE */
        GetExtensionData(&data, ctx->m_rgucReadBuffer + 1, 21);
        break;
    case k_eWiiInputReportIDs_ButtonDataE:
    case k_eWiiInputReportIDs_ButtonDataF:
    default:
        SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Unsupported button data type %02x", ctx->m_rgucReadBuffer[0]);
        return;
    }
    HandleButtonData(ctx, joystick, &data);
}

static void HandleInput(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick)
{
    EWiiInputReportIDs type = ctx->m_rgucReadBuffer[0];
    if (type == k_eWiiInputReportIDs_Status) {
        HandleStatus(ctx, joystick);
    } else if (type == k_eWiiInputReportIDs_Acknowledge || type == k_eWiiInputReportIDs_ReadMemory) {
        HandleResponse(ctx, joystick);
    } else if (type >= k_eWiiInputReportIDs_ButtonData0 && type <= k_eWiiInputReportIDs_ButtonDataF) {
        HandleButtonPacket(ctx, joystick);
    } else {
        SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Unexpected input packet of type %x", type);
    }
}

static SDL_bool HIDAPI_DriverWii_UpdateDevice(SDL_HIDAPI_Device *device)
{
    SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;
    SDL_Joystick *joystick = NULL;
    int size;
    Uint32 now;

    if (device->num_joysticks > 0) {
        joystick = SDL_JoystickFromInstanceID(device->joysticks[0]);
    } else {
        return SDL_FALSE;
    }

    now = SDL_GetTicks();

    while ((size = ReadInput(ctx)) > 0) {
        if (joystick) {
            HandleInput(ctx, joystick);
        }
        ctx->m_unLastInput = now;
    }

    /* Check to see if we've lost connection to the controller.
     * We have continuous reporting enabled, so this should be reliable now.
     */
    {
        SDL_COMPILE_TIME_ASSERT(ENABLE_CONTINUOUS_REPORTING, ENABLE_CONTINUOUS_REPORTING);
    }
    if (SDL_TICKS_PASSED(now, ctx->m_unLastInput + INPUT_WAIT_TIMEOUT_MS)) {
        /* Bluetooth may have disconnected, try reopening the controller */
        size = -1;
    }

    if (joystick) {
        /* These checks aren't needed on the Wii U Pro Controller */
        if (ctx->m_eExtensionControllerType != k_eWiiExtensionControllerType_WiiUPro) {

            /* Check to see if the Motion Plus extension status has changed */
            if (ctx->m_unNextMotionPlusCheck &&
                SDL_TICKS_PASSED(now, ctx->m_unNextMotionPlusCheck)) {
                CheckMotionPlusConnection(ctx);
                if (NeedsPeriodicMotionPlusCheck(ctx, SDL_FALSE)) {
                    SchedulePeriodicMotionPlusCheck(ctx);
                } else {
                    ctx->m_unNextMotionPlusCheck = 0;
                }
            }

            /* Request a status update periodically to make sure our battery value is up to date */
            if (!ctx->m_unLastStatus ||
                SDL_TICKS_PASSED(now, ctx->m_unLastStatus + STATUS_UPDATE_TIME_MS)) {
                Uint8 data[2];

                data[0] = k_eWiiOutputReportIDs_StatusRequest;
                data[1] = ctx->m_bRumbleActive;
                WriteOutput(ctx, data, sizeof(data), SDL_FALSE);

                ctx->m_unLastStatus = now;
            }
        }
    }

    if (size < 0 || ctx->m_bDisconnected) {
        /* Read error, device is disconnected */
        HIDAPI_JoystickDisconnected(device, device->joysticks[0]);
    }
    return size >= 0;
}

static void HIDAPI_DriverWii_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
{
    SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;

    SDL_DelHintCallback(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS,
                        SDL_GameControllerButtonReportingHintChanged, ctx);

    SDL_DelHintCallback(SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED,
                        SDL_PlayerLEDHintChanged, ctx);

    ctx->joystick = NULL;
}

static void HIDAPI_DriverWii_FreeDevice(SDL_HIDAPI_Device *device)
{
}

SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverWii = {
    SDL_HINT_JOYSTICK_HIDAPI_WII,
    SDL_TRUE,
    HIDAPI_DriverWii_RegisterHints,
    HIDAPI_DriverWii_UnregisterHints,
    HIDAPI_DriverWii_IsEnabled,
    HIDAPI_DriverWii_IsSupportedDevice,
    HIDAPI_DriverWii_InitDevice,
    HIDAPI_DriverWii_GetDevicePlayerIndex,
    HIDAPI_DriverWii_SetDevicePlayerIndex,
    HIDAPI_DriverWii_UpdateDevice,
    HIDAPI_DriverWii_OpenJoystick,
    HIDAPI_DriverWii_RumbleJoystick,
    HIDAPI_DriverWii_RumbleJoystickTriggers,
    HIDAPI_DriverWii_GetJoystickCapabilities,
    HIDAPI_DriverWii_SetJoystickLED,
    HIDAPI_DriverWii_SendJoystickEffect,
    HIDAPI_DriverWii_SetJoystickSensorsEnabled,
    HIDAPI_DriverWii_CloseJoystick,
    HIDAPI_DriverWii_FreeDevice,
};

#endif /* SDL_JOYSTICK_HIDAPI_WII */

#endif /* SDL_JOYSTICK_HIDAPI */

/* vi: set ts=4 sw=4 expandtab: */
