// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)

#include "sdl_input_source.h"
#include "common/assert.h"
#include "common/bitutils.h"
#include "common/log.h"
#include "common/string_util.h"
#include "core/host.h"
#include "core/host_settings.h"
#include "input_manager.h"
#include <cmath>
#ifdef __APPLE__
#include <dispatch/dispatch.h>
#endif
Log_SetChannel(SDLInputSource);

static constexpr const char* s_sdl_axis_names[] = {
  "LeftX",        // SDL_CONTROLLER_AXIS_LEFTX
  "LeftY",        // SDL_CONTROLLER_AXIS_LEFTY
  "RightX",       // SDL_CONTROLLER_AXIS_RIGHTX
  "RightY",       // SDL_CONTROLLER_AXIS_RIGHTY
  "LeftTrigger",  // SDL_CONTROLLER_AXIS_TRIGGERLEFT
  "RightTrigger", // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
};
static constexpr const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = {
  {GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight},   // SDL_CONTROLLER_AXIS_LEFTX
  {GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown},      // SDL_CONTROLLER_AXIS_LEFTY
  {GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // SDL_CONTROLLER_AXIS_RIGHTX
  {GenericInputBinding::RightStickUp, GenericInputBinding::RightStickDown},    // SDL_CONTROLLER_AXIS_RIGHTY
  {GenericInputBinding::Unknown, GenericInputBinding::L2},                     // SDL_CONTROLLER_AXIS_TRIGGERLEFT
  {GenericInputBinding::Unknown, GenericInputBinding::R2},                     // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
};

static constexpr const char* s_sdl_button_names[] = {
  "A",             // SDL_CONTROLLER_BUTTON_A
  "B",             // SDL_CONTROLLER_BUTTON_B
  "X",             // SDL_CONTROLLER_BUTTON_X
  "Y",             // SDL_CONTROLLER_BUTTON_Y
  "Back",          // SDL_CONTROLLER_BUTTON_BACK
  "Guide",         // SDL_CONTROLLER_BUTTON_GUIDE
  "Start",         // SDL_CONTROLLER_BUTTON_START
  "LeftStick",     // SDL_CONTROLLER_BUTTON_LEFTSTICK
  "RightStick",    // SDL_CONTROLLER_BUTTON_RIGHTSTICK
  "LeftShoulder",  // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
  "RightShoulder", // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
  "DPadUp",        // SDL_CONTROLLER_BUTTON_DPAD_UP
  "DPadDown",      // SDL_CONTROLLER_BUTTON_DPAD_DOWN
  "DPadLeft",      // SDL_CONTROLLER_BUTTON_DPAD_LEFT
  "DPadRight",     // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
  "Misc1",         // SDL_CONTROLLER_BUTTON_MISC1
  "Paddle1",       // SDL_CONTROLLER_BUTTON_PADDLE1
  "Paddle2",       // SDL_CONTROLLER_BUTTON_PADDLE2
  "Paddle3",       // SDL_CONTROLLER_BUTTON_PADDLE3
  "Paddle4",       // SDL_CONTROLLER_BUTTON_PADDLE4
  "Touchpad",      // SDL_CONTROLLER_BUTTON_TOUCHPAD
};
static constexpr const GenericInputBinding s_sdl_generic_binding_button_mapping[] = {
  GenericInputBinding::Cross,     // SDL_CONTROLLER_BUTTON_A
  GenericInputBinding::Circle,    // SDL_CONTROLLER_BUTTON_B
  GenericInputBinding::Square,    // SDL_CONTROLLER_BUTTON_X
  GenericInputBinding::Triangle,  // SDL_CONTROLLER_BUTTON_Y
  GenericInputBinding::Select,    // SDL_CONTROLLER_BUTTON_BACK
  GenericInputBinding::System,    // SDL_CONTROLLER_BUTTON_GUIDE
  GenericInputBinding::Start,     // SDL_CONTROLLER_BUTTON_START
  GenericInputBinding::L3,        // SDL_CONTROLLER_BUTTON_LEFTSTICK
  GenericInputBinding::R3,        // SDL_CONTROLLER_BUTTON_RIGHTSTICK
  GenericInputBinding::L1,        // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
  GenericInputBinding::R1,        // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
  GenericInputBinding::DPadUp,    // SDL_CONTROLLER_BUTTON_DPAD_UP
  GenericInputBinding::DPadDown,  // SDL_CONTROLLER_BUTTON_DPAD_DOWN
  GenericInputBinding::DPadLeft,  // SDL_CONTROLLER_BUTTON_DPAD_LEFT
  GenericInputBinding::DPadRight, // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
  GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_MISC1
  GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_PADDLE1
  GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_PADDLE2
  GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_PADDLE3
  GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_PADDLE4
  GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_TOUCHPAD
};

static constexpr const char* s_sdl_hat_direction_names[] = {
  // clang-format off
	"North",
	"East",
	"South",
	"West",
  // clang-format on
};

SDLInputSource::SDLInputSource() = default;

SDLInputSource::~SDLInputSource()
{
  Assert(m_controllers.empty());
}

bool SDLInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
{
  std::optional<std::vector<u8>> controller_db_data = Host::ReadResourceFile("gamecontrollerdb.txt");
  if (controller_db_data.has_value())
  {
    SDL_RWops* ops = SDL_RWFromConstMem(controller_db_data->data(), static_cast<int>(controller_db_data->size()));
    if (SDL_GameControllerAddMappingsFromRW(ops, true) < 0)
      Log_ErrorPrintf("SDL_GameControllerAddMappingsFromRW() failed: %s", SDL_GetError());
  }
  else
  {
    Log_ErrorPrintf("Controller database resource is missing.");
  }

  LoadSettings(si);
  settings_lock.unlock();
  SetHints();
  bool result = InitializeSubsystem();
  settings_lock.lock();
  return result;
}

void SDLInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
{
  const bool old_controller_enhanced_mode = m_controller_enhanced_mode;

  LoadSettings(si);

  if (m_controller_enhanced_mode != old_controller_enhanced_mode)
  {
    settings_lock.unlock();
    ShutdownSubsystem();
    SetHints();
    InitializeSubsystem();
    settings_lock.lock();
  }
}

bool SDLInputSource::ReloadDevices()
{
  // We'll get a GC added/removed event here.
  PollEvents();
  return false;
}

void SDLInputSource::Shutdown()
{
  ShutdownSubsystem();
}

void SDLInputSource::LoadSettings(SettingsInterface& si)
{
  m_controller_enhanced_mode = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false);
  m_sdl_hints = si.GetKeyValueList("SDLHints");
}

void SDLInputSource::SetHints()
{
  SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
  SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
  // Enable Wii U Pro Controller support
  // New as of SDL 2.26, so use string
  SDL_SetHint("SDL_JOYSTICK_HIDAPI_WII", "1");
#ifndef _WIN32
  // Gets us pressure sensitive button support on Linux
  // Apparently doesn't work on Windows, so leave it off there
  // New as of SDL 2.26, so use string
  SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS3", "1");
#endif

  for (const std::pair<std::string, std::string>& hint : m_sdl_hints)
    SDL_SetHint(hint.first.c_str(), hint.second.c_str());
}

bool SDLInputSource::InitializeSubsystem()
{
  if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0)
  {
    Log_ErrorPrint("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed");
    return false;
  }

  // we should open the controllers as the connected events come in, so no need to do any more here
  m_sdl_subsystem_initialized = true;
  return true;
}

void SDLInputSource::ShutdownSubsystem()
{
  while (!m_controllers.empty())
    CloseDevice(m_controllers.begin()->joystick_id);

  if (m_sdl_subsystem_initialized)
  {
    SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
    m_sdl_subsystem_initialized = false;
  }
}

void SDLInputSource::PollEvents()
{
  for (;;)
  {
    SDL_Event ev;
    if (SDL_PollEvent(&ev))
      ProcessSDLEvent(&ev);
    else
      break;
  }
}

std::vector<std::pair<std::string, std::string>> SDLInputSource::EnumerateDevices()
{
  std::vector<std::pair<std::string, std::string>> ret;

  for (const ControllerData& cd : m_controllers)
  {
    std::string id(StringUtil::StdStringFromFormat("SDL-%d", cd.player_id));

    const char* name = cd.game_controller ? SDL_GameControllerName(cd.game_controller) : SDL_JoystickName(cd.joystick);
    if (name)
      ret.emplace_back(std::move(id), name);
    else
      ret.emplace_back(std::move(id), "Unknown Device");
  }

  return ret;
}

std::optional<InputBindingKey> SDLInputSource::ParseKeyString(const std::string_view& device,
                                                              const std::string_view& binding)
{
  if (!StringUtil::StartsWith(device, "SDL-") || binding.empty())
    return std::nullopt;

  const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4));
  if (!player_id.has_value() || player_id.value() < 0)
    return std::nullopt;

  InputBindingKey key = {};
  key.source_type = InputSourceType::SDL;
  key.source_index = static_cast<u32>(player_id.value());

  if (StringUtil::EndsWith(binding, "Motor"))
  {
    key.source_subtype = InputSubclass::ControllerMotor;
    if (binding == "LargeMotor")
    {
      key.data = 0;
      return key;
    }
    else if (binding == "SmallMotor")
    {
      key.data = 1;
      return key;
    }
    else
    {
      return std::nullopt;
    }
  }
  else if (StringUtil::EndsWith(binding, "Haptic"))
  {
    key.source_subtype = InputSubclass::ControllerHaptic;
    key.data = 0;
    return key;
  }
  else if (binding[0] == '+' || binding[0] == '-')
  {
    // likely an axis
    const std::string_view axis_name(binding.substr(1));

    if (StringUtil::StartsWith(axis_name, "Axis"))
    {
      std::string_view end;
      if (auto value = StringUtil::FromChars<u32>(axis_name.substr(4), 10, &end))
      {
        key.source_subtype = InputSubclass::ControllerAxis;
        key.data = *value + static_cast<u32>(std::size(s_sdl_axis_names));
        key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
        key.invert = (end == "~");
        return key;
      }
    }
    for (u32 i = 0; i < std::size(s_sdl_axis_names); i++)
    {
      if (axis_name == s_sdl_axis_names[i])
      {
        // found an axis!
        key.source_subtype = InputSubclass::ControllerAxis;
        key.data = i;
        key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
        return key;
      }
    }
  }
  else if (StringUtil::StartsWith(binding, "FullAxis"))
  {
    std::string_view end;
    if (auto value = StringUtil::FromChars<u32>(binding.substr(8), 10, &end))
    {
      key.source_subtype = InputSubclass::ControllerAxis;
      key.data = *value + static_cast<u32>(std::size(s_sdl_axis_names));
      key.modifier = InputModifier::FullAxis;
      key.invert = (end == "~");
      return key;
    }
  }
  else if (StringUtil::StartsWith(binding, "Hat"))
  {
    std::string_view hat_dir;
    if (auto value = StringUtil::FromChars<u32>(binding.substr(3), 10, &hat_dir); value.has_value() && !hat_dir.empty())
    {
      for (u8 dir = 0; dir < static_cast<u8>(std::size(s_sdl_hat_direction_names)); dir++)
      {
        if (hat_dir == s_sdl_hat_direction_names[dir])
        {
          key.source_subtype = InputSubclass::ControllerHat;
          key.data = value.value() * static_cast<u32>(std::size(s_sdl_hat_direction_names)) + dir;
          return key;
        }
      }
    }
  }
  else
  {
    // must be a button
    if (StringUtil::StartsWith(binding, "Button"))
    {
      if (auto value = StringUtil::FromChars<u32>(binding.substr(6)))
      {
        key.source_subtype = InputSubclass::ControllerButton;
        key.data = *value + static_cast<u32>(std::size(s_sdl_button_names));
        return key;
      }
    }
    for (u32 i = 0; i < std::size(s_sdl_button_names); i++)
    {
      if (binding == s_sdl_button_names[i])
      {
        key.source_subtype = InputSubclass::ControllerButton;
        key.data = i;
        return key;
      }
    }
  }

  // unknown axis/button
  return std::nullopt;
}

std::string SDLInputSource::ConvertKeyToString(InputBindingKey key)
{
  std::string ret;

  if (key.source_type == InputSourceType::SDL)
  {
    if (key.source_subtype == InputSubclass::ControllerAxis)
    {
      const char* modifier =
        (key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+"));
      if (key.data < std::size(s_sdl_axis_names))
      {
        ret = StringUtil::StdStringFromFormat("SDL-%u/%s%s", key.source_index, modifier, s_sdl_axis_names[key.data]);
      }
      else
      {
        ret = StringUtil::StdStringFromFormat("SDL-%u/%sAxis%u%s", key.source_index, modifier,
                                              key.data - static_cast<u32>(std::size(s_sdl_axis_names)),
                                              key.invert ? "~" : "");
      }
    }
    else if (key.source_subtype == InputSubclass::ControllerButton)
    {
      if (key.data < std::size(s_sdl_button_names))
      {
        ret = StringUtil::StdStringFromFormat("SDL-%u/%s", key.source_index, s_sdl_button_names[key.data]);
      }
      else
      {
        ret = StringUtil::StdStringFromFormat("SDL-%u/Button%u", key.source_index,
                                              key.data - static_cast<u32>(std::size(s_sdl_button_names)));
      }
    }
    else if (key.source_subtype == InputSubclass::ControllerHat)
    {
      const u32 hat_index = key.data / static_cast<u32>(std::size(s_sdl_hat_direction_names));
      const u32 hat_direction = key.data % static_cast<u32>(std::size(s_sdl_hat_direction_names));
      ret = StringUtil::StdStringFromFormat("SDL-%u/Hat%u%s", key.source_index, hat_index,
                                            s_sdl_hat_direction_names[hat_direction]);
    }
    else if (key.source_subtype == InputSubclass::ControllerMotor)
    {
      ret = StringUtil::StdStringFromFormat("SDL-%u/%sMotor", key.source_index, key.data ? "Large" : "Small");
    }
    else if (key.source_subtype == InputSubclass::ControllerHaptic)
    {
      ret = StringUtil::StdStringFromFormat("SDL-%u/Haptic", key.source_index);
    }
  }

  return ret;
}

bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event)
{
  switch (event->type)
  {
    case SDL_CONTROLLERDEVICEADDED:
    {
      Log_InfoPrintf("(SDLInputSource) Controller %d inserted", event->cdevice.which);
      OpenDevice(event->cdevice.which, true);
      return true;
    }

    case SDL_CONTROLLERDEVICEREMOVED:
    {
      Log_InfoPrintf("(SDLInputSource) Controller %d removed", event->cdevice.which);
      CloseDevice(event->cdevice.which);
      return true;
    }

    case SDL_JOYDEVICEADDED:
    {
      // Let game controller handle.. well.. game controllers.
      if (SDL_IsGameController(event->jdevice.which))
        return false;

      Log_InfoPrintf("(SDLInputSource) Joystick %d inserted", event->jdevice.which);
      OpenDevice(event->cdevice.which, false);
      return true;
    }
    break;

    case SDL_JOYDEVICEREMOVED:
    {
      if (auto it = GetControllerDataForJoystickId(event->cdevice.which);
          it != m_controllers.end() && it->game_controller)
        return false;

      Log_InfoPrintf("(SDLInputSource) Joystick %d removed", event->jdevice.which);
      CloseDevice(event->cdevice.which);
      return true;
    }

    case SDL_CONTROLLERAXISMOTION:
      return HandleControllerAxisEvent(&event->caxis);

    case SDL_CONTROLLERBUTTONDOWN:
    case SDL_CONTROLLERBUTTONUP:
      return HandleControllerButtonEvent(&event->cbutton);

    case SDL_JOYAXISMOTION:
      return HandleJoystickAxisEvent(&event->jaxis);

    case SDL_JOYBUTTONDOWN:
    case SDL_JOYBUTTONUP:
      return HandleJoystickButtonEvent(&event->jbutton);

    case SDL_JOYHATMOTION:
      return HandleJoystickHatEvent(&event->jhat);

    default:
      return false;
  }
}

SDL_Joystick* SDLInputSource::GetJoystickForDevice(const std::string_view& device)
{
  if (!StringUtil::StartsWith(device, "SDL-"))
    return nullptr;

  const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4));
  if (!player_id.has_value() || player_id.value() < 0)
    return nullptr;

  auto it = GetControllerDataForPlayerId(player_id.value());
  if (it == m_controllers.end())
    return nullptr;

  return it->joystick;
}

SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForJoystickId(int id)
{
  return std::find_if(m_controllers.begin(), m_controllers.end(),
                      [id](const ControllerData& cd) { return cd.joystick_id == id; });
}

SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForPlayerId(int id)
{
  return std::find_if(m_controllers.begin(), m_controllers.end(),
                      [id](const ControllerData& cd) { return cd.player_id == id; });
}

int SDLInputSource::GetFreePlayerId() const
{
  for (int player_id = 0;; player_id++)
  {
    size_t i;
    for (i = 0; i < m_controllers.size(); i++)
    {
      if (m_controllers[i].player_id == player_id)
        break;
    }
    if (i == m_controllers.size())
      return player_id;
  }

  return 0;
}

bool SDLInputSource::OpenDevice(int index, bool is_gamecontroller)
{
  SDL_GameController* gcontroller;
  SDL_Joystick* joystick;

  if (is_gamecontroller)
  {
    gcontroller = SDL_GameControllerOpen(index);
    joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
  }
  else
  {
    gcontroller = nullptr;
    joystick = SDL_JoystickOpen(index);
  }

  if (!gcontroller && !joystick)
  {
    Log_ErrorPrintf("(SDLInputSource) Failed to open controller %d", index);
    if (gcontroller)
      SDL_GameControllerClose(gcontroller);

    return false;
  }

  const int joystick_id = SDL_JoystickInstanceID(joystick);
  int player_id = gcontroller ? SDL_GameControllerGetPlayerIndex(gcontroller) : SDL_JoystickGetPlayerIndex(joystick);
  if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
  {
    const int free_player_id = GetFreePlayerId();
    Log_WarningPrintf("(SDLInputSource) Controller %d (joystick %d) returned player ID %d, which is invalid or in "
                      "use. Using ID %d instead.",
                      index, joystick_id, player_id, free_player_id);
    player_id = free_player_id;
  }

  const char* name = gcontroller ? SDL_GameControllerName(gcontroller) : SDL_JoystickName(joystick);
  if (!name)
    name = "Unknown Device";

  Log_VerbosePrintf("(SDLInputSource) Opened %s %d (instance id %d, player id %d): %s",
                    is_gamecontroller ? "game controller" : "joystick", index, joystick_id, player_id, name);

  ControllerData cd = {};
  cd.player_id = player_id;
  cd.joystick_id = joystick_id;
  cd.haptic_left_right_effect = -1;
  cd.game_controller = gcontroller;
  cd.joystick = joystick;

  if (gcontroller)
  {
    const int num_axes = SDL_JoystickNumAxes(joystick);
    const int num_buttons = SDL_JoystickNumButtons(joystick);
    cd.joy_axis_used_in_gc.resize(num_axes, false);
    cd.joy_button_used_in_gc.resize(num_buttons, false);
    auto mark_bind = [&](SDL_GameControllerButtonBind bind) {
      if (bind.bindType == SDL_CONTROLLER_BINDTYPE_AXIS && bind.value.axis < num_axes)
        cd.joy_axis_used_in_gc[bind.value.axis] = true;
      if (bind.bindType == SDL_CONTROLLER_BINDTYPE_BUTTON && bind.value.button < num_buttons)
        cd.joy_button_used_in_gc[bind.value.button] = true;
    };
    for (size_t i = 0; i < std::size(s_sdl_axis_names); i++)
      mark_bind(SDL_GameControllerGetBindForAxis(gcontroller, static_cast<SDL_GameControllerAxis>(i)));
    for (size_t i = 0; i < std::size(s_sdl_button_names); i++)
      mark_bind(SDL_GameControllerGetBindForButton(gcontroller, static_cast<SDL_GameControllerButton>(i)));
  }
  else
  {
    // GC doesn't have the concept of hats, so we only need to do this for joysticks.
    const int num_hats = SDL_JoystickNumHats(joystick);
    if (num_hats > 0)
      cd.last_hat_state.resize(static_cast<size_t>(num_hats), u8(0));
  }

  cd.use_game_controller_rumble = (gcontroller && SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
  if (cd.use_game_controller_rumble)
  {
    Log_VerbosePrintf("(SDLInputSource) Rumble is supported on '%s' via gamecontroller", name);
  }
  else
  {
    SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
    if (haptic)
    {
      SDL_HapticEffect ef = {};
      ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
      ef.leftright.length = 1000;

      int ef_id = SDL_HapticNewEffect(haptic, &ef);
      if (ef_id >= 0)
      {
        cd.haptic = haptic;
        cd.haptic_left_right_effect = ef_id;
      }
      else
      {
        Log_ErrorPrintf("(SDLInputSource) Failed to create haptic left/right effect: %s", SDL_GetError());
        if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
        {
          cd.haptic = haptic;
        }
        else
        {
          Log_ErrorPrintf("(SDLInputSource) No haptic rumble supported: %s", SDL_GetError());
          SDL_HapticClose(haptic);
        }
      }
    }

    if (cd.haptic)
      Log_VerbosePrintf("(SDLInputSource) Rumble is supported on '%s' via haptic", name);
  }

  if (!cd.haptic && !cd.use_game_controller_rumble)
    Log_VerbosePrintf("(SDLInputSource) Rumble is not supported on '%s'", name);

  m_controllers.push_back(std::move(cd));

  InputManager::OnInputDeviceConnected(StringUtil::StdStringFromFormat("SDL-%d", player_id), name);
  return true;
}

bool SDLInputSource::CloseDevice(int joystick_index)
{
  auto it = GetControllerDataForJoystickId(joystick_index);
  if (it == m_controllers.end())
    return false;

  InputManager::OnInputDeviceDisconnected(StringUtil::StdStringFromFormat("SDL-%d", it->player_id));

  if (it->haptic)
    SDL_HapticClose(it->haptic);

  if (it->game_controller)
    SDL_GameControllerClose(it->game_controller);
  else
    SDL_JoystickClose(it->joystick);

  m_controllers.erase(it);
  return true;
}

static float NormalizeS16(s16 value)
{
  return static_cast<float>(value) / (value < 0 ? 32768.0f : 32767.0f);
}

bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev)
{
  auto it = GetControllerDataForJoystickId(ev->which);
  if (it == m_controllers.end())
    return false;

  const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, ev->axis));
  InputManager::InvokeEvents(key, NormalizeS16(ev->value));
  return true;
}

bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev)
{
  auto it = GetControllerDataForJoystickId(ev->which);
  if (it == m_controllers.end())
    return false;

  const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, ev->button));
  const GenericInputBinding generic_key = (ev->button < std::size(s_sdl_generic_binding_button_mapping)) ?
                                            s_sdl_generic_binding_button_mapping[ev->button] :
                                            GenericInputBinding::Unknown;
  InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f, generic_key);
  return true;
}

bool SDLInputSource::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* ev)
{
  auto it = GetControllerDataForJoystickId(ev->which);
  if (it == m_controllers.end())
    return false;
  if (ev->axis < it->joy_axis_used_in_gc.size() && it->joy_axis_used_in_gc[ev->axis])
    return false;                                                            // Will get handled by GC event
  const u32 axis = ev->axis + static_cast<u32>(std::size(s_sdl_axis_names)); // Ensure we don't conflict with GC axes
  const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, axis));
  InputManager::InvokeEvents(key, NormalizeS16(ev->value));
  return true;
}

bool SDLInputSource::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* ev)
{
  auto it = GetControllerDataForJoystickId(ev->which);
  if (it == m_controllers.end())
    return false;
  if (ev->button < it->joy_button_used_in_gc.size() && it->joy_button_used_in_gc[ev->button])
    return false; // Will get handled by GC event
  const u32 button =
    ev->button + static_cast<u32>(std::size(s_sdl_button_names)); // Ensure we don't conflict with GC buttons
  const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, button));
  InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f);
  return true;
}

bool SDLInputSource::HandleJoystickHatEvent(const SDL_JoyHatEvent* ev)
{
  auto it = GetControllerDataForJoystickId(ev->which);
  if (it == m_controllers.end() || ev->hat >= it->last_hat_state.size())
    return false;

  const unsigned long last_direction = it->last_hat_state[ev->hat];
  it->last_hat_state[ev->hat] = ev->value;

  unsigned long changed_direction = last_direction ^ ev->value;
  while (changed_direction != 0)
  {
    const u32 pos = CountTrailingZeros(changed_direction);

    const unsigned long mask = (1u << pos);
    changed_direction &= ~mask;

    const InputBindingKey key(MakeGenericControllerHatKey(InputSourceType::SDL, it->player_id, ev->hat,
                                                          static_cast<u8>(pos),
                                                          static_cast<u32>(std::size(s_sdl_hat_direction_names))));
    InputManager::InvokeEvents(key, (last_direction & mask) ? 0.0f : 1.0f);
  }

  return true;
}

std::vector<InputBindingKey> SDLInputSource::EnumerateMotors()
{
  std::vector<InputBindingKey> ret;

  InputBindingKey key = {};
  key.source_type = InputSourceType::SDL;

  for (ControllerData& cd : m_controllers)
  {
    key.source_index = cd.player_id;

    if (cd.use_game_controller_rumble || cd.haptic_left_right_effect)
    {
      // two motors
      key.source_subtype = InputSubclass::ControllerMotor;
      key.data = 0;
      ret.push_back(key);
      key.data = 1;
      ret.push_back(key);
    }
    else if (cd.haptic)
    {
      // haptic effect
      key.source_subtype = InputSubclass::ControllerHaptic;
      key.data = 0;
      ret.push_back(key);
    }
  }

  return ret;
}

bool SDLInputSource::GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping)
{
  if (!StringUtil::StartsWith(device, "SDL-"))
    return false;

  const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4));
  if (!player_id.has_value() || player_id.value() < 0)
    return false;

  ControllerDataVector::iterator it = GetControllerDataForPlayerId(player_id.value());
  if (it == m_controllers.end())
    return false;

  if (it->game_controller)
  {
    // assume all buttons are present.
    const s32 pid = player_id.value();
    for (u32 i = 0; i < std::size(s_sdl_generic_binding_axis_mapping); i++)
    {
      const GenericInputBinding negative = s_sdl_generic_binding_axis_mapping[i][0];
      const GenericInputBinding positive = s_sdl_generic_binding_axis_mapping[i][1];
      if (negative != GenericInputBinding::Unknown)
        mapping->emplace_back(negative, StringUtil::StdStringFromFormat("SDL-%d/-%s", pid, s_sdl_axis_names[i]));

      if (positive != GenericInputBinding::Unknown)
        mapping->emplace_back(positive, StringUtil::StdStringFromFormat("SDL-%d/+%s", pid, s_sdl_axis_names[i]));
    }
    for (u32 i = 0; i < std::size(s_sdl_generic_binding_button_mapping); i++)
    {
      const GenericInputBinding binding = s_sdl_generic_binding_button_mapping[i];
      if (binding != GenericInputBinding::Unknown)
        mapping->emplace_back(binding, StringUtil::StdStringFromFormat("SDL-%d/%s", pid, s_sdl_button_names[i]));
    }

    if (it->use_game_controller_rumble || it->haptic_left_right_effect)
    {
      mapping->emplace_back(GenericInputBinding::SmallMotor, StringUtil::StdStringFromFormat("SDL-%d/SmallMotor", pid));
      mapping->emplace_back(GenericInputBinding::LargeMotor, StringUtil::StdStringFromFormat("SDL-%d/LargeMotor", pid));
    }
    else
    {
      mapping->emplace_back(GenericInputBinding::SmallMotor, StringUtil::StdStringFromFormat("SDL-%d/Haptic", pid));
      mapping->emplace_back(GenericInputBinding::LargeMotor, StringUtil::StdStringFromFormat("SDL-%d/Haptic", pid));
    }

    return true;
  }
  else
  {
    // joysticks have arbitrary axis numbers, so automapping isn't going to work here.
    return false;
  }
}

void SDLInputSource::UpdateMotorState(InputBindingKey key, float intensity)
{
  if (key.source_subtype != InputSubclass::ControllerMotor && key.source_subtype != InputSubclass::ControllerHaptic)
    return;

  auto it = GetControllerDataForPlayerId(key.source_index);
  if (it == m_controllers.end())
    return;

  it->rumble_intensity[key.data] = static_cast<u16>(intensity * 65535.0f);
  SendRumbleUpdate(&(*it));
}

void SDLInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
                                      float small_intensity)
{
  if (large_key.source_index != small_key.source_index || large_key.source_subtype != InputSubclass::ControllerMotor ||
      small_key.source_subtype != InputSubclass::ControllerMotor)
  {
    // bonkers config where they're mapped to different controllers... who would do such a thing?
    UpdateMotorState(large_key, large_intensity);
    UpdateMotorState(small_key, small_intensity);
    return;
  }

  auto it = GetControllerDataForPlayerId(large_key.source_index);
  if (it == m_controllers.end())
    return;

  it->rumble_intensity[large_key.data] = static_cast<u16>(large_intensity * 65535.0f);
  it->rumble_intensity[small_key.data] = static_cast<u16>(small_intensity * 65535.0f);
  SendRumbleUpdate(&(*it));
}

void SDLInputSource::SendRumbleUpdate(ControllerData* cd)
{
  // we'll update before this duration is elapsed
  static constexpr u32 DURATION = 65535; // SDL_MAX_RUMBLE_DURATION_MS

  if (cd->use_game_controller_rumble)
  {
    SDL_GameControllerRumble(cd->game_controller, cd->rumble_intensity[0], cd->rumble_intensity[1], DURATION);
    return;
  }

  if (cd->haptic_left_right_effect >= 0)
  {
    if ((static_cast<u32>(cd->rumble_intensity[0]) + static_cast<u32>(cd->rumble_intensity[1])) > 0)
    {
      SDL_HapticEffect ef;
      ef.type = SDL_HAPTIC_LEFTRIGHT;
      ef.leftright.large_magnitude = cd->rumble_intensity[0];
      ef.leftright.small_magnitude = cd->rumble_intensity[1];
      ef.leftright.length = DURATION;
      SDL_HapticUpdateEffect(cd->haptic, cd->haptic_left_right_effect, &ef);
      SDL_HapticRunEffect(cd->haptic, cd->haptic_left_right_effect, SDL_HAPTIC_INFINITY);
    }
    else
    {
      SDL_HapticStopEffect(cd->haptic, cd->haptic_left_right_effect);
    }
  }
  else
  {
    const float strength =
      static_cast<float>(std::max(cd->rumble_intensity[0], cd->rumble_intensity[1])) * (1.0f / 65535.0f);
    if (strength > 0.0f)
      SDL_HapticRumblePlay(cd->haptic, strength, DURATION);
    else
      SDL_HapticRumbleStop(cd->haptic);
  }
}

std::unique_ptr<InputSource> InputSource::CreateSDLSource()
{
  return std::make_unique<SDLInputSource>();
}