/*
 * Rosalie's Mupen GUI - https://github.com/Rosalie241/RMG
 *  Copyright (C) 2020-2025 Rosalie Wanders <rosalie@mailbox.org>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 3.
 *  You should have received a copy of the GNU General Public License
 *  along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
#define INPUT_PLUGIN_API_VERSION 0x020100

#define M64P_PLUGIN_PROTOTYPES 1
#include <RMG-Core/m64p/api/m64p_common.h>
#include <RMG-Core/m64p/api/m64p_plugin.h>
#include <RMG-Core/m64p/api/m64p_custom.h>
#include <RMG-Core/m64p/api/m64p_types.h>

#include <RMG-Core/Settings.hpp>
#include <libusb.h>

#include "UserInterface/MainDialog.hpp"

#include <atomic>
#include <chrono>
#include <thread>
#include <array>
#include <mutex>

//
// Local Defines
//

#define NUM_CONTROLLERS    4
#define N64_AXIS_PEAK      85

#define GCA_VENDOR_ID  0x057e
#define GCA_PRODUCT_ID 0x0337

#define GCA_ENDPOINT_IN  0x81
#define GCA_ENDPOINT_OUT 0x02

#define GCA_COMMAND_POLL 0x13

//
// Local Structures
//

struct SettingsProfile
{
    double DeadzoneValue = 0.09;
    double SensitivityValue = 1.0;

    double TriggerTreshold = 0.5;
    double CButtonTreshold = 0.4;

    bool SwapZL = false;
};

struct GameCubeAdapterControllerState
{
    uint8_t Status;

    union
    {
        uint8_t Buttons1;
        struct
        {
            bool A : 1;
            bool B : 1;
            bool X : 1;
            bool Y : 1;

            bool DpadLeft  : 1;
            bool DpadRight : 1;
            bool DpadDown  : 1;
            bool DpadUp    : 1;
        };
    };
    

    union
    {
        uint8_t Buttons2;
        struct
        {
            bool Start : 1;
            bool Z : 1;
            bool R : 1;
            bool L : 1;
        };
    };

    uint8_t LeftStickX;
    uint8_t LeftStickY;
    uint8_t RightStickX;
    uint8_t RightStickY;

    uint8_t LeftTrigger;
    uint8_t RightTrigger;
};

//
// Local variables
//

// libusb variables
static bool l_UsbInitialized = false;

// GCA variables
static libusb_device_handle* l_DeviceHandle = nullptr;
static std::atomic<bool> l_PollThreadRunning;
static std::atomic<bool> l_PolledState;
static std::mutex l_ControllerStateMutex;
static std::array<GameCubeAdapterControllerState, 4> l_ControllerState;
static std::thread l_PollThread;
static SettingsProfile l_Settings = {0};

// mupen64plus debug callback
static void (*l_DebugCallback)(void *, int, const char *) = nullptr;
static void *l_DebugCallContext                           = nullptr;

//
// Custom Internal Plugin Functions
//

void PluginDebugMessage(int level, std::string message)
{
    if (l_DebugCallback == nullptr)
    {
        return;
    }

    l_DebugCallback(l_DebugCallContext, level, message.c_str());
}

//
// Internal Functions
//

static bool usb_init(void)
{
    std::string debugMessage;

    int ret = libusb_init(nullptr);
    if (ret != LIBUSB_SUCCESS)
    {
        debugMessage = "usb_init(): failed to initialize libusb: ";
        debugMessage += libusb_error_name(ret);
        PluginDebugMessage(M64MSG_ERROR, debugMessage);
        return false;
    }

    l_UsbInitialized = true;
    return true;
}

static void usb_quit(void)
{
    if (l_UsbInitialized)
    {
        libusb_exit(nullptr);
        l_UsbInitialized = false;
    }
}

static void gca_reset_state(void)
{
    l_ControllerStateMutex.lock();
    l_ControllerState = {0};
    l_ControllerStateMutex.unlock();
}

static bool gca_init(void)
{
    std::string debugMessage;
    int ret;

    // reset state
    gca_reset_state();
    l_PolledState.store(false);
    l_PollThreadRunning.store(true);

    // attempt open device
    l_DeviceHandle = libusb_open_device_with_vid_pid(nullptr, GCA_VENDOR_ID, GCA_PRODUCT_ID);
    if (l_DeviceHandle == nullptr)
    {
        debugMessage = "gca_init(): failed to open adapter!";
        PluginDebugMessage(M64MSG_ERROR, debugMessage);
        return false;
    }

    // according to dolphin, this makes
    // the Nyko-brand adapters work
    libusb_control_transfer(l_DeviceHandle, 0x21, 11, 0x0001, 0, nullptr, 0, 1000);

    // only detach kernel driver when required
    if (libusb_kernel_driver_active(l_DeviceHandle, 0) == 1)
    {
        ret = libusb_detach_kernel_driver(l_DeviceHandle, 0);
        if (ret != LIBUSB_SUCCESS)
        {
            libusb_close(l_DeviceHandle);
            debugMessage = "gca_init(): failed to detach kernel driver: ";
            debugMessage += libusb_error_name(ret);
            PluginDebugMessage(M64MSG_ERROR, debugMessage);
            return false;
        }
    }

    ret = libusb_claim_interface(l_DeviceHandle, 0);
    if (ret != LIBUSB_SUCCESS)
    {
        libusb_close(l_DeviceHandle);
        debugMessage = "gca_init(): failed to claim interface: ";
        debugMessage += libusb_error_name(ret);
        PluginDebugMessage(M64MSG_ERROR, debugMessage);
        return false;
    }

    // attempt to begin polling
    uint8_t cmd = GCA_COMMAND_POLL;
    ret = libusb_interrupt_transfer(l_DeviceHandle, GCA_ENDPOINT_OUT, &cmd, sizeof(cmd), nullptr, 16);
    if (ret != LIBUSB_SUCCESS)
    {
        libusb_release_interface(l_DeviceHandle, 0);
        libusb_close(l_DeviceHandle);
        debugMessage = "gca_init(): failed to send polling cmd: ";
        debugMessage += libusb_error_name(ret);
        PluginDebugMessage(M64MSG_ERROR, debugMessage);
        return false;
    }

    debugMessage = "gca_init(): successfully opened adapter";
    PluginDebugMessage(M64MSG_INFO, debugMessage);
    return true;
}

static void gca_quit(void)
{
    if (l_DeviceHandle != nullptr)
    {
        libusb_release_interface(l_DeviceHandle, 0);
        libusb_close(l_DeviceHandle);
        l_DeviceHandle = nullptr;
    }
}

static void gca_poll_thread(void)
{
    uint8_t readBuf[37] = {0};
    int transferred = 0;
    int ret;
    int offset;
    std::array<GameCubeAdapterControllerState, 4> state;
    std::string debugMessage;

    while (l_PollThreadRunning.load(std::memory_order_relaxed))
    {
        ret = libusb_interrupt_transfer(l_DeviceHandle, GCA_ENDPOINT_IN, readBuf, sizeof(readBuf), &transferred, 16);
        if (ret == LIBUSB_ERROR_NO_DEVICE)
        {
            debugMessage = "gca_poll_thread(): adapter disconnected, stopping polling thread";
            PluginDebugMessage(M64MSG_WARNING, debugMessage);

            // reset state
            gca_reset_state();

            // ensure that we don't get stuck in InitiateControllers(),
            // because that might be waiting on l_PolledState to be set
            l_PolledState.store(true);
            return;
        }
        else if (ret != LIBUSB_SUCCESS || transferred != sizeof(readBuf))
        {
            debugMessage = "gca_poll_thread(): failed to retrieve input buffer: ";
            debugMessage += libusb_error_name(ret);
            PluginDebugMessage(M64MSG_WARNING, debugMessage);
            continue;
        }

        for (int i = 0; i < NUM_CONTROLLERS; i++)
        {
            offset = (i * 9);
            state[i].Status       = readBuf[offset + 1];
            state[i].Buttons1     = readBuf[offset + 2];
            state[i].Buttons2     = readBuf[offset + 3];
            state[i].LeftStickX   = readBuf[offset + 4];
            state[i].LeftStickY   = readBuf[offset + 5];
            state[i].RightStickX  = readBuf[offset + 6];
            state[i].RightStickY  = readBuf[offset + 7];
            state[i].LeftTrigger  = readBuf[offset + 8];
            state[i].RightTrigger = readBuf[offset + 9];
        }

        l_ControllerStateMutex.lock();
        l_ControllerState = state;
        l_ControllerStateMutex.unlock();

        l_PolledState.store(true, std::memory_order_relaxed);

        // poll every 1ms
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
}

static void load_settings(void)
{
    l_Settings.DeadzoneValue = static_cast<double>(CoreSettingsGetIntValue(SettingsID::GCAInput_Deadzone)) / 100.0;
    l_Settings.SensitivityValue = static_cast<double>(CoreSettingsGetIntValue(SettingsID::GCAInput_Sensitivity)) / 100.0;
    l_Settings.CButtonTreshold = static_cast<double>(CoreSettingsGetIntValue(SettingsID::GCAInput_CButtonTreshold)) / 100.0;
    l_Settings.TriggerTreshold = static_cast<double>(CoreSettingsGetIntValue(SettingsID::GCAInput_TriggerTreshold)) / 100.0;
    l_Settings.SwapZL = CoreSettingsGetBoolValue(SettingsID::GCAInput_SwapZL);
}

static double apply_deadzone(const double input, const double deadzone)
{
    const double inputAbsolute = std::abs(input) / INT8_MAX;

    if (inputAbsolute <= deadzone)
    {
        return 0;
    }

    return input;
}

//
// Basic Plugin Functions
//

EXPORT m64p_error CALL PluginStartup(m64p_dynlib_handle CoreLibHandle, void *Context, void (*DebugCallback)(void *, int, const char *))
{
    // setup debug callback
    l_DebugCallback    = DebugCallback;
    l_DebugCallContext = Context;

    if (!usb_init())
    {
        return M64ERR_SYSTEM_FAIL;
    }

    load_settings();
    return M64ERR_SUCCESS;
}

EXPORT m64p_error CALL PluginShutdown(void)
{
    // clear debug callback
    l_DebugCallback    = nullptr;
    l_DebugCallContext = nullptr;

    usb_quit();

    return M64ERR_SUCCESS;
}

EXPORT m64p_error CALL PluginGetVersion(m64p_plugin_type *pluginType, int *pluginVersion, 
    int *apiVersion, const char **pluginNamePtr, int *capabilities)
{
    if (pluginType != nullptr)
    {
        *pluginType = M64PLUGIN_INPUT;
    }

    if (pluginVersion != nullptr)
    {
        *pluginVersion = 0x010000;
    }

    if (apiVersion != nullptr)
    {
        *apiVersion = INPUT_PLUGIN_API_VERSION;
    }

    if (pluginNamePtr != nullptr)
    {
        *pluginNamePtr = "Rosalie's Mupen GUI - GameCube Adapter Input Plugin";
    }

    if (capabilities != nullptr)
    {
        *capabilities = 0;
    }

    return M64ERR_SUCCESS;
}

//
// Custom Plugin Functions
//

EXPORT m64p_error CALL PluginConfig(void* parent)
{
    UserInterface::MainDialog dialog((QWidget*)parent);
    dialog.exec();

    // re-load settings
    load_settings();

    return M64ERR_SUCCESS;
}

//
// Input Plugin Functions
//

EXPORT void CALL ControllerCommand(int Control, unsigned char* Command)
{
}

EXPORT void CALL GetKeys(int Control, BUTTONS* Keys)
{
    l_ControllerStateMutex.lock();
    GameCubeAdapterControllerState state = l_ControllerState[Control];
    l_ControllerStateMutex.unlock();

    if (!state.Status)
    {
        Keys->Value = 0;
        return;
    }

    Keys->A_BUTTON = state.A;
    Keys->B_BUTTON = state.B;
    Keys->START_BUTTON = state.Start;
    Keys->L_DPAD = state.DpadLeft;
    Keys->R_DPAD = state.DpadRight;
    Keys->D_DPAD = state.DpadDown;
    Keys->U_DPAD = state.DpadUp;

    const int triggerTreshold = INT8_MAX * l_Settings.TriggerTreshold;
    const int cStickTreshold  = INT8_MAX * l_Settings.CButtonTreshold;
    const int8_t cX = static_cast<int8_t>(state.RightStickX + 128);
    const int8_t cY = static_cast<int8_t>(state.RightStickY + 128);

    Keys->R_TRIG = state.RightTrigger > triggerTreshold;
    Keys->L_TRIG = l_Settings.SwapZL ? state.Z : (state.LeftTrigger > triggerTreshold);
    Keys->Z_TRIG = l_Settings.SwapZL ? (state.LeftTrigger > triggerTreshold) : state.Z;

    Keys->L_CBUTTON = cX < -cStickTreshold;
    Keys->R_CBUTTON = cX > cStickTreshold;
    Keys->U_CBUTTON = cY > cStickTreshold;
    Keys->D_CBUTTON = cY < -cStickTreshold;

    const int8_t x = static_cast<int8_t>(state.LeftStickX + 128);
    const int8_t y = static_cast<int8_t>(state.LeftStickY + 128);

    double modX = (static_cast<double>(x) / static_cast<double>(INT8_MAX)) * N64_AXIS_PEAK * l_Settings.SensitivityValue;
    double modY = (static_cast<double>(y) / static_cast<double>(INT8_MAX)) * N64_AXIS_PEAK * l_Settings.SensitivityValue;

    modX = apply_deadzone(modX, l_Settings.DeadzoneValue);
    modY = apply_deadzone(modY, l_Settings.DeadzoneValue);

    Keys->X_AXIS = static_cast<int>(modX);
    Keys->Y_AXIS = static_cast<int>(modY);
}

EXPORT void CALL InitiateControllers(CONTROL_INFO ControlInfo)
{
    if (!gca_init())
    {
        return;
    }

    // start polling thread
    l_PollThread = std::thread(gca_poll_thread);

    // wait for initial state to be polled
    while (!l_PolledState.load())
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
    }

    l_ControllerStateMutex.lock();
    for (int i = 0; i < NUM_CONTROLLERS; i++)
    {
        GameCubeAdapterControllerState state = l_ControllerState[i];
        ControlInfo.Controls[i].Present = (state.Status > 0) ? 1 : 0;
    }
    l_ControllerStateMutex.unlock();

    // load settings
    load_settings();
}

EXPORT void CALL ReadController(int Control, unsigned char *Command)
{
}

EXPORT int CALL RomOpen(void)
{
    return 1;
}

EXPORT void CALL RomClosed(void)
{
    // wait for polling thread to stop
    if (l_PollThread.joinable())
    {
        l_PollThreadRunning.store(false);
        l_PollThread.join();
    }

    gca_quit();
}

EXPORT void CALL SDL_KeyDown(int keymod, int keysym)
{
}

EXPORT void CALL SDL_KeyUp(int keymod, int keysym)
{
}
