// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *  USB HID driver for Kysona
 *  Kysona M600 mice.
 *
 *  Copyright (c) 2024 Lode Willems <me@lodewillems.com>
 */

#include <linux/device.h>
#include <linux/hid.h>
#include <linux/usb.h>

#include "hid-ids.h"

#define BATTERY_TIMEOUT_MS 5000

#define BATTERY_REPORT_ID 4

struct kysona_drvdata {
	struct hid_device *hdev;
	bool online;

	struct power_supply_desc battery_desc;
	struct power_supply *battery;
	u8 battery_capacity;
	bool battery_charging;
	u16 battery_voltage;
	struct delayed_work battery_work;
};

static enum power_supply_property kysona_battery_props[] = {
	POWER_SUPPLY_PROP_STATUS,
	POWER_SUPPLY_PROP_PRESENT,
	POWER_SUPPLY_PROP_CAPACITY,
	POWER_SUPPLY_PROP_SCOPE,
	POWER_SUPPLY_PROP_MODEL_NAME,
	POWER_SUPPLY_PROP_VOLTAGE_NOW,
	POWER_SUPPLY_PROP_ONLINE
};

static int kysona_battery_get_property(struct power_supply *psy,
				       enum power_supply_property psp,
				       union power_supply_propval *val)
{
	struct kysona_drvdata *drv_data = power_supply_get_drvdata(psy);
	int ret = 0;

	switch (psp) {
	case POWER_SUPPLY_PROP_PRESENT:
		val->intval = 1;
		break;
	case POWER_SUPPLY_PROP_ONLINE:
		val->intval = drv_data->online;
		break;
	case POWER_SUPPLY_PROP_STATUS:
		if (drv_data->online)
			val->intval = drv_data->battery_charging ?
					POWER_SUPPLY_STATUS_CHARGING :
					POWER_SUPPLY_STATUS_DISCHARGING;
		else
			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
		break;
	case POWER_SUPPLY_PROP_SCOPE:
		val->intval = POWER_SUPPLY_SCOPE_DEVICE;
		break;
	case POWER_SUPPLY_PROP_CAPACITY:
		val->intval = drv_data->battery_capacity;
		break;
	case POWER_SUPPLY_PROP_VOLTAGE_NOW:
		/* hardware reports voltage in mV. sysfs expects uV */
		val->intval = drv_data->battery_voltage * 1000;
		break;
	case POWER_SUPPLY_PROP_MODEL_NAME:
		val->strval = drv_data->hdev->name;
		break;
	default:
		ret = -EINVAL;
		break;
	}
	return ret;
}

static const char kysona_battery_request[] = {
	0x08, BATTERY_REPORT_ID, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49
};

static int kysona_m600_fetch_battery(struct hid_device *hdev)
{
	u8 *write_buf;
	int ret;

	/* Request battery information */
	write_buf = kmemdup(kysona_battery_request, sizeof(kysona_battery_request), GFP_KERNEL);
	if (!write_buf)
		return -ENOMEM;

	ret = hid_hw_raw_request(hdev, kysona_battery_request[0],
				 write_buf, sizeof(kysona_battery_request),
				 HID_OUTPUT_REPORT, HID_REQ_SET_REPORT);
	if (ret < (int)sizeof(kysona_battery_request)) {
		hid_err(hdev, "hid_hw_raw_request() failed with %d\n", ret);
		ret = -ENODATA;
	}
	kfree(write_buf);
	return ret;
}

static void kysona_fetch_battery(struct hid_device *hdev)
{
	int ret = kysona_m600_fetch_battery(hdev);

	if (ret < 0)
		hid_dbg(hdev,
			"Battery query failed (err: %d)\n", ret);
}

static void kysona_battery_timer_tick(struct work_struct *work)
{
	struct kysona_drvdata *drv_data = container_of(work,
		struct kysona_drvdata, battery_work.work);
	struct hid_device *hdev = drv_data->hdev;

	kysona_fetch_battery(hdev);
	schedule_delayed_work(&drv_data->battery_work,
			      msecs_to_jiffies(BATTERY_TIMEOUT_MS));
}

static int kysona_battery_probe(struct hid_device *hdev)
{
	struct kysona_drvdata *drv_data = hid_get_drvdata(hdev);
	struct power_supply_config pscfg = { .drv_data = drv_data };
	int ret = 0;

	drv_data->online = false;
	drv_data->battery_capacity = 100;
	drv_data->battery_voltage = 4200;

	drv_data->battery_desc.properties = kysona_battery_props;
	drv_data->battery_desc.num_properties = ARRAY_SIZE(kysona_battery_props);
	drv_data->battery_desc.get_property = kysona_battery_get_property;
	drv_data->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
	drv_data->battery_desc.use_for_apm = 0;
	drv_data->battery_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL,
						     "kysona-%s-battery",
						     strlen(hdev->uniq) ?
						     hdev->uniq : dev_name(&hdev->dev));
	if (!drv_data->battery_desc.name)
		return -ENOMEM;

	drv_data->battery = devm_power_supply_register(&hdev->dev,
						       &drv_data->battery_desc, &pscfg);
	if (IS_ERR(drv_data->battery)) {
		ret = PTR_ERR(drv_data->battery);
		drv_data->battery = NULL;
		hid_err(hdev, "Unable to register battery device\n");
		return ret;
	}

	power_supply_powers(drv_data->battery, &hdev->dev);

	INIT_DELAYED_WORK(&drv_data->battery_work, kysona_battery_timer_tick);
	kysona_fetch_battery(hdev);
	schedule_delayed_work(&drv_data->battery_work,
			      msecs_to_jiffies(BATTERY_TIMEOUT_MS));

	return ret;
}

static int kysona_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
	int ret;
	struct kysona_drvdata *drv_data;
	struct usb_interface *usbif;

	if (!hid_is_usb(hdev))
		return -EINVAL;

	usbif = to_usb_interface(hdev->dev.parent);

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

	hid_set_drvdata(hdev, drv_data);
	drv_data->hdev = hdev;

	ret = hid_parse(hdev);
	if (ret)
		return ret;

	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
	if (ret)
		return ret;

	if (usbif->cur_altsetting->desc.bInterfaceNumber == 1) {
		if (kysona_battery_probe(hdev) < 0)
			hid_err(hdev, "Kysona hid battery_probe failed: %d\n", ret);
	}

	return 0;
}

static int kysona_raw_event(struct hid_device *hdev,
			    struct hid_report *report, u8 *data, int size)
{
	struct kysona_drvdata *drv_data = hid_get_drvdata(hdev);

	if (drv_data->battery && size == sizeof(kysona_battery_request) &&
	    data[0] == 8 && data[1] == BATTERY_REPORT_ID) {
		drv_data->battery_capacity = data[6];
		drv_data->battery_charging = data[7];
		drv_data->battery_voltage = (data[8] << 8) | data[9];
		drv_data->online = true;
	}

	return 0;
}

static void kysona_remove(struct hid_device *hdev)
{
	struct kysona_drvdata *drv_data = hid_get_drvdata(hdev);

	if (drv_data->battery)
		cancel_delayed_work_sync(&drv_data->battery_work);

	hid_hw_stop(hdev);
}

static const struct hid_device_id kysona_devices[] = {
	{ HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_DONGLE) },
	{ HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_WIRED) },
	{ }
};
MODULE_DEVICE_TABLE(hid, kysona_devices);

static struct hid_driver kysona_driver = {
	.name			= "kysona",
	.id_table		= kysona_devices,
	.probe			= kysona_probe,
	.raw_event		= kysona_raw_event,
	.remove			= kysona_remove
};
module_hid_driver(kysona_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("HID driver for Kysona devices");
MODULE_AUTHOR("Lode Willems <me@lodewillems.com>");