// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

/*
  Originally based on code from "core/input/input.cpp" from Godot Engine v4.0
  Copyright (c) 2014-present Godot Engine contributors
  Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
*/

#include "quniversalinput.h"
#include "quniversalinput_p.h"
#include "qjoystickinput_p.h"
#include "qjoystickinputfactory_p.h"
#include "qjoydevicemappingparser_p.h"
#include "qmouseinput_p.h"
#include "qmouseinputfactory_p.h"

#include <QDateTime>

QT_BEGIN_NAMESPACE

static JoyAxis _combine_device(JoyAxis p_value, int p_device) {
    return JoyAxis((int)p_value | (p_device << 20));
}

static JoyButton _combine_device(JoyButton p_value, int p_device) {
    return JoyButton((int)p_value | (p_device << 20));
}

static QByteArray _hex_str(quint8 p_byte) {
    static const char *dict = "0123456789abcdef";
    char ret[3];
    ret[2] = 0;

    ret[0] = dict[p_byte >> 4];
    ret[1] = dict[p_byte & 0xf];

    return QByteArray(ret);
}

QUniversalInputPrivate::QUniversalInputPrivate() {

}

QUniversalInputPrivate::~QUniversalInputPrivate()
{
    delete joystickInput;
}

void QUniversalInputPrivate::_q_init()
{
    loadMappingDatabase();

    QStringList keys = QJoystickInputFactory::keys();
    if (!keys.isEmpty())
        joystickInput = QJoystickInputFactory::create(keys.first(), QStringList());

    // If we fail to load a plugin, create a dummy joystick input
    if (!joystickInput)
        joystickInput = new QJoystickInput();

    keys = QMouseInputFactory::keys();
    if (!keys.isEmpty())
        mouseInput = QMouseInputFactory::create(keys.first(), QStringList());

    // If we fail to load a plugin, create a dummy mouse input
    if (!mouseInput)
        mouseInput = new QMouseInput();
}

void QUniversalInputPrivate::loadMappingDatabase()
{
    QJoyDeviceMappingParser parser(QString::fromUtf8(":/qt-project.org/qtuniversalinput/gamecontrollerdb.txt"));
    auto mapping = parser.next();
    for (; mapping.has_value(); mapping = parser.next())
        mappingDatabase.push_back(mapping.value());
}

void QUniversalInput::VelocityTrack::update(const QVector2D &valueDelta) {
    float delta_t = frameTimer.restart() / 1000.0f;

    if (delta_t > maxRefFrame) {
        // First movement in a long time, reset and start again.
        velocity = QVector2D();
        accum = valueDelta;
        accumTime = 0.0f;
        return;
    }

    accum += valueDelta;
    accumTime += delta_t;

    if (accumTime < minRefFrame) {
        // Not enough time has passed to calculate speed precisely.
        return;
    }

    velocity = accum / accumTime;
    accum = QVector2D();
    accumTime = 0.0f;
}

void QUniversalInput::VelocityTrack::reset()
{
    frameTimer.restart();
    velocity = QVector2D();
    accum = QVector2D();
    accumTime = 0.0f;
}

QUniversalInput::VelocityTrack::VelocityTrack() {
    minRefFrame = 0.1f;
    maxRefFrame = 3.0f;
    frameTimer.start();
    reset();
}

void QUniversalInput::loadPlugins(){
    Q_D(QUniversalInput);
    d->_q_init();
}

QUniversalInput::QUniversalInput()
    : QObject(*new QUniversalInputPrivate(), nullptr)
{
    // We need to delay the plugin loading until the event loop is running
    QMetaObject::invokeMethod(this, "loadPlugins", Qt::QueuedConnection);
}

QUniversalInput::~QUniversalInput()
{
}

QUniversalInput *QUniversalInput::instance()
{
    static QUniversalInput instance;
    return &instance;
}

QString QUniversalInput::getJoyName(int device) const
{
    Q_D(const QUniversalInput);
    // If the device does not exist, an empty Joypad
    // Struct should be returned, so name will be empty
    const auto joypad = d->joypadNames[device];
    return joypad.name;
}

bool QUniversalInput::isJoyConnected(int device) const
{
    Q_D(const QUniversalInput);
    // If the device does not exist, an empty Joypad
    // Struct should be returned
    const auto joypad = d->joypadNames[device];
    return joypad.isConnected;
}

bool QUniversalInput::isGamepad(int device) const
{
    Q_D(const QUniversalInput);
    // If the device does not exist, an empty Joypad
    // Struct should be returned
    const auto joypad = d->joypadNames[device];
    // If a device has a mapping, it is a gamepad
    return joypad.mapping != -1;
}

int QUniversalInput::getUnusedJoyId() {
    Q_D(QUniversalInput);
    for (int i = 0; i < JoypadsMax; i++)
        if (!d->joypadNames.contains(i) || !d->joypadNames[i].isConnected)
            return i;
    return -1;
}

void QUniversalInput::updateJoyConnection(int index, bool isConnected, const QString &name, const QString &guid) {
    Q_D(QUniversalInput);
    QMutexLocker locker(&d->mutex);

    Joypad js;
    js.name = isConnected ? name : QString();
    js.uid = isConnected ? guid : QString();

    if (isConnected) {
        QByteArray uidname = guid.toLocal8Bit();
        if (guid.isEmpty()) {
            int uidlen = int(qMin(name.length(), 16LL));
            QByteArray localName = name.toLocal8Bit();
            for (int i = 0; i < uidlen; i++)
                uidname = uidname + _hex_str(localName[i]);
        }
        js.uid = QString::fromLocal8Bit(uidname);
        js.isConnected = true;
        int mapping = d->fallbackMapping;
        for (int i = 0; i < d->mappingDatabase.size(); i++) {
            if (js.uid == d->mappingDatabase[i].uid) {
                mapping = i;
                js.name = d->mappingDatabase[i].name;
            }
        }
        js.mapping = mapping;
    } else {
        js.isConnected = false;
        for (int i = 0; i < (int)JoyButton::MAX; i++) {
            JoyButton c = _combine_device((JoyButton)i, index);
            d->joystickButtonsPressed.remove(c);
        }
        for (int i = 0; i < (int)JoyAxis::MAX; i++)
            setJoyAxis(index, (JoyAxis)i, 0.0f);

    }
    d->joypadNames[index] = js;

    Q_EMIT joyConnectionChanged(index, isConnected);
}

void QUniversalInput::joyButton(int device, JoyButton button, bool isPressed) {
    Q_D(QUniversalInput);
    QMutexLocker locker(&d->mutex);

    Joypad &joy = d->joypadNames[device];
    Q_ASSERT(int(button) < int(JoyButton::MAX));

    if (joy.lastButtons[size_t(button)] == isPressed)
        return;

    joy.lastButtons[size_t(button)] = isPressed;
    if (joy.mapping == -1) {
        sendButtonEvent(device, button, isPressed);
        return;
    }

    JoyEvent map = mappedButtonEvent(d->mappingDatabase[joy.mapping], button);

    if (map.type == TypeButton) {
        sendButtonEvent(device, JoyButton(map.index), isPressed);
        return;
    }

    if (map.type == TypeAxis)
        sendAxisEvent(device, JoyAxis(map.index), isPressed ? map.value : 0.0f);
}

void QUniversalInput::joyAxis(int device, JoyAxis axis, float value)
{
    Q_D(QUniversalInput);
    QMutexLocker locker(&d->mutex);

    Q_ASSERT(int(axis) < int(JoyAxis::MAX));

    Joypad &joy = d->joypadNames[device];

    if (joy.lastAxis[size_t(axis)] == value)
        return;

    joy.lastAxis[size_t(axis)] = value;

    if (joy.mapping == -1) {
        sendAxisEvent(device, axis, value);
        return;
    }

    JoyEvent map = mappedAxisEvent(d->mappingDatabase[joy.mapping], axis, value);

    if (map.type == TypeButton) {
        bool pressed = map.value > 0.5;
        if (pressed != d->joystickButtonsPressed.contains(_combine_device(JoyButton(map.index), device)))
            sendButtonEvent(device, JoyButton(map.index), pressed);

        // Ensure opposite D-Pad button is also released.
        switch (JoyButton(map.index)) {
        case JoyButton::DpadUp:
            if (d->joystickButtonsPressed.contains(_combine_device(JoyButton::DpadDown, device)))
                sendButtonEvent(device, JoyButton::DpadDown, false);
            break;
        case JoyButton::DpadDown:
            if (d->joystickButtonsPressed.contains(_combine_device(JoyButton::DpadUp, device)))
                sendButtonEvent(device, JoyButton::DpadUp, false);
            break;
        case JoyButton::DpadLeft:
            if (d->joystickButtonsPressed.contains(_combine_device(JoyButton::DpadRight, device)))
                sendButtonEvent(device, JoyButton::DpadRight, false);
            break;
        case JoyButton::DpadRight:
            if (d->joystickButtonsPressed.contains(_combine_device(JoyButton::DpadLeft, device)))
                sendButtonEvent(device, JoyButton::DpadLeft, false);
            break;
        default:
            // Nothing to do.
            break;
        }
        return;
    }

    if (map.type == TypeAxis) {
        JoyAxis axis = JoyAxis(map.index);
        float value = map.value;
        if (axis == JoyAxis::TriggerLeft || axis == JoyAxis::TriggerRight)
            value = 0.5f + value / 2.0f; // Convert to a value between 0.0f and 1.0f.
        sendAxisEvent(device, axis, value);
        return;
    }
}

void QUniversalInput::joyHat(int device, HatMask value)
{
    Q_D(QUniversalInput);
    QMutexLocker locker(&d->mutex);

    const Joypad &joy = d->joypadNames[device];

    JoyEvent map[size_t(HatDirection::Max)];
    map[size_t(HatDirection::Up)].type = TypeButton;
    map[size_t(HatDirection::Up)].index = int(JoyButton::DpadUp);
    map[size_t(HatDirection::Up)].value = 0;

    map[size_t(HatDirection::Right)].type = TypeButton;
    map[size_t(HatDirection::Right)].index = int(JoyButton::DpadRight);
    map[size_t(HatDirection::Right)].value = 0;

    map[size_t(HatDirection::Down)].type = TypeButton;
    map[size_t(HatDirection::Down)].index = int(JoyButton::DpadDown);
    map[size_t(HatDirection::Down)].value = 0;

    map[size_t(HatDirection::Left)].type = TypeButton;
    map[size_t(HatDirection::Left)].index = int(JoyButton::DpadLeft);
    map[size_t(HatDirection::Left)].value = 0;

    // This is a bit weird... as it overwrites index [0] DirectionUp
    if (joy.mapping != -1)
        mappedHatEvents(d->mappingDatabase[joy.mapping], HatDirection(0), map);

    int cur_val = d->joypadNames[device].hatCurrent;

    for (int hat_direction = 0, hat_mask = 1; hat_direction < (int)HatDirection::Max; hat_direction++, hat_mask <<= 1) {
        if ((int(value) & hat_mask) != (cur_val & hat_mask)) {
            if (map[hat_direction].type == TypeButton)
                sendButtonEvent(device, JoyButton(map[hat_direction].index), int(value) & hat_mask);
            if (map[hat_direction].type == TypeAxis)
                sendAxisEvent(device, JoyAxis(map[hat_direction].index), (int(value) & hat_mask) ? map[hat_direction].value : 0.0f);
        }
    }

    d->joypadNames[device].hatCurrent = int(value);
}

QVector2D QUniversalInput::getJoyVibrationStrength(int device)
{
    Q_D(QUniversalInput);
    if (d->joystickVibrations.contains(device))
        return QVector2D(d->joystickVibrations[device].weakMagnitude, d->joystickVibrations[device].strongMagnitude);
    else
        return QVector2D(0.0f, 0.0f);
}

float QUniversalInput::getJoyVibrationDuration(int device)
{
    Q_D(QUniversalInput);
    if (d->joystickVibrations.contains(device))
        return d->joystickVibrations[device].duration;
    else
        return 0.0f;
}

quint64 QUniversalInput::getJoyVibrationTimestamp(int device)
{
    Q_D(QUniversalInput);
    if (d->joystickVibrations.contains(device))
        return d->joystickVibrations[device].timestamp;
    else
        return 0;
}

void QUniversalInput::addForce(int deivce, QVector2D strength, float duration) {
    Q_D(QUniversalInput);
    QMutexLocker locker(&d->mutex);
    d->joystickVibrations[deivce].weakMagnitude = strength.x();
    d->joystickVibrations[deivce].strongMagnitude = strength.y();
    d->joystickVibrations[deivce].duration = duration; // sec
    d->joystickVibrations[deivce].timestamp = QDateTime::currentMSecsSinceEpoch();
}

void QUniversalInput::setJoyAxis(int device, JoyAxis axis, float value)
{
    Q_D(QUniversalInput);
    QMutexLocker locker(&d->mutex);

    JoyAxis c = _combine_device(axis, device);
    d->joystickAxes[c] = value;
}

void QUniversalInput::sendButtonEvent(int device, JoyButton index, bool pressed)
{
    Q_UNUSED(device);
    Q_UNUSED(index);
    Q_UNUSED(pressed);
    Q_EMIT joyButtonEvent(device, index, pressed);
    // qDebug() << "Button event" << device << int(index) << pressed;
}

void QUniversalInput::sendAxisEvent(int device, JoyAxis axis, float value)
{
    Q_UNUSED(device);
    Q_UNUSED(axis);
    Q_UNUSED(value);
    // qDebug() << "Axis event" << device << int(axis) << value;
    Q_EMIT joyAxisEvent(device, axis, value);
}

// mouse disable
void QUniversalInput::setMouseDisabled(bool disabled) {
    Q_D(QUniversalInput);
    if (d->mouseDisabled == disabled)
        return;

    d->mouseDisabled = disabled;
    Q_EMIT mouseDisabledChanged();
}

bool QUniversalInput::isMouseDisabled() const {
    Q_D(const QUniversalInput);
    return d->mouseDisabled;
}

void QUniversalInput::mouseMove(const QVector2D &deltas)
{
    Q_EMIT mouseMovedWithDeltas(deltas);
}

// mouse disable

QUniversalInput::JoyEvent QUniversalInput::mappedButtonEvent(const JoyDeviceMapping &mapping, JoyButton button)
{
    JoyEvent event;

    for (int i = 0; i < mapping.bindings.size(); i++) {
        const JoyBinding binding = mapping.bindings[i];
        if (binding.inputType == TypeButton && binding.input.button == button) {
            event.type = binding.outputType;
            switch (binding.outputType) {
            case TypeButton:
                event.index = (int)binding.output.button;
                return event;
            case TypeAxis:
                event.index = (int)binding.output.axis.axis;
                switch (binding.output.axis.range) {
                case PositiveHalfAxis:
                    event.value = 1;
                    break;
                case NegativeHalfAxis:
                    event.value = -1;
                    break;
                case FullAxis:
                    // It doesn't make sense for a button to map to a full axis,
                    // but keeping as a default for a trigger with a positive half-axis.
                    event.value = 1;
                    break;
                }
                return event;
            default:
                qWarning("Joypad button mapping error.");
            }
        }
    }
    return event;
}

QUniversalInput::JoyEvent QUniversalInput::mappedAxisEvent(const JoyDeviceMapping &mapping, JoyAxis axis, float inValue)
{
    JoyEvent event;

    for (int i = 0; i < mapping.bindings.size(); i++) {
        const JoyBinding binding = mapping.bindings[i];
        if (binding.inputType == TypeAxis && binding.input.axis.axis == axis) {
            float value = inValue;
            if (binding.input.axis.invert)
                value = -value;
            if (binding.input.axis.range == FullAxis ||
                    (binding.input.axis.range == PositiveHalfAxis && value >= 0) ||
                    (binding.input.axis.range == NegativeHalfAxis && value < 0)) {
                event.type = binding.outputType;
                float shifted_positive_value = 0;
                switch (binding.input.axis.range) {
                case PositiveHalfAxis:
                    shifted_positive_value = value;
                    break;
                case NegativeHalfAxis:
                    shifted_positive_value = value + 1;
                    break;
                case FullAxis:
                    shifted_positive_value = (value + 1) / 2;
                    break;
                }
                switch (binding.outputType) {
                case TypeButton:
                    event.index = (int)binding.output.button;
                    switch (binding.input.axis.range) {
                    case PositiveHalfAxis:
                        event.value = shifted_positive_value;
                        break;
                    case NegativeHalfAxis:
                        event.value = 1 - shifted_positive_value;
                        break;
                    case FullAxis:
                        // It doesn't make sense for a full axis to map to a button,
                        // but keeping as a default for a trigger with a positive half-axis.
                        event.value = (shifted_positive_value * 2) - 1;
                        break;
                    }
                    return event;
                case TypeAxis:
                    event.index = (int)binding.output.axis.axis;
                    event.value = value;
                    if (binding.output.axis.range != binding.input.axis.range) {
                        switch (binding.output.axis.range) {
                        case PositiveHalfAxis:
                            event.value = shifted_positive_value;
                            break;
                        case NegativeHalfAxis:
                            event.value = shifted_positive_value - 1;
                            break;
                        case FullAxis:
                            event.value = (shifted_positive_value * 2) - 1;
                            break;
                        }
                    }
                    return event;
                default:
                    qWarning("Joypad axis mapping error.");
                }
            }
        }
    }
    return event;
}

void QUniversalInput::mappedHatEvents(const JoyDeviceMapping &mapping, HatDirection hat, JoyEvent events[size_t(HatDirection::Max)])
{
    for (int i = 0; i < mapping.bindings.size(); i++) {
        const JoyBinding binding = mapping.bindings[i];
        if (binding.inputType == TypeHat && binding.input.hat.hat == hat) {
            HatDirection hat_direction;
            switch (binding.input.hat.hat_mask) {
            case HatMask::Up:
                hat_direction = HatDirection::Up;
                break;
            case HatMask::Right:
                hat_direction = HatDirection::Right;
                break;
            case HatMask::Down:
                hat_direction = HatDirection::Down;
                break;
            case HatMask::Left:
                hat_direction = HatDirection::Left;
                break;
            default:
                qWarning("Joypad button mapping error.");
                continue;
            }

            events[size_t(hat_direction)].type = binding.outputType;
            switch (binding.outputType) {
            case TypeButton:
                events[size_t(hat_direction)].index = int(binding.output.button);
                break;
            case TypeAxis:
                events[size_t(hat_direction)].index = int(binding.output.axis.axis);
                switch (binding.output.axis.range) {
                case PositiveHalfAxis:
                    events[size_t(hat_direction)].value = 1;
                    break;
                case NegativeHalfAxis:
                    events[size_t(hat_direction)].value = -1;
                    break;
                case FullAxis:
                    // It doesn't make sense for a hat direction to map to a full axis,
                    // but keeping as a default for a trigger with a positive half-axis.
                    events[size_t(hat_direction)].value = 1;
                    break;
                }
                break;
            default:
                qWarning("Joypad button mapping error.");
            }
        }
    }
}

QDebug operator<<(QDebug debug, const JoyButton &joyButton)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "JoyButton::";

    switch (joyButton) {
    case JoyButton::Invalid: debug.nospace() << "Invalid"; break;
    case JoyButton::A: debug.nospace() << "A"; break;
    case JoyButton::B: debug.nospace() << "B"; break;
    case JoyButton::X: debug.nospace() << "X"; break;
    case JoyButton::Y: debug.nospace() << "Y"; break;
    case JoyButton::Back: debug.nospace() << "Back"; break;
    case JoyButton::Guide: debug.nospace() << "Guide"; break;
    case JoyButton::Start: debug.nospace() << "Start"; break;
    case JoyButton::LeftStick: debug.nospace() << "LeftStick"; break;
    case JoyButton::RightStick: debug.nospace() << "RightStick"; break;
    case JoyButton::LeftShoulder: debug.nospace() << "LeftShoulder"; break;
    case JoyButton::RightShoulder: debug.nospace() << "RightShoulder"; break;
    case JoyButton::DpadUp: debug.nospace() << "DpadUp"; break;
    case JoyButton::DpadDown: debug.nospace() << "DpadDown"; break;
    case JoyButton::DpadLeft: debug.nospace() << "DpadLeft"; break;
    case JoyButton::DpadRight: debug.nospace() << "DpadRight"; break;
    case JoyButton::Misc1: debug.nospace() << "Misc1"; break;
    case JoyButton::Paddle1: debug.nospace() << "Paddle1"; break;
    case JoyButton::Paddle2: debug.nospace() << "Paddle2"; break;
    case JoyButton::Paddle3: debug.nospace() << "Paddle3"; break;
    case JoyButton::Paddle4: debug.nospace() << "Paddle4"; break;
    case JoyButton::Touchpad: debug.nospace() << "Touchpad"; break;
    default:
        debug.nospace() << "Button(" << static_cast<int>(joyButton) << ")";
        break;
    }
    return debug;
}

QDebug operator<<(QDebug debug, const JoyAxis &axis)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "JoyAxis::";

    switch (axis) {
    case JoyAxis::Invalid: debug.nospace() << "Invalid"; break;
    case JoyAxis::LeftX: debug.nospace() << "LeftX"; break;
    case JoyAxis::LeftY: debug.nospace() << "LeftY"; break;
    case JoyAxis::RightX: debug.nospace() << "RightX"; break;
    case JoyAxis::RightY: debug.nospace() << "RightY"; break;
    case JoyAxis::TriggerLeft: debug.nospace() << "TriggerLeft"; break;
    case JoyAxis::TriggerRight: debug.nospace() << "TriggerRight"; break;
    default:
        debug.nospace() << "(" << static_cast<int>(axis) << ")";
        break;
    }
    return debug;
}

QDebug operator<<(QDebug debug, const QUniversalInput::JoyAxisRange &range)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "JoyAxisRange::";
    switch (range) {
    case QUniversalInput::NegativeHalfAxis: debug.nospace() << "NegativeHalfAxis"; break;
    case QUniversalInput::FullAxis: debug.nospace() << "FullAxis"; break;
    case QUniversalInput::PositiveHalfAxis: debug.nospace() << "PositiveHalfAxis"; break;
    default:
        debug.nospace() << "(" << static_cast<int>(range) << ")";
        break;
    }

    return debug;
}

QDebug operator<<(QDebug debug, const HatDirection &hatDirection)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "HatDirection::";
    switch (hatDirection) {
    case HatDirection::Up: debug.nospace() << "Up"; break;
    case HatDirection::Right: debug.nospace() << "Right"; break;
    case HatDirection::Down: debug.nospace() << "Down"; break;
    case HatDirection::Left: debug.nospace() << "Left"; break;
    default:
        debug.nospace() << "(" << static_cast<int>(hatDirection) << ")";
        break;
    }
    return debug;
}

QDebug operator<<(QDebug debug, const HatMask &hatMask)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "HatMask::";

    switch (hatMask) {
    case HatMask::Center: debug.nospace() << "Center"; break;
    case HatMask::Up: debug.nospace() << "Up"; break;
    case HatMask::Right: debug.nospace() << "Right"; break;
    case HatMask::Down: debug.nospace() << "Down"; break;
    case HatMask::Left: debug.nospace() << "Left"; break;
    default:
        debug.nospace() << "(" << static_cast<int>(hatMask) << ")";
        break;
    }
    return debug;
}

QDebug operator<<(QDebug debug, const QUniversalInput::JoyBinding &binding)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "JoyBinding( inputType: ";
    switch (binding.inputType) {

    case QUniversalInput::TypeButton:
        debug.nospace() << "TypeButton, input: " << binding.input.button;
        break;
    case QUniversalInput::TypeAxis:
        debug.nospace() << "TypeAxis, input: " << binding.input.axis.axis << ", range: " << binding.input.axis.range;
        break;
    case QUniversalInput::TypeHat:
        debug.nospace() << "TypeHat, input: " << binding.input.hat.hat << ", mask: " << binding.input.hat.hat_mask;
        break;
    case QUniversalInput::TypeMax:
        break;
    }

    debug.nospace() << ", outputType: ";
    switch (binding.outputType) {
    case QUniversalInput::TypeButton:
        debug.nospace() << "TypeButton, output: " << binding.output.button;
        break;
    case QUniversalInput::TypeAxis:
        debug.nospace() << "TypeAxis, output: " << binding.output.axis.axis << ", range: " << binding.output.axis.range;
        break;
    case QUniversalInput::TypeHat:
    case QUniversalInput::TypeMax:
        break;
    }

    debug.nospace() << ")";

    return debug;
}

QDebug operator<<(QDebug debug, const QUniversalInput::JoyDeviceMapping &mapping)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "JoyDeviceMapping(" << mapping.uid << ", " << mapping.name << ", bindings: [";
    for (const QUniversalInput::JoyBinding &binding : mapping.bindings) {
        debug.nospace() << binding << ", ";
    }
    debug.nospace() << "])";
    return debug;
}

QT_END_NAMESPACE
