/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the Qt Gamepad module
**
** $QT_BEGIN_LICENSE:LGPL3$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPLv3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or later as published by the Free
** Software Foundation and appearing in the file LICENSE.GPL included in
** the packaging of this file. Please review the following information to
** ensure the GNU General Public License version 2.0 requirements will be
** met: http://www.gnu.org/licenses/gpl-2.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qxinputgamepadbackend_p.h"
#include <QtCore/QLoggingCategory>
#include <QtCore/QThread>
#include <qmath.h>
#include <windows.h>

QT_BEGIN_NAMESPACE

Q_LOGGING_CATEGORY(lcXGB, "qt.gamepad")

#define POLL_SLEEP_MS 5
#define POLL_SLOT_CHECK_MS 4000

#define XUSER_MAX_COUNT 4

#define XINPUT_GAMEPAD_DPAD_UP        0x0001
#define XINPUT_GAMEPAD_DPAD_DOWN      0x0002
#define XINPUT_GAMEPAD_DPAD_LEFT      0x0004
#define XINPUT_GAMEPAD_DPAD_RIGHT     0x0008
#define XINPUT_GAMEPAD_START          0x0010
#define XINPUT_GAMEPAD_BACK           0x0020
#define XINPUT_GAMEPAD_LEFT_THUMB     0x0040
#define XINPUT_GAMEPAD_RIGHT_THUMB    0x0080
#define XINPUT_GAMEPAD_LEFT_SHOULDER  0x0100
#define XINPUT_GAMEPAD_RIGHT_SHOULDER 0x0200
#define XINPUT_GAMEPAD_A              0x1000
#define XINPUT_GAMEPAD_B              0x2000
#define XINPUT_GAMEPAD_X              0x4000
#define XINPUT_GAMEPAD_Y              0x8000

#define XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE  7849
#define XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE 8689
#define XINPUT_GAMEPAD_TRIGGER_THRESHOLD    30

struct XINPUT_GAMEPAD
{
    unsigned short wButtons;
    unsigned char bLeftTrigger;
    unsigned char bRightTrigger;
    short sThumbLX;
    short sThumbLY;
    short sThumbRX;
    short sThumbRY;
};

struct XINPUT_STATE
{
    unsigned long dwPacketNumber;
    XINPUT_GAMEPAD Gamepad;
};

typedef DWORD (WINAPI *XInputGetState_t)(DWORD dwUserIndex, XINPUT_STATE *pState);
static XInputGetState_t XInputGetState;

class QXInputThread : public QThread
{
public:
    QXInputThread(QXInputGamepadBackend *backend);
    void run() override;
    void signalQuit() { m_quit.fetchAndStoreAcquire(1); }

private:
    void dispatch(int idx, XINPUT_GAMEPAD *state);

    QXInputGamepadBackend *m_backend;
    QAtomicInt m_quit;
    struct Controller {
        bool connected;
        int skippedPolls;
        unsigned long lastPacketNumber;
        // State cache. Only want to emit signals for values that really change.
        unsigned short buttons;
        unsigned char triggers[2];
        double axis[2][2];
    } m_controllers[XUSER_MAX_COUNT];
};

QXInputThread::QXInputThread(QXInputGamepadBackend *backend)
    : m_backend(backend),
      m_quit(false)
{
    memset(m_controllers, 0, sizeof(m_controllers));
}

void QXInputThread::dispatch(int idx, XINPUT_GAMEPAD *state)
{
    static const struct ButtonMap {
        unsigned short xbutton;
        QGamepadManager::GamepadButton qbutton;
    } buttonMap[] = {
        { XINPUT_GAMEPAD_DPAD_UP, QGamepadManager::ButtonUp },
        { XINPUT_GAMEPAD_DPAD_DOWN, QGamepadManager::ButtonDown },
        { XINPUT_GAMEPAD_DPAD_LEFT, QGamepadManager::ButtonLeft },
        { XINPUT_GAMEPAD_DPAD_RIGHT, QGamepadManager::ButtonRight },
        { XINPUT_GAMEPAD_START, QGamepadManager::ButtonStart },
        { XINPUT_GAMEPAD_BACK, QGamepadManager::ButtonSelect },
        { XINPUT_GAMEPAD_LEFT_SHOULDER, QGamepadManager::ButtonL1 },
        { XINPUT_GAMEPAD_RIGHT_SHOULDER, QGamepadManager::ButtonR1 },
        { XINPUT_GAMEPAD_LEFT_THUMB, QGamepadManager::ButtonL3 },
        { XINPUT_GAMEPAD_RIGHT_THUMB, QGamepadManager::ButtonR3 },
        { XINPUT_GAMEPAD_A, QGamepadManager::ButtonA },
        { XINPUT_GAMEPAD_B, QGamepadManager::ButtonB },
        { XINPUT_GAMEPAD_X, QGamepadManager::ButtonX },
        { XINPUT_GAMEPAD_Y, QGamepadManager::ButtonY }
    };
    for (uint i = 0; i < sizeof(buttonMap) / sizeof(ButtonMap); ++i) {
        const unsigned short xb = buttonMap[i].xbutton;
        unsigned short isDown = state->wButtons & xb;
        if (isDown != (m_controllers[idx].buttons & xb)) {
            if (isDown) {
                m_controllers[idx].buttons |= xb;
                emit m_backend->gamepadButtonPressed(idx, buttonMap[i].qbutton, 1);
            } else {
                m_controllers[idx].buttons &= ~xb;
                emit m_backend->gamepadButtonReleased(idx, buttonMap[i].qbutton);
            }
        }
    }

    if (m_controllers[idx].triggers[0] != state->bLeftTrigger) {
        m_controllers[idx].triggers[0] = state->bLeftTrigger;
        const double value = state->bLeftTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD
                ? (state->bLeftTrigger - XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
                  / (255.0 - XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
                : 0.0;
        if (!qFuzzyIsNull(value))
            emit m_backend->gamepadButtonPressed(idx, QGamepadManager::ButtonL2, value);
        else
            emit m_backend->gamepadButtonReleased(idx, QGamepadManager::ButtonL2);
    }
    if (m_controllers[idx].triggers[1] != state->bRightTrigger) {
        m_controllers[idx].triggers[1] = state->bRightTrigger;
        const double value = state->bRightTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD
                ? (state->bRightTrigger - XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
                  / (255.0 - XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
                : 0.0;
        if (!qFuzzyIsNull(value))
            emit m_backend->gamepadButtonPressed(idx, QGamepadManager::ButtonR2, value);
        else
            emit m_backend->gamepadButtonReleased(idx, QGamepadManager::ButtonR2);
    }

    double x, y;
    if (qSqrt(state->sThumbLX * state->sThumbLX + state->sThumbLY * state->sThumbLY) > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) {
        x = 2 * (state->sThumbLX + 32768.0) / 65535.0 - 1.0;
        y = 2 * (-state->sThumbLY + 32768.0) / 65535.0 - 1.0;
    } else {
        x = y = 0;
    }
    if (m_controllers[idx].axis[0][0] != x) {
        m_controllers[idx].axis[0][0] = x;
        emit m_backend->gamepadAxisMoved(idx, QGamepadManager::AxisLeftX, x);
    }
    if (m_controllers[idx].axis[0][1] != y) {
        m_controllers[idx].axis[0][1] = y;
        emit m_backend->gamepadAxisMoved(idx, QGamepadManager::AxisLeftY, y);
    }
    if (qSqrt(state->sThumbRX * state->sThumbRX + state->sThumbRY * state->sThumbRY) > XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) {
        x = 2 * (state->sThumbRX + 32768.0) / 65535.0 - 1.0;
        y = 2 * (-state->sThumbRY + 32768.0) / 65535.0 - 1.0;
    } else {
        x = y = 0;
    }
    if (m_controllers[idx].axis[1][0] != x) {
        m_controllers[idx].axis[1][0] = x;
        emit m_backend->gamepadAxisMoved(idx, QGamepadManager::AxisRightX, x);
    }
    if (m_controllers[idx].axis[1][1] != y) {
        m_controllers[idx].axis[1][1] = y;
        emit m_backend->gamepadAxisMoved(idx, QGamepadManager::AxisRightY, y);
    }
}

void QXInputThread::run()
{
    qCDebug(lcXGB, "XInput thread running");
    bool firstPoll = true;
    while (!m_quit.testAndSetAcquire(1, 0)) {
        for (int i = 0; i < XUSER_MAX_COUNT; ++i) {
            Controller *controller = m_controllers + i;

            if (!firstPoll && !controller->connected && controller->skippedPolls < POLL_SLOT_CHECK_MS / POLL_SLEEP_MS) {
                controller->skippedPolls++;
                continue;
            }

            firstPoll = false;
            controller->skippedPolls = 0;
            XINPUT_STATE state;
            memset(&state, 0, sizeof(state));

            if (XInputGetState(i, &state) == ERROR_SUCCESS) {
                if (controller->connected) {
                    if (controller->lastPacketNumber != state.dwPacketNumber) {
                        controller->lastPacketNumber = state.dwPacketNumber;
                        dispatch(i, &state.Gamepad);
                    }
                } else {
                    controller->connected = true;
                    controller->lastPacketNumber = state.dwPacketNumber;
                    emit m_backend->gamepadAdded(i);
                    dispatch(i, &state.Gamepad);
                }
            } else {
                if (controller->connected) {
                    controller->connected = false;
                    emit m_backend->gamepadRemoved(i);
                }
            }
        }

        Sleep(POLL_SLEEP_MS);
    }
    qCDebug(lcXGB, "XInput thread stopping");
}

QXInputGamepadBackend::QXInputGamepadBackend()
    : m_thread(0)
{
}

bool QXInputGamepadBackend::start()
{
    qCDebug(lcXGB) << "start";

    m_lib.setFileName(QStringLiteral("xinput1_4.dll"));
    if (!m_lib.load()) {
        m_lib.setFileName(QStringLiteral("xinput1_3.dll"));
        m_lib.load();
    }

    if (m_lib.isLoaded()) {
        qCDebug(lcXGB, "Loaded XInput library %s", qPrintable(m_lib.fileName()));
        XInputGetState = (XInputGetState_t) m_lib.resolve("XInputGetState");
        if (XInputGetState) {
            m_thread = new QXInputThread(this);
            m_thread->start();
        } else {
            qWarning("Failed to resolve XInputGetState");
        }
    } else {
        qWarning("Failed to load XInput library %s", qPrintable(m_lib.fileName()));
    }

    return m_lib.isLoaded();
}

void QXInputGamepadBackend::stop()
{
    qCDebug(lcXGB) << "stop";
    m_thread->signalQuit();
    m_thread->wait();
    XInputGetState = 0;
}

QT_END_NAMESPACE
