// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *  HID driver for MSI Claw Handheld PC gamepads.
 *
 *  Provides configuration support for the MSI Claw series of handheld PC
 *  gamepads. Multiple iterations of the device firmware has led to some
 *  quirks for how certain attributes are handled. The original firmware
 *  did not support remapping of the M1 (right) and M2 (left) rear paddles.
 *  Additionally, the MCU RAM address for writing configuration data has
 *  changed twice. Checks are done during probe to enumerate these variances.
 *
 *  Copyright (c) 2026 Zhouwang Huang <honjow311@gmail.com>
 *  Copyright (c) 2026 Denis Benato <denis.benato@linux.dev>
 *  Copyright (c) 2026 Valve Corporation
 */

#include <linux/array_size.h>
#include <linux/cleanup.h>
#include <linux/completion.h>
#include <linux/container_of.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/hid.h>
#include <linux/kobject.h>
#include <linux/led-class-multicolor.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/pm.h>
#include <linux/spinlock.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/unaligned.h>
#include <linux/usb.h>
#include <linux/workqueue.h>

#include "hid-ids.h"

#define CLAW_OUTPUT_REPORT_ID	0x0f
#define CLAW_INPUT_REPORT_ID	0x10

#define CLAW_PACKET_SIZE	64

#define CLAW_DINPUT_CFG_INTF_IN	0x82
#define CLAW_XINPUT_CFG_INTF_IN	0x83

#define CLAW_KEYS_MAX		5

#define CLAW_RGB_ZONES		9
#define CLAW_RGB_MAX_FRAMES	8
#define CLAW_RGB_FRAME_OFFSET	0x24

enum claw_command_index {
	CLAW_COMMAND_TYPE_NONE =			0x00,
	CLAW_COMMAND_TYPE_READ_PROFILE =		0x04,
	CLAW_COMMAND_TYPE_READ_PROFILE_ACK =		0x05,
	CLAW_COMMAND_TYPE_ACK =				0x06,
	CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA =		0x21,
	CLAW_COMMAND_TYPE_SYNC_TO_ROM =			0x22,
	CLAW_COMMAND_TYPE_SWITCH_MODE =			0x24,
	CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE =		0x26,
	CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK =		0x27,
	CLAW_COMMAND_TYPE_RESET_DEVICE =		0x28,
};

enum claw_gamepad_mode_index {
	CLAW_GAMEPAD_MODE_XINPUT =	0x01,
	CLAW_GAMEPAD_MODE_DINPUT =	0x02,
	CLAW_GAMEPAD_MODE_DESKTOP =	0x04,
};

static const char * const claw_gamepad_mode_text[] = {
	[CLAW_GAMEPAD_MODE_XINPUT] =	"xinput",
	[CLAW_GAMEPAD_MODE_DINPUT] =	"dinput",
	[CLAW_GAMEPAD_MODE_DESKTOP] =	"desktop",
};

enum claw_profile_ack_pending {
	CLAW_NO_PENDING,
	CLAW_M1_PENDING,
	CLAW_M2_PENDING,
	CLAW_RGB_PENDING,
	CLAW_RUMBLE_LEFT_PENDING,
	CLAW_RUMBLE_RIGHT_PENDING,
};

enum claw_key_index {
	CLAW_KEY_M1,
	CLAW_KEY_M2,
};

enum claw_mkeys_function_index {
	CLAW_MKEY_FUNCTION_MACRO,
	CLAW_MKEY_FUNCTION_DISABLED,
	CLAW_MKEY_FUNCTION_COMBO,
};

enum claw_mode_field {
	CLAW_FIELD_GAMEPAD_MODE,
	CLAW_FIELD_MKEYS_FUNCTION,
};

static const char * const claw_mkeys_function_text[] = {
	[CLAW_MKEY_FUNCTION_MACRO] =	"macro",
	[CLAW_MKEY_FUNCTION_DISABLED] =	"disabled",
	[CLAW_MKEY_FUNCTION_COMBO] =	"combination",
};

static const struct {
	u8 code;
	const char *name;
} claw_button_mapping_key_map[] = {
	/* Gamepad buttons */
	{ 0x01, "ABS_HAT0Y_UP" },
	{ 0x02, "ABS_HAT0Y_DOWN" },
	{ 0x03, "ABS_HAT0X_LEFT" },
	{ 0x04, "ABS_HAT0X_RIGHT" },
	{ 0x05, "BTN_TL" },
	{ 0x06, "BTN_TR" },
	{ 0x07, "BTN_THUMBL" },
	{ 0x08, "BTN_THUMBR" },
	{ 0x09, "BTN_SOUTH" },
	{ 0x0a, "BTN_EAST" },
	{ 0x0b, "BTN_NORTH" },
	{ 0x0c, "BTN_WEST" },
	{ 0x0d, "BTN_MODE" },
	{ 0x0e, "BTN_SELECT" },
	{ 0x0f, "BTN_START" },
	{ 0x13, "BTN_TL2"},
	{ 0x14, "BTN_TR2"},
	{ 0x15, "ABS_Y_UP"},
	{ 0x16, "ABS_Y_DOWN"},
	{ 0x17, "ABS_X_LEFT"},
	{ 0x18, "ABS_X_RIGHT"},
	{ 0x19, "ABS_RY_UP"},
	{ 0x1a, "ABS_RY_DOWN"},
	{ 0x1b, "ABS_RX_LEFT"},
	{ 0x1c, "ABS_RX_RIGHT"},
	/* Keyboard keys */
	{ 0x32, "KEY_ESC" },
	{ 0x33, "KEY_F1" },
	{ 0x34, "KEY_F2" },
	{ 0x35, "KEY_F3" },
	{ 0x36, "KEY_F4" },
	{ 0x37, "KEY_F5" },
	{ 0x38, "KEY_F6" },
	{ 0x39, "KEY_F7" },
	{ 0x3a, "KEY_F8" },
	{ 0x3b, "KEY_F9" },
	{ 0x3c, "KEY_F10" },
	{ 0x3d, "KEY_F11" },
	{ 0x3e, "KEY_F12" },
	{ 0x3f, "KEY_GRAVE" },
	{ 0x40, "KEY_1" },
	{ 0x41, "KEY_2" },
	{ 0x42, "KEY_3" },
	{ 0x43, "KEY_4" },
	{ 0x44, "KEY_5" },
	{ 0x45, "KEY_6" },
	{ 0x46, "KEY_7" },
	{ 0x47, "KEY_8" },
	{ 0x48, "KEY_9" },
	{ 0x49, "KEY_0" },
	{ 0x4a, "KEY_MINUS" },
	{ 0x4b, "KEY_EQUAL" },
	{ 0x4c, "KEY_BACKSPACE" },
	{ 0x4d, "KEY_TAB" },
	{ 0x4e, "KEY_Q" },
	{ 0x4f, "KEY_W" },
	{ 0x50, "KEY_E" },
	{ 0x51, "KEY_R" },
	{ 0x52, "KEY_T" },
	{ 0x53, "KEY_Y" },
	{ 0x54, "KEY_U" },
	{ 0x55, "KEY_I" },
	{ 0x56, "KEY_O" },
	{ 0x57, "KEY_P" },
	{ 0x58, "KEY_LEFTBRACE" },
	{ 0x59, "KEY_RIGHTBRACE" },
	{ 0x5a, "KEY_BACKSLASH" },
	{ 0x5b, "KEY_CAPSLOCK" },
	{ 0x5c, "KEY_A" },
	{ 0x5d, "KEY_S" },
	{ 0x5e, "KEY_D" },
	{ 0x5f, "KEY_F" },
	{ 0x60, "KEY_G" },
	{ 0x61, "KEY_H" },
	{ 0x62, "KEY_J" },
	{ 0x63, "KEY_K" },
	{ 0x64, "KEY_L" },
	{ 0x65, "KEY_SEMICOLON" },
	{ 0x66, "KEY_APOSTROPHE" },
	{ 0x67, "KEY_ENTER" },
	{ 0x68, "KEY_LEFTSHIFT" },
	{ 0x69, "KEY_Z" },
	{ 0x6a, "KEY_X" },
	{ 0x6b, "KEY_C" },
	{ 0x6c, "KEY_V" },
	{ 0x6d, "KEY_B" },
	{ 0x6e, "KEY_N" },
	{ 0x6f, "KEY_M" },
	{ 0x70, "KEY_COMMA" },
	{ 0x71, "KEY_DOT" },
	{ 0x72, "KEY_SLASH" },
	{ 0x73, "KEY_RIGHTSHIFT" },
	{ 0x74, "KEY_LEFTCTRL" },
	{ 0x75, "KEY_LEFTMETA" },
	{ 0x76, "KEY_LEFTALT" },
	{ 0x77, "KEY_SPACE" },
	{ 0x78, "KEY_RIGHTALT" },
	{ 0x79, "KEY_RIGHTCTRL" },
	{ 0x7a, "KEY_INSERT" },
	{ 0x7b, "KEY_HOME" },
	{ 0x7c, "KEY_PAGEUP" },
	{ 0x7d, "KEY_DELETE" },
	{ 0x7e, "KEY_END" },
	{ 0x7f, "KEY_PAGEDOWN" },
	{ 0x8a, "KEY_KPENTER" },
	{ 0x8b, "KEY_KP0" },
	{ 0x8c, "KEY_KP1" },
	{ 0x8d, "KEY_KP2" },
	{ 0x8e, "KEY_KP3" },
	{ 0x8f, "KEY_KP4" },
	{ 0x90, "KEY_KP5" },
	{ 0x91, "KEY_KP6" },
	{ 0x92, "KEY_KP7" },
	{ 0x93, "KEY_KP8" },
	{ 0x94, "KEY_KP9" },
	{ 0x95, "MD_PLAY" },
	{ 0x96, "MD_STOP" },
	{ 0x97, "MD_NEXT" },
	{ 0x98, "MD_PREV" },
	{ 0x99, "MD_VOL_UP" },
	{ 0x9a, "MD_VOL_DOWN" },
	{ 0x9b, "MD_VOL_MUTE" },
	{ 0x9c, "KEY_F23" },
	/* Mouse events */
	{ 0xc8, "BTN_LEFT" },
	{ 0xc9, "BTN_MIDDLE" },
	{ 0xca, "BTN_RIGHT" },
	{ 0xcb, "BTN_SIDE" },
	{ 0xcc, "BTN_EXTRA" },
	{ 0xcd, "REL_WHEEL_UP" },
	{ 0xce, "REL_WHEEL_DOWN" },
	{ 0xff, "DISABLED" },
};

enum claw_rgb_effect_index {
	CLAW_RGB_EFFECT_MONOCOLOR,
	CLAW_RGB_EFFECT_BREATHE,
	CLAW_RGB_EFFECT_CHROMA,
	CLAW_RGB_EFFECT_RAINBOW,
	CLAW_RGB_EFFECT_FROSTFIRE,
};

static const char * const claw_rgb_effect_text[] = {
	[CLAW_RGB_EFFECT_MONOCOLOR] =	"monocolor",
	[CLAW_RGB_EFFECT_BREATHE] =	"breathe",
	[CLAW_RGB_EFFECT_CHROMA] =	"chroma",
	[CLAW_RGB_EFFECT_RAINBOW] =	"rainbow",
	[CLAW_RGB_EFFECT_FROSTFIRE] =	"frostfire",
};

static const u16 button_mapping_addr_old[] = {
	0x007a,  /* M1 */
	0x011f,  /* M2 */
};

static const u16 button_mapping_addr_new[] = {
	0x00bb,  /* M1 */
	0x0164,  /* M2 */
};

static const u16 rgb_addr_old = 0x01fa;
static const u16 rgb_addr_new = 0x024a;

static const u16 rumble_addr[] = {
	0x0022,  /* left  */
	0x0023,  /* right */
};

struct claw_command_report {
	u8 report_id;
	u8 padding[2];
	u8 header_tail;
	u8 cmd;
	u8 data[59];
} __packed;

struct claw_profile_report {
	u8 profile;
	__be16 read_addr;
} __packed;

struct claw_mkey_report {
	struct claw_profile_report;
	u8 padding_0;
	u8 padding_1;
	u8 padding_2;
	u8 codes[5];
} __packed;

struct rgb_zone {
	u8 red;
	u8 green;
	u8 blue;
};

struct rgb_frame {
	struct rgb_zone zone[CLAW_RGB_ZONES];
};

struct claw_rgb_report {
	struct claw_profile_report;
	u8 frame_bytes;
	u8 padding;
	u8 frame_count;
	u8 state; /* Always 0x09 */
	u8 speed;
	u8 brightness;
	struct rgb_frame zone_data;
} __packed;

struct claw_rumble_report {
	struct claw_profile_report;
	u8 padding;
	u8 intensity;
} __packed;

struct claw_drvdata {
	/* MCU General Variables */
	enum claw_profile_ack_pending profile_pending;
	struct completion orphan_ack_complete;
	struct completion send_cmd_complete;
	struct delayed_work cfg_resume;
	struct delayed_work cfg_setup;
	spinlock_t registration_lock; /* Lock for registration read/write */
	struct mutex profile_mutex; /* mutex for profile_pending calls */
	spinlock_t profile_lock; /* Lock for profile_pending read/write */
	struct hid_device *hdev;
	bool orphan_ack_pending;
	struct mutex cfg_mutex; /* mutex for synchronous data */
	struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */
	spinlock_t cmd_lock; /* Lock for cmd data read/write */
	u8 waiting_cmd;
	int cmd_status;
	u16 bcd_device;
	u8 ep;

	/* Gamepad Variables */
	enum claw_mkeys_function_index mkeys_function;
	enum claw_gamepad_mode_index gamepad_mode;
	u8 m1_codes[CLAW_KEYS_MAX];
	u8 m2_codes[CLAW_KEYS_MAX];
	u8 rumble_intensity_right;
	u8 rumble_intensity_left;
	const u16 *bmap_addr;
	spinlock_t rumble_lock; /* lock for rumble_intensity read/write */
	spinlock_t mode_lock; /* Lock for mode data read/write */
	bool rumble_support;
	bool gp_registered;
	bool bmap_support;

	/* RGB Variables */
	struct rgb_frame rgb_frames[CLAW_RGB_MAX_FRAMES];
	enum claw_rgb_effect_index rgb_effect;
	struct led_classdev_mc led_mc;
	struct delayed_work rgb_queue;
	spinlock_t frame_lock; /* lock for rgb_frames read/write */
	bool rgb_registered;
	u8 rgb_frame_count;
	bool rgb_enabled;
	u8 rgb_speed;
	u16 rgb_addr;
};

static int get_endpoint_address(struct hid_device *hdev)
{
	struct usb_host_endpoint *ep;
	struct usb_interface *intf;

	intf = to_usb_interface(hdev->dev.parent);
	ep = intf->cur_altsetting->endpoint;
	if (ep)
		return ep->desc.bEndpointAddress;

	return -ENODEV;
}

static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
				   struct claw_command_report *cmd_rep)
{
	if (cmd_rep->data[0] >= ARRAY_SIZE(claw_gamepad_mode_text) ||
	    !claw_gamepad_mode_text[cmd_rep->data[0]] ||
	    cmd_rep->data[1] >= ARRAY_SIZE(claw_mkeys_function_text))
		return -EINVAL;

	scoped_guard(spinlock_irqsave, &drvdata->mode_lock) {
		drvdata->gamepad_mode = cmd_rep->data[0];
		drvdata->mkeys_function = cmd_rep->data[1];
	}

	return 0;
}

static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep)
{
	enum claw_profile_ack_pending profile;
	struct claw_rumble_report *rumble;
	struct claw_mkey_report *mkeys;
	struct claw_rgb_report *frame;
	u16 rgb_addr, read_addr;
	u8 *codes, key, f_idx;
	u16 frame_calc;
	int i;

	scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
		profile = drvdata->profile_pending;

	switch (profile) {
	case CLAW_M1_PENDING:
	case CLAW_M2_PENDING:
		key = (profile == CLAW_M1_PENDING) ? CLAW_KEY_M1 : CLAW_KEY_M2;
		mkeys = (struct claw_mkey_report *)cmd_rep->data;
		if (be16_to_cpu(mkeys->read_addr) != drvdata->bmap_addr[key])
			return -EAGAIN;
		codes = (profile == CLAW_M1_PENDING) ? drvdata->m1_codes : drvdata->m2_codes;
		for (i = 0; i < CLAW_KEYS_MAX; i++)
			codes[i] = (mkeys->codes[i]);
		break;
	case CLAW_RGB_PENDING:
		frame = (struct claw_rgb_report *)cmd_rep->data;
		rgb_addr = drvdata->rgb_addr;
		read_addr = be16_to_cpu(frame->read_addr);

		if (read_addr < drvdata->rgb_addr)
			return -EAGAIN;

		frame_calc = (read_addr - rgb_addr) / CLAW_RGB_FRAME_OFFSET;
		if (frame_calc >= CLAW_RGB_MAX_FRAMES) {
			dev_err(&drvdata->hdev->dev, "Got unsupported frame index: %x\n",
				frame_calc);
			return -EAGAIN;
		}
		f_idx = frame_calc;

		scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
			memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data,
			       sizeof(struct rgb_frame));

			/* Only use frame 0 for remaining variable assignment */
			if (f_idx != 0)
				break;

			drvdata->rgb_speed = frame->speed;
			drvdata->led_mc.led_cdev.brightness = frame->brightness;
			drvdata->led_mc.subled_info[0].intensity = frame->zone_data.zone[0].red;
			drvdata->led_mc.subled_info[1].intensity = frame->zone_data.zone[0].green;
			drvdata->led_mc.subled_info[2].intensity = frame->zone_data.zone[0].blue;
		}

		break;
	case CLAW_RUMBLE_LEFT_PENDING:
		rumble = (struct claw_rumble_report *)cmd_rep->data;
		if (be16_to_cpu(rumble->read_addr) != rumble_addr[0])
			return -EAGAIN;
		scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
			drvdata->rumble_intensity_left = rumble->intensity;
		break;
	case CLAW_RUMBLE_RIGHT_PENDING:
		rumble = (struct claw_rumble_report *)cmd_rep->data;
		if (be16_to_cpu(rumble->read_addr) != rumble_addr[1])
			return -EAGAIN;
		scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
			drvdata->rumble_intensity_right = rumble->intensity;
		break;
	default:
		dev_dbg(&drvdata->hdev->dev,
			"Got profile event without changes pending from command: %x\n",
			cmd_rep->cmd);
		return -EINVAL;
	}
	scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
		drvdata->profile_pending = CLAW_NO_PENDING;

	return 0;
}

static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report,
			  u8 *data, int size)
{
	struct claw_command_report *cmd_rep;
	int ret = 0;

	if (size != CLAW_PACKET_SIZE)
		return 0;

	cmd_rep = (struct claw_command_report *)data;

	if (cmd_rep->report_id != CLAW_INPUT_REPORT_ID || cmd_rep->header_tail != 0x3c)
		return 0;

	dev_dbg(&drvdata->hdev->dev, "Rx data as raw input report: [%*ph]\n",
		CLAW_PACKET_SIZE, data);

	guard(spinlock_irqsave)(&drvdata->cmd_lock);
	switch (cmd_rep->cmd) {
	case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK:
		ret = claw_gamepad_mode_event(drvdata, cmd_rep);
		if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE) {
			drvdata->cmd_status = ret;
			complete(&drvdata->send_cmd_complete);
		}

		break;
	case CLAW_COMMAND_TYPE_READ_PROFILE_ACK:
		ret = claw_profile_event(drvdata, cmd_rep);
		/* Stale address received, ignore and keep waiting */
		if (ret == -EAGAIN)
			return 0;
		if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_PROFILE) {
			drvdata->cmd_status = ret;
			complete(&drvdata->send_cmd_complete);
		}

		break;
	case CLAW_COMMAND_TYPE_ACK:
		if (drvdata->orphan_ack_pending) {
			drvdata->orphan_ack_pending = false;
			complete(&drvdata->orphan_ack_complete);
			break;
		}

		if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_NONE) {
			dev_warn(&drvdata->hdev->dev, "Got unexpected ACK from MCU, ignoring\n");
			break;
		}

		drvdata->cmd_status = 0;
		complete(&drvdata->send_cmd_complete);

		dev_dbg(&drvdata->hdev->dev, "Waiting CMD: %x\n", drvdata->waiting_cmd);

		break;
	default:
		dev_dbg(&drvdata->hdev->dev, "Unknown command: %x\n", cmd_rep->cmd);
		return 0;
	}

	return ret;
}

static int msi_raw_event(struct hid_device *hdev, struct hid_report *report,
			 u8 *data, int size)
{
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);

	if (!drvdata || (drvdata->ep != CLAW_XINPUT_CFG_INTF_IN &&
			 drvdata->ep != CLAW_DINPUT_CFG_INTF_IN))
		return 0;

	return claw_raw_event(drvdata, report, data, size);
}

/* Caller must hold drvdata->cfg_mutex. */
static int __claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data,
				   size_t len, unsigned int timeout)
{
	unsigned char *dmabuf __free(kfree) = NULL;
	u8 header[] = { CLAW_OUTPUT_REPORT_ID, 0, 0, 0x3c, index };
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	size_t header_size = ARRAY_SIZE(header);
	bool orphaned;
	int ret;

	lockdep_assert_held(&drvdata->cfg_mutex);

	/* If expecting an orphan ack, hold next event until MCU has time to clear it */
	scoped_guard(spinlock_irqsave, &drvdata->cmd_lock)
		orphaned = drvdata->orphan_ack_pending;

	if (orphaned) {
		wait_for_completion_timeout(&drvdata->orphan_ack_complete, msecs_to_jiffies(25));
		scoped_guard(spinlock_irqsave, &drvdata->cmd_lock)
			drvdata->orphan_ack_pending = false;
	}

	if (header_size + len > CLAW_PACKET_SIZE)
		return -EINVAL;

	/* We can't use a devm_alloc reusable buffer without side effects during suspend */
	dmabuf = kzalloc(CLAW_PACKET_SIZE, GFP_KERNEL);
	if (!dmabuf)
		return -ENOMEM;

	memcpy(dmabuf, header, header_size);
	if (data && len)
		memcpy(dmabuf + header_size, data, len);

	reinit_completion(&drvdata->send_cmd_complete);

	scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
		if (timeout) {
			drvdata->waiting_cmd = index;
			drvdata->cmd_status = -ETIMEDOUT;
		} else {
			reinit_completion(&drvdata->orphan_ack_complete);
			drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;
			drvdata->orphan_ack_pending = true;
		}
	}

	dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
		CLAW_PACKET_SIZE, dmabuf);

	ret = hid_hw_output_report(hdev, dmabuf, CLAW_PACKET_SIZE);
	if (ret < 0)
		goto err;

	ret = ret == CLAW_PACKET_SIZE ? 0 : -EIO;
	if (ret)
		goto err;

	if (timeout) {
		ret = wait_for_completion_interruptible_timeout(&drvdata->send_cmd_complete,
								msecs_to_jiffies(timeout));

		dev_dbg(&hdev->dev, "Remaining timeout: %u\n", ret);
		ret = ret > 0 ? drvdata->cmd_status : ret ?: -EBUSY;
		if (ret)
			goto err;
	}

	scoped_guard(spinlock_irqsave, &drvdata->cmd_lock)
		drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;

	return ret;

err:
	scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
		drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;
		drvdata->orphan_ack_pending = false;
	}
	return ret;
}

static int claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data,
				 size_t len, unsigned int timeout)
{
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);

	guard(mutex)(&drvdata->cfg_mutex);
	return __claw_hw_output_report(hdev, index, data, len, timeout);
}

static int claw_switch_mode(struct hid_device *hdev, enum claw_mode_field field, u8 val)
{
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	u8 data[2];

	guard(mutex)(&drvdata->cfg_mutex);

	scoped_guard(spinlock_irqsave, &drvdata->mode_lock) {
		switch (field) {
		case CLAW_FIELD_GAMEPAD_MODE:
			data[0] = val;
			data[1] = drvdata->mkeys_function;
			break;
		case CLAW_FIELD_MKEYS_FUNCTION:
			data[0] = drvdata->gamepad_mode;
			data[1] = val;
			break;
		}
	}

	return __claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data,
				       ARRAY_SIZE(data), 0);
}

static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr,
				  const char *buf, size_t count)
{
	struct hid_device *hdev = to_hid_device(dev);
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	int i, ret = -EINVAL;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->gp_registered))
			return -ENODEV;
	}

	for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
		if (claw_gamepad_mode_text[i] && sysfs_streq(buf, claw_gamepad_mode_text[i])) {
			ret = i;
			break;
		}
	}
	if (ret < 0)
		return ret;

	ret = claw_switch_mode(hdev, CLAW_FIELD_GAMEPAD_MODE, ret);
	if (ret)
		return ret;

	return count;
}

static ssize_t gamepad_mode_show(struct device *dev,
				 struct device_attribute *attr, char *buf)
{
	struct hid_device *hdev = to_hid_device(dev);
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	int ret, i;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->gp_registered))
			return -ENODEV;
	}

	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 25);
	if (ret)
		return ret;

	scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
		i = drvdata->gamepad_mode;

	if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
		return sysfs_emit(buf, "unsupported\n");

	return sysfs_emit(buf, "%s\n", claw_gamepad_mode_text[i]);
}
static DEVICE_ATTR_RW(gamepad_mode);

static ssize_t gamepad_mode_index_show(struct device *dev,
				       struct device_attribute *attr, char *buf)
{
	ssize_t count = 0;
	int i;

	for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
		if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
			continue;
		count += sysfs_emit_at(buf, count, "%s ", claw_gamepad_mode_text[i]);
	}

	if (count)
		buf[count - 1] = '\n';

	return count;
}
static DEVICE_ATTR_RO(gamepad_mode_index);

static ssize_t mkeys_function_store(struct device *dev, struct device_attribute *attr,
				    const char *buf, size_t count)
{
	struct hid_device *hdev = to_hid_device(dev);
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	int i, ret = -EINVAL;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->gp_registered))
			return -ENODEV;
	}

	for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) {
		if (claw_mkeys_function_text[i] && sysfs_streq(buf, claw_mkeys_function_text[i])) {
			ret = i;
			break;
		}
	}
	if (ret < 0)
		return ret;

	ret = claw_switch_mode(hdev, CLAW_FIELD_MKEYS_FUNCTION, ret);
	if (ret)
		return ret;

	return count;
}

static ssize_t mkeys_function_show(struct device *dev, struct device_attribute *attr,
				   char *buf)
{
	struct hid_device *hdev = to_hid_device(dev);
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	int ret, i;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->gp_registered))
			return -ENODEV;
	}

	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 25);
	if (ret)
		return ret;

	scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
		i = drvdata->mkeys_function;

	if (i >= ARRAY_SIZE(claw_mkeys_function_text))
		return sysfs_emit(buf, "unsupported\n");

	return sysfs_emit(buf, "%s\n", claw_mkeys_function_text[i]);
}
static DEVICE_ATTR_RW(mkeys_function);

static ssize_t mkeys_function_index_show(struct device *dev,
					 struct device_attribute *attr, char *buf)
{
	int i, count = 0;

	for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++)
		count += sysfs_emit_at(buf, count, "%s ", claw_mkeys_function_text[i]);

	if (count)
		buf[count - 1] = '\n';

	return count;
}
static DEVICE_ATTR_RO(mkeys_function_index);

static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
			   const char *buf, size_t count)
{
	struct hid_device *hdev = to_hid_device(dev);
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	bool val;
	int ret;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->gp_registered))
			return -ENODEV;
	}

	ret = kstrtobool(buf, &val);
	if (ret)
		return ret;

	if (!val)
		return -EINVAL;

	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_RESET_DEVICE, NULL, 0, 0);
	if (ret)
		return ret;

	return count;
}
static DEVICE_ATTR_WO(reset);

static int mkey_mapping_name_to_code(const char *name)
{
	int i;

	for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
		if (!strcmp(name, claw_button_mapping_key_map[i].name))
			return claw_button_mapping_key_map[i].code;
	}

	return -EINVAL;
}

static const char *mkey_mapping_code_to_name(u8 code)
{
	int i;

	if (code == 0xff)
		return NULL;

	for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
		if (claw_button_mapping_key_map[i].code == code)
			return claw_button_mapping_key_map[i].name;
	}

	return NULL;
}

static int claw_mkey_store(struct device *dev, const char *buf, u8 mkey)
{
	struct hid_device *hdev = to_hid_device(dev);
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	struct claw_mkey_report report = { {0x01, cpu_to_be16(drvdata->bmap_addr[mkey])},
				   0x07, 0x04, 0x00, {0xff, 0xff, 0xff, 0xff, 0xff} };
	char **raw_keys __free(argv_free) = NULL;
	int ret, key_count, i;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->gp_registered))
			return -ENODEV;
	}

	raw_keys = argv_split(GFP_KERNEL, buf, &key_count);
	if (!raw_keys)
		return -ENOMEM;

	if (key_count > CLAW_KEYS_MAX)
		return -EINVAL;

	if (key_count == 0)
		goto set_buttons;

	for (i = 0; i < key_count; i++) {
		ret = mkey_mapping_name_to_code(raw_keys[i]);
		if (ret < 0)
			return ret;

		report.codes[i] = ret;
	}

set_buttons:
	scoped_guard(mutex, &drvdata->rom_mutex) {
		ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
					    (u8 *)&report, sizeof(report), 25);
		if (ret)
			return ret;
		/* MCU will not send ACK until the USB transaction completes. ACK is sent
		 * immediately after and will hit the stale state machine, before the next
		 * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
		 * for ACK that ill never come.
		 */
		ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
	}

	return ret;
}

static int claw_mkey_show(struct device *dev, char *buf, enum claw_key_index m_key)
{
	struct hid_device *hdev = to_hid_device(dev);
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	struct claw_mkey_report report = { {0x01, cpu_to_be16(drvdata->bmap_addr[m_key])}, 0x07 };
	int i, ret, count = 0;
	const char *name;
	u8 *codes;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->gp_registered))
			return -ENODEV;
	}

	codes = (m_key == CLAW_KEY_M1) ? drvdata->m1_codes : drvdata->m2_codes;

	guard(mutex)(&drvdata->profile_mutex);
	scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
		drvdata->profile_pending = (m_key == CLAW_KEY_M1) ? CLAW_M1_PENDING
								  : CLAW_M2_PENDING;

	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
				    (u8 *)&report, sizeof(report), 25);
	if (ret)
		return ret;

	for (i = 0; i < CLAW_KEYS_MAX; i++) {
		name = mkey_mapping_code_to_name(codes[i]);
		if (name)
			count += sysfs_emit_at(buf, count, "%s ", name);
	}

	if (!count)
		return sysfs_emit(buf, "(not set)\n");

	buf[count - 1] = '\n';

	return count;
}

static ssize_t button_m1_store(struct device *dev, struct device_attribute *attr,
			       const char *buf, size_t count)
{
	int ret;

	ret = claw_mkey_store(dev, buf, CLAW_KEY_M1);
	if (ret)
		return ret;

	return count;
}

static ssize_t button_m1_show(struct device *dev, struct device_attribute *attr,
			      char *buf)
{
	return claw_mkey_show(dev, buf, CLAW_KEY_M1);
}
static DEVICE_ATTR_RW(button_m1);

static ssize_t button_m2_store(struct device *dev, struct device_attribute *attr,
			       const char *buf, size_t count)
{
	int ret;

	ret = claw_mkey_store(dev, buf, CLAW_KEY_M2);
	if (ret)
		return ret;

	return count;
}

static ssize_t button_m2_show(struct device *dev, struct device_attribute *attr,
			      char *buf)
{
	return claw_mkey_show(dev, buf, CLAW_KEY_M2);
}
static DEVICE_ATTR_RW(button_m2);

static ssize_t button_mapping_options_show(struct device *dev,
					   struct device_attribute *attr, char *buf)
{
	int i, count = 0;

	for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++)
		count += sysfs_emit_at(buf, count, "%s ", claw_button_mapping_key_map[i].name);

	if (count)
		buf[count - 1] = '\n';

	return count;
}
static DEVICE_ATTR_RO(button_mapping_options);

static ssize_t rumble_intensity_left_store(struct device *dev,
					   struct device_attribute *attr,
					   const char *buf, size_t count)
{
	struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[0])}, 0x01 };
	struct hid_device *hdev = to_hid_device(dev);
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	u8 val;
	int ret;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->gp_registered))
			return -ENODEV;
	}

	ret = kstrtou8(buf, 10, &val);
	if (ret)
		return ret;

	if (val > 100)
		return -EINVAL;

	report.intensity = val;

	guard(mutex)(&drvdata->rom_mutex);
	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
				    (u8 *)&report, sizeof(report), 25);
	if (ret)
		return ret;

	/* MCU will not send ACK until the USB transaction completes. ACK is sent
	 * immediately after and will hit the stale state machine, before the next
	 * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
	 * for ACK that ill never come.
	 */
	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
	if (ret)
		return ret;

	return count;
}

static ssize_t rumble_intensity_left_show(struct device *dev,
					  struct device_attribute *attr,
					  char *buf)
{
	struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[0])}, 0x01 };
	struct hid_device *hdev = to_hid_device(dev);
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	int ret;
	u8 val;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->gp_registered))
			return -ENODEV;
	}

	guard(mutex)(&drvdata->profile_mutex);
	scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
		drvdata->profile_pending = CLAW_RUMBLE_LEFT_PENDING;
	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
				    (u8 *)&report, sizeof(report), 25);
	if (ret)
		return ret;

	scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
		val = drvdata->rumble_intensity_left;

	return sysfs_emit(buf, "%u\n", val);
}
static DEVICE_ATTR_RW(rumble_intensity_left);

static ssize_t rumble_intensity_right_store(struct device *dev,
					    struct device_attribute *attr,
					    const char *buf, size_t count)
{
	struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[1])}, 0x01 };
	struct hid_device *hdev = to_hid_device(dev);
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	u8 val;
	int ret;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->gp_registered))
			return -ENODEV;
	}

	ret = kstrtou8(buf, 10, &val);
	if (ret)
		return ret;

	if (val > 100)
		return -EINVAL;

	report.intensity = val;

	guard(mutex)(&drvdata->rom_mutex);
	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
				    (u8 *)&report, sizeof(report), 25);
	if (ret)
		return ret;

	/* MCU will not send ACK until the USB transaction completes. ACK is sent
	 * immediately after and will hit the stale state machine, before the next
	 * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
	 * for ACK that ill never come.
	 */
	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
	if (ret)
		return ret;

	return count;
}

static ssize_t rumble_intensity_right_show(struct device *dev,
					   struct device_attribute *attr,
					   char *buf)
{
	struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[1])}, 0x01 };
	struct hid_device *hdev = to_hid_device(dev);
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	int ret;
	u8 val;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->gp_registered))
			return -ENODEV;
	}

	guard(mutex)(&drvdata->profile_mutex);
	scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
		drvdata->profile_pending = CLAW_RUMBLE_RIGHT_PENDING;
	ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
				    (u8 *)&report, sizeof(report), 25);
	if (ret)
		return ret;

	scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
		val = drvdata->rumble_intensity_right;

	return sysfs_emit(buf, "%u\n", val);
}
static DEVICE_ATTR_RW(rumble_intensity_right);

static ssize_t rumble_intensity_range_show(struct device *dev,
					   struct device_attribute *attr,
					   char *buf)
{
	return sysfs_emit(buf, "0-100\n");
}
static DEVICE_ATTR_RO(rumble_intensity_range);

static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
					    int n)
{
	struct hid_device *hdev = to_hid_device(kobj_to_dev(kobj));
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);

	if (!drvdata) {
		dev_warn(&hdev->dev,
			 "Failed to get drvdata from kobj. Gamepad attributes are not available.\n");
		return 0;
	}

	/* Always show attrs available on all firmware */
	if (attr == &dev_attr_gamepad_mode.attr ||
	    attr == &dev_attr_gamepad_mode_index.attr ||
	    attr == &dev_attr_mkeys_function.attr ||
	    attr == &dev_attr_mkeys_function_index.attr ||
	    attr == &dev_attr_reset.attr)
		return attr->mode;

	/* Hide rumble attrs if not supported */
	if (attr == &dev_attr_rumble_intensity_left.attr ||
	    attr == &dev_attr_rumble_intensity_right.attr ||
	    attr == &dev_attr_rumble_intensity_range.attr)
		return drvdata->rumble_support ? attr->mode : 0;

	/* Hide button mapping attrs if it isn't supported */
	return drvdata->bmap_support ? attr->mode : 0;
}

static struct attribute *claw_gamepad_attrs[] = {
	&dev_attr_button_m1.attr,
	&dev_attr_button_m2.attr,
	&dev_attr_button_mapping_options.attr,
	&dev_attr_gamepad_mode.attr,
	&dev_attr_gamepad_mode_index.attr,
	&dev_attr_mkeys_function.attr,
	&dev_attr_mkeys_function_index.attr,
	&dev_attr_reset.attr,
	&dev_attr_rumble_intensity_left.attr,
	&dev_attr_rumble_intensity_right.attr,
	&dev_attr_rumble_intensity_range.attr,
	NULL,
};

static const struct attribute_group claw_gamepad_attr_group = {
	.attrs = claw_gamepad_attrs,
	.is_visible = claw_gamepad_attr_is_visible,
};

/* Read RGB config from device */
static int claw_read_rgb_config(struct hid_device *hdev)
{
	u8 data[4] = { 0x01, 0x00, 0x00, CLAW_RGB_FRAME_OFFSET };
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	u16 read_addr = drvdata->rgb_addr;
	size_t len = ARRAY_SIZE(data);
	int ret, i;

	if (!drvdata->rgb_addr)
		return -ENODEV;

	/* Loop through all 8 pages of RGB data */
	guard(mutex)(&drvdata->profile_mutex);
	for (i = 0; i < CLAW_RGB_MAX_FRAMES; i++) {
		scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
			drvdata->profile_pending = CLAW_RGB_PENDING;
		data[1] = (read_addr >> 8) & 0xff;
		data[2] = read_addr & 0x00ff;
		ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 25);
		if (ret)
			return ret;

		read_addr += CLAW_RGB_FRAME_OFFSET;
	}

	return 0;
}

/* Send RGB configuration to device */
static int claw_write_rgb_state(struct claw_drvdata *drvdata)
{
	struct claw_rgb_report report = { {0x01, 0}, CLAW_RGB_FRAME_OFFSET, 0x00,
			drvdata->rgb_frame_count, 0x09, drvdata->rgb_speed,
			drvdata->led_mc.led_cdev.brightness };
	u16 write_addr = drvdata->rgb_addr;
	int f, ret;

	if (!drvdata->rgb_addr)
		return -ENODEV;

	if (!drvdata->rgb_frame_count)
		return -EINVAL;

	guard(mutex)(&drvdata->rom_mutex);
	/* Loop through (up to) 8 pages of RGB data */
	for (f = 0; f < drvdata->rgb_frame_count; f++) {
		scoped_guard(spinlock_irqsave, &drvdata->frame_lock)
			report.zone_data = drvdata->rgb_frames[f];

		/* Set the MCU address to write the frame data to */
		report.read_addr = cpu_to_be16(write_addr);

		/* Serialize the rgb_report and write it to MCU */
		ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
					    (u8 *)&report, sizeof(report), 25);
		if (ret)
			return ret;

		/* Increment the write addr by the offset for the next frame */
		write_addr += CLAW_RGB_FRAME_OFFSET;
	}

	/* MCU will not send ACK until the USB transaction completes. ACK is sent
	 * immediately after and will hit the stale state machine, before the next
	 * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
	 * for ACK that ill never come.
	 */
	ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);

	return ret;
}

/* Fill all zones with the same color */
static void claw_frame_fill_solid(struct rgb_frame *frame, struct rgb_zone zone)
{
	int z;

	for (z = 0; z < CLAW_RGB_ZONES; z++)
		frame->zone[z] = zone;
}

/* Apply solid effect (1 frame, no color) */
static int claw_apply_disabled(struct claw_drvdata *drvdata)
{
	struct rgb_zone off = { 0x00, 0x00, 0x00};

	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
		drvdata->rgb_frame_count = 1;
		claw_frame_fill_solid(&drvdata->rgb_frames[0], off);
	}

	return claw_write_rgb_state(drvdata);
}

/* Apply solid effect (1 frame, all zones same color) */
static int claw_apply_monocolor(struct claw_drvdata *drvdata)
{
	struct mc_subled *subleds = drvdata->led_mc.subled_info;
	struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity,
				 subleds[2].intensity };

	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
		drvdata->rgb_frame_count = 1;
		claw_frame_fill_solid(&drvdata->rgb_frames[0], zone);
	}

	return claw_write_rgb_state(drvdata);
}

/* Apply breathe effect (2 frames: color -> off) */
static int claw_apply_breathe(struct claw_drvdata *drvdata)
{
	struct mc_subled *subleds = drvdata->led_mc.subled_info;
	struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity,
				 subleds[2].intensity };
	static const struct rgb_zone off = { 0, 0, 0 };

	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
		drvdata->rgb_frame_count = 2;
		claw_frame_fill_solid(&drvdata->rgb_frames[0], zone);
		claw_frame_fill_solid(&drvdata->rgb_frames[1], off);
	}

	return claw_write_rgb_state(drvdata);
}

/* Apply chroma effect (6 frames: rainbow cycle, all zones sync) */
static int claw_apply_chroma(struct claw_drvdata *drvdata)
{
	static const struct rgb_zone colors[] = {
		{255,   0,   0},  /* red     */
		{255, 255,   0},  /* yellow  */
		{  0, 255,   0},  /* green   */
		{  0, 255, 255},  /* cyan    */
		{  0,   0, 255},  /* blue    */
		{255,   0, 255},  /* magenta */
	};
	u8 frame_count = ARRAY_SIZE(colors);
	int f;

	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
		drvdata->rgb_frame_count = frame_count;

		for (f = 0; f < frame_count; f++)
			claw_frame_fill_solid(&drvdata->rgb_frames[f], colors[f]);
	}

	return claw_write_rgb_state(drvdata);
}

/* Apply rainbow effect (4 frames: rotating colors around joysticks) */
static int claw_apply_rainbow(struct claw_drvdata *drvdata)
{
	static const struct rgb_zone colors[] = {
		{255,   0,   0},  /* red   */
		{  0, 255,   0},  /* green */
		{  0, 255, 255},  /* cyan  */
		{  0,   0, 255},  /* blue  */
	};
	u8 frame_count = ARRAY_SIZE(colors);
	int f, z;

	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
		drvdata->rgb_frame_count = frame_count;

		for (f = 0; f < frame_count; f++) {
			for (z = 0; z < 4; z++) {
				drvdata->rgb_frames[f].zone[z]     = colors[(z + f) % 4];
				drvdata->rgb_frames[f].zone[z + 4] = colors[(z + f) % 4];
			}
			drvdata->rgb_frames[f].zone[8] = colors[f];
		}
	}

	return claw_write_rgb_state(drvdata);
}

/*
 * Apply frostfire effect (4 frames: fire vs ice rotating)
 * Right joystick: fire red -> dark -> ice blue -> dark (clockwise)
 * Left joystick: ice blue -> dark -> fire red -> dark (counter-clockwise)
 * ABXY: fire red -> dark -> ice blue -> dark
 */
static int claw_apply_frostfire(struct claw_drvdata *drvdata)
{
	static const struct rgb_zone colors[] = {
		{255,   0,   0},  /* fire red */
		{  0,   0,   0},  /* dark     */
		{  0,   0, 255},  /* ice blue */
		{  0,   0,   0},  /* dark     */
	};
	u8 frame_count = ARRAY_SIZE(colors);
	int f, z;

	scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
		drvdata->rgb_frame_count = frame_count;

		for (f = 0; f < frame_count; f++) {
			for (z = 0; z < 4; z++) {
				drvdata->rgb_frames[f].zone[z]     = colors[(z + f) % 4];
				drvdata->rgb_frames[f].zone[z + 4] = colors[(z - f + 6) % 4];
			}
			drvdata->rgb_frames[f].zone[8] = colors[f];
		}
	}

	return claw_write_rgb_state(drvdata);
}

/* Apply current state to device */
static int claw_apply_rgb_state(struct claw_drvdata *drvdata)
{
	if (!drvdata->rgb_enabled)
		return claw_apply_disabled(drvdata);

	switch (drvdata->rgb_effect) {
	case CLAW_RGB_EFFECT_MONOCOLOR:
		return claw_apply_monocolor(drvdata);
	case CLAW_RGB_EFFECT_BREATHE:
		return claw_apply_breathe(drvdata);
	case CLAW_RGB_EFFECT_CHROMA:
		return claw_apply_chroma(drvdata);
	case CLAW_RGB_EFFECT_RAINBOW:
		return claw_apply_rainbow(drvdata);
	case CLAW_RGB_EFFECT_FROSTFIRE:
		return claw_apply_frostfire(drvdata);
	default:
		dev_err(&drvdata->hdev->dev, "No supported rgb_effect selected\n");
		return -EINVAL;
	}
}

static void claw_rgb_queue_fn(struct work_struct *work)
{
	struct delayed_work *dwork = container_of(work, struct delayed_work, work);
	struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, rgb_queue);
	int ret;

	if (!drvdata)
		return;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->rgb_registered))
			return;
	}

	ret = claw_apply_rgb_state(drvdata);
	if (ret)
		dev_err(&drvdata->hdev->dev, "Failed to apply RGB state: %d\n", ret);
}

static ssize_t effect_store(struct device *dev,
			    struct device_attribute *attr,
			    const char *buf, size_t count)
{
	struct led_classdev *led_cdev = dev_get_drvdata(dev);
	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
	int ret;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->rgb_registered))
			return -ENODEV;
	}

	ret = sysfs_match_string(claw_rgb_effect_text, buf);
	if (ret < 0)
		return ret;

	scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
		drvdata->rgb_effect = ret;

	mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));

	return count;
}

static ssize_t effect_show(struct device *dev,
			   struct device_attribute *attr, char *buf)
{
	struct led_classdev *led_cdev = dev_get_drvdata(dev);
	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->rgb_registered))
			return -ENODEV;
	}

	if (drvdata->rgb_effect >= ARRAY_SIZE(claw_rgb_effect_text))
		return -EINVAL;

	return sysfs_emit(buf, "%s\n", claw_rgb_effect_text[drvdata->rgb_effect]);
}

static DEVICE_ATTR_RW(effect);

static ssize_t effect_index_show(struct device *dev,
				 struct device_attribute *attr, char *buf)
{
	int i, count = 0;

	for (i = 0; i < ARRAY_SIZE(claw_rgb_effect_text); i++)
		count += sysfs_emit_at(buf, count, "%s ", claw_rgb_effect_text[i]);

	if (count)
		buf[count - 1] = '\n';

	return count;
}
static DEVICE_ATTR_RO(effect_index);

static ssize_t enabled_store(struct device *dev,
			     struct device_attribute *attr,
			     const char *buf, size_t count)
{
	struct led_classdev *led_cdev = dev_get_drvdata(dev);
	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
	bool val;
	int ret;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->rgb_registered))
			return -ENODEV;
	}

	ret = kstrtobool(buf, &val);
	if (ret)
		return ret;

	scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
		drvdata->rgb_enabled = val;

	mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));

	return count;
}

static ssize_t enabled_show(struct device *dev,
			    struct device_attribute *attr, char *buf)
{
	struct led_classdev *led_cdev = dev_get_drvdata(dev);
	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->rgb_registered))
			return -ENODEV;
	}

	return sysfs_emit(buf, "%s\n", drvdata->rgb_enabled ? "true" : "false");
}
static DEVICE_ATTR_RW(enabled);

static ssize_t enabled_index_show(struct device *dev,
				  struct device_attribute *attr, char *buf)
{
	return sysfs_emit(buf, "true false\n");
}
static DEVICE_ATTR_RO(enabled_index);

static ssize_t speed_store(struct device *dev, struct device_attribute *attr,
			   const char *buf, size_t count)
{
	struct led_classdev *led_cdev = dev_get_drvdata(dev);
	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
	unsigned int val, speed;
	int ret;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->rgb_registered))
			return -ENODEV;
	}

	ret = kstrtouint(buf, 10, &val);
	if (ret)
		return ret;

	if (val > 20)
		return -EINVAL;

	/* 0 is fastest, invert value for intuitive userspace speed */
	speed = 20 - val;

	scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
		drvdata->rgb_speed = speed;

	mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));

	return count;
}

static ssize_t speed_show(struct device *dev, struct device_attribute *attr,
			  char *buf)
{
	struct led_classdev *led_cdev = dev_get_drvdata(dev);
	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
	u8 speed = 20 - drvdata->rgb_speed;

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->rgb_registered))
			return -ENODEV;
	}

	return sysfs_emit(buf, "%u\n", speed);
}
static DEVICE_ATTR_RW(speed);

static ssize_t speed_range_show(struct device *dev,
				struct device_attribute *attr, char *buf)
{
	return sysfs_emit(buf, "0-20\n");
}
static DEVICE_ATTR_RO(speed_range);

static void claw_led_brightness_set(struct led_classdev *led_cdev,
				    enum led_brightness _brightness)
{
	struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
	struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		if (!smp_load_acquire(&drvdata->rgb_registered))
			return;
	}

	mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
}

static struct attribute *claw_rgb_attrs[] = {
	&dev_attr_effect.attr,
	&dev_attr_effect_index.attr,
	&dev_attr_enabled.attr,
	&dev_attr_enabled_index.attr,
	&dev_attr_speed.attr,
	&dev_attr_speed_range.attr,
	NULL,
};

static const struct attribute_group claw_rgb_attr_group = {
	.attrs = claw_rgb_attrs,
};

static struct mc_subled claw_rgb_subled_info[] = {
	{
		.color_index = LED_COLOR_ID_RED,
		.channel = 0x1,
	},
	{
		.color_index = LED_COLOR_ID_GREEN,
		.channel = 0x2,
	},
	{
		.color_index = LED_COLOR_ID_BLUE,
		.channel = 0x3,
	},
};

static void cfg_setup_fn(struct work_struct *work)
{
	struct delayed_work *dwork = container_of(work, struct delayed_work, work);
	struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_setup);
	bool gamepad_ready = false, rgb_ready = false, gp_registered, rgb_registered;
	int ret;

	ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE,
				    NULL, 0, 25);
	if (ret) {
		dev_err(&drvdata->hdev->dev,
			"Failed to read gamepad mode: %d\n", ret);
		goto prep_rgb;
	}
	gamepad_ready = true;

prep_rgb:
	ret = claw_read_rgb_config(drvdata->hdev);
	if (ret) {
		dev_err(&drvdata->hdev->dev,
			"Failed to read RGB config: %d\n", ret);
		goto try_gamepad;
	}
	rgb_ready = true;

	/* Add sysfs attributes after we get the device state */
try_gamepad:
	scoped_guard(spinlock_irqsave, &drvdata->registration_lock)
		/* Pairs with smp_store_release from below */
		gp_registered = smp_load_acquire(&drvdata->gp_registered);

	if (!gp_registered && gamepad_ready) {
		ret = device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
		if (ret) {
			dev_err(&drvdata->hdev->dev,
				"Failed to create gamepad attrs: %d\n", ret);
			goto try_rgb;
		}

		scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
			/* Pairs with smp_load_acquire in attribute show/store functions */
			smp_store_release(&drvdata->gp_registered, true);
			gp_registered = true;
		}
	}

try_rgb:
	/* Add and enable RGB interface once we have the device state */
	scoped_guard(spinlock_irqsave, &drvdata->registration_lock)
		/* Pairs with smp_store_release from below */
		rgb_registered = smp_load_acquire(&drvdata->rgb_registered);

	if (!rgb_registered && rgb_ready) {
		ret = led_classdev_multicolor_register(&drvdata->hdev->dev,
						       &drvdata->led_mc);
		if (ret) {
			dev_err(&drvdata->hdev->dev, "Failed to create led device: %d\n", ret);
			goto update_kobjects;
		}

		ret = device_add_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group);
		if (ret) {
			dev_err(&drvdata->hdev->dev, "Failed to create RGB attrs: %d\n", ret);
			led_classdev_multicolor_unregister(&drvdata->led_mc);
			goto update_kobjects;
		}

		scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
			/* Pairs with smp_load_acquire in attribute show/store functions */
			smp_store_release(&drvdata->rgb_registered, true);
			rgb_registered = true;
		}
	}

update_kobjects:
	if (gp_registered)
		kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE);
	if (rgb_registered)
		kobject_uevent(&drvdata->led_mc.led_cdev.dev->kobj, KOBJ_CHANGE);
}

static void cfg_resume_fn(struct work_struct *work)
{
	struct delayed_work *dwork = container_of(work, struct delayed_work, work);
	struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_resume);

	guard(spinlock_irqsave)(&drvdata->registration_lock);
	    /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
	if (!smp_load_acquire(&drvdata->gp_registered) ||
	    /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
	    !smp_load_acquire(&drvdata->rgb_registered))
		schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
}

static void claw_features_supported(struct claw_drvdata *drvdata)
{
	u8 major = (drvdata->bcd_device >> 8) & 0xff;
	u8 minor = drvdata->bcd_device & 0xff;

	if (major == 0x01) {
		drvdata->bmap_support = true;
		if (minor >= 0x66) {
			drvdata->bmap_addr = button_mapping_addr_new;
			drvdata->rumble_support = true;
			drvdata->rgb_addr = rgb_addr_new;
		} else {
			drvdata->bmap_addr = button_mapping_addr_old;
			drvdata->rgb_addr = rgb_addr_old;
		}
		return;
	}

	if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
		drvdata->bmap_support = true;
		drvdata->bmap_addr = button_mapping_addr_new;
		drvdata->rumble_support = true;
		drvdata->rgb_addr = rgb_addr_new;
		return;
	}

	drvdata->rgb_addr = rgb_addr_old;
}

static int claw_probe(struct hid_device *hdev, u8 ep)
{
	struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
	struct usb_device *udev = interface_to_usbdev(intf);
	struct claw_drvdata *drvdata;
	int ret;

	drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
	if (!drvdata)
		return -ENOMEM;

	drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT;
	drvdata->rgb_enabled = true;
	drvdata->hdev = hdev;
	drvdata->ep = ep;

	/* Determine feature level from firmware version */
	drvdata->bcd_device = le16_to_cpu(udev->descriptor.bcdDevice);
	claw_features_supported(drvdata);

	if (!drvdata->bmap_support)
		dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n");

	/* Device is hardwired and name is guaranteed to be unique */
	drvdata->led_mc.led_cdev.name = "msi_claw:rgb:joystick_rings";
	drvdata->led_mc.led_cdev.brightness = 0x50;
	drvdata->led_mc.led_cdev.max_brightness = 0x64;
	drvdata->led_mc.led_cdev.color = LED_COLOR_ID_RGB;
	drvdata->led_mc.led_cdev.brightness_set = claw_led_brightness_set;
	drvdata->led_mc.num_colors = 3;
	drvdata->led_mc.subled_info = devm_kmemdup(&hdev->dev, claw_rgb_subled_info,
						   sizeof(claw_rgb_subled_info), GFP_KERNEL);
	if (!drvdata->led_mc.subled_info)
		return -ENOMEM;

	mutex_init(&drvdata->cfg_mutex);
	mutex_init(&drvdata->profile_mutex);
	mutex_init(&drvdata->rom_mutex);
	spin_lock_init(&drvdata->registration_lock);
	spin_lock_init(&drvdata->cmd_lock);
	spin_lock_init(&drvdata->mode_lock);
	spin_lock_init(&drvdata->profile_lock);
	spin_lock_init(&drvdata->frame_lock);
	spin_lock_init(&drvdata->rumble_lock);
	init_completion(&drvdata->orphan_ack_complete);
	init_completion(&drvdata->send_cmd_complete);
	INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
	INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
	INIT_DELAYED_WORK(&drvdata->rgb_queue, &claw_rgb_queue_fn);

	/* For control interface: open the HID transport for sending commands. */
	ret = hid_hw_open(hdev);
	if (ret)
		return ret;

	hid_set_drvdata(hdev, drvdata);
	schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));

	return 0;
}

static int msi_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
	int ret;
	u8 ep;

	if (!hid_is_usb(hdev)) {
		ret = -ENODEV;
		goto err_probe;
	}

	ret = hid_parse(hdev);
	if (ret)
		goto err_probe;

	/* Set quirk to create separate input devices per HID application */
	hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT;
	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
	if (ret)
		goto err_probe;

	/* For non-control interfaces (keyboard/mouse), allow userspace to grab the devices. */
	ret = get_endpoint_address(hdev);
	if (ret < 0)
		goto err_stop_hw;

	ep = ret;
	if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) {
		ret = claw_probe(hdev, ep);
		if (ret)
			goto err_stop_hw;
	}

	return 0;

err_stop_hw:
	hid_hw_stop(hdev);
err_probe:
	return dev_err_probe(&hdev->dev, ret, "Failed to init device\n");
}

static void claw_remove(struct hid_device *hdev)
{
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
	bool gp_registered;
	bool rgb_registered;

	if (!drvdata)
		return;

	cancel_delayed_work_sync(&drvdata->cfg_resume);
	cancel_delayed_work_sync(&drvdata->cfg_setup);

	scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		gp_registered = smp_load_acquire(&drvdata->gp_registered);
		/* Pairs with smp_load_acquire in attribute show/store functions */
		smp_store_release(&drvdata->gp_registered, false);
		/* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
		rgb_registered = smp_load_acquire(&drvdata->rgb_registered);
		/* Pairs with smp_load_acquire in attribute show/store functions */
		smp_store_release(&drvdata->rgb_registered, false);
	}

	if (gp_registered)
		device_remove_group(&hdev->dev, &claw_gamepad_attr_group);

	if (rgb_registered) {
		device_remove_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group);
		led_classdev_multicolor_unregister(&drvdata->led_mc);
	}
	cancel_delayed_work_sync(&drvdata->rgb_queue);

	hid_hw_close(hdev);
}

static void msi_remove(struct hid_device *hdev)
{
	int ret;
	u8 ep;

	/* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
	ret = get_endpoint_address(hdev);
	if (ret <= 0)
		goto hw_stop;

	ep = ret;
	if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
		claw_remove(hdev);

hw_stop:
	hid_hw_stop(hdev);
}

static int claw_resume(struct hid_device *hdev)
{
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);

	if (!drvdata)
		return -ENODEV;

	/* MCU can take up to 500ms to be ready after resume */
	schedule_delayed_work(&drvdata->cfg_resume, msecs_to_jiffies(500));
	return 0;
}

static int msi_resume(struct hid_device *hdev)
{
	int ret;
	u8 ep;

	/* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
	ret = get_endpoint_address(hdev);
	if (ret <= 0)
		return 0;

	ep = ret;
	if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
		return claw_resume(hdev);

	return 0;
}

static int claw_suspend(struct hid_device *hdev)
{
	struct claw_drvdata *drvdata = hid_get_drvdata(hdev);

	if (!drvdata)
		return -ENODEV;

	cancel_delayed_work_sync(&drvdata->cfg_resume);
	cancel_delayed_work_sync(&drvdata->cfg_setup);
	cancel_delayed_work_sync(&drvdata->rgb_queue);

	return 0;
}

static int msi_suspend(struct hid_device *hdev, pm_message_t msg)
{
	int ret;
	u8 ep;

	/* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
	ret = get_endpoint_address(hdev);
	if (ret <= 0)
		return 0;

	ep = ret;
	if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
		return claw_suspend(hdev);

	return 0;
}

static const struct hid_device_id msi_devices[] = {
	{ HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_XINPUT) },
	{ HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DINPUT) },
	{ HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DESKTOP) },
	{ HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_BIOS) },
	{ }
};
MODULE_DEVICE_TABLE(hid, msi_devices);

static struct hid_driver msi_driver = {
	.name		= "hid-msi",
	.id_table	= msi_devices,
	.raw_event	= msi_raw_event,
	.probe		= msi_probe,
	.remove		= msi_remove,
	.resume		= pm_ptr(msi_resume),
	.suspend	= pm_ptr(msi_suspend),
};
module_hid_driver(msi_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Denis Benato <denis.benato@linux.dev>");
MODULE_AUTHOR("Zhouwang Huang <honjow311@gmail.com>");
MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
MODULE_DESCRIPTION("HID driver for MSI Claw Handheld PC gamepads");
