// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "input_handler.h"

#include <fstream>
#include <iostream>
#include <list>
#include <map>
#include <ranges>
#include <sstream>
#include <string>
#include <string_view>
#include <typeinfo>
#include <unordered_map>
#include <vector>

#include "SDL3/SDL_events.h"
#include "SDL3/SDL_timer.h"

#include "common/elf_info.h"
#include "common/io_file.h"
#include "common/path_util.h"
#include "common/singleton.h"
#include "core/devtools/layer.h"
#include "core/emulator_settings.h"
#include "core/emulator_state.h"
#include "input/controller.h"
#include "input/input_mouse.h"

namespace Input {
/*
Project structure:
n to m connection between inputs and outputs
Keyup and keydown events update a dynamic list* of u32 'flags' (what is currently in the list is
'pressed') On every event, after flag updates, we check for every input binding -> controller output
pair if all their flags are 'on' If not, disable; if so, enable them. For axes, we gather their data
into a struct cumulatively from all inputs, then after we checked all of those, we update them all
at once. Wheel inputs generate a timer that doesn't turn off their outputs automatically, but push a
userevent to do so.

What structs are needed?
InputBinding(key1, key2, key3)
ControllerOutput(button, axis) - we only need a const array of these, and one of the attr-s is
always 0 BindingConnection(inputBinding (member), controllerOutput (ref to the array element))
*/

constexpr std::string_view GetDefaultGlobalConfig() {
    return R"(# Anything put here will be loaded for all games,
# alongside the game's config or default.ini depending on your preference.
)";
}

constexpr std::string_view GetDefaultInputConfig() {
    return R"(#Feeling lost? Check out the Help section!

# Keyboard bindings

triangle = kp8
circle = kp6
cross = kp2
square = kp4
# Alternatives for users without a keypad
triangle = c
circle = b
cross = n
square = v

l1 = q
r1 = u
l2 = e
r2 = o
l3 = x
r3 = m

options = enter
touchpad_center = space

pad_up = up
pad_down = down
pad_left = left
pad_right = right

axis_left_x_minus = a
axis_left_x_plus = d
axis_left_y_minus = w
axis_left_y_plus = s

axis_right_x_minus = j
axis_right_x_plus = l
axis_right_y_minus = i
axis_right_y_plus = k

# Controller bindings

triangle = triangle
cross = cross
square = square
circle = circle

l1 = l1
l2 = l2
l3 = l3
r1 = r1
r2 = r2
r3 = r3

options = options
touchpad_center = back

pad_up = pad_up
pad_down = pad_down
pad_left = pad_left
pad_right = pad_right

axis_left_x = axis_left_x
axis_left_y = axis_left_y
axis_right_x = axis_right_x
axis_right_y = axis_right_y

# Range of deadzones: 1 (almost none) to 127 (max)
analog_deadzone = leftjoystick, 2, 127
analog_deadzone = rightjoystick, 2, 127

override_controller_color = false, 0, 0, 255
)";
}
std::filesystem::path GetInputConfigFile(const std::string& game_id) {
    // Read configuration file of the game, and if it doesn't exist, generate it from default
    // If that doesn't exist either, generate that from getDefaultConfig() and try again
    // If even the folder is missing, we start with that.

    const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "input_config";
    const auto config_file = config_dir / (game_id + ".ini");
    const auto default_config_file = config_dir / "default.ini";

    // Ensure the config directory exists
    if (!std::filesystem::exists(config_dir)) {
        std::filesystem::create_directories(config_dir);
    }

    // Check if the default config exists
    if (!std::filesystem::exists(default_config_file)) {
        // If the default config is also missing, create it from getDefaultConfig()
        const auto default_config = GetDefaultInputConfig();
        std::ofstream default_config_stream(default_config_file);
        if (default_config_stream) {
            default_config_stream << default_config;
        }
    }

    // if empty, we only need to execute the function up until this point
    if (game_id.empty()) {
        return default_config_file;
    }

    // Create global config if it doesn't exist yet
    if (game_id == "global" && !std::filesystem::exists(config_file)) {
        if (!std::filesystem::exists(config_file)) {
            const auto global_config = GetDefaultGlobalConfig();
            std::ofstream global_config_stream(config_file);
            if (global_config_stream) {
                global_config_stream << global_config;
            }
        }
    }
    if (game_id == "global") {
        std::map<std::string, std::string> default_bindings_to_add = {
            {"hotkey_capture_frame", "f12"},
            {"hotkey_screenshot_with_overlays", "lalt, f12"},
            {"hotkey_fullscreen", "f11"},
            {"hotkey_show_fps", "f10"},
            {"hotkey_pause", "f9"},
            {"hotkey_reload_inputs", "f8"},
            {"hotkey_toggle_mouse_to_joystick", "f7"},
            {"hotkey_toggle_mouse_to_gyro", "f6"},
            {"hotkey_add_virtual_user", "f5"},
            {"hotkey_remove_virtual_user", "f4"},
            {"hotkey_toggle_mouse_to_touchpad", "delete"},
            {"hotkey_quit", "lctrl, lshift, end"},
            {"hotkey_volume_up", "kpplus"},
            {"hotkey_volume_down", "kpminus"},
        };
        std::string legacy_capture_binding;
        bool legacy_capture_binding_found = false;
        std::ifstream global_in(config_file);
        std::string line;
        while (std::getline(global_in, line)) {
            line.erase(std::remove_if(line.begin(), line.end(),
                                      [](unsigned char c) { return std::isspace(c); }),
                       line.end());
            std::size_t equal_pos = line.find('=');
            if (equal_pos == std::string::npos) {
                continue;
            }
            std::string output_string = line.substr(0, equal_pos);
            if (output_string == "hotkey_renderdoc_capture") {
                legacy_capture_binding = line.substr(equal_pos + 1);
                legacy_capture_binding_found = true;
            }
            default_bindings_to_add.erase(output_string);
        }
        global_in.close();
        if (legacy_capture_binding_found) {
            if (auto it = default_bindings_to_add.find("hotkey_capture_frame");
                it != default_bindings_to_add.end()) {
                it->second = legacy_capture_binding;
            }
        }
        std::ofstream global_out(config_file, std::ios::app);
        for (auto const& b : default_bindings_to_add) {
            global_out << b.first << " = " << b.second << "\n";
        }
    }

    // If game-specific config doesn't exist, create it from the default config
    if (!std::filesystem::exists(config_file)) {
        std::filesystem::copy(default_config_file, config_file);
    }
    return config_file;
}

bool leftjoystick_halfmode = false, rightjoystick_halfmode = false;
std::array<std::pair<int, int>, 4> leftjoystick_deadzone, rightjoystick_deadzone,
    lefttrigger_deadzone, righttrigger_deadzone;

std::list<std::pair<InputEvent, bool>> pressed_keys;
std::list<InputID> toggled_keys;
static std::vector<BindingConnection> connections;

GameControllers ControllerOutput::controllers =
    *Common::Singleton<Input::GameControllers>::Instance();

std::array<ControllerAllOutputs, 9> output_arrays = {
    ControllerAllOutputs(0), ControllerAllOutputs(1), ControllerAllOutputs(2),
    ControllerAllOutputs(3), ControllerAllOutputs(4), ControllerAllOutputs(5),
    ControllerAllOutputs(6), ControllerAllOutputs(7), ControllerAllOutputs(8),
};

void ControllerOutput::LinkJoystickAxes() {
    // for (int i = 17; i < 23; i += 2) {
    //     delete output_array[i].new_param;
    //     output_array[i].new_param = output_array[i + 1].new_param;
    // }
}

static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) {
    using OPBDO = OrbisPadButtonDataOffset;

    switch (button) {
    case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
        return OPBDO::Down;
    case SDL_GAMEPAD_BUTTON_DPAD_UP:
        return OPBDO::Up;
    case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
        return OPBDO::Left;
    case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
        return OPBDO::Right;
    case SDL_GAMEPAD_BUTTON_SOUTH:
        return OPBDO::Cross;
    case SDL_GAMEPAD_BUTTON_NORTH:
        return OPBDO::Triangle;
    case SDL_GAMEPAD_BUTTON_WEST:
        return OPBDO::Square;
    case SDL_GAMEPAD_BUTTON_EAST:
        return OPBDO::Circle;
    case SDL_GAMEPAD_BUTTON_START:
        return OPBDO::Options;
    case SDL_GAMEPAD_BUTTON_TOUCHPAD:
        return OPBDO::TouchPad;
    case SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT:
        return OPBDO::TouchPad;
    case SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER:
        return OPBDO::TouchPad;
    case SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT:
        return OPBDO::TouchPad;
    case SDL_GAMEPAD_BUTTON_BACK:
        return OPBDO::TouchPad;
    case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
        return OPBDO::L1;
    case SDL_GAMEPAD_BUTTON_MISC1: // Move
        return OPBDO::L1;
    case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
        return OPBDO::R1;
    case SDL_GAMEPAD_BUTTON_LEFT_STICK:
        return OPBDO::L3;
    case SDL_GAMEPAD_BUTTON_RIGHT_STICK:
        return OPBDO::R3;
    default:
        return OPBDO::None;
    }
}

Axis GetAxisFromSDLAxis(u8 sdl_axis) {
    switch (sdl_axis) {
    case SDL_GAMEPAD_AXIS_LEFTX:
        return Axis::LeftX;
    case SDL_GAMEPAD_AXIS_LEFTY:
        return Axis::LeftY;
    case SDL_GAMEPAD_AXIS_RIGHTX:
        return Axis::RightX;
    case SDL_GAMEPAD_AXIS_RIGHTY:
        return Axis::RightY;
    case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
        return Axis::TriggerLeft;
    case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
        return Axis::TriggerRight;
    default:
        return Axis::AxisMax;
    }
}

// syntax: 'name, name,name' or 'name,name' or 'name'
InputBinding GetBindingFromString(std::string& line) {
    std::array<InputID, 3> keys = {InputID(), InputID(), InputID()};

    // Check and process tokens
    for (const auto token : std::views::split(line, ',')) { // Split by comma
        const std::string t(token.begin(), token.end());
        InputID input;

        if (string_to_keyboard_key_map.find(t) != string_to_keyboard_key_map.end()) {
            input = InputID(InputType::KeyboardMouse, string_to_keyboard_key_map.at(t));
        } else if (string_to_axis_map.find(t) != string_to_axis_map.end()) {
            input = InputID(InputType::Axis, string_to_axis_map.at(t).axis);
        } else if (string_to_cbutton_map.find(t) != string_to_cbutton_map.end()) {
            input = InputID(InputType::Controller, string_to_cbutton_map.at(t));
        } else if (string_to_hotkey_map.find(t) != string_to_hotkey_map.end()) {
            input = InputID(InputType::Controller, string_to_hotkey_map.at(t));
        } else {
            // Invalid token found; return default binding
            LOG_DEBUG(Input, "Invalid token found: {}", t);
            return InputBinding();
        }

        // Assign to the first available slot
        for (auto& key : keys) {
            if (!key.IsValid()) {
                key = input;
                break;
            }
        }
    }
    LOG_DEBUG(Input, "Parsed line: {}", InputBinding(keys[0], keys[1], keys[2]).ToString());
    return InputBinding(keys[0], keys[1], keys[2]);
}

std::optional<int> parseInt(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;
    }
};

void ParseInputConfig(const std::string game_id = "") {
    std::string game_id_or_default =
        EmulatorSettings.IsUseUnifiedInputConfig() ? "default" : game_id;
    const auto config_file = GetInputConfigFile(game_id_or_default);
    const auto global_config_file = GetInputConfigFile("global");

    // we reset these here so in case the user fucks up or doesn't include some of these,
    // we can fall back to default
    connections.clear();
    float mouse_deadzone_offset = 0.5;
    float mouse_speed = 1;
    float mouse_speed_offset = 0.125;

    // me when I'm in a type deduction tournament and my opponent is clang
    constexpr std::array<std::pair<int, int>, 4> default_deadzone = {
        std::pair{1, 127}, {1, 127}, {1, 127}, {1, 127}};

    leftjoystick_deadzone = default_deadzone;
    rightjoystick_deadzone = default_deadzone;
    lefttrigger_deadzone = default_deadzone;
    righttrigger_deadzone = default_deadzone;

    int lineCount = 0;

    std::ifstream config_stream(config_file);
    std::ifstream global_config_stream(global_config_file);
    std::string line = "";
    auto ProcessLine = [&]() -> void {
        lineCount++;

        // Strip the ; and whitespace
        line.erase(std::remove_if(line.begin(), line.end(),
                                  [](unsigned char c) { return std::isspace(c); }),
                   line.end());
        if (line.empty()) {
            return;
        }

        // Truncate lines starting at #
        std::size_t comment_pos = line.find('#');
        if (comment_pos != std::string::npos) {
            line = line.substr(0, comment_pos);
        }
        if (line.empty()) {
            return;
        }

        // Split the line by '='
        std::size_t equal_pos = line.find('=');
        if (equal_pos == std::string::npos) {
            LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
                        lineCount, line);
            return;
        }

        std::string output_string = line.substr(0, equal_pos);
        std::string input_string = line.substr(equal_pos + 1);
        s8 input_gamepad_id = -1, output_gamepad_id = -1; // -1 means it's not specified

        // input gamepad id is only for controllers, it's discarded otherwise
        std::size_t input_colon_pos = input_string.find(':');
        if (input_colon_pos != std::string::npos) {
            auto temp = parseInt(input_string.substr(input_colon_pos + 1));
            if (!temp) {
                LOG_WARNING(Input, "Invalid gamepad ID value at line {}: \"{}\"", lineCount, line);
            } else {
                input_gamepad_id = *temp;
            }
            input_string = input_string.substr(0, input_colon_pos);
        }

        // if not provided, assume it's for all gamepads, if the input is a controller and that also
        // doesn't have an ID, and for the first otherwise
        std::size_t output_colon_pos = output_string.find(':');
        if (output_colon_pos != std::string::npos) {
            auto temp = parseInt(output_string.substr(output_colon_pos + 1));
            if (!temp) {
                LOG_WARNING(Input, "Invalid gamepad ID value at line {}: \"{}\"", lineCount, line);
            } else {
                output_gamepad_id = *temp;
            }
            output_string = output_string.substr(0, output_colon_pos);
        }

        std::size_t comma_pos = input_string.find(',');

        // todo make override_controller_color and analog_deadzone be controller specific
        // instead of global
        if (output_string == "mouse_to_joystick") {
            if (input_string == "left") {
                SetMouseToJoystick(1);
            } else if (input_string == "right") {
                SetMouseToJoystick(2);
            } else {
                LOG_WARNING(Input, "Invalid argument for mouse-to-joystick binding");
                SetMouseToJoystick(0);
            }
            return;
        } else if (output_string == "key_toggle") {
            if (comma_pos != std::string::npos) {
                // handle key-to-key toggling (separate list?)
                InputBinding toggle_keys = GetBindingFromString(input_string);
                if (toggle_keys.KeyCount() != 2) {
                    LOG_WARNING(Input,
                                "Syntax error: Please provide exactly 2 keys: "
                                "first is the toggler, the second is the key to toggle: {}",
                                line);
                    return;
                }
                ControllerOutput* toggle_out =
                    &*std::ranges::find(output_arrays[0].data, ControllerOutput(KEY_TOGGLE));
                BindingConnection toggle_connection = BindingConnection(
                    InputBinding(toggle_keys.keys[0]), toggle_out, 0, toggle_keys.keys[1]);
                connections.insert(connections.end(), toggle_connection);
                return;
            }
            LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
                        lineCount, line);
            return;
        } else if (output_string == "mouse_movement_params") {
            std::stringstream ss(input_string);
            char comma; // To hold the comma separators between the floats
            ss >> mouse_deadzone_offset >> comma >> mouse_speed >> comma >> mouse_speed_offset;

            // Check for invalid input (in case there's an unexpected format)
            if (ss.fail()) {
                LOG_WARNING(Input, "Failed to parse mouse movement parameters from line: {}", line);
                return;
            }
            SetMouseParams(mouse_deadzone_offset, mouse_speed, mouse_speed_offset);
            return;
        } else if (output_string == "analog_deadzone") {
            std::stringstream ss(input_string);
            std::string device, inner_deadzone_str, outer_deadzone_str;

            if (!std::getline(ss, device, ',') || !std::getline(ss, inner_deadzone_str, ',') ||
                !std::getline(ss, outer_deadzone_str)) {
                LOG_WARNING(Input, "Malformed deadzone config at line {}: \"{}\"", lineCount, line);
                return;
            }

            auto inner_deadzone = parseInt(inner_deadzone_str);
            auto outer_deadzone = parseInt(outer_deadzone_str);

            if (!inner_deadzone || !outer_deadzone) {
                LOG_WARNING(Input, "Invalid deadzone values at line {}: \"{}\"", lineCount, line);
                return;
            }

            std::pair<int, int> deadzone = {*inner_deadzone, *outer_deadzone};

            static std::unordered_map<std::string, std::array<std::pair<int, int>, 4>&>
                deadzone_map = {
                    {"leftjoystick", leftjoystick_deadzone},
                    {"rightjoystick", rightjoystick_deadzone},
                    {"l2", lefttrigger_deadzone},
                    {"r2", righttrigger_deadzone},
                };
            output_gamepad_id = output_gamepad_id == -1 ? 1 : output_gamepad_id;

            if (auto it = deadzone_map.find(device); it != deadzone_map.end()) {
                it->second[output_gamepad_id - 1] = deadzone;
                LOG_DEBUG(Input, "Parsed deadzone: {} {} {}", device, inner_deadzone_str,
                          outer_deadzone_str);
            } else {
                LOG_WARNING(Input, "Invalid axis name at line {}: \"{}\", skipping line.",
                            lineCount, line);
            }
            return;
        } else if (output_string == "override_controller_color") {
            std::stringstream ss(input_string);
            std::string enable, r_s, g_s, b_s;
            std::optional<int> r, g, b;
            if (!std::getline(ss, enable, ',') || !std::getline(ss, r_s, ',') ||
                !std::getline(ss, g_s, ',') || !std::getline(ss, b_s)) {
                LOG_WARNING(Input, "Malformed controller color config at line {}: \"{}\"",
                            lineCount, line);
                return;
            }
            r = parseInt(r_s);
            g = parseInt(g_s);
            b = parseInt(b_s);
            if (!r || !g || !b) {
                LOG_WARNING(Input, "Invalid RGB values at line {}: \"{}\", skipping line.",
                            lineCount, line);
                return;
            }
            output_gamepad_id = output_gamepad_id == -1 ? 1 : output_gamepad_id;
            if (enable == "true") {
                GameControllers::SetControllerCustomColor(output_gamepad_id - 1, *r, *g, *b);
            }
            LOG_DEBUG(Input, "Parsed color settings: {} {} - {} {} {}",
                      enable == "true" ? "override" : "no override", output_gamepad_id, *r, *b, *g);
            return;
        }

        // normal cases
        InputBinding binding = GetBindingFromString(input_string);
        if (binding.IsEmpty()) {
            LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
                        lineCount, line);
            return;
        }
        BindingConnection connection(InputID(), nullptr);
        auto button_it = string_to_cbutton_map.find(output_string);
        auto hotkey_it = string_to_hotkey_map.find(output_string);
        auto axis_it = string_to_axis_map.find(output_string);
        if (button_it != string_to_cbutton_map.end()) {
            connection = BindingConnection(
                binding,
                &*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data,
                                    ControllerOutput(button_it->second)));
        } else if (hotkey_it != string_to_hotkey_map.end()) {
            connection = BindingConnection(
                binding,
                &*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data,
                                    ControllerOutput(hotkey_it->second)));
        } else if (axis_it != string_to_axis_map.end()) {
            int value_to_set = binding.keys[2].type == InputType::Axis ? 0 : axis_it->second.value;
            connection = BindingConnection(
                binding,
                &*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data,
                                    ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID,
                                                     axis_it->second.axis,
                                                     axis_it->second.value >= 0)),
                value_to_set);
        } else {
            LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
                        lineCount, line);
            return;
        }
        // if the input binding contains a controller input, and gamepad ID
        // isn't specified for either inputs or output (both are -1), then multiply the binding and
        // add it to all 4 controllers
        if (connection.HasGamepadInput() && input_gamepad_id == -1 && output_gamepad_id == -1) {
            for (int i = 0; i < output_arrays.size(); i++) {
                BindingConnection copy = connection.CopyWithChangedGamepadId(i + 1);
                copy.output = &*std::ranges::find(output_arrays[i].data, *connection.output);
                connections.push_back(copy);
            }
        } else {
            connections.push_back(connection);
        }
        LOG_DEBUG(Input, "Succesfully parsed line {}", lineCount);
    };
    while (std::getline(global_config_stream, line)) {
        ProcessLine();
    }
    lineCount = 0;
    while (std::getline(config_stream, line)) {
        ProcessLine();
    }
    config_stream.close();
    std::sort(connections.begin(), connections.end());
    for (auto& c : connections) {
        LOG_DEBUG(Input, "Binding: {} : {}", c.output->ToString(), c.binding.ToString());
    }
    LOG_DEBUG(Input, "Done parsing the input config!");
}

BindingConnection BindingConnection::CopyWithChangedGamepadId(u8 gamepad) {
    BindingConnection copy = *this;
    for (auto& key : copy.binding.keys) {
        if (key.type == InputType::Controller || key.type == InputType::Axis) {
            key.gamepad_id = gamepad;
        }
    }
    return copy;
}

u32 GetMouseWheelEvent(const SDL_Event& event) {
    if (event.type != SDL_EVENT_MOUSE_WHEEL && event.type != SDL_EVENT_MOUSE_WHEEL_OFF) {
        LOG_WARNING(Input, "Something went wrong with wheel input parsing!");
        return SDL_UNMAPPED;
    }
    if (event.wheel.y > 0) {
        return SDL_MOUSE_WHEEL_UP;
    } else if (event.wheel.y < 0) {
        return SDL_MOUSE_WHEEL_DOWN;
    } else if (event.wheel.x > 0) {
        return SDL_MOUSE_WHEEL_RIGHT;
    } else if (event.wheel.x < 0) {
        return SDL_MOUSE_WHEEL_LEFT;
    }
    return SDL_UNMAPPED;
}

InputEvent InputBinding::GetInputEventFromSDLEvent(const SDL_Event& e) {
    u8 gamepad = 1;
    switch (e.type) {
    case SDL_EVENT_KEY_DOWN:
    case SDL_EVENT_KEY_UP:
        return InputEvent(InputType::KeyboardMouse, e.key.key, e.key.down, 0);
    case SDL_EVENT_MOUSE_BUTTON_DOWN:
    case SDL_EVENT_MOUSE_BUTTON_UP:
        return InputEvent(InputType::KeyboardMouse, static_cast<u32>(e.button.button),
                          e.button.down, 0);
    case SDL_EVENT_MOUSE_WHEEL:
    case SDL_EVENT_MOUSE_WHEEL_OFF:
        return InputEvent(InputType::KeyboardMouse, GetMouseWheelEvent(e),
                          e.type == SDL_EVENT_MOUSE_WHEEL, 0);
    case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
    case SDL_EVENT_GAMEPAD_BUTTON_UP:
        gamepad = ControllerOutput::controllers.GetGamepadIndexFromJoystickId(e.gbutton.which) + 1;
        return InputEvent({InputType::Controller, (u32)e.gbutton.button, gamepad}, e.gbutton.down,
                          0);
    case SDL_EVENT_GAMEPAD_AXIS_MOTION:
        gamepad = ControllerOutput::controllers.GetGamepadIndexFromJoystickId(e.gaxis.which) + 1;
        return InputEvent({InputType::Axis, (u32)e.gaxis.axis, gamepad}, true, e.gaxis.value / 256);
    default:
        return InputEvent();
    }
}

void ToggleKeyInList(InputID input) {
    if (input.type == InputType::Axis) {
        LOG_ERROR(Input, "Toggling analog inputs is not supported!");
        return;
    }
    auto it = std::find(toggled_keys.begin(), toggled_keys.end(), input);
    if (it == toggled_keys.end()) {
        toggled_keys.insert(toggled_keys.end(), input);
        LOG_DEBUG(Input, "Added {} to toggled keys", input.ToString());
    } else {
        toggled_keys.erase(it);
        LOG_DEBUG(Input, "Removed {} from toggled keys", input.ToString());
    }
}

void ControllerOutput::ResetUpdate() {
    state_changed = false;
    new_button_state = false;
    *new_param = 0; // bruh
}
void ControllerOutput::AddUpdate(InputEvent event) {
    switch (button) {
    case KEY_TOGGLE:
        if (event.active) {
            ToggleKeyInList(event.input);
        }
        return;
    default:
        break;
    }
    if (button != SDL_GAMEPAD_BUTTON_INVALID) {
        if (event.input.type == InputType::Axis) {
            bool temp = event.axis_value * (positive_axis ? 1 : -1) > 0x40;
            new_button_state |= event.active && event.axis_value * (positive_axis ? 1 : -1) > 0x40;
            if (temp) {
                LOG_DEBUG(Input, "Toggled a button from an axis");
            }
        } else {
            new_button_state |= event.active;
        }

    } else if (axis != SDL_GAMEPAD_AXIS_INVALID) {
        *new_param = (event.active ? event.axis_value : 0) + *new_param;
    }
}

void ControllerOutput::FinalizeUpdate(u8 gamepad_index) {
    auto PushSDLEvent = [&](u32 event_type) {
        if (new_button_state) {
            SDL_Event e;
            SDL_memset(&e, 0, sizeof(e));
            e.type = event_type;
            SDL_PushEvent(&e);
        }
    };
    state_changed = old_button_state != new_button_state || old_param != *new_param;
    if (!state_changed) {
        return;
    }
    old_button_state = new_button_state;
    old_param = *new_param;
    GameController* controller;
    if (gamepad_index < 5)
        controller = controllers[gamepad_index];
    else
        UNREACHABLE();
    if (button != SDL_GAMEPAD_BUTTON_INVALID) {
        switch (button) {
        case SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT:
            controller->SetTouchpadState(0, new_button_state, 0.25f, 0.5f);
            controller->Button(SDLGamepadToOrbisButton(button), new_button_state);
            break;
        case SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER:
            controller->SetTouchpadState(0, new_button_state, 0.50f, 0.5f);
            controller->Button(SDLGamepadToOrbisButton(button), new_button_state);
            break;
        case SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT:
            controller->SetTouchpadState(0, new_button_state, 0.75f, 0.5f);
            controller->Button(SDLGamepadToOrbisButton(button), new_button_state);
            break;
        case LEFTJOYSTICK_HALFMODE:
            leftjoystick_halfmode = new_button_state;
            break;
        case RIGHTJOYSTICK_HALFMODE:
            rightjoystick_halfmode = new_button_state;
            break;
        case HOTKEY_RELOAD_INPUTS:
            ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial()));
            break;
        case HOTKEY_FULLSCREEN:
            PushSDLEvent(SDL_EVENT_TOGGLE_FULLSCREEN);
            break;
        case HOTKEY_PAUSE:
            PushSDLEvent(SDL_EVENT_TOGGLE_PAUSE);
            break;
        case HOTKEY_SIMPLE_FPS:
            PushSDLEvent(SDL_EVENT_TOGGLE_SIMPLE_FPS);
            break;
        case HOTKEY_TOGGLE_MOUSE_TO_JOYSTICK:
            PushSDLEvent(SDL_EVENT_MOUSE_TO_JOYSTICK);
            break;
        case HOTKEY_TOGGLE_MOUSE_TO_GYRO:
            PushSDLEvent(SDL_EVENT_MOUSE_TO_GYRO);
            break;
        case HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD:
            PushSDLEvent(SDL_EVENT_MOUSE_TO_TOUCHPAD);
            break;
        case HOTKEY_RENDERDOC:
            PushSDLEvent(SDL_EVENT_RDOC_CAPTURE);
            break;
        case HOTKEY_SCREENSHOT_WITH_OVERLAYS:
            PushSDLEvent(SDL_EVENT_SCREENSHOT_WITH_OVERLAYS);
            break;
        case HOTKEY_ADD_VIRTUAL_USER:
            PushSDLEvent(SDL_EVENT_ADD_VIRTUAL_USER);
            break;
        case HOTKEY_REMOVE_VIRTUAL_USER:
            PushSDLEvent(SDL_EVENT_REMOVE_VIRTUAL_USER);
            break;
        case HOTKEY_VOLUME_UP:
            EmulatorSettings.SetVolumeSlider(
                std::clamp(EmulatorSettings.GetVolumeSlider() + 10, 0, 500));
            Overlay::ShowVolume();
            break;
        case HOTKEY_VOLUME_DOWN:
            EmulatorSettings.SetVolumeSlider(
                std::clamp(EmulatorSettings.GetVolumeSlider() - 10, 0, 500));
            Overlay::ShowVolume();
            break;
        case HOTKEY_QUIT:
            PushSDLEvent(SDL_EVENT_QUIT_DIALOG);
            break;
        case KEY_TOGGLE:
            // noop
            break;
        case MOUSE_GYRO_ROLL_MODE:
            SetMouseGyroRollMode(new_button_state);
            break;
        default: // is a normal key (hopefully)
            controller->Button(SDLGamepadToOrbisButton(button), new_button_state);
            break;
        }
    } else if (axis != SDL_GAMEPAD_AXIS_INVALID && positive_axis) {
        // avoid double-updating axes, but don't skip directional button bindings
        auto ApplyDeadzone = [](s16* value, std::pair<int, int> deadzone) {
            if (std::abs(*value) <= deadzone.first || deadzone.first == deadzone.second) {
                *value = 0;
            } else {
                *value = (*value >= 0 ? 1 : -1) *
                         std::clamp(static_cast<s32>((128.0 * (std::abs(*value) - deadzone.first)) /
                                                     (float)(deadzone.second - deadzone.first)),
                                    0, 128);
            }
        };
        float multiplier = 1.0;
        Axis c_axis = GetAxisFromSDLAxis(axis);
        switch (c_axis) {
        case Axis::LeftX:
        case Axis::LeftY:
            ApplyDeadzone(new_param, leftjoystick_deadzone[gamepad_index]);
            multiplier = leftjoystick_halfmode ? 0.5 : 1.0;
            break;
        case Axis::RightX:
        case Axis::RightY:
            ApplyDeadzone(new_param, rightjoystick_deadzone[gamepad_index]);
            multiplier = rightjoystick_halfmode ? 0.5 : 1.0;
            break;
        case Axis::TriggerLeft:
            ApplyDeadzone(new_param, lefttrigger_deadzone[gamepad_index]);
            controller->Axis(c_axis, GetAxis(0x0, 0x7f, *new_param));
            controller->Button(OrbisPadButtonDataOffset::L2, *new_param > 0x20);
            return;
        case Axis::TriggerRight:
            ApplyDeadzone(new_param, righttrigger_deadzone[gamepad_index]);
            controller->Axis(c_axis, GetAxis(0x0, 0x7f, *new_param));
            controller->Button(OrbisPadButtonDataOffset::R2, *new_param > 0x20);
            return;
        default:
            break;
        }
        controller->Axis(c_axis, GetAxis(-0x80, 0x7f, *new_param * multiplier));
    }
}

// Updates the list of pressed keys with the given input.
// Returns whether the list was updated or not.
bool UpdatePressedKeys(InputEvent event) {
    // Skip invalid inputs
    InputID input = event.input;
    if (input.sdl_id == SDL_UNMAPPED) {
        return false;
    }
    if (input.type == InputType::Axis) {
        // analog input, it gets added when it first sends an event,
        // and from there, it only changes the parameter
        auto it = std::lower_bound(
            pressed_keys.begin(), pressed_keys.end(), input,
            [](const std::pair<InputEvent, bool>& e, InputID i) { return e.first.input < i; });
        if (it == pressed_keys.end() || it->first.input != input) {
            pressed_keys.insert(it, {event, false});
            LOG_DEBUG(Input, "Added axis {} to the input list", event.input.sdl_id);
        } else {
            // noise filter
            if (std::abs(it->first.axis_value - event.axis_value) <= 1) {
                return false;
            }
            it->first.axis_value = event.axis_value;
        }
        return true;
    } else if (event.active) {
        // Find the correct position for insertion to maintain order
        auto it = std::lower_bound(pressed_keys.begin(), pressed_keys.end(), input,
                                   [](const std::pair<InputEvent, bool>& e, InputID i) {
                                       return std::tie(e.first.input.type, e.first.input.sdl_id) <
                                              std::tie(i.type, i.sdl_id);
                                   });

        // Insert only if 'value' is not already in the list
        if (it == pressed_keys.end() || it->first.input != input) {
            pressed_keys.insert(it, {event, false});
            return true;
        }
    } else {
        // Remove 'value' from the list if it's not pressed
        auto it = std::find_if(
            pressed_keys.begin(), pressed_keys.end(),
            [input](const std::pair<InputEvent, bool>& e) { return e.first.input == input; });
        if (it != pressed_keys.end()) {
            pressed_keys.erase(it);
            return true;
        }
    }
    LOG_DEBUG(Input, "No change was made!");
    return false;
}
// Check if the binding's all keys are currently active.
// It also extracts the analog inputs' parameters, and updates the input hierarchy flags.
InputEvent BindingConnection::ProcessBinding() {
    // the last key is always set (if the connection isn't empty),
    // and the analog inputs are always the last one due to how they are sorted,
    // so this signifies whether or not the input is analog
    InputEvent event = InputEvent(binding.keys[0]);
    if (pressed_keys.empty()) {
        return event;
    }
    if (event.input.type != InputType::Axis) {
        // for button inputs
        event.axis_value = axis_param;
    }
    // it's a bit scuffed, but if the output is a toggle, then we put the key here
    if (output->button == KEY_TOGGLE) {
        event.input = toggle;
    }

    // Extract keys from InputBinding and ignore unused or toggled keys
    std::list<InputID> input_keys = {binding.keys[0], binding.keys[1], binding.keys[2]};
    input_keys.remove(InputID());
    for (auto key = input_keys.begin(); key != input_keys.end();) {
        if (std::find(toggled_keys.begin(), toggled_keys.end(), *key) != toggled_keys.end()) {
            key = input_keys.erase(key); // Use the returned iterator
        } else {
            ++key; // Increment only if no erase happened
        }
    }
    if (input_keys.empty()) {
        LOG_DEBUG(Input, "No actual inputs to check, returning true");
        event.active = true;
        return event;
    }

    // Iterator for pressed_keys, starting from the beginning
    auto pressed_it = pressed_keys.begin();

    // Store pointers to flags in pressed_keys that need to be set if all keys are active
    std::list<bool*> flags_to_set;

    // Check if all keys in input_keys are active
    for (InputID key : input_keys) {
        bool key_found = false;

        while (pressed_it != pressed_keys.end()) {
            if (pressed_it->first.input == key && (pressed_it->second == false)) {
                key_found = true;
                if (output->positive_axis) {
                    flags_to_set.push_back(&pressed_it->second);
                }
                if (pressed_it->first.input.type == InputType::Axis) {
                    event.axis_value = pressed_it->first.axis_value;
                }
                ++pressed_it;
                break;
            }
            ++pressed_it;
        }
        if (!key_found) {
            return event;
        }
    }

    for (bool* flag : flags_to_set) {
        *flag = true;
    }
    if (binding.keys[0].type != InputType::Axis) { // the axes spam inputs, making this unreadable
        LOG_DEBUG(Input, "Input found: {}", binding.ToString());
    }
    event.active = true;
    return event; // All keys are active
}

void ActivateOutputsFromInputs() {

    // todo find a better solution
    for (int i = 0; i < output_arrays.size(); i++) {

        // Reset values and flags
        for (auto& it : pressed_keys) {
            it.second = false;
        }
        for (auto& it : output_arrays[i].data) {
            it.ResetUpdate();
        }

        // Check for input blockers
        ApplyMouseInputBlockers();

        // Iterate over all inputs, and update their respecive outputs accordingly
        for (auto& it : connections) {
            // only update this when it's the correct pass
            if (it.output->gamepad_id == i) {
                it.output->AddUpdate(it.ProcessBinding());
            }
        }

        // Update all outputs
        for (auto& it : output_arrays[i].data) {
            it.FinalizeUpdate(i);
        }
    }
}

} // namespace Input
