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

// General keyboard handling code for SDL

#include "SDL_events_c.h"
#include "SDL_keymap_c.h"
#include "../video/SDL_sysvideo.h"

#if 0
#define DEBUG_KEYBOARD
#endif

// Global keyboard information

#define KEYBOARD_HARDWARE        0x01
#define KEYBOARD_VIRTUAL         0x02
#define KEYBOARD_AUTORELEASE     0x04
#define KEYBOARD_IGNOREMODIFIERS 0x08

#define KEYBOARD_SOURCE_MASK (KEYBOARD_HARDWARE | KEYBOARD_AUTORELEASE)

#define KEYCODE_OPTION_HIDE_NUMPAD      0x01
#define KEYCODE_OPTION_FRENCH_NUMBERS   0x02
#define KEYCODE_OPTION_LATIN_LETTERS    0x04
#define DEFAULT_KEYCODE_OPTIONS         (KEYCODE_OPTION_FRENCH_NUMBERS | KEYCODE_OPTION_LATIN_LETTERS)

typedef struct SDL_Keyboard
{
    // Data common to all keyboards
    SDL_Window *focus;
    SDL_Keymod modstate;
    Uint8 keysource[SDL_SCANCODE_COUNT];
    bool keystate[SDL_SCANCODE_COUNT];
    SDL_Keymap *keymap;
    Uint32 keycode_options;
    bool autorelease_pending;
    Uint64 hardware_timestamp;
} SDL_Keyboard;

static SDL_Keyboard SDL_keyboard;
static int SDL_keyboard_count;
static SDL_KeyboardID *SDL_keyboards;
static SDL_HashTable *SDL_keyboard_names;
static bool SDL_keyboard_quitting;

static void SDLCALL SDL_KeycodeOptionsChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
{
    SDL_Keyboard *keyboard = (SDL_Keyboard *)userdata;

    if (hint && *hint) {
        keyboard->keycode_options = 0;
        if (!SDL_strstr(hint, "none")) {
            if (SDL_strstr(hint, "hide_numpad")) {
                keyboard->keycode_options |= KEYCODE_OPTION_HIDE_NUMPAD;
            }
            if (SDL_strstr(hint, "french_numbers")) {
                keyboard->keycode_options |= KEYCODE_OPTION_FRENCH_NUMBERS;
            }
            if (SDL_strstr(hint, "latin_letters")) {
                keyboard->keycode_options |= KEYCODE_OPTION_LATIN_LETTERS;
            }
        }
    } else {
        keyboard->keycode_options = DEFAULT_KEYCODE_OPTIONS;
    }
}

// Public functions
bool SDL_InitKeyboard(void)
{
    SDL_AddHintCallback(SDL_HINT_KEYCODE_OPTIONS,
                        SDL_KeycodeOptionsChanged, &SDL_keyboard);

    SDL_keyboard_names = SDL_CreateHashTable(0, true, SDL_HashID, SDL_KeyMatchID, SDL_DestroyHashValue, NULL);

    return true;
}

bool SDL_IsKeyboard(Uint16 vendor, Uint16 product, int num_keys)
{
    const int REAL_KEYBOARD_KEY_COUNT = 50;
    if (num_keys > 0 && num_keys < REAL_KEYBOARD_KEY_COUNT) {
        return false;
    }

    // Eventually we'll have a blacklist of devices that enumerate as keyboards but aren't really
    return true;
}

static int SDL_GetKeyboardIndex(SDL_KeyboardID keyboardID)
{
    for (int i = 0; i < SDL_keyboard_count; ++i) {
        if (keyboardID == SDL_keyboards[i]) {
            return i;
        }
    }
    return -1;
}

void SDL_AddKeyboard(SDL_KeyboardID keyboardID, const char *name)
{
    int keyboard_index = SDL_GetKeyboardIndex(keyboardID);
    if (keyboard_index >= 0) {
        // We already know about this keyboard
        return;
    }

    SDL_assert(keyboardID != 0);

    SDL_KeyboardID *keyboards = (SDL_KeyboardID *)SDL_realloc(SDL_keyboards, (SDL_keyboard_count + 1) * sizeof(*keyboards));
    if (!keyboards) {
        return;
    }
    keyboards[SDL_keyboard_count] = keyboardID;
    SDL_keyboards = keyboards;
    ++SDL_keyboard_count;

    if (!name) {
        name = "Keyboard";
    }
    SDL_InsertIntoHashTable(SDL_keyboard_names, (const void *)(uintptr_t)keyboardID, SDL_strdup(name), true);

    SDL_Event event;
    SDL_zero(event);
    event.type = SDL_EVENT_KEYBOARD_ADDED;
    event.kdevice.which = keyboardID;
    SDL_PushEvent(&event);
}

void SDL_RemoveKeyboard(SDL_KeyboardID keyboardID)
{
    int keyboard_index = SDL_GetKeyboardIndex(keyboardID);
    if (keyboard_index < 0) {
        // We don't know about this keyboard
        return;
    }

    if (keyboard_index != SDL_keyboard_count - 1) {
        SDL_memmove(&SDL_keyboards[keyboard_index], &SDL_keyboards[keyboard_index + 1], (SDL_keyboard_count - keyboard_index - 1) * sizeof(SDL_keyboards[keyboard_index]));
    }
    --SDL_keyboard_count;

    if (!SDL_keyboard_quitting) {
        SDL_Event event;
        SDL_zero(event);
        event.type = SDL_EVENT_KEYBOARD_REMOVED;
        event.kdevice.which = keyboardID;
        SDL_PushEvent(&event);
    }
}

bool SDL_HasKeyboard(void)
{
    return (SDL_keyboard_count > 0);
}

SDL_KeyboardID *SDL_GetKeyboards(int *count)
{
    int i;
    SDL_KeyboardID *keyboards;

    keyboards = (SDL_KeyboardID *)SDL_malloc((SDL_keyboard_count + 1) * sizeof(*keyboards));
    if (keyboards) {
        if (count) {
            *count = SDL_keyboard_count;
        }

        for (i = 0; i < SDL_keyboard_count; ++i) {
            keyboards[i] = SDL_keyboards[i];
        }
        keyboards[i] = 0;
    } else {
        if (count) {
            *count = 0;
        }
    }

    return keyboards;
}

const char *SDL_GetKeyboardNameForID(SDL_KeyboardID instance_id)
{
    const char *name = NULL;

    switch (instance_id) {
    case SDL_GLOBAL_KEYBOARD_ID:
        name = "Keyboard";
        break;
    default:
        if (!SDL_FindInHashTable(SDL_keyboard_names, (const void *)(uintptr_t)instance_id, (const void **)&name)) {
            SDL_SetError("Keyboard %" SDL_PRIu32 " not found", instance_id);
            return NULL;
        }
        if (!name) {
            // SDL_strdup() failed during insert
            SDL_OutOfMemory();
            return NULL;
        }
        break;
    }
    return name;
}

void SDL_ResetKeyboard(void)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;
    int scancode;

#ifdef DEBUG_KEYBOARD
    SDL_Log("Resetting keyboard");
#endif
    for (scancode = SDL_SCANCODE_UNKNOWN; scancode < SDL_SCANCODE_COUNT; ++scancode) {
        if (keyboard->keystate[scancode]) {
            SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, 0, (SDL_Scancode)scancode, false);
        }
    }
}

SDL_Keymap *SDL_GetCurrentKeymap(bool ignore_options)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;
    SDL_Keymap *keymap = SDL_keyboard.keymap;

    if (!ignore_options) {
        if (keymap && keymap->thai_keyboard) {
            // Thai keyboards are QWERTY plus Thai characters, use the default QWERTY keymap
            return NULL;
        }

        if ((keyboard->keycode_options & KEYCODE_OPTION_LATIN_LETTERS) &&
            keymap && !keymap->latin_letters) {
            // We'll use the default QWERTY keymap
            return NULL;
        }
    }

    return keyboard->keymap;
}

void SDL_SetKeymap(SDL_Keymap *keymap, bool send_event)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    if (keyboard->keymap && keyboard->keymap->auto_release) {
        SDL_DestroyKeymap(keyboard->keymap);
    }

    keyboard->keymap = keymap;

    if (keymap && !keymap->layout_determined) {
        keymap->layout_determined = true;

        // Detect French number row (all symbols)
        keymap->french_numbers = true;
        for (int i = SDL_SCANCODE_1; i <= SDL_SCANCODE_0; ++i) {
            if (SDL_isdigit(SDL_GetKeymapKeycode(keymap, (SDL_Scancode)i, SDL_KMOD_NONE)) ||
                !SDL_isdigit(SDL_GetKeymapKeycode(keymap, (SDL_Scancode)i, SDL_KMOD_SHIFT))) {
                keymap->french_numbers = false;
                break;
            }
        }

        // Detect non-Latin keymap
        keymap->thai_keyboard = false;
        keymap->latin_letters = false;
        for (int i = SDL_SCANCODE_A; i <= SDL_SCANCODE_D; ++i) {
            SDL_Keycode key = SDL_GetKeymapKeycode(keymap, (SDL_Scancode)i, SDL_KMOD_NONE);
            if (key <= 0xFF) {
                keymap->latin_letters = true;
                break;
            }

            if (key >= 0x0E00 && key <= 0x0E7F) {
                keymap->thai_keyboard = true;
                break;
            }
        }
    }

    if (send_event) {
        SDL_SendKeymapChangedEvent();
    }
}

static SDL_Scancode GetNextReservedScancode(void)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    if (!keyboard->keymap) {
        keyboard->keymap = SDL_CreateKeymap(true);
    }

    return SDL_GetKeymapNextReservedScancode(keyboard->keymap);
}

static void SetKeymapEntry(SDL_Scancode scancode, SDL_Keymod modstate, SDL_Keycode keycode)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    if (!keyboard->keymap) {
        keyboard->keymap = SDL_CreateKeymap(true);
    }

    SDL_SetKeymapEntry(keyboard->keymap, scancode, modstate, keycode);
}

SDL_Window *SDL_GetKeyboardFocus(void)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    return keyboard->focus;
}

bool SDL_SetKeyboardFocus(SDL_Window *window)
{
#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID)
    SDL_VideoDevice *video = SDL_GetVideoDevice();
#endif
    SDL_Keyboard *keyboard = &SDL_keyboard;
    SDL_Mouse *mouse = SDL_GetMouse();

    if (window) {
        if (!SDL_ObjectValid(window, SDL_OBJECT_TYPE_WINDOW) || window->is_destroying) {
            return SDL_SetError("Invalid window");
        }
    }

    if (keyboard->focus && !window) {
        // We won't get anymore keyboard messages, so reset keyboard state
        SDL_ResetKeyboard();
    }

    // See if the current window has lost focus
    if (keyboard->focus && keyboard->focus != window) {
        SDL_SendWindowEvent(keyboard->focus, SDL_EVENT_WINDOW_FOCUS_LOST, 0, 0);

#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID)
        // Ensures IME compositions are committed
        if (SDL_TextInputActive(keyboard->focus)) {
            if (video && video->StopTextInput) {
                video->StopTextInput(video, keyboard->focus);
            }
        }
#endif // !SDL_PLATFORM_IOS && !SDL_PLATFORM_ANDROID
    }

    if (keyboard->focus && !window) {
        // Also leave mouse relative mode
        if (mouse->relative_mode) {
            SDL_SetRelativeMouseMode(false);

            SDL_Window *focus = keyboard->focus;
            if ((focus->flags & SDL_WINDOW_MINIMIZED) != 0) {
                // We can't warp the mouse within minimized windows, so manually restore the position
                float x = focus->x + mouse->x;
                float y = focus->y + mouse->y;
                SDL_WarpMouseGlobal(x, y);
            }
        }
    }

    keyboard->focus = window;

    if (keyboard->focus) {
        SDL_SendWindowEvent(keyboard->focus, SDL_EVENT_WINDOW_FOCUS_GAINED, 0, 0);

#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID)
        if (SDL_TextInputActive(keyboard->focus)) {
            if (video && video->StartTextInput) {
                video->StartTextInput(video, keyboard->focus, keyboard->focus->text_input_props);
            }
        }
#endif // !SDL_PLATFORM_IOS && !SDL_PLATFORM_ANDROID
    }

    SDL_UpdateRelativeMouseMode();

    return true;
}

static SDL_Keycode SDL_ConvertNumpadKeycode(SDL_Keycode keycode, bool numlock)
{
    switch (keycode) {
    case SDLK_KP_DIVIDE:
        return SDLK_SLASH;
    case SDLK_KP_MULTIPLY:
        return SDLK_ASTERISK;
    case SDLK_KP_MINUS:
        return SDLK_MINUS;
    case SDLK_KP_PLUS:
        return SDLK_PLUS;
    case SDLK_KP_ENTER:
        return SDLK_RETURN;
    case SDLK_KP_1:
        return numlock ? SDLK_1 : SDLK_END;
    case SDLK_KP_2:
        return numlock ? SDLK_2 : SDLK_DOWN;
    case SDLK_KP_3:
        return numlock ? SDLK_3 : SDLK_PAGEDOWN;
    case SDLK_KP_4:
        return numlock ? SDLK_4 : SDLK_LEFT;
    case SDLK_KP_5:
        return numlock ? SDLK_5 : SDLK_CLEAR;
    case SDLK_KP_6:
        return numlock ? SDLK_6 : SDLK_RIGHT;
    case SDLK_KP_7:
        return numlock ? SDLK_7 : SDLK_HOME;
    case SDLK_KP_8:
        return numlock ? SDLK_8 : SDLK_UP;
    case SDLK_KP_9:
        return numlock ? SDLK_9 : SDLK_PAGEUP;
    case SDLK_KP_0:
        return numlock ? SDLK_0 : SDLK_INSERT;
    case SDLK_KP_PERIOD:
        return numlock ? SDLK_PERIOD : SDLK_DELETE;
    case SDLK_KP_EQUALS:
        return SDLK_EQUALS;
    case SDLK_KP_COMMA:
        return SDLK_COMMA;
    case SDLK_KP_EQUALSAS400:
        return SDLK_EQUALS;
    case SDLK_KP_LEFTPAREN:
        return SDLK_LEFTPAREN;
    case SDLK_KP_RIGHTPAREN:
        return SDLK_RIGHTPAREN;
    case SDLK_KP_LEFTBRACE:
        return SDLK_LEFTBRACE;
    case SDLK_KP_RIGHTBRACE:
        return SDLK_RIGHTBRACE;
    case SDLK_KP_TAB:
        return SDLK_TAB;
    case SDLK_KP_BACKSPACE:
        return SDLK_BACKSPACE;
    case SDLK_KP_A:
        return SDLK_A;
    case SDLK_KP_B:
        return SDLK_B;
    case SDLK_KP_C:
        return SDLK_C;
    case SDLK_KP_D:
        return SDLK_D;
    case SDLK_KP_E:
        return SDLK_E;
    case SDLK_KP_F:
        return SDLK_F;
    case SDLK_KP_PERCENT:
        return SDLK_PERCENT;
    case SDLK_KP_LESS:
        return SDLK_LESS;
    case SDLK_KP_GREATER:
        return SDLK_GREATER;
    case SDLK_KP_AMPERSAND:
        return SDLK_AMPERSAND;
    case SDLK_KP_COLON:
        return SDLK_COLON;
    case SDLK_KP_HASH:
        return SDLK_HASH;
    case SDLK_KP_SPACE:
        return SDLK_SPACE;
    case SDLK_KP_AT:
        return SDLK_AT;
    case SDLK_KP_EXCLAM:
        return SDLK_EXCLAIM;
    case SDLK_KP_PLUSMINUS:
        return SDLK_PLUSMINUS;
    default:
        return keycode;
    }
}

SDL_Keycode SDL_GetKeyFromScancode(SDL_Scancode scancode, SDL_Keymod modstate, bool key_event)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    if (key_event) {
        SDL_Keymap *keymap = SDL_GetCurrentKeymap(false);
        bool numlock = (modstate & SDL_KMOD_NUM) != 0;
        SDL_Keycode keycode;

        // We won't be applying any modifiers by default
        modstate = SDL_KMOD_NONE;

        if ((keyboard->keycode_options & KEYCODE_OPTION_FRENCH_NUMBERS) &&
            keymap && keymap->french_numbers &&
            (scancode >= SDL_SCANCODE_1 && scancode <= SDL_SCANCODE_0)) {
            // Add the shift state to generate a numeric keycode
            modstate |= SDL_KMOD_SHIFT;
        }

        keycode = SDL_GetKeymapKeycode(keymap, scancode, modstate);

        if (keyboard->keycode_options & KEYCODE_OPTION_HIDE_NUMPAD) {
            keycode = SDL_ConvertNumpadKeycode(keycode, numlock);
        }
        return keycode;
    }

    return SDL_GetKeymapKeycode(keyboard->keymap, scancode, modstate);
}

SDL_Scancode SDL_GetScancodeFromKey(SDL_Keycode key, SDL_Keymod *modstate)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    return SDL_GetKeymapScancode(keyboard->keymap, key, modstate);
}

static bool SDL_SendKeyboardKeyInternal(Uint64 timestamp, Uint32 flags, SDL_KeyboardID keyboardID, int rawcode, SDL_Scancode scancode, bool down)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;
    bool posted = false;
    SDL_Keycode keycode = SDLK_UNKNOWN;
    Uint32 type;
    bool repeat = false;
    const Uint8 source = flags & KEYBOARD_SOURCE_MASK;

#ifdef DEBUG_KEYBOARD
    SDL_Log("The '%s' key has been %s", SDL_GetScancodeName(scancode), down ? "pressed" : "released");
#endif

    // Figure out what type of event this is
    if (down) {
        type = SDL_EVENT_KEY_DOWN;
    } else {
        type = SDL_EVENT_KEY_UP;
    }

    if (scancode > SDL_SCANCODE_UNKNOWN && scancode < SDL_SCANCODE_COUNT) {
        // Drop events that don't change state
        if (down) {
            if (keyboard->keystate[scancode]) {
                if (!(keyboard->keysource[scancode] & source)) {
                    keyboard->keysource[scancode] |= source;
                    return false;
                }
                repeat = true;
            }
            keyboard->keysource[scancode] |= source;
        } else {
            if (!keyboard->keystate[scancode]) {
                return false;
            }
            keyboard->keysource[scancode] = 0;
        }

        // Update internal keyboard state
        keyboard->keystate[scancode] = down;

        keycode = SDL_GetKeyFromScancode(scancode, keyboard->modstate, true);

    } else if (rawcode == 0) {
        // Nothing to do!
        return false;
    }

    if (source == KEYBOARD_HARDWARE) {
        keyboard->hardware_timestamp = SDL_GetTicks();
    } else if (source == KEYBOARD_AUTORELEASE) {
        keyboard->autorelease_pending = true;
    }

    // Update modifiers state if applicable
    if (!(flags & KEYBOARD_IGNOREMODIFIERS) && !repeat) {
        SDL_Keymod modifier;

        switch (keycode) {
        case SDLK_LCTRL:
            modifier = SDL_KMOD_LCTRL;
            break;
        case SDLK_RCTRL:
            modifier = SDL_KMOD_RCTRL;
            break;
        case SDLK_LSHIFT:
            modifier = SDL_KMOD_LSHIFT;
            break;
        case SDLK_RSHIFT:
            modifier = SDL_KMOD_RSHIFT;
            break;
        case SDLK_LALT:
            modifier = SDL_KMOD_LALT;
            break;
        case SDLK_RALT:
            modifier = SDL_KMOD_RALT;
            break;
        case SDLK_LGUI:
            modifier = SDL_KMOD_LGUI;
            break;
        case SDLK_RGUI:
            modifier = SDL_KMOD_RGUI;
            break;
        case SDLK_MODE:
            modifier = SDL_KMOD_MODE;
            break;
        default:
            modifier = SDL_KMOD_NONE;
            break;
        }
        if (SDL_EVENT_KEY_DOWN == type) {
            switch (keycode) {
            case SDLK_NUMLOCKCLEAR:
                keyboard->modstate ^= SDL_KMOD_NUM;
                break;
            case SDLK_CAPSLOCK:
                keyboard->modstate ^= SDL_KMOD_CAPS;
                break;
            case SDLK_SCROLLLOCK:
                keyboard->modstate ^= SDL_KMOD_SCROLL;
                break;
            default:
                keyboard->modstate |= modifier;
                break;
            }
        } else {
            keyboard->modstate &= ~modifier;
        }
    }

    // Post the event, if desired
    if (SDL_EventEnabled(type)) {
        SDL_Event event;
        event.type = type;
        event.common.timestamp = timestamp;
        event.key.scancode = scancode;
        event.key.key = keycode;
        event.key.mod = keyboard->modstate;
        event.key.raw = (Uint16)rawcode;
        event.key.down = down;
        event.key.repeat = repeat;
        event.key.windowID = keyboard->focus ? keyboard->focus->id : 0;
        event.key.which = keyboardID;
        posted = SDL_PushEvent(&event);
    }

    /* If the keyboard is grabbed and the grabbed window is in full-screen,
       minimize the window when we receive Alt+Tab, unless the application
       has explicitly opted out of this behavior. */
    if (keycode == SDLK_TAB && down &&
        (keyboard->modstate & SDL_KMOD_ALT) &&
        keyboard->focus &&
        (keyboard->focus->flags & SDL_WINDOW_KEYBOARD_GRABBED) &&
        (keyboard->focus->flags & SDL_WINDOW_FULLSCREEN) &&
        SDL_GetHintBoolean(SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED, true)) {
        /* We will temporarily forfeit our grab by minimizing our window,
           allowing the user to escape the application */
        SDL_MinimizeWindow(keyboard->focus);
    }

    return posted;
}

void SDL_SendKeyboardUnicodeKey(Uint64 timestamp, Uint32 ch)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;
    SDL_Keymod modstate = SDL_KMOD_NONE;
    SDL_Scancode scancode;

    if (ch == '\n') {
        ch = SDLK_RETURN;
    }
    scancode = SDL_GetKeymapScancode(keyboard->keymap, ch, &modstate);

    // Make sure we have this keycode in our keymap
    if (scancode == SDL_SCANCODE_UNKNOWN && ch < SDLK_SCANCODE_MASK) {
        scancode = GetNextReservedScancode();
        SetKeymapEntry(scancode, modstate, ch);
    }

    if (modstate & SDL_KMOD_SHIFT) {
        // If the character uses shift, press shift down
        SDL_SendKeyboardKeyInternal(timestamp, KEYBOARD_VIRTUAL, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_LSHIFT, true);
    }

    // Send a keydown and keyup for the character
    SDL_SendKeyboardKeyInternal(timestamp, KEYBOARD_VIRTUAL, SDL_GLOBAL_KEYBOARD_ID, 0, scancode, true);
    SDL_SendKeyboardKeyInternal(timestamp, KEYBOARD_VIRTUAL, SDL_GLOBAL_KEYBOARD_ID, 0, scancode, false);

    if (modstate & SDL_KMOD_SHIFT) {
        // If the character uses shift, release shift
        SDL_SendKeyboardKeyInternal(timestamp, KEYBOARD_VIRTUAL, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_LSHIFT, false);
    }
}

bool SDL_SendKeyboardKey(Uint64 timestamp, SDL_KeyboardID keyboardID, int rawcode, SDL_Scancode scancode, bool down)
{
    return SDL_SendKeyboardKeyInternal(timestamp, KEYBOARD_HARDWARE, keyboardID, rawcode, scancode, down);
}

bool SDL_SendKeyboardKeyAndKeycode(Uint64 timestamp, SDL_KeyboardID keyboardID, int rawcode, SDL_Scancode scancode, SDL_Keycode keycode, bool down)
{
    if (down) {
        // Make sure we have this keycode in our keymap
        SetKeymapEntry(scancode, SDL_GetModState(), keycode);
    }

    return SDL_SendKeyboardKeyInternal(timestamp, KEYBOARD_HARDWARE, keyboardID, rawcode, scancode, down);
}

bool SDL_SendKeyboardKeyIgnoreModifiers(Uint64 timestamp, SDL_KeyboardID keyboardID, int rawcode, SDL_Scancode scancode, bool down)
{
    return SDL_SendKeyboardKeyInternal(timestamp, KEYBOARD_HARDWARE | KEYBOARD_IGNOREMODIFIERS, keyboardID, rawcode, scancode, down);
}

bool SDL_SendKeyboardKeyAutoRelease(Uint64 timestamp, SDL_Scancode scancode)
{
    return SDL_SendKeyboardKeyInternal(timestamp, KEYBOARD_AUTORELEASE, SDL_GLOBAL_KEYBOARD_ID, 0, scancode, true);
}

void SDL_ReleaseAutoReleaseKeys(void)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;
    int scancode;

    if (keyboard->autorelease_pending) {
        for (scancode = SDL_SCANCODE_UNKNOWN; scancode < SDL_SCANCODE_COUNT; ++scancode) {
            if (keyboard->keysource[scancode] == KEYBOARD_AUTORELEASE) {
                SDL_SendKeyboardKeyInternal(0, KEYBOARD_AUTORELEASE, SDL_GLOBAL_KEYBOARD_ID, 0, (SDL_Scancode)scancode, false);
            }
        }
        keyboard->autorelease_pending = false;
    }

    if (keyboard->hardware_timestamp) {
        // Keep hardware keyboard "active" for 250 ms
        if (SDL_GetTicks() >= keyboard->hardware_timestamp + 250) {
            keyboard->hardware_timestamp = 0;
        }
    }
}

bool SDL_HardwareKeyboardKeyPressed(void)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;
    int scancode;

    for (scancode = SDL_SCANCODE_UNKNOWN; scancode < SDL_SCANCODE_COUNT; ++scancode) {
        if (keyboard->keysource[scancode] & KEYBOARD_HARDWARE) {
            return true;
        }
    }

    return keyboard->hardware_timestamp ? true : false;
}

void SDL_SendKeyboardText(const char *text)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    if (!keyboard->focus || !SDL_TextInputActive(keyboard->focus)) {
        return;
    }

    if (!text || !*text) {
        return;
    }

    // Don't post text events for unprintable characters
    if (SDL_iscntrl((unsigned char)*text)) {
        return;
    }

    // Post the event, if desired
    if (SDL_EventEnabled(SDL_EVENT_TEXT_INPUT)) {
        SDL_Event event;
        event.type = SDL_EVENT_TEXT_INPUT;
        event.common.timestamp = 0;
        event.text.windowID = keyboard->focus ? keyboard->focus->id : 0;
        event.text.text = SDL_CreateTemporaryString(text);
        if (!event.text.text) {
            return;
        }
        SDL_PushEvent(&event);
    }
}

void SDL_SendEditingText(const char *text, int start, int length)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    if (!keyboard->focus || !SDL_TextInputActive(keyboard->focus)) {
        return;
    }

    if (!text) {
        return;
    }

    // Post the event, if desired
    if (SDL_EventEnabled(SDL_EVENT_TEXT_EDITING)) {
        SDL_Event event;

        event.type = SDL_EVENT_TEXT_EDITING;
        event.common.timestamp = 0;
        event.edit.windowID = keyboard->focus ? keyboard->focus->id : 0;
        event.edit.start = start;
        event.edit.length = length;
        event.edit.text = SDL_CreateTemporaryString(text);
        if (!event.edit.text) {
            return;
        }
        SDL_PushEvent(&event);
    }
}

static const char * const *CreateCandidatesForEvent(char **candidates, int num_candidates)
{
    const char **event_candidates;
    int i;
    char *ptr;
    size_t total_length = (num_candidates + 1) * sizeof(*event_candidates);

    for (i = 0; i < num_candidates; ++i) {
        size_t length = SDL_strlen(candidates[i]) + 1;

        total_length += length;
    }

    event_candidates = (const char **)SDL_AllocateTemporaryMemory(total_length);
    if (!event_candidates) {
        return NULL;
    }
    ptr = (char *)(event_candidates + (num_candidates + 1));

    for (i = 0; i < num_candidates; ++i) {
        size_t length = SDL_strlen(candidates[i]) + 1;

        event_candidates[i] = ptr;
        SDL_memcpy(ptr, candidates[i], length);
        ptr += length;
    }
    event_candidates[i] = NULL;

    return event_candidates;
}

void SDL_SendEditingTextCandidates(char **candidates, int num_candidates, int selected_candidate, bool horizontal)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    if (!keyboard->focus || !SDL_TextInputActive(keyboard->focus)) {
        return;
    }

    // Post the event, if desired
    if (SDL_EventEnabled(SDL_EVENT_TEXT_EDITING_CANDIDATES)) {
        SDL_Event event;

        event.type = SDL_EVENT_TEXT_EDITING_CANDIDATES;
        event.common.timestamp = 0;
        event.edit.windowID = keyboard->focus ? keyboard->focus->id : 0;
        if (num_candidates > 0) {
            const char * const *event_candidates = CreateCandidatesForEvent(candidates, num_candidates);
            if (!event_candidates) {
                return;
            }
            event.edit_candidates.candidates = event_candidates;
            event.edit_candidates.num_candidates = num_candidates;
            event.edit_candidates.selected_candidate = selected_candidate;
            event.edit_candidates.horizontal = horizontal;
        } else {
            event.edit_candidates.candidates = NULL;
            event.edit_candidates.num_candidates = 0;
            event.edit_candidates.selected_candidate = -1;
            event.edit_candidates.horizontal = false;
        }
        SDL_PushEvent(&event);
    }
}

void SDL_QuitKeyboard(void)
{
    SDL_keyboard_quitting = true;

    for (int i = SDL_keyboard_count; i--;) {
        SDL_RemoveKeyboard(SDL_keyboards[i]);
    }
    SDL_free(SDL_keyboards);
    SDL_keyboards = NULL;

    SDL_DestroyHashTable(SDL_keyboard_names);
    SDL_keyboard_names = NULL;

    if (SDL_keyboard.keymap && SDL_keyboard.keymap->auto_release) {
        SDL_DestroyKeymap(SDL_keyboard.keymap);
        SDL_keyboard.keymap = NULL;
    }

    SDL_RemoveHintCallback(SDL_HINT_KEYCODE_OPTIONS,
                        SDL_KeycodeOptionsChanged, &SDL_keyboard);

    SDL_keyboard_quitting = false;
}

const bool *SDL_GetKeyboardState(int *numkeys)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    if (numkeys) {
        *numkeys = SDL_SCANCODE_COUNT;
    }
    return keyboard->keystate;
}

SDL_Keymod SDL_GetModState(void)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    return keyboard->modstate;
}

void SDL_SetModState(SDL_Keymod modstate)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;

    keyboard->modstate = modstate;
}

// Note that SDL_ToggleModState() is not a public API. SDL_SetModState() is.
void SDL_ToggleModState(SDL_Keymod modstate, bool toggle)
{
    SDL_Keyboard *keyboard = &SDL_keyboard;
    if (toggle) {
        keyboard->modstate |= modstate;
    } else {
        keyboard->modstate &= ~modstate;
    }
}

