/*
 * Copyright 2023 Denis Pynkin <denis.pynkin@collabora.com>
 * Copyright 2024 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include "fu-qc-s5gen2-device.h"
#include "fu-qc-s5gen2-hid-device.h"
#include "fu-qc-s5gen2-hid-struct.h"
#include "fu-qc-s5gen2-impl.h"
#include "fu-qc-s5gen2-struct.h"

#define HID_IFACE  0x01
#define HID_EP_IN  0x82
#define HID_EP_OUT 0x01

#define FU_QC_S5GEN2_HID_DEVICE_TIMEOUT 500 /* ms */

struct _FuQcS5gen2HidDevice {
	FuHidDevice parent_instance;
};

static void
fu_qc_s5gen2_hid_device_impl_iface_init(FuQcS5gen2ImplInterface *iface);

G_DEFINE_TYPE_WITH_CODE(FuQcS5gen2HidDevice,
			fu_qc_s5gen2_hid_device,
			FU_TYPE_HID_DEVICE,
			G_IMPLEMENT_INTERFACE(FU_TYPE_QC_S5GEN2_IMPL,
					      fu_qc_s5gen2_hid_device_impl_iface_init))

static gboolean
fu_qc_s5gen2_hid_device_msg_out(FuQcS5gen2Impl *impl, guint8 *data, gsize data_len, GError **error)
{
	FuQcS5gen2HidDevice *self = FU_QC_S5GEN2_HID_DEVICE(impl);
	g_autoptr(FuStructQcHidDataTransfer) st_req = fu_struct_qc_hid_data_transfer_new();

	fu_struct_qc_hid_data_transfer_set_payload_len(st_req, data_len);
	if (!fu_struct_qc_hid_data_transfer_set_payload(st_req, data, data_len, error))
		return FALSE;

	return fu_hid_device_set_report(FU_HID_DEVICE(self),
					0x00,
					st_req->buf->data,
					st_req->buf->len,
					FU_QC_S5GEN2_HID_DEVICE_TIMEOUT,
					FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER,
					error);
}

static gboolean
fu_qc_s5gen2_hid_device_msg_in(FuQcS5gen2Impl *impl,
			       guint8 *data,
			       gsize data_len,
			       gsize *read_len,
			       GError **error)
{
	FuQcS5gen2HidDevice *self = FU_QC_S5GEN2_HID_DEVICE(impl);
	guint8 buf[FU_STRUCT_QC_HID_RESPONSE_SIZE] = {0x0};
	g_autoptr(FuStructQcHidResponse) st_rsp = NULL;

	if (!fu_hid_device_get_report(FU_HID_DEVICE(self),
				      0x00,
				      buf,
				      sizeof(buf),
				      FU_QC_S5GEN2_HID_DEVICE_TIMEOUT,
				      FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER,
				      error))
		return FALSE;

	st_rsp = fu_struct_qc_hid_response_parse(buf, FU_STRUCT_QC_HID_RESPONSE_SIZE, 0, error);
	if (st_rsp == NULL)
		return FALSE;

	if (!fu_memcpy_safe(data,
			    data_len,
			    0,
			    st_rsp->buf->data,
			    st_rsp->buf->len,
			    FU_STRUCT_QC_HID_RESPONSE_OFFSET_PAYLOAD,
			    fu_struct_qc_hid_response_get_payload_len(st_rsp),
			    error))
		return FALSE;

	*read_len = fu_struct_qc_hid_response_get_payload_len(st_rsp);

	return TRUE;
}

static gboolean
fu_qc_s5gen2_hid_device_msg_cmd(FuQcS5gen2Impl *impl, guint8 *data, gsize data_len, GError **error)
{
	FuQcS5gen2HidDevice *self = FU_QC_S5GEN2_HID_DEVICE(impl);
	g_autoptr(FuStructQcHidCommand) st_req = fu_struct_qc_hid_command_new();

	fu_struct_qc_hid_command_set_payload_len(st_req, data_len);
	if (!fu_struct_qc_hid_command_set_payload(st_req, data, data_len, error))
		return FALSE;

	return fu_hid_device_set_report(FU_HID_DEVICE(self),
					0x03,
					st_req->buf->data,
					st_req->buf->len,
					0,
					FU_HID_DEVICE_FLAG_IS_FEATURE,
					error);
}

static gboolean
fu_qc_s5gen2_hid_device_cmd_req_disconnect(FuQcS5gen2Impl *impl, GError **error)
{
	g_autoptr(FuStructQcDisconnectReq) st_req = fu_struct_qc_disconnect_req_new();
	return fu_qc_s5gen2_hid_device_msg_cmd(impl, st_req->buf->data, st_req->buf->len, error);
}

static gboolean
fu_qc_s5gen2_hid_device_cmd_req_connect(FuQcS5gen2Impl *impl, GError **error)
{
	guint8 data_in[FU_STRUCT_QC_UPDATE_STATUS_SIZE] = {0x0};
	gsize read_len;
	FuQcStatus update_status;
	g_autoptr(FuStructQcConnectReq) st_req = fu_struct_qc_connect_req_new();
	g_autoptr(FuStructQcUpdateStatus) st = NULL;

	if (!fu_qc_s5gen2_hid_device_msg_cmd(impl, st_req->buf->data, st_req->buf->len, error))
		return FALSE;
	if (!fu_qc_s5gen2_hid_device_msg_in(impl, data_in, sizeof(data_in), &read_len, error))
		return FALSE;
	st = fu_struct_qc_update_status_parse(data_in, read_len, 0, error);
	if (st == NULL)
		return FALSE;

	update_status = fu_struct_qc_update_status_get_status(st);
	switch (update_status) {
	case FU_QC_STATUS_SUCCESS:
		break;
	case FU_QC_STATUS_ALREADY_CONNECTED_WARNING:
		g_debug("device is already connected");
		break;
	default:
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_DATA,
			    "invalid update status (%s)",
			    fu_qc_status_to_string(update_status));
		return FALSE;
	}

	return TRUE;
}

static gboolean
fu_qc_s5gen2_hid_device_data_size(FuQcS5gen2Impl *impl, gsize *data_sz, GError **error)
{
	if (FU_STRUCT_QC_HID_DATA_TRANSFER_SIZE <= FU_STRUCT_QC_DATA_SIZE + 2) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_DATA,
				    "MTU is not sufficient");
		return FALSE;
	}

	*data_sz = FU_STRUCT_QC_HID_DATA_TRANSFER_SIZE - FU_STRUCT_QC_DATA_SIZE - 2;
	return TRUE;
}

static gboolean
fu_qc_s5gen2_hid_device_probe(FuDevice *device, GError **error)
{
	FuHidDevice *hid_device = FU_HID_DEVICE(device);
	FuUsbInterface *iface = NULL;
	g_autoptr(GPtrArray) ifaces = NULL;

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

	/* need the second HID interface */
	if (ifaces->len <= HID_IFACE) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "transitional device detected");
		return FALSE;
	}

	iface = g_ptr_array_index(ifaces, HID_IFACE);
	if (fu_usb_interface_get_class(iface) != FU_USB_CLASS_HID) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "target interface is not HID");
		return FALSE;
	}

	fu_hid_device_set_interface(hid_device, HID_IFACE);
	fu_hid_device_set_ep_addr_in(hid_device, HID_EP_IN);
	fu_hid_device_set_ep_addr_out(hid_device, HID_EP_OUT);

	/* FuHidDevice->probe */
	if (!FU_DEVICE_CLASS(fu_qc_s5gen2_hid_device_parent_class)->probe(device, error))
		return FALSE;

	/* success */
	return TRUE;
}

static gboolean
fu_qc_s5gen2_hid_device_setup(FuDevice *device, GError **error)
{
	guint idx;

	/* FuHidDevice->setup */
	if (!FU_DEVICE_CLASS(fu_qc_s5gen2_hid_device_parent_class)->setup(device, error))
		return FALSE;

	fu_device_add_instance_u16(device, "VID", fu_device_get_vid(device));
	fu_device_add_instance_u16(device, "PID", fu_device_get_pid(device));

	idx = fu_usb_device_get_manufacturer_index(FU_USB_DEVICE(device));
	if (idx != 0x00) {
		g_autofree gchar *tmp = NULL;
		tmp = fu_usb_device_get_string_descriptor(FU_USB_DEVICE(device), idx, NULL);
		if (tmp != NULL)
			fu_device_add_instance_str(device, "MANUFACTURER", tmp);
	}

	idx = fu_usb_device_get_product_index(FU_USB_DEVICE(device));
	if (idx != 0x00) {
		g_autofree gchar *tmp = NULL;
		tmp = fu_usb_device_get_string_descriptor(FU_USB_DEVICE(device), idx, NULL);
		if (tmp != NULL)
			fu_device_add_instance_str(device, "PRODUCT", tmp);
	}

	return fu_device_build_instance_id_full(device,
						FU_DEVICE_INSTANCE_FLAG_QUIRKS |
						    FU_DEVICE_INSTANCE_FLAG_VISIBLE,
						error,
						"USB",
						"VID",
						"PID",
						"MANUFACTURER",
						"PRODUCT",
						NULL);
}

static void
fu_qc_s5gen2_hid_device_init(FuQcS5gen2HidDevice *self)
{
	fu_hid_device_add_flag(FU_HID_DEVICE(self), FU_HID_DEVICE_FLAG_RETRY_FAILURE);
	fu_device_set_remove_delay(FU_DEVICE(self), FU_QC_S5GEN2_DEVICE_REMOVE_DELAY);
	fu_device_set_battery_threshold(FU_DEVICE(self), 0);
}

static void
fu_qc_s5gen2_hid_device_impl_iface_init(FuQcS5gen2ImplInterface *iface)
{
	iface->msg_in = fu_qc_s5gen2_hid_device_msg_in;
	iface->msg_out = fu_qc_s5gen2_hid_device_msg_out;
	iface->req_connect = fu_qc_s5gen2_hid_device_cmd_req_connect;
	iface->req_disconnect = fu_qc_s5gen2_hid_device_cmd_req_disconnect;
	iface->data_size = fu_qc_s5gen2_hid_device_data_size;
}

static void
fu_qc_s5gen2_hid_device_class_init(FuQcS5gen2HidDeviceClass *klass)
{
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
	device_class->probe = fu_qc_s5gen2_hid_device_probe;
	device_class->setup = fu_qc_s5gen2_hid_device_setup;
}
