/*
 * Copyright (C) 2021 Kyoken, kyoken@kyoken.ninja
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#include "config.h"

#include "asus.h"

#include <assert.h>

#include "libratbag-data.h"
#include "libratbag-private.h"

/* ASUS commands */
#define ASUS_CMD_GET_LED_DATA		0x0312  /* get all LEDs */
#define ASUS_CMD_GET_SETTINGS		0x0412  /* dpi, rate, button response, angle snapping */
#define ASUS_CMD_GET_BUTTON_DATA	0x0512  /* get all buttons */
#define ASUS_CMD_GET_PROFILE_DATA	0x0012  /* get current profile info */
#define ASUS_CMD_SET_LED		0x2851  /* set single led */
#define ASUS_CMD_SET_SETTING		0x3151  /* dpi / rate / button response / angle snapping */
#define ASUS_CMD_SET_BUTTON		0x2151  /* set single button */
#define ASUS_CMD_SET_PROFILE		0x0250  /* switch profile */
#define ASUS_CMD_SAVE			0x0350  /* save settings */

/* fields order in _asus_dpiX_data, used for setting with ASUS_CMD_SET_SETTING */
#define ASUS_FIELD_RATE	0
#define ASUS_FIELD_RESPONSE	1
#define ASUS_FIELD_SNAPPING	2

/* key mapping, the index is actual ASUS code */
static const unsigned char ASUS_KEY_MAPPING[] = {
/* 00 */	0,		0,		0,		0,
/* 04 */	KEY_A,		KEY_B,		KEY_C,		KEY_D,
/* 08 */	KEY_E,		KEY_F,		KEY_G,		KEY_H,
/* 0C */	KEY_I,		KEY_J,		KEY_K,		KEY_L,
/* 0E */	KEY_M,		KEY_N,		KEY_O,		KEY_P,
/* 14 */	KEY_Q,		KEY_R,		KEY_S,		KEY_T,
/* 18 */	KEY_U,		KEY_V,		KEY_W,		KEY_X,
/* 1C */	KEY_Y,		KEY_Z,		KEY_1,		KEY_2,
/* 1E */	KEY_3,		KEY_4,		KEY_5,		KEY_6,
/* 24 */	KEY_7,		KEY_8,		KEY_9,		KEY_0,
/* 28 */	KEY_ENTER,	KEY_ESC,	KEY_BACKSPACE,	KEY_TAB,
/* 2C */	KEY_SPACE,	KEY_MINUS,	KEY_KPPLUS,	0,
/* 2E */	0,		0,		0,		0,
/* 34 */	0,		KEY_GRAVE,	KEY_EQUAL,	0,
/* 38 */	KEY_SLASH,	0,		KEY_F1,		KEY_F2,
/* 3C */	KEY_F3,		KEY_F4,		KEY_F5,		KEY_F6,
/* 3E */	KEY_F7,		KEY_F8,		KEY_F9,		KEY_F10,
/* 44 */	KEY_F11,	KEY_F12,	0,		0,
/* 48 */	0,		0,		KEY_HOME,	KEY_PAGEUP,
/* 4C */	KEY_DELETE,	0,		KEY_PAGEDOWN,	KEY_RIGHT,
/* 4E */	KEY_LEFT,	KEY_DOWN,	KEY_UP,		0,
/* 54 */	0,		0,		0,		0,
/* 58 */	0,		KEY_KP1,	KEY_KP2,	KEY_KP3,
/* 5C */	KEY_KP4,	KEY_KP5,	KEY_KP6,	KEY_KP7,
/* 5E */	KEY_KP8,	KEY_KP9,	0,
};

static unsigned int ASUS_POLLING_RATES[] = { 125, 250, 500, 1000 };
static unsigned int ASUS_DEBOUNCE_TIMES[] = { 4, 8, 12, 16, 20, 24, 28, 32 };

/* search for ASUS button by ratbag types */
const struct asus_button *
asus_find_button_by_action(struct ratbag_button_action action)
{
	const struct asus_button *asus_button;
	ARRAY_FOR_EACH(ASUS_BUTTON_MAPPING, asus_button) {
		if ((action.type == RATBAG_BUTTON_ACTION_TYPE_BUTTON && asus_button->button == action.action.button) ||
				(action.type == RATBAG_BUTTON_ACTION_TYPE_SPECIAL && asus_button->special == action.action.special))
			return asus_button;
	}
	return NULL;
}

/* search for ASUS button by ASUS button code */
const struct asus_button *
asus_find_button_by_code(uint8_t asus_code)
{
	for (unsigned int i = 0; i < ARRAY_LENGTH(ASUS_BUTTON_MAPPING); i++) {
		if (ASUS_BUTTON_MAPPING[i].asus_code == asus_code)
			return &ASUS_BUTTON_MAPPING[i];
	}
	return NULL;
}

/* search for ASUS key code by Linux key code */
int
asus_find_key_code(unsigned int linux_code)
{
	for (unsigned int i = 0; i < ARRAY_LENGTH(ASUS_KEY_MAPPING); i++) {
		if (ASUS_KEY_MAPPING[i] == linux_code)
			return i;
	}
	return -1;
}

int
asus_get_linux_key_code(uint8_t asus_code) {
	if (asus_code > ARRAY_LENGTH(ASUS_KEY_MAPPING)) {
		return -1;
	}
	return ASUS_KEY_MAPPING[asus_code];
}

int
asus_query(struct ratbag_device *device,
		union asus_request *request, union asus_response *response)
{
	int rc;

	rc = ratbag_hidraw_output_report(device, request->raw, ASUS_PACKET_SIZE);
	if (rc < 0)
		return rc;

	memset(response, 0, sizeof(union asus_response));
	rc = ratbag_hidraw_read_input_report(device, response->raw, ASUS_PACKET_SIZE, NULL);
	if (rc < 0)
		return rc;

	/* invalid state, disconnected or sleeping */
	if (response->data.code == ASUS_STATUS_ERROR) {
		return ASUS_STATUS_ERROR;
	}

	return 0;
}

void
asus_setup_profile(struct ratbag_device *device, struct ratbag_profile *profile)
{
	ratbag_profile_set_report_rate_list(
		profile, ASUS_POLLING_RATES, ARRAY_LENGTH(ASUS_POLLING_RATES));
	ratbag_profile_set_debounce_list(
		 profile, ASUS_DEBOUNCE_TIMES, ARRAY_LENGTH(ASUS_DEBOUNCE_TIMES));
}

void
asus_setup_button(struct ratbag_device *device, struct ratbag_button *button)
{
	ratbag_button_enable_action_type(button, RATBAG_BUTTON_ACTION_TYPE_BUTTON);
	ratbag_button_enable_action_type(button, RATBAG_BUTTON_ACTION_TYPE_SPECIAL);
	ratbag_button_enable_action_type(button, RATBAG_BUTTON_ACTION_TYPE_KEY);
}

void
asus_setup_resolution(struct ratbag_device *device, struct ratbag_resolution *resolution)
{
	const struct dpi_range *dpirange = ratbag_device_data_asus_get_dpi_range(device->data);
	if (!dpirange)
		return;

	ratbag_resolution_set_dpi_list_from_range(
		resolution, dpirange->min, dpirange->max);
}

void
asus_setup_led(struct ratbag_device *device, struct ratbag_led *led)
{
	led->colordepth = RATBAG_LED_COLORDEPTH_RGB_888;
	ratbag_led_set_mode_capability(led, RATBAG_LED_ON);
	ratbag_led_set_mode_capability(led, RATBAG_LED_CYCLE);
	ratbag_led_set_mode_capability(led, RATBAG_LED_BREATHING);
}

int
asus_save_profile(struct ratbag_device *device)
{
	int rc;
	union asus_response response;
	union asus_request request = {
		.data.cmd = ASUS_CMD_SAVE,
	};

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	return 0;
}

int
asus_get_profile_data(struct ratbag_device *device, struct asus_profile_data *data)
{
	int rc;
	uint32_t quirks = ratbag_device_data_asus_get_quirks(device->data);
	union asus_response response;
	union asus_request request = {
		.data.cmd = ASUS_CMD_GET_PROFILE_DATA,
	};

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	if (quirks & ASUS_QUIRK_STRIX_PROFILE)
		data->profile_id = response.data.results[7];
	else
		data->profile_id = response.data.results[8];

	data->version_primary_major = response.data.results[13];
	data->version_primary_minor = response.data.results[12];
	data->version_primary_build = response.data.results[11];

	data->version_secondary_major = response.data.results[4];
	data->version_secondary_minor = response.data.results[3];
	data->version_secondary_build = response.data.results[2];

	return 0;
}

int
asus_set_profile(struct ratbag_device *device, unsigned int index)
{
	int rc;
	union asus_response response;
	union asus_request request = {
		.data.cmd = ASUS_CMD_SET_PROFILE,
		.data.params[0] = index,
	};

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	return 0;
}

/* read button bindings */
int
asus_get_binding_data(struct ratbag_device *device, union asus_binding_data *data)
{
	int rc;
	union asus_response response;
	union asus_request request = {
		.data.cmd = ASUS_CMD_GET_BUTTON_DATA,
	};

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	memcpy(data->raw, response.raw, sizeof(data->raw));
	return 0;
}

/* set button binding using ASUS code of the button */
int
asus_set_button_action(struct ratbag_device *device, uint8_t asus_code_src,
		       uint8_t asus_code_dst, uint8_t asus_type)
{
	int rc;
	union asus_response response;
	union asus_request request = {
		.data.cmd = ASUS_CMD_SET_BUTTON,
	};

	/* source (physical mouse button) */
	request.data.params[2] = asus_code_src;
	request.data.params[3] = ASUS_BUTTON_ACTION_TYPE_BUTTON;

	/* destination (mouse button or keyboard key action) */
	request.data.params[4] = asus_code_dst;
	request.data.params[5] = asus_type;

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	return 0;
}

int
asus_get_resolution_data(struct ratbag_device *device, union asus_resolution_data *data)
{
	int rc;
	uint32_t quirks = ratbag_device_data_asus_get_quirks(device->data);
	union asus_response response;
	unsigned int dpi_count = ratbag_device_get_profile(device, 0)->num_resolutions;
	unsigned int i;
	union asus_request request = {
		.data.cmd = ASUS_CMD_GET_SETTINGS,
	};

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	memcpy(data->raw, response.raw, sizeof(data->raw));

	/* convert DPI rates */
	switch (dpi_count) {
	case 2:  /* 2 DPI presets */
		for (i = 0; i < dpi_count; i++) {
			data->data2.dpi[i] = data->data2.dpi[i] * 50 + 50;
			if (quirks & ASUS_QUIRK_DOUBLE_DPI)
				data->data2.dpi[i] *= 2;
		}
		data->data2.rate = ASUS_POLLING_RATES[data->data2.rate];
		data->data2.response = ASUS_DEBOUNCE_TIMES[data->data2.response];
		break;

	case 4:  /* 4 DPI presets */
		for (i = 0; i < dpi_count; i++) {
			data->data4.dpi[i] = data->data4.dpi[i] * 50 + 50;
			if (quirks & ASUS_QUIRK_DOUBLE_DPI)
				data->data4.dpi[i] *= 2;
		}
		data->data4.rate = ASUS_POLLING_RATES[data->data4.rate];
		data->data4.response = ASUS_DEBOUNCE_TIMES[data->data4.response];
		break;

	default:
		break;
	}

	return 0;
}

/* set DPI for the specified preset */
int
asus_set_dpi(struct ratbag_device *device, unsigned int index, unsigned int dpi)
{
	int rc;
	uint32_t quirks = ratbag_device_data_asus_get_quirks(device->data);
	union asus_response response;
	unsigned int idpi;

	idpi = dpi;
	if (quirks & ASUS_QUIRK_DOUBLE_DPI)
		idpi /= 2;

	union asus_request request = {
		.data.cmd = ASUS_CMD_SET_SETTING,
		.data.params[0] = index,
		.data.params[2] = (idpi - 50) / 50,
	};

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	return 0;
}

/* set polling rate in Hz */
int
asus_set_polling_rate(struct ratbag_device *device, unsigned int hz)
{
	int rc;
	union asus_response response;
	unsigned int dpi_count = ratbag_device_get_profile(device, 0)->num_resolutions;
	unsigned int i;

	union asus_request request = {
		.data.cmd = ASUS_CMD_SET_SETTING,
		.data.params[0] = dpi_count + ASUS_FIELD_RATE,  /* field index to set */
	};

	for (i = 0; i < ARRAY_LENGTH(ASUS_POLLING_RATES); i++) {
		if (ASUS_POLLING_RATES[i] == hz) {
			request.data.params[2] = i;
			break;
		}
	}

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	return 0;
}

/* set button response/debounce in ms (from 4 to 32 with step of 4) */
int
asus_set_button_response(struct ratbag_device *device, unsigned int ms)
{
	assert(ms >= 4);

	int rc;
	unsigned int dpi_count = ratbag_device_get_profile(device, 0)->num_resolutions;
	union asus_response response;
	unsigned int index = 0;
	for (unsigned int i = 0; i < ARRAY_LENGTH(ASUS_DEBOUNCE_TIMES); i++) {
		if (ASUS_DEBOUNCE_TIMES[i] == ms) {
			index = i;
			break;
		}
	}

	union asus_request request = {
		.data.cmd = ASUS_CMD_SET_SETTING,
		.data.params[0] = dpi_count + ASUS_FIELD_RESPONSE,  /* field index to set */
		.data.params[2] = index,
	};

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	return 0;
}

int
asus_set_angle_snapping(struct ratbag_device *device, bool is_enabled)
{
	int rc;
	union asus_response response;
	unsigned int dpi_count = ratbag_device_get_profile(device, 0)->num_resolutions;

	union asus_request request = {
		.data.cmd = ASUS_CMD_SET_SETTING,
		.data.params[0] = dpi_count + ASUS_FIELD_SNAPPING,  /* field index to set */
		.data.params[2] = is_enabled ? 1 : 0,
	};

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	return 0;
}

int
asus_get_led_data(struct ratbag_device *device, union asus_led_data *data)
{
	int rc;
	union asus_response response;
	union asus_request request = {
		.data.cmd = ASUS_CMD_GET_LED_DATA,
	};

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	memcpy(data->raw, response.raw, sizeof(data->raw));
	return 0;
}

/* set LED mode, brightness (0-4) and color */
int
asus_set_led(struct ratbag_device *device,
		uint8_t index, uint8_t mode, uint8_t brightness,
		struct ratbag_color color)
{
	int rc;
	union asus_response response;
	union asus_request request = {
		.data.cmd = ASUS_CMD_SET_LED,
		.data.params[0] = index,
		.data.params[2] = mode,
		.data.params[3] = brightness,
		.data.params[4] = color.red,
		.data.params[5] = color.green,
		.data.params[6] = color.blue,
	};

	rc = asus_query(device, &request, &response);
	if (rc)
		return rc;

	return 0;
}
