/*
 * Copyright (C) 2022 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#define G_LOG_DOMAIN "FuUsbDeviceDs20"

#include "config.h"

#include "fu-byte-array.h"
#include "fu-bytes.h"
#include "fu-dump.h"
#include "fu-mem.h"
#include "fu-usb-device-ds20.h"

/**
 * FuUsbDeviceDs20:
 *
 * A USB DS20 BOS descriptor.
 *
 * See also: [class@FuUsbDevice]
 */

typedef struct {
	guint32 version_lowest;
} FuUsbDeviceDs20Private;

G_DEFINE_TYPE_WITH_PRIVATE(FuUsbDeviceDs20, fu_usb_device_ds20, FU_TYPE_FIRMWARE)
#define GET_PRIVATE(o) (fu_usb_device_ds20_get_instance_private(o))

typedef struct __attribute__((packed)) {
	guint32 platform_ver;
	guint16 total_length;
	guint8 vendor_code;
	guint8 alt_code;
} FuUsbDeviceDs20Item;

/**
 * fu_usb_device_ds20_set_version_lowest:
 * @self: a #FuUsbDeviceDs20
 * @version_lowest: version number
 *
 * Sets the lowest possible `platform_ver` for a DS20 descriptor.
 *
 * Since: 1.8.5
 **/
void
fu_usb_device_ds20_set_version_lowest(FuUsbDeviceDs20 *self, guint32 version_lowest)
{
	FuUsbDeviceDs20Private *priv = GET_PRIVATE(self);
	g_return_if_fail(FU_IS_USB_DEVICE_DS20(self));
	priv->version_lowest = version_lowest;
}

/**
 * fu_usb_device_ds20_apply_to_device:
 * @self: a #FuUsbDeviceDs20
 * @device: a #FuUsbDevice
 * @error: (nullable): optional return location for an error
 *
 * Sets the DS20 descriptor onto @device.
 *
 * Returns: %TRUE for success
 *
 * Since: 1.8.5
 **/
gboolean
fu_usb_device_ds20_apply_to_device(FuUsbDeviceDs20 *self, FuUsbDevice *device, GError **error)
{
#ifdef HAVE_GUSB
	FuUsbDeviceDs20Class *klass = FU_USB_DEVICE_DS20_GET_CLASS(self);
	GUsbDevice *usb_device = fu_usb_device_get_dev(device);
	gsize actual_length = 0;
	gsize total_length = fu_firmware_get_size(FU_FIRMWARE(self));
	guint8 vendor_code = fu_firmware_get_idx(FU_FIRMWARE(self));
	g_autofree guint8 *buf = g_malloc0(total_length);
	g_autoptr(GBytes) blob = NULL;

	g_return_val_if_fail(FU_IS_USB_DEVICE_DS20(self), FALSE);
	g_return_val_if_fail(FU_IS_USB_DEVICE(device), FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	if (!g_usb_device_control_transfer(usb_device,
					   G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
					   G_USB_DEVICE_REQUEST_TYPE_VENDOR,
					   G_USB_DEVICE_RECIPIENT_DEVICE,
					   vendor_code, /* bRequest */
					   0x0,		/* wValue */
					   0x07,	/* wIndex */
					   buf,
					   total_length,
					   &actual_length,
					   500,
					   NULL, /* cancellable */
					   error)) {
		g_prefix_error(error, "requested vendor code 0x%02x: ", vendor_code);
		return FALSE;
	}
	if (total_length != actual_length) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "expected 0x%x bytes from vendor code 0x%02x, but got 0x%x",
			    (guint)total_length,
			    vendor_code,
			    (guint)actual_length);
		return FALSE;
	}

	/* debug */
	fu_dump_raw(G_LOG_DOMAIN, "PlatformCapabilityOs20", buf, actual_length);

	/* FuUsbDeviceDs20->parse */
	blob = g_bytes_new_take(g_steal_pointer(&buf), actual_length);
	return klass->parse(self, blob, device, error);
#else
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "GUsb support is unavailable");
	return FALSE;
#endif
}

static gboolean
fu_usb_device_ds20_check_magic(FuFirmware *firmware, GBytes *fw, gsize offset, GError **error)
{
	fwupd_guid_t guid = {0x0};
	g_autofree gchar *guid_str = NULL;

	/* parse GUID */
	if (!fu_memcpy_safe((guint8 *)&guid,
			    sizeof(guid),
			    0x0, /* dst */
			    g_bytes_get_data(fw, NULL),
			    g_bytes_get_size(fw),
			    offset + 0x1, /* src */
			    sizeof(guid),
			    error))
		return FALSE;

	/* matches the correct UUID */
	guid_str = fwupd_guid_to_string(&guid, FWUPD_GUID_FLAG_MIXED_ENDIAN);
	if (g_strcmp0(guid_str, fu_firmware_get_id(firmware)) != 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "invalid UUID for DS20, expected %s",
			    fu_firmware_get_id(firmware));
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gint
fu_usb_device_ds20_sort_by_platform_ver_cb(gconstpointer a, gconstpointer b)
{
	FuUsbDeviceDs20Item *ds1 = *((FuUsbDeviceDs20Item **)a);
	FuUsbDeviceDs20Item *ds2 = *((FuUsbDeviceDs20Item **)b);
	if (ds1->platform_ver < ds2->platform_ver)
		return -1;
	if (ds1->platform_ver > ds2->platform_ver)
		return 1;
	return 0;
}

static gboolean
fu_usb_device_ds20_parse(FuFirmware *firmware,
			 GBytes *fw,
			 gsize offset,
			 FwupdInstallFlags flags,
			 GError **error)
{
	FuUsbDeviceDs20 *self = FU_USB_DEVICE_DS20(firmware);
	FuUsbDeviceDs20Private *priv = GET_PRIVATE(self);
	const guint8 *buf;
	gsize bufsz = 0;
	guint version_lowest = fu_firmware_get_version_raw(firmware);
	g_autoptr(GBytes) blob = NULL;
	g_autoptr(GPtrArray) dsinfos = g_ptr_array_new_with_free_func(g_free);

	/* cut out the data */
	blob = fu_bytes_new_offset(fw,
				   1 + sizeof(fwupd_guid_t),
				   g_bytes_get_size(fw) - (1 + sizeof(fwupd_guid_t)),
				   error);
	if (blob == NULL)
		return FALSE;
	buf = g_bytes_get_data(blob, &bufsz);
	for (gsize off = 0; off < bufsz; off += sizeof(FuUsbDeviceDs20Item)) {
		FuUsbDeviceDs20Item *dsinfo = g_new0(FuUsbDeviceDs20Item, 1);
		guint16 total_length = 0;
		guint32 platform_ver = 0;

		g_ptr_array_add(dsinfos, dsinfo);
		if (!fu_memread_uint32_safe(buf,
					    bufsz,
					    off +
						G_STRUCT_OFFSET(FuUsbDeviceDs20Item, platform_ver),
					    &platform_ver,
					    G_LITTLE_ENDIAN,
					    error))
			return FALSE;
		if (!fu_memread_uint16_safe(buf,
					    bufsz,
					    off +
						G_STRUCT_OFFSET(FuUsbDeviceDs20Item, total_length),
					    &total_length,
					    G_LITTLE_ENDIAN,
					    error))
			return FALSE;
		if (!fu_memread_uint8_safe(buf,
					   bufsz,
					   off + G_STRUCT_OFFSET(FuUsbDeviceDs20Item, vendor_code),
					   &dsinfo->vendor_code,
					   error))
			return FALSE;
		if (!fu_memread_uint8_safe(buf,
					   bufsz,
					   off + G_STRUCT_OFFSET(FuUsbDeviceDs20Item, alt_code),
					   &dsinfo->alt_code,
					   error))
			return FALSE;
		dsinfo->platform_ver = platform_ver;
		dsinfo->total_length = total_length;
		g_debug("PlatformVersion=0x%08x, TotalLength=0x%04x, VendorCode=0x%02x, "
			"AltCode=0x%02x",
			dsinfo->platform_ver,
			dsinfo->total_length,
			dsinfo->vendor_code,
			dsinfo->alt_code);
	}

	/* sort by platform_ver, highest first */
	g_ptr_array_sort(dsinfos, fu_usb_device_ds20_sort_by_platform_ver_cb);

	/* find the newest info that's not newer than the lowest version */
	for (guint i = 0; i < dsinfos->len; i++) {
		FuUsbDeviceDs20Item *dsinfo = g_ptr_array_index(dsinfos, i);

		/* not valid */
		if (dsinfo->platform_ver == 0x0) {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_NOT_SUPPORTED,
				    "invalid platform version 0x%08x",
				    dsinfo->platform_ver);
			return FALSE;
		}
		if (dsinfo->platform_ver < priv->version_lowest) {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_NOT_SUPPORTED,
				    "invalid platform version 0x%08x, expected >= 0x%08x",
				    dsinfo->platform_ver,
				    priv->version_lowest);
			return FALSE;
		}

		/* dwVersion is effectively the minimum version */
		if (dsinfo->platform_ver <= version_lowest) {
			fu_firmware_set_size(firmware, dsinfo->total_length);
			fu_firmware_set_idx(firmware, dsinfo->vendor_code);
			return TRUE;
		}
	}

	/* failed */
	g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "no supported platform version");
	return FALSE;
}

static GBytes *
fu_usb_device_ds20_write(FuFirmware *firmware, GError **error)
{
	fwupd_guid_t guid = {0x0};
	g_autoptr(GByteArray) buf = g_byte_array_new();

	/* bReserved */
	fu_byte_array_append_uint8(buf, 0x0);

	/* PlatformCapabilityUUID */
	if (!fwupd_guid_from_string(fu_firmware_get_id(firmware),
				    &guid,
				    FWUPD_GUID_FLAG_MIXED_ENDIAN,
				    error))
		return NULL;
	g_byte_array_append(buf, (const guint8 *)&guid, sizeof(guid));

	/* CapabilityData */
	fu_byte_array_append_uint32(buf, fu_firmware_get_version_raw(firmware), G_LITTLE_ENDIAN);
	fu_byte_array_append_uint16(buf, fu_firmware_get_size(firmware), G_LITTLE_ENDIAN);
	fu_byte_array_append_uint8(buf, fu_firmware_get_idx(firmware));
	fu_byte_array_append_uint8(buf, 0x0); /* AltCode */

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

static void
fu_usb_device_ds20_init(FuUsbDeviceDs20 *self)
{
	fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_STORED_SIZE);
}

static void
fu_usb_device_ds20_class_init(FuUsbDeviceDs20Class *klass)
{
	FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass);
	klass_firmware->check_magic = fu_usb_device_ds20_check_magic;
	klass_firmware->parse = fu_usb_device_ds20_parse;
	klass_firmware->write = fu_usb_device_ds20_write;
}
