/*
 * Copyright 2016 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include "fu-steelseries-device.h"

typedef struct {
	gint iface_number;
	guint8 iface_idx;
	guint8 ep;
	gsize ep_in_size;
} FuSteelseriesDevicePrivate;

G_DEFINE_TYPE_WITH_PRIVATE(FuSteelseriesDevice, fu_steelseries_device, FU_TYPE_USB_DEVICE)
#define GET_PRIVATE(o) (fu_steelseries_device_get_instance_private(o))

/* @iface_idx_offset can be negative to specify from the end */
void
fu_steelseries_device_set_iface_number(FuSteelseriesDevice *self, gint iface_number)
{
	FuSteelseriesDevicePrivate *priv = GET_PRIVATE(self);
	priv->iface_number = iface_number;
}

gboolean
fu_steelseries_device_request(FuSteelseriesDevice *self, const GByteArray *buf, GError **error)
{
	FuSteelseriesDevicePrivate *priv = GET_PRIVATE(self);
	gsize actual_len = 0;
	g_autoptr(GByteArray) buf_padded = g_byte_array_new();

	g_return_val_if_fail(buf != NULL, FALSE);

	/* pad out */
	g_byte_array_append(buf_padded, buf->data, buf->len);
	fu_byte_array_set_size(buf_padded, FU_STEELSERIES_BUFFER_CONTROL_SIZE, 0x00);
	fu_dump_raw(G_LOG_DOMAIN, "Request", buf_padded->data, buf_padded->len);
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_CLASS,
					    FU_USB_RECIPIENT_INTERFACE,
					    0x09,
					    0x0200,
					    priv->iface_idx,
					    buf_padded->data,
					    buf_padded->len,
					    &actual_len,
					    FU_STEELSERIES_TRANSACTION_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "failed to do control transfer: ");
		return FALSE;
	}
	if (actual_len != buf_padded->len) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "only wrote %" G_GSIZE_FORMAT "bytes",
			    actual_len);
		return FALSE;
	}

	/* success */
	return TRUE;
}

GByteArray *
fu_steelseries_device_response(FuSteelseriesDevice *self, GError **error)
{
	FuSteelseriesDevicePrivate *priv = GET_PRIVATE(self);
	gsize actual_len = 0;
	g_autoptr(GByteArray) buf = g_byte_array_new();

	fu_byte_array_set_size(buf, priv->ep_in_size, 0x00);
	if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self),
					      priv->ep,
					      buf->data,
					      buf->len,
					      &actual_len,
					      FU_STEELSERIES_TRANSACTION_TIMEOUT,
					      NULL,
					      error)) {
		g_prefix_error_literal(error, "failed to do EP transfer: ");
		return NULL;
	}
	fu_dump_raw(G_LOG_DOMAIN, "Response", buf->data, actual_len);
	if (actual_len != priv->ep_in_size) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "only read %" G_GSIZE_FORMAT "bytes",
			    actual_len);
		return NULL;
	}

	/* success */
	return g_steal_pointer(&buf);
}

static gboolean
fu_steelseries_device_find_cmd_iface(gconstpointer item, gconstpointer number)
{
	FuUsbInterface *iface = (FuUsbInterface *)item;
	gint iface_number_requested = *(gint *)number;

	/* must be HID */
	if (fu_usb_interface_get_class(iface) != FU_USB_CLASS_HID)
		return FALSE;

	/* in case of autodetection first found fits */
	if (iface_number_requested != -1 &&
	    iface_number_requested != (gint)fu_usb_interface_get_number(iface))
		return FALSE;

	return TRUE;
}

static gboolean
fu_steelseries_device_probe(FuDevice *device, GError **error)
{
	FuSteelseriesDevice *self = FU_STEELSERIES_DEVICE(device);
	FuSteelseriesDevicePrivate *priv = GET_PRIVATE(self);
	FuUsbInterface *iface = NULL;
	FuUsbEndpoint *ep = NULL;
	guint iface_idx;
	guint8 ep_id;
	guint16 packet_size;
	g_autoptr(GPtrArray) ifaces = NULL;
	g_autoptr(GPtrArray) endpoints = NULL;

	ifaces = fu_usb_device_get_interfaces(FU_USB_DEVICE(device), error);
	if (ifaces == NULL)
		return FALSE;

	/* use the correct interface for interrupt transfer, either specifying an absolute
	 * offset, or a negative offset value for the default HID interface autodetection */
	if (!g_ptr_array_find_with_equal_func(ifaces,
					      priv,
					      fu_steelseries_device_find_cmd_iface,
					      &iface_idx)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_FOUND,
			    "update interface 0x%x not found",
			    (guint)priv->iface_number);
		return FALSE;
	}
	iface = g_ptr_array_index(ifaces, iface_idx);
	priv->iface_idx = fu_usb_interface_get_number(iface);

	endpoints = fu_usb_interface_get_endpoints(iface);
	/* expecting to have only one endpoint for communication */
	if (endpoints == NULL || endpoints->len != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_FOUND,
				    "endpoint not found");
		return FALSE;
	}

	ep = g_ptr_array_index(endpoints, 0);
	ep_id = fu_usb_endpoint_get_address(ep);
	packet_size = fu_usb_endpoint_get_maximum_packet_size(ep);

	priv->ep = ep_id;
	priv->ep_in_size = packet_size;

	fu_usb_device_add_interface(FU_USB_DEVICE(self), priv->iface_idx);

	/* success */
	return TRUE;
}

static void
fu_steelseries_device_to_string(FuDevice *device, guint idt, GString *str)
{
	FuSteelseriesDevice *self = FU_STEELSERIES_DEVICE(device);
	FuSteelseriesDevicePrivate *priv = GET_PRIVATE(self);
	fwupd_codec_string_append_hex(str, idt, "Interface", priv->iface_idx);
	fwupd_codec_string_append_hex(str, idt, "Endpoint", priv->ep);
}

static gboolean
fu_steelseries_device_set_quirk_kv(FuDevice *device,
				   const gchar *key,
				   const gchar *value,
				   GError **error)
{
	FuSteelseriesDevice *self = FU_STEELSERIES_DEVICE(device);
	guint64 tmp = 0;

	if (g_strcmp0(key, "SteelSeriesCmdInterface") == 0) {
		if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error))
			return FALSE;

		fu_steelseries_device_set_iface_number(FU_STEELSERIES_DEVICE(self), tmp);
		return TRUE;
	}

	g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported");
	return FALSE;
}

static void
fu_steelseries_device_init(FuSteelseriesDevice *self)
{
	fu_device_register_private_flag(FU_DEVICE(self), FU_STEELSERIES_DEVICE_FLAG_IS_RECEIVER);
	fu_steelseries_device_set_iface_number(FU_STEELSERIES_DEVICE(self), -1);
}

static void
fu_steelseries_device_class_init(FuSteelseriesDeviceClass *klass)
{
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
	device_class->to_string = fu_steelseries_device_to_string;
	device_class->probe = fu_steelseries_device_probe;
	device_class->set_quirk_kv = fu_steelseries_device_set_quirk_kv;
}
