#include "sdl_controller_interface.h"
#include "common/assert.h"
#include "common/file_system.h"
#include "common/log.h"
#include "core/controller.h"
#include "core/host_interface.h"
#include "core/system.h"
#include "sdl_initializer.h"
#include <SDL.h>
#include <cmath>
Log_SetChannel(SDLControllerInterface);

SDLControllerInterface::SDLControllerInterface() = default;

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

ControllerInterface::Backend SDLControllerInterface::GetBackend() const
{
  return ControllerInterface::Backend::SDL;
}

bool SDLControllerInterface::Initialize(CommonHostInterface* host_interface)
{
  if (!ControllerInterface::Initialize(host_interface))
    return false;

  FrontendCommon::EnsureSDLInitialized();

  const std::string gcdb_file_name = GetGameControllerDBFileName();
  if (!gcdb_file_name.empty())
  {
    Log_InfoPrintf("Loading game controller mappings from '%s'", gcdb_file_name.c_str());
    if (SDL_GameControllerAddMappingsFromFile(gcdb_file_name.c_str()) < 0)
    {
      Log_ErrorPrintf("SDL_GameControllerAddMappingsFromFile(%s) failed: %s", gcdb_file_name.c_str(), SDL_GetError());
    }
  }

  if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0)
  {
    Log_ErrorPrintf("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 SDLControllerInterface::Shutdown()
{
  while (!m_controllers.empty())
    CloseGameController(m_controllers.begin()->joystick_id, false);

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

  ControllerInterface::Shutdown();
}

std::string SDLControllerInterface::GetGameControllerDBFileName() const
{
  // prefer the userdir copy
  std::string filename(m_host_interface->GetUserDirectoryRelativePath("gamecontrollerdb.txt"));
  if (FileSystem::FileExists(filename.c_str()))
    return filename;

  filename =
    m_host_interface->GetProgramDirectoryRelativePath("database" FS_OSPATH_SEPARATOR_STR "gamecontrollerdb.txt");
  if (FileSystem::FileExists(filename.c_str()))
    return filename;

  return {};
}

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

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

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

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

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

    case SDL_JOYDEVICEADDED:
      if (SDL_IsGameController(event->jdevice.which))
        return true;

      Log_InfoPrintf("Joystick %d inserted", event->jdevice.which);
      OpenJoystick(event->jdevice.which);
      return true;

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

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

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

    default:
      return false;
  }
}

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

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

int SDLControllerInterface::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 SDLControllerInterface::OpenGameController(int index)
{
  SDL_GameController* gcontroller = SDL_GameControllerOpen(index);
  SDL_Joystick* joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
  if (!gcontroller || !joystick)
  {
    Log_WarningPrintf("Failed to open controller %d", index);
    if (gcontroller)
      SDL_GameControllerClose(gcontroller);

    return false;
  }

  int joystick_id = SDL_JoystickInstanceID(joystick);
#if SDL_VERSION_ATLEAST(2, 0, 9)
  int player_id = SDL_GameControllerGetPlayerIndex(gcontroller);
#else
  int player_id = -1;
#endif
  if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
  {
    const int free_player_id = GetFreePlayerId();
    Log_WarningPrintf(
      "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;
  }

  Log_InfoPrintf("Opened controller %d (instance id %d, player id %d): %s", index, joystick_id, player_id,
                 SDL_GameControllerName(gcontroller));

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

#if SDL_VERSION_ATLEAST(2, 0, 9)
  cd.use_game_controller_rumble = (SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
#else
  cd.use_game_controller_rumble = false;
#endif

  if (cd.use_game_controller_rumble)
  {
    Log_InfoPrintf("Rumble is supported on '%s' via gamecontroller", SDL_GameControllerName(gcontroller));
  }
  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("Failed to create haptic left/right effect: %s", SDL_GetError());
        if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
        {
          cd.haptic = haptic;
        }
        else
        {
          Log_ErrorPrintf("No haptic rumble supported: %s", SDL_GetError());
          SDL_HapticClose(haptic);
        }
      }
    }

    if (cd.haptic)
      Log_InfoPrintf("Rumble is supported on '%s' via haptic", SDL_GameControllerName(gcontroller));
  }

  if (!cd.haptic && !cd.use_game_controller_rumble)
    Log_WarningPrintf("Rumble is not supported on '%s'", SDL_GameControllerName(gcontroller));

  m_controllers.push_back(std::move(cd));
  OnControllerConnected(player_id);
  return true;
}

bool SDLControllerInterface::CloseGameController(int joystick_index, bool notify)
{
  auto it = GetControllerDataForJoystickId(joystick_index);
  if (it == m_controllers.end())
    return false;

  const int player_id = it->player_id;

  if (it->haptic)
    SDL_HapticClose(static_cast<SDL_Haptic*>(it->haptic));

  SDL_GameControllerClose(static_cast<SDL_GameController*>(it->game_controller));
  m_controllers.erase(it);

  if (notify)
    OnControllerDisconnected(player_id);
  return true;
}

bool SDLControllerInterface::OpenJoystick(int index)
{
  SDL_Joystick* joystick = SDL_JoystickOpen(index);
  if (!joystick)
  {
    Log_WarningPrintf("Failed to open joystick %d", index);

    return false;
  }

  int joystick_id = SDL_JoystickInstanceID(joystick);
#if SDL_VERSION_ATLEAST(2, 0, 9)
  int player_id = SDL_JoystickGetDevicePlayerIndex(index);
#else
  int player_id = -1;
#endif
  if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
  {
    const int free_player_id = GetFreePlayerId();
    Log_WarningPrintf(
      "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 = SDL_JoystickName(joystick);

  Log_InfoPrintf("Opened controller %d (instance id %d, player id %d): %s", 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 = nullptr;
  cd.use_game_controller_rumble = false;

  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("Failed to create haptic left/right effect: %s", SDL_GetError());
      if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
      {
        cd.haptic = haptic;
      }
      else
      {
        Log_ErrorPrintf("No haptic rumble supported: %s", SDL_GetError());
        SDL_HapticClose(haptic);
      }
    }
  }

  if (cd.haptic)
    Log_InfoPrintf("Rumble is supported on '%s'", name);
  else
    Log_WarningPrintf("Rumble is not supported on '%s'", name);

  m_controllers.push_back(std::move(cd));
  OnControllerConnected(player_id);
  return true;
}

bool SDLControllerInterface::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* event)
{
  const float value = static_cast<float>(event->value) / (event->value < 0 ? 32768.0f : 32767.0f);
  Log_DebugPrintf("controller %d axis %d %d %f", event->which, event->axis, event->value, value);

  auto it = GetControllerDataForJoystickId(event->which);
  if (it == m_controllers.end() || it->IsGameController())
    return false;

  if (DoEventHook(Hook::Type::Axis, it->player_id, event->axis, value, true))
    return true;

  bool processed = false;

  const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Full];
  if (cb)
  {
    cb(value);
    processed = true;
  }

  if (value > 0.0f)
  {
    const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Positive];
    if (cb)
    {
      // Expand 0..1 - -1..1
      cb(value * 2.0f - 1.0f);
      processed = true;
    }
  }
  else if (value < 0.0f)
  {
    const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Negative];
    if (cb)
    {
      // Expand 0..-1 - -1..1
      cb(value * -2.0f - 1.0f);
      processed = true;
    }
  }

  if (processed)
    return true;

  // set the other direction to false so large movements don't leave the opposite on
  const bool outside_deadzone = (std::abs(value) >= it->deadzone);
  const bool positive = (value >= 0.0f);
  const ButtonCallback& other_button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(!positive)];
  const ButtonCallback& button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(positive)];
  if (button_cb)
  {
    button_cb(outside_deadzone);
    if (other_button_cb)
      other_button_cb(false);
    return true;
  }
  else if (other_button_cb)
  {
    other_button_cb(false);
    return true;
  }
  else
  {
    return false;
  }
}

bool SDLControllerInterface::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* event)
{
  Log_DebugPrintf("controller %d button %d %s", event->which, event->button,
                  event->state == SDL_PRESSED ? "pressed" : "released");

  auto it = GetControllerDataForJoystickId(event->which);
  if (it == m_controllers.end() || it->IsGameController())
    return false;

  const bool pressed = (event->state == SDL_PRESSED);
  if (DoEventHook(Hook::Type::Button, it->player_id, event->button, pressed ? 1.0f : 0.0f))
    return true;

  const ButtonCallback& cb = it->button_mapping[event->button];
  if (cb)
  {
    cb(pressed);
    return true;
  }

  const AxisCallback& axis_cb = it->button_axis_mapping[event->button];
  if (axis_cb)
  {
    axis_cb(pressed ? 1.0f : -1.0f);
    return true;
  }

  return false;
}

bool SDLControllerInterface::HandleJoystickHatEvent(const SDL_JoyHatEvent* event)
{
  Log_DebugPrintf("controller %d hat %d %d", event->which, event->hat, event->value);

  auto it = GetControllerDataForJoystickId(event->which);
  if (it == m_controllers.end() || it->IsGameController())
    return false;

  auto HatEventHook = [hat = event->hat, value = event->value, player_id = it->player_id, this](int hat_position) {
    if ((value & hat_position) == 0)
      return false;

    std::string_view position_str;
    switch (value)
    {
      case SDL_HAT_UP:
        position_str = "Up";
        break;
      case SDL_HAT_RIGHT:
        position_str = "Right";
        break;
      case SDL_HAT_DOWN:
        position_str = "Down";
        break;
      case SDL_HAT_LEFT:
        position_str = "Left";
        break;
      default:
        return false;
    }

    return DoEventHook(Hook::Type::Hat, player_id, hat, position_str);
  };

  if (event->value == SDL_HAT_CENTERED)
  {
    if (HatEventHook(SDL_HAT_CENTERED))
      return true;
  }
  else
  {
    // event->value can be a bitmask of multiple direction, so probe them all
    if (HatEventHook(SDL_HAT_UP) || HatEventHook(SDL_HAT_RIGHT) || HatEventHook(SDL_HAT_DOWN) ||
        HatEventHook(SDL_HAT_LEFT))
      return true;
  }

  bool processed = false;

  if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][0]; cb)
  {
    cb(event->value & SDL_HAT_UP);
    processed = true;
  }
  if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][1]; cb)
  {
    cb(event->value & SDL_HAT_RIGHT);
    processed = true;
  }
  if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][2]; cb)
  {
    cb(event->value & SDL_HAT_DOWN);
    processed = true;
  }
  if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][3]; cb)
  {
    cb(event->value & SDL_HAT_LEFT);
    processed = true;
  }

  return processed;
}

void SDLControllerInterface::ClearBindings()
{
  for (auto& it : m_controllers)
  {
    it.axis_mapping.fill({});
    it.button_mapping.fill({});
    it.axis_button_mapping.fill({});
    it.button_axis_mapping.fill({});
    it.hat_button_mapping.clear();
  }
}

bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
                                                AxisCallback callback)
{
  auto it = GetControllerDataForPlayerId(controller_index);
  if (it == m_controllers.end())
    return false;

  if (axis_number < 0 || axis_number >= MAX_NUM_AXISES)
    return false;

  it->axis_mapping[axis_number][axis_side] = std::move(callback);
  return true;
}

bool SDLControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback)
{
  auto it = GetControllerDataForPlayerId(controller_index);
  if (it == m_controllers.end())
    return false;

  if (button_number < 0 || button_number >= MAX_NUM_BUTTONS)
    return false;

  it->button_mapping[button_number] = std::move(callback);
  return true;
}

bool SDLControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
                                                        ButtonCallback callback)
{
  auto it = GetControllerDataForPlayerId(controller_index);
  if (it == m_controllers.end())
    return false;

  if (axis_number < 0 || axis_number >= MAX_NUM_AXISES)
    return false;

  it->axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback);
  return true;
}

bool SDLControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
                                                       std::string_view hat_position, ButtonCallback callback)
{
  auto it = GetControllerDataForPlayerId(controller_index);
  if (it == m_controllers.end())
    return false;

  size_t index;
  if (hat_position == "Up")
    index = 0;
  else if (hat_position == "Right")
    index = 1;
  else if (hat_position == "Down")
    index = 2;
  else if (hat_position == "Left")
    index = 3;
  else
    return false;

  // We need 4 entries per hat_number
  if (static_cast<int>(it->hat_button_mapping.size()) < hat_number + 1)
    it->hat_button_mapping.resize(hat_number + 1);

  it->hat_button_mapping[hat_number][index] = std::move(callback);
  return true;
}

bool SDLControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback)
{
  auto it = GetControllerDataForPlayerId(controller_index);
  if (it == m_controllers.end())
    return false;

  if (button_number < 0 || button_number >= MAX_NUM_BUTTONS)
    return false;

  it->button_axis_mapping[button_number] = std::move(callback);
  return true;
}

bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev)
{
  const float value = static_cast<float>(ev->value) / (ev->value < 0 ? 32768.0f : 32767.0f);
  Log_DebugPrintf("controller %d axis %d %d %f", ev->which, ev->axis, ev->value, value);

  auto it = GetControllerDataForJoystickId(ev->which);
  if (it == m_controllers.end())
    return false;

  if (DoEventHook(Hook::Type::Axis, it->player_id, ev->axis, value))
    return true;

  const AxisCallback& cb = it->axis_mapping[ev->axis][AxisSide::Full];
  if (cb)
  {
    // Extend triggers from a 0 - 1 range to a -1 - 1 range for consistency with other inputs
    if (ev->axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT || ev->axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT)
    {
      cb((value * 2.0f) - 1.0f);
    }
    else
    {
      cb(value);
    }
    return true;
  }

  // set the other direction to false so large movements don't leave the opposite on
  const bool outside_deadzone = (std::abs(value) >= it->deadzone);
  const bool positive = (value >= 0.0f);
  const ButtonCallback& other_button_cb = it->axis_button_mapping[ev->axis][BoolToUInt8(!positive)];
  const ButtonCallback& button_cb = it->axis_button_mapping[ev->axis][BoolToUInt8(positive)];
  if (button_cb)
  {
    button_cb(outside_deadzone);
    if (other_button_cb)
      other_button_cb(false);
    return true;
  }
  else if (other_button_cb)
  {
    other_button_cb(false);
    return true;
  }
  else
  {
    return false;
  }
}

bool SDLControllerInterface::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev)
{
  Log_DebugPrintf("controller %d button %d %s", ev->which, ev->button,
                  ev->state == SDL_PRESSED ? "pressed" : "released");

  auto it = GetControllerDataForJoystickId(ev->which);
  if (it == m_controllers.end())
    return false;

  static constexpr std::array<FrontendCommon::ControllerNavigationButton, SDL_CONTROLLER_BUTTON_MAX>
    nav_button_mapping = {{
      FrontendCommon::ControllerNavigationButton::Activate,      // SDL_CONTROLLER_BUTTON_A
      FrontendCommon::ControllerNavigationButton::Cancel,        // SDL_CONTROLLER_BUTTON_B
      FrontendCommon::ControllerNavigationButton::Count,         // SDL_CONTROLLER_BUTTON_X
      FrontendCommon::ControllerNavigationButton::Count,         // SDL_CONTROLLER_BUTTON_Y
      FrontendCommon::ControllerNavigationButton::Count,         // SDL_CONTROLLER_BUTTON_BACK
      FrontendCommon::ControllerNavigationButton::Count,         // SDL_CONTROLLER_BUTTON_GUIDE
      FrontendCommon::ControllerNavigationButton::Count,         // SDL_CONTROLLER_BUTTON_START
      FrontendCommon::ControllerNavigationButton::Count,         // SDL_CONTROLLER_BUTTON_LEFTSTICK
      FrontendCommon::ControllerNavigationButton::Count,         // SDL_CONTROLLER_BUTTON_RIGHTSTICK
      FrontendCommon::ControllerNavigationButton::LeftShoulder,  // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
      FrontendCommon::ControllerNavigationButton::RightShoulder, // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
      FrontendCommon::ControllerNavigationButton::DPadUp,        // SDL_CONTROLLER_BUTTON_DPAD_UP
      FrontendCommon::ControllerNavigationButton::DPadDown,      // SDL_CONTROLLER_BUTTON_DPAD_DOWN
      FrontendCommon::ControllerNavigationButton::DPadLeft,      // SDL_CONTROLLER_BUTTON_DPAD_LEFT
      FrontendCommon::ControllerNavigationButton::DPadRight,     // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
    }};

  const bool pressed = (ev->state == SDL_PRESSED);
  if (DoEventHook(Hook::Type::Button, it->player_id, ev->button, pressed ? 1.0f : 0.0f))
    return true;

  if (ev->button < nav_button_mapping.size() &&
      nav_button_mapping[ev->button] != FrontendCommon::ControllerNavigationButton::Count &&
      m_host_interface->SetControllerNavigationButtonState(nav_button_mapping[ev->button], pressed))
  {
    // UI consumed the event
    return true;
  }

  const ButtonCallback& cb = it->button_mapping[ev->button];
  if (cb)
  {
    cb(pressed);
    return true;
  }

  const AxisCallback& axis_cb = it->button_axis_mapping[ev->button];
  if (axis_cb)
  {
    axis_cb(pressed ? 1.0f : -1.0f);
    return true;
  }

  return false;
}

u32 SDLControllerInterface::GetControllerRumbleMotorCount(int controller_index)
{
  auto it = GetControllerDataForPlayerId(controller_index);
  if (it == m_controllers.end())
    return 0;

  return (it->use_game_controller_rumble ? 2 : ((it->haptic_left_right_effect >= 0) ? 2 : (it->haptic ? 1 : 0)));
}

void SDLControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors)
{
  auto it = GetControllerDataForPlayerId(controller_index);
  if (it == m_controllers.end())
    return;

  // we'll update before this duration is elapsed
  static constexpr u32 DURATION = 65535; // SDL_MAX_RUMBLE_DURATION_MS

#if SDL_VERSION_ATLEAST(2, 0, 9)
  if (it->use_game_controller_rumble)
  {
    const u16 large = static_cast<u16>(strengths[0] * 65535.0f);
    const u16 small = static_cast<u32>(strengths[1] * 65535.0f);
    SDL_GameControllerRumble(static_cast<SDL_GameController*>(it->game_controller), large, small, DURATION);
    return;
  }
#endif

  SDL_Haptic* haptic = static_cast<SDL_Haptic*>(it->haptic);
  if (it->haptic_left_right_effect >= 0 && num_motors > 1)
  {
    if (strengths[0] > 0.0f || strengths[1] > 0.0f)
    {
      SDL_HapticEffect ef;
      ef.type = SDL_HAPTIC_LEFTRIGHT;
      ef.leftright.large_magnitude = static_cast<u32>(strengths[0] * 65535.0f);
      ef.leftright.small_magnitude = static_cast<u32>(strengths[1] * 65535.0f);
      ef.leftright.length = DURATION;
      SDL_HapticUpdateEffect(haptic, it->haptic_left_right_effect, &ef);
      SDL_HapticRunEffect(haptic, it->haptic_left_right_effect, SDL_HAPTIC_INFINITY);
    }
    else
    {
      SDL_HapticStopEffect(haptic, it->haptic_left_right_effect);
    }
  }
  else
  {
    float max_strength = 0.0f;
    for (u32 i = 0; i < num_motors; i++)
      max_strength = std::max(max_strength, strengths[i]);

    if (max_strength > 0.0f)
      SDL_HapticRumblePlay(haptic, max_strength, DURATION);
    else
      SDL_HapticRumbleStop(haptic);
  }
}

bool SDLControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */)
{
  auto it = GetControllerDataForPlayerId(controller_index);
  if (it == m_controllers.end())
    return false;

  it->deadzone = std::clamp(std::abs(size), 0.01f, 0.99f);
  Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index, it->deadzone);
  return true;
}
