// Copyright 2007-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

// For detailed info about RAWKEYBOARD handling:
// https://blog.molecular-matters.com/2011/09/05/properly-handling-keyboard-input

#ifdef _MSVC_LANG
#	pragma warning(push)
// SPSCQueue does some funky alignment tricks which trigger the C4316
// warning about potential misalignment on the heap.
// We just have to trust the SPSCQueue implementation here.
#	pragma warning(disable : 4316)
#endif

#include "GlobalShortcut_win.h"

#include "Global.h"

#ifdef USE_GKEY
#	include "GKey.h"
#endif

#include <codecvt>
#include <iomanip>
#include <mutex>
#include <sstream>

#include <QUuid>

extern "C" {
// clang-format off
// Do NOT change the order of the includes below or compile errors will occur!
#include <hidsdi.h>
#include <hidpi.h>
// clang-format on
}

// From os_win.cpp
extern HWND mumble_mw_hwnd;

struct InputHid {
	uint32_t button = 0;
	std::string deviceName;
	std::string devicePrefix;

	friend QDataStream &operator<<(QDataStream &stream, const InputHid &output) {
		stream << static_cast< quint32 >(output.button);
		stream << QString::fromStdString(output.deviceName);
		stream << QString::fromStdString(output.devicePrefix);

		return stream;
	}

	friend QDataStream &operator>>(QDataStream &stream, InputHid &input) {
		quint32 button;
		QString deviceName;
		QString devicePrefix;

		stream >> button;
		stream >> deviceName;
		stream >> devicePrefix;

		input.button       = static_cast< uint32_t >(button);
		input.deviceName   = deviceName.toStdString();
		input.devicePrefix = devicePrefix.toStdString();

		return stream;
	}

	bool operator==(const InputHid &rhs) const {
		return this->button == rhs.button && this->deviceName == rhs.deviceName
			   && this->devicePrefix == rhs.devicePrefix;
	}

	bool operator<(const InputHid &rhs) const {
		return this->button < rhs.button && this->deviceName < rhs.deviceName && this->devicePrefix < rhs.devicePrefix;
	}
};
Q_DECLARE_METATYPE(InputHid)

struct InputKeyboard {
	bool e0;
	uint16_t code;

	friend QDataStream &operator<<(QDataStream &stream, const InputKeyboard &output) {
		stream << output.e0;
		stream << static_cast< quint16 >(output.code);

		return stream;
	}

	friend QDataStream &operator>>(QDataStream &stream, InputKeyboard &input) {
		quint16 code;

		stream >> input.e0;
		stream >> code;

		input.code = static_cast< uint16_t >(code);

		return stream;
	}

	bool operator==(const InputKeyboard &rhs) const { return this->e0 == rhs.e0 && this->code == rhs.code; }

	bool operator<(const InputKeyboard &rhs) const { return this->e0 < rhs.e0 && this->code < rhs.code; }
};
Q_DECLARE_METATYPE(InputKeyboard)

enum class InputMouse { Left = 1, Right, Middle, Side_1, Side_2 };
Q_DECLARE_METATYPE(InputMouse)

#ifdef USE_XBOXINPUT
struct InputXinput {
	uint8_t device;
	uint8_t code;

	friend QDataStream &operator<<(QDataStream &stream, const InputXinput &output) {
		stream << static_cast< qint8 >(output.device);
		stream << static_cast< qint8 >(output.code);

		return stream;
	}

	friend QDataStream &operator>>(QDataStream &stream, InputXinput &input) {
		quint8 device;
		quint8 code;

		stream >> device;
		stream >> code;

		input.device = static_cast< uint8_t >(device);
		input.code   = static_cast< uint8_t >(code);

		return stream;
	}

	bool operator==(const InputXinput &rhs) const { return this->device == rhs.device && this->code == rhs.code; }

	bool operator<(const InputXinput &rhs) const { return this->device < rhs.device && this->code < rhs.code; }
};
Q_DECLARE_METATYPE(InputXinput)
#endif
#ifdef USE_GKEY
struct InputGkey {
	bool keyboard;
	uint8_t button;
	uint8_t mode;

	friend QDataStream &operator<<(QDataStream &stream, const InputGkey &output) {
		stream << output.keyboard;
		stream << static_cast< qint8 >(output.button);
		stream << static_cast< qint8 >(output.mode);

		return stream;
	}

	friend QDataStream &operator>>(QDataStream &stream, InputGkey &input) {
		quint8 button;
		quint8 mode;

		stream >> input.keyboard;
		stream >> button;
		stream >> mode;

		input.button = static_cast< uint8_t >(button);
		input.mode   = static_cast< uint8_t >(mode);

		return stream;
	}

	bool operator==(const InputGkey &rhs) const {
		return this->keyboard == rhs.keyboard && this->button == rhs.button && this->mode == rhs.mode;
	}

	bool operator<(const InputGkey &rhs) const {
		return this->keyboard < rhs.keyboard && this->button < rhs.button && this->mode < rhs.mode;
	}
};
Q_DECLARE_METATYPE(InputGkey)
#endif

GlobalShortcutEngine *GlobalShortcutEngine::platformInit() {
	return new GlobalShortcutWin();
}

void GlobalShortcutWin::registerMetaTypes() {
	static bool registered = false;

	// Only register the MetaTypes once.
	if (!registered) {
		registered = true;

		qRegisterMetaType< InputHid >();
		qRegisterMetaTypeStreamOperators< InputHid >();
		QMetaType::registerComparators< InputHid >();

		qRegisterMetaType< InputKeyboard >();
		qRegisterMetaTypeStreamOperators< InputKeyboard >();
		QMetaType::registerComparators< InputKeyboard >();

		qRegisterMetaType< InputMouse >();
		qRegisterMetaTypeStreamOperators< InputMouse >();
		QMetaType::registerComparators< InputMouse >();
#ifdef USE_XBOXINPUT
		qRegisterMetaType< InputXinput >();
		qRegisterMetaTypeStreamOperators< InputXinput >();
		QMetaType::registerComparators< InputXinput >();
#endif
#ifdef USE_GKEY
		qRegisterMetaType< InputGkey >();
		qRegisterMetaTypeStreamOperators< InputGkey >();
		QMetaType::registerComparators< InputGkey >();
#endif
	}
}

QList< Shortcut > GlobalShortcutWin::migrateSettings(const QList< Shortcut > &oldShortcuts) {
	constexpr QUuid KEYBOARD_UUID(0x6F1D2B61, 0xD5A0, 0x11CF, 0xBF, 0xC7, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00);
	constexpr QUuid MOUSE_UUID(0x6F1D2B60, 0xD5A0, 0x11CF, 0xBF, 0xC7, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00);
	constexpr QUuid XINPUT_UUID(0xCA3937E3, 0x640C, 0x4D9E, 0x9E, 0xF3, 0x90, 0x3F, 0x8B, 0x4F, 0xBC, 0xAB);
	constexpr QUuid GKEY_KEYBOARD_UUID(0x153E64E6, 0x98C8, 0x4E03, 0x80, 0xEF, 0x5F, 0xFD, 0x33, 0xD2, 0x5B, 0x8A);
	constexpr QUuid GKEY_MOUSE_UUID(0xC41E60AF, 0x9022, 0x46CF, 0xBC, 0x39, 0x37, 0x98, 0x10, 0x82, 0xD7, 0x16);

	QList< Shortcut > newShortcuts;

	for (Shortcut shortcut : oldShortcuts) {
		bool ok = true;

		for (QVariant &button : shortcut.qlButtons) {
			if (!button.isValid()) {
				// This happens when the user runs a version that uses
				// the old format after the migration is performed.
				ok = false;
				break;
			}

			if (button.userType() == qMetaTypeId< InputHid >() || button.userType() == qMetaTypeId< InputKeyboard >()
				|| button.userType() == qMetaTypeId< InputMouse >()
#ifdef USE_XBOXINPUT
				|| button.userType() == qMetaTypeId< InputXinput >()
#endif
#ifdef USE_GKEY
				|| button.userType() == qMetaTypeId< InputGkey >()
#endif
			) {
				// Already in the new format.
				continue;
			}

			const QVariantList entries = button.toList();
			if (entries.size() < 2) {
				ok = false;
				break;
			}

			auto value = entries.at(0).toUInt(&ok);
			if (!ok) {
				break;
			}

			const auto uuid = entries.at(1).toUuid();
			if (uuid == KEYBOARD_UUID) {
				InputKeyboard input;
				input.code = (value & ~0x8000U) >> 8;
				input.e0   = value & 0x8000U;

				// With DirectInput the extended bit is:
				// - Set for the Pause key.
				// - Unset for the Numlock key.
				// With raw input it's the opposite.
				if (input.code == 0x45) {
					input.e0 = !input.e0;
				}

				button = QVariant::fromValue(input);
			} else if (uuid == MOUSE_UUID) {
				value >>= 8;
				if (value < 3 || value > 7) {
					ok = false;
					break;
				}

				button = QVariant::fromValue(static_cast< InputMouse >(value - 2));
#ifdef USE_XBOXINPUT
			} else if (uuid == XINPUT_UUID) {
				InputXinput input;
				input.device = (value >> 24) & 0xFF;
				input.code   = value & 0x00FFFFFF;

				button = QVariant::fromValue(input);
#endif
#ifdef USE_GKEY
			} else if (uuid == GKEY_KEYBOARD_UUID) {
				InputGkey input = {};
				input.keyboard  = true;
				input.mode      = value >> 16;
				input.button    = value & 0xFFFF;

				button = QVariant::fromValue(input);
			} else if (uuid == GKEY_MOUSE_UUID) {
				InputGkey input = {};
				input.button    = value;

				button = QVariant::fromValue(input);
#endif
			} else {
				ok = false;
				break;
			}
		}

		if (ok) {
			newShortcuts << shortcut;
		}
	}

	return newShortcuts;
}

GlobalShortcutWin::GlobalShortcutWin()
	: m_msgQueue(GlobalShortcutWin::QUEUE_CAPACITY)
#ifdef USE_XBOXINPUT
	  ,
	  m_xinputDevices(0), m_xinputLastPacket()
#endif
{
	// Register the MetaTypes if they have not already been registered (e.g in Settings)
	registerMetaTypes();

	start();
}

GlobalShortcutWin::~GlobalShortcutWin() {
	requestInterruption();
	m_condVar.notify_all();
	wait();
}

void GlobalShortcutWin::run() {
#ifdef USE_XBOXINPUT
	if (Global::get().s.bEnableXboxInput) {
		static_assert(std::numeric_limits< uint8_t >::max() >= XBOXINPUT_MAX_DEVICES,
					  "Higher XBOXINPUT_MAX_DEVICES than anticipated.");

		m_xinput = std::make_unique< XboxInput >();
		if (!m_xinput->isValid()) {
			qWarning("GlobalShortcutWin: Failed to initialize XboxInput!");
			m_xinput.reset();
		}
	}
#endif
#ifdef USE_GKEY
	if (Global::get().s.bEnableGKey) {
		static_assert(std::numeric_limits< uint8_t >::max() >= GKEY_MAX_KEYBOARD_MODE,
					  "GKEY_MAX_KEYBOARD_MODE is higher than anticipated.");
		static_assert(std::numeric_limits< uint8_t >::max() >= GKEY_MAX_MOUSE_BUTTON,
					  "GKEY_MAX_MOUSE_BUTTON is higher than anticipated.");

		m_gkey = std::make_unique< GKeyLibrary >();
		if (!m_gkey->isValid()) {
			qWarning("GlobalShortcutWin: Failed to initialize GKey!");
			m_gkey.reset();
		}
	}
#endif
	// Wait for MainWindow's constructor to finish, so that we can get the window's handle.
	while (!mumble_mw_hwnd) {
		yieldCurrentThread();
	}

	constexpr uint8_t NRID   = 5;
	RAWINPUTDEVICE rid[NRID] = {};

	rid[0].usUsagePage = HID_USAGE_PAGE_GENERIC;
	rid[0].usUsage     = HID_USAGE_GENERIC_MOUSE;
	rid[0].dwFlags     = RIDEV_INPUTSINK;
	rid[0].hwndTarget  = mumble_mw_hwnd;

	rid[1].usUsagePage = HID_USAGE_PAGE_GENERIC;
	rid[1].usUsage     = HID_USAGE_GENERIC_JOYSTICK;
	rid[1].dwFlags     = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY;
	rid[1].hwndTarget  = mumble_mw_hwnd;

	rid[2].usUsagePage = HID_USAGE_PAGE_GENERIC;
	rid[2].usUsage     = HID_USAGE_GENERIC_GAMEPAD;
	rid[2].dwFlags     = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY;
	rid[2].hwndTarget  = mumble_mw_hwnd;

	rid[3].usUsagePage = HID_USAGE_PAGE_GENERIC;
	rid[3].usUsage     = HID_USAGE_GENERIC_KEYBOARD;
	rid[3].dwFlags     = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY;
	rid[3].hwndTarget  = mumble_mw_hwnd;

	rid[4].usUsagePage = HID_USAGE_PAGE_GENERIC;
	rid[4].usUsage     = HID_USAGE_GENERIC_MULTI_AXIS_CONTROLLER;
	rid[4].dwFlags     = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY;
	rid[4].hwndTarget  = mumble_mw_hwnd;

	if (!RegisterRawInputDevices(rid, NRID, sizeof(RAWINPUTDEVICE))) {
		qWarning("GlobalShortcutWindows: RegisterRawInputDevices() failed with error %u!", GetLastError());
	}

	std::mutex mutex;
	std::unique_lock< decltype(mutex) > lock(mutex);

	do {
		m_condVar.wait(lock);

		if (bNeedRemap) {
			remap();
		}

		while (const std::unique_ptr< MsgRaw > *item = m_msgQueue.front()) {
			const std::unique_ptr< MsgRaw > &msg = *item;

			switch (msg->type()) {
				case MsgRaw::Hid:
					processOther();
					processMsgHid(static_cast< MsgHid & >(*msg));
					break;
				case MsgRaw::Keyboard:
					processMsgKeyboard(static_cast< MsgKeyboard & >(*msg));
					break;
				case MsgRaw::Mouse:
					processMsgMouse(static_cast< MsgMouse & >(*msg));
					break;
			}

			m_msgQueue.pop();
		}
	} while (!isInterruptionRequested());
}

bool GlobalShortcutWin::isRateLimited(HANDLE device) {
	if (m_throttleThreshold[device] > 0) {
		m_throttleCounter[device]++;

		if (m_throttleCounter[device] < m_throttleThreshold[device]) {
			// Only let every throttleThreshold-th event through
			return true;
		}

		m_throttleCounter[device] = 0;

		if (m_msgQueue.size() < GlobalShortcutWin::QUEUE_CAPACITY / 2) {
			// Queue is half empty, relax threshold
			m_throttleThreshold[device]--;
			if (m_throttleThreshold[device] == 0) {
				qInfo() << "GlobalShortcutWin: Throttle threshold disabled for device " << device;
			}
		}
	}

	return false;
}

void GlobalShortcutWin::tryPlaceEvent(HANDLE device, bool rawhid, std::unique_ptr< MsgRaw > &&event) {
	const bool enqueued = m_msgQueue.try_emplace(std::move(event));
	if (!enqueued) {
		qWarning() << "GlobalShortcutWin: Failed to enqueue input event - queue full?";

		if (rawhid) {
			// Cap m_throttle at 8192 to prevent excessive event dropping while still protecting the queue
			// 8192 = 2^13 feels like a good upper limit
			m_throttleThreshold[device] = std::min(static_cast< unsigned int >(8192), m_throttleThreshold[device] + 1);
			if (m_throttleThreshold[device] == 1) {
				qInfo() << "GlobalShortcutWin: Throttle threshold enabled for device " << device;
			}

			qInfo() << "GlobalShortcutWin: New throttle threshold for device " << device << " is "
					<< m_throttleThreshold[device];
		}
	}
}

void GlobalShortcutWin::injectRawInputMessage(HRAWINPUT handle) {
	UINT size = 0;
	if (GetRawInputData(handle, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER)) != 0) {
		return;
	}

	auto buffer = std::make_unique< uint8_t[] >(size);
	if (GetRawInputData(handle, RID_INPUT, buffer.get(), &size, sizeof(RAWINPUTHEADER)) <= 0) {
		return;
	}

	auto input = reinterpret_cast< const PRAWINPUT >(buffer.get());
	switch (input->header.dwType) {
		case RIM_TYPEMOUSE: {
			const RAWMOUSE &mouse = input->data.mouse;
			tryPlaceEvent(input->header.hDevice, false, std::make_unique< MsgMouse >(mouse.usButtonFlags));
			break;
		}
		case RIM_TYPEKEYBOARD: {
			const RAWKEYBOARD &keyboard = input->data.keyboard;
			if (keyboard.MakeCode == KEYBOARD_OVERRUN_MAKE_CODE) {
				// Invalid or unrecognizable combination of keys is pressed or
				// the number of keys pressed exceeds the limit for this keyboard.
				return;
			}

			if (keyboard.VKey == 0xFF) {
				// Discard "fake keys" which are part of an escaped sequence.
				return;
			}

			tryPlaceEvent(input->header.hDevice, false,
						  std::make_unique< MsgKeyboard >(keyboard.Flags, keyboard.MakeCode, keyboard.VKey));
			break;
		}
		case RIM_TYPEHID: {
			const RAWHID &hid = input->data.hid;
			MsgHid::RawReports reports(hid.bRawData, hid.dwSizeHid * hid.dwCount);

			if (reports.empty()) {
				break;
			}

			// We have had the problem that certain analog input devices (joysticks, wheels) would
			// overwhelm our input event queue with messages and freeze Mumble as a whole. Therefore
			// we have to implement rate-limiting for analog input events, while keyboard and mouse
			// inputs are fine. See: https://github.com/mumble-voip/mumble/issues/6696
			if (isRateLimited(input->header.hDevice)) {
				break;
			}

			tryPlaceEvent(input->header.hDevice, true,
						  std::make_unique< MsgHid >(input->header.hDevice, reports, hid.dwSizeHid));
			break;
		}
		default:
			return;
	}

	m_condVar.notify_all();
}

void GlobalShortcutWin::processMsgHid(MsgHid &msg) {
	auto iter = m_devices.find(msg.deviceHandle);
	if (iter == m_devices.end()) {
		iter = addDevice(msg.deviceHandle);
		if (iter == m_devices.cend()) {
			return;
		}
	}

	auto &device = (*iter).second;
#ifdef USE_XBOXINPUT
	if (device.xinput && m_xinput) {
		return;
	}
#endif
	auto data = reinterpret_cast< PHIDP_PREPARSED_DATA >(&device.data[0]);

	ULONG nUsages = static_cast< ULONG >(device.buttons.size());
	std::vector< USAGE > usages(nUsages);
	if (HidP_GetUsages(HidP_Input, HID_USAGE_PAGE_BUTTON, 0, &usages[0], &nUsages, data, &msg.reports[0],
					   msg.reportSize)
		!= HIDP_STATUS_SUCCESS) {
		return;
	}

	device.buttons.assign(device.buttons.size(), false);
	for (ULONG i = 0; i < nUsages; ++i) {
		device.buttons[usages[i] - device.usageRange.first] = true;
	}

	for (uint32_t i = 0; i < device.buttons.size(); ++i) {
		InputHid input;
		input.button       = i;
		input.deviceName   = device.name;
		input.devicePrefix = device.prefix;

		handleButton(QVariant::fromValue(input), device.buttons[i]);
	}
}

void GlobalShortcutWin::processMsgKeyboard(MsgKeyboard &msg) {
	if (msg.virtualKey == VK_NUMLOCK) {
		// Keys like “Pause / Break” and “Numlock” act strangely,
		// sometimes like they are not even the same physical key.
		// They use the so called escaped sequences, which we have to decipher.
		msg.scanCode = MapVirtualKey(msg.virtualKey, MAPVK_VK_TO_VSC) | 0x100;
	}

	// E0 and E1 are escape sequences used for certain special keys, such as PRINT and PAUSE/BREAK.
	// See https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html.
	if (msg.flags & RI_KEY_E1) {
		// For escaped sequences, turn the virtual key into the correct scan code using MapVirtualKey().
		// MapVirtualKey() is unable to map VK_PAUSE (this is a known bug), hence we map that by hand.
		msg.scanCode = msg.virtualKey != VK_PAUSE ? MapVirtualKey(msg.virtualKey, MAPVK_VK_TO_VSC) : 0x45;
	}

	InputKeyboard input = {};
	input.code          = msg.scanCode;
	input.e0            = msg.flags & RI_KEY_E0;

	handleButton(QVariant::fromValue(input), !(msg.flags & RI_KEY_BREAK));
}

void GlobalShortcutWin::processMsgMouse(const MsgMouse &msg) {
	// Multiple mouse transitions can be contained in a single message.
	// See https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawmouse.
	if (msg.buttonFlags & RI_MOUSE_BUTTON_1_DOWN) {
		handleButton(QVariant::fromValue(InputMouse::Left), true);
	}

	if (msg.buttonFlags & RI_MOUSE_BUTTON_1_UP) {
		handleButton(QVariant::fromValue(InputMouse::Left), false);
	}

	if (msg.buttonFlags & RI_MOUSE_BUTTON_2_DOWN) {
		handleButton(QVariant::fromValue(InputMouse::Right), true);
	}

	if (msg.buttonFlags & RI_MOUSE_BUTTON_2_UP) {
		handleButton(QVariant::fromValue(InputMouse::Right), false);
	}

	if (msg.buttonFlags & RI_MOUSE_BUTTON_3_DOWN) {
		handleButton(QVariant::fromValue(InputMouse::Middle), true);
	}

	if (msg.buttonFlags & RI_MOUSE_BUTTON_3_UP) {
		handleButton(QVariant::fromValue(InputMouse::Middle), false);
	}

	if (msg.buttonFlags & RI_MOUSE_BUTTON_4_DOWN) {
		handleButton(QVariant::fromValue(InputMouse::Side_1), true);
	}

	if (msg.buttonFlags & RI_MOUSE_BUTTON_4_UP) {
		handleButton(QVariant::fromValue(InputMouse::Side_1), false);
	}

	if (msg.buttonFlags & RI_MOUSE_BUTTON_5_DOWN) {
		handleButton(QVariant::fromValue(InputMouse::Side_2), true);
	}

	if (msg.buttonFlags & RI_MOUSE_BUTTON_5_UP) {
		handleButton(QVariant::fromValue(InputMouse::Side_2), false);
	}
}

GlobalShortcutWin::DeviceMap::iterator GlobalShortcutWin::addDevice(const HANDLE deviceHandle) {
	RID_DEVICE_INFO deviceInfo = {};
	UINT size                  = sizeof(deviceInfo);
	if (GetRawInputDeviceInfo(deviceHandle, RIDI_DEVICEINFO, &deviceInfo, &size) <= 0) {
		return m_devices.end();
	}

	Device device;

	// E.g. "[045E:02EA]"
	std::stringstream nameStream;
	nameStream << std::uppercase << std::hex << std::setfill('0');
	nameStream << '[';
	nameStream << std::setw(4) << deviceInfo.hid.dwVendorId;
	nameStream << ':';
	nameStream << std::setw(4) << deviceInfo.hid.dwProductId;
	nameStream << ']';

	device.prefix = nameStream.str() + ':';

	if (GetRawInputDeviceInfo(deviceHandle, RIDI_DEVICENAME, nullptr, &size) == 0) {
		std::wstring name;
		name.resize(size);
		// E.g. "\\?\HID#VID_045E&PID_02FF&IG_00#8&15c9c520&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"
		if (GetRawInputDeviceInfo(deviceHandle, RIDI_DEVICENAME, &name[0], &size) > 0) {
#ifdef USE_XBOXINPUT
			if (name.find(L"IG_") != name.npos) {
				device.xinput = true;
			}
#endif
			auto handle = CreateFile(name.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
									 nullptr, OPEN_EXISTING, 0, nullptr);
			if (handle != INVALID_HANDLE_VALUE) {
				// From
				// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_getproductstring:
				// "For USB devices, the maximum string length is 126 wide characters (not including the terminating
				// NULL character)."
				//
				// std::string::resize() doesn't take the NULL character into account, but an extra byte doesn't hurt.
				name.clear();
				name.resize(127);

				std::wstring_convert< std::codecvt_utf8_utf16< wchar_t > > conv;

				if (HidD_GetManufacturerString(handle, &name[0], static_cast< ULONG >(sizeof(wchar_t) * name.size()))) {
					nameStream << ' ' << conv.to_bytes(name);
					name.clear();
				}

				if (HidD_GetProductString(handle, &name[0], static_cast< ULONG >(sizeof(wchar_t) * name.size()))) {
					nameStream << ' ' << conv.to_bytes(name);
				}

				CloseHandle(handle);
			}
		}
	}

	device.name = nameStream.str();

	if (GetRawInputDeviceInfo(deviceHandle, RIDI_PREPARSEDDATA, nullptr, &size) != 0) {
		return m_devices.end();
	}

	device.data.resize(size);
	auto data = reinterpret_cast< PHIDP_PREPARSED_DATA >(&device.data[0]);
	if (GetRawInputDeviceInfo(deviceHandle, RIDI_PREPARSEDDATA, data, &size) <= 0) {
		return m_devices.end();
	}

	HIDP_CAPS caps;
	if (HidP_GetCaps(data, &caps) != HIDP_STATUS_SUCCESS) {
		return m_devices.end();
	}

	USHORT nCaps = caps.NumberInputButtonCaps;
	std::vector< HIDP_BUTTON_CAPS > buttonCaps(nCaps);
	if (HidP_GetSpecificButtonCaps(HidP_Input, HID_USAGE_PAGE_BUTTON, 0, 0, &buttonCaps[0], &nCaps, data)
		!= HIDP_STATUS_SUCCESS) {
		return m_devices.end();
	}

	device.usageRange.first  = buttonCaps[0].Range.UsageMin;
	device.usageRange.second = buttonCaps[0].Range.UsageMax;
	device.buttons.resize(device.usageRange.second - device.usageRange.first + 1);

	if (device.xinput) {
		++m_xinputDevices;
	}

	qInfo("GlobalShortcutWin: \"%s\" added", device.name.c_str());

	return m_devices.insert({ deviceHandle, device }).first;
}

void GlobalShortcutWin::deviceRemoved(const HANDLE deviceHandle) {
	const auto iter = m_devices.find(deviceHandle);
	if (iter == m_devices.cend()) {
		return;
	}

	const auto &device = (*iter).second;
	if (device.xinput) {
		--m_xinputDevices;
	}

	qInfo("GlobalShortcutWin: \"%s\" removed", device.name.c_str());

	m_devices.erase(iter);
}
#ifdef USE_XBOXINPUT
bool GlobalShortcutWin::xinputIsPressed(const uint8_t bit, const XboxInputState &state) {
	// The buttons field of XboxInputState contains a bit
	// for each button on the Xbox controller. The official
	// headers enumerate the bits via XINPUT_GAMEPAD_*.
	// The official mapping uses all 16-bits, but leaves
	// bit 10 and 11 (counting from 0) undocumented.
	//
	// It turns out that bit 10 is the guide button,
	// which can be queried using the non-public
	// XInputGetStateEx() function.
	//
	// Our mapping uses the bit number as a button index.
	// So 0x1 -> 0, 0x2 -> 1, 0x4 -> 2, and so on...
	//
	// However, since the buttons field is only a 16-bit value,
	// and we also want to use the left and right triggers as
	// buttons, we assign them the button indexes 16 and 17.
	switch (bit) {
		case 16:
			return state.leftTrigger > XBOXINPUT_TRIGGER_THRESHOLD;
		case 17:
			return state.rightTrigger > XBOXINPUT_TRIGGER_THRESHOLD;
		default:
			return state.buttons & (1 << bit);
	}
}
#endif
void GlobalShortcutWin::processOther() {
#ifdef USE_XBOXINPUT
	if (m_xinput && m_xinputDevices > 0) {
		for (uint8_t i = 0; i < XBOXINPUT_MAX_DEVICES; ++i) {
			XboxInputState state;
			if (m_xinput->GetState(i, &state) != ERROR_SUCCESS) {
				continue;
			}

			// Skip the result of GetState() if the packet number hasn't changed,
			// or if we're at the first packet.
			if (m_xinputLastPacket[i] != 0 && state.packetNumber == m_xinputLastPacket[i]) {
				continue;
			}

			InputXinput input = {};
			input.device      = i;

			for (uint8_t j = 0; j < 18; ++j) {
				input.code = j;
				handleButton(QVariant::fromValue(input), xinputIsPressed(j, state));
			}

			m_xinputLastPacket[i] = state.packetNumber;
		}
	}
#endif
#ifdef USE_GKEY
	if (m_gkey) {
		InputGkey input = {};
		input.keyboard  = false;

		for (uint8_t button = GKEY_MIN_MOUSE_BUTTON; button <= GKEY_MAX_MOUSE_BUTTON; ++button) {
			input.button = button;
			handleButton(QVariant::fromValue(input), m_gkey->isMouseButtonPressed(button));
		}

		input.keyboard = true;

		for (uint8_t mode = GKEY_MIN_KEYBOARD_MODE; mode <= GKEY_MAX_KEYBOARD_MODE; ++mode) {
			input.mode = mode;

			for (uint8_t button = GKEY_MIN_KEYBOARD_BUTTON; button <= GKEY_MAX_KEYBOARD_BUTTON; ++button) {
				input.button = button;
				handleButton(QVariant::fromValue(input), m_gkey->isKeyboardGkeyPressed(button, mode));
			}
		}
	}
#endif
}

GlobalShortcutWin::ButtonInfo GlobalShortcutWin::buttonInfo(const QVariant &button) {
	ButtonInfo info;

	// Unfortunately qMetaTypeId() is not a constexpr function.
	// As a result, we cannot use a switch-case.
	if (button.userType() == qMetaTypeId< InputHid >()) {
		const auto input  = button.value< InputHid >();
		info.device       = QString::fromStdString(input.deviceName);
		info.devicePrefix = QString::fromStdString(input.devicePrefix);
		info.name         = QString::number(input.button);
	} else if (button.userType() == qMetaTypeId< InputKeyboard >()) {
		info.device = tr("Keyboard");

		const auto input = button.value< InputKeyboard >();
		wchar_t buffer[MAX_PATH];
		if (GetKeyNameText((input.code << 16) | (input.e0 << 24), buffer, MAX_PATH)) {
			info.name = QString::fromWCharArray(buffer);
		} else {
			// If GetKeyNameText() cannot find the name. Show a K prefix and the scan code instead of Unknown.
			// For keys like F13 - F24
			info.devicePrefix = QStringLiteral("K");
			info.name         = QString::number(static_cast< uint >(input.code));
		}
	} else if (button.userType() == qMetaTypeId< InputMouse >()) {
		info.device       = tr("Mouse");
		info.devicePrefix = QStringLiteral("M");

		const auto input = button.value< InputMouse >();
		info.name        = QString::number(static_cast< uint >(input));
	}
#ifdef USE_XBOXINPUT
	else if (button.userType() == qMetaTypeId< InputXinput >()) {
		const auto input  = button.value< InputXinput >();
		info.device       = QString::fromLatin1("Xbox controller #%1").arg(input.device + 1);
		info.devicePrefix = QString::fromLatin1("Xbox%1:").arg(input.device + 1);

		// Translate from our own button index mapping to
		// the actual Xbox controller button names.
		// For a description of the mapping, see the state
		// querying code in GlobalShortcutWin::timeTicked().
		switch (input.code) {
			case 0:
				info.name = QStringLiteral("Up");
				break;
			case 1:
				info.name = QStringLiteral("Down");
				break;
			case 2:
				info.name = QStringLiteral("Left");
				break;
			case 3:
				info.name = QStringLiteral("Right");
				break;
			case 4:
				info.name = QStringLiteral("Start");
				break;
			case 5:
				info.name = QStringLiteral("Back");
				break;
			case 6:
				info.name = QStringLiteral("LeftThumb");
				break;
			case 7:
				info.name = QStringLiteral("RightThumb");
				break;
			case 8:
				info.name = QStringLiteral("LeftShoulder");
				break;
			case 9:
				info.name = QStringLiteral("RightShoulder");
				break;
			case 10:
				info.name = QStringLiteral("Guide");
				break;
			case 11:
				info.name = QStringLiteral("11");
				break;
			case 12:
				info.name = QStringLiteral("A");
				break;
			case 13:
				info.name = QStringLiteral("B");
				break;
			case 14:
				info.name = QStringLiteral("X");
				break;
			case 15:
				info.name = QStringLiteral("Y");
				break;
			case 16:
				info.name = QStringLiteral("LeftTrigger");
				break;
			case 17:
				info.name = QStringLiteral("RightTrigger");
		}
	}
#endif
#ifdef USE_GKEY
	else if (button.userType() == qMetaTypeId< InputGkey >()) {
		info.device       = QStringLiteral("Logitech G-keys");
		info.devicePrefix = QStringLiteral("GKey:");

		const auto input = button.value< InputGkey >();
		if (input.keyboard) {
			info.name = m_gkey->getKeyboardGkeyString(input.button, input.mode);
		} else {
			info.name = m_gkey->getMouseButtonString(input.button);
		}
	}
#endif

	return info;
}

#ifdef _MSVC_LANG
#	pragma warning(pop)
#endif
