/*
 * Copyright 2018 Richard Hughes <richard@hughsie.com>
 * Copyright 2021-2023 Jason Gerecke <jason.gerecke@wacom.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include <string.h>

#include "fu-wacom-usb-common.h"
#include "fu-wacom-usb-device.h"
#include "fu-wacom-usb-module-bluetooth-id9.h"
#include "fu-wacom-usb-struct.h"

struct _FuWacomUsbModuleBluetoothId9 {
	FuWacomUsbModule parent_instance;
};

G_DEFINE_TYPE(FuWacomUsbModuleBluetoothId9,
	      fu_wacom_usb_module_bluetooth_id9,
	      FU_TYPE_WACOM_USB_MODULE)

#define FU_WACOM_USB_MODULE_BLUETOOTH_ID9_PAYLOAD_SZ	256
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID9_START_NORMAL	0x00
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID9_CMD_NORMAL	0x00
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID9_CMD_FULLERASE 0xFE
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID9_LOADER_RAM	0x02
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID9_LOADER_BEGIN	0x03
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID9_LOADER_DATA	0x04

#define FU_WACOM_USB_MODULE_BLUETOOTH_ID9_POLL_INTERVAL 5     /* ms */
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID9_START_TIMEOUT 75000 /* ms */
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID9_DATA_TIMEOUT	10000 /* ms */
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID9_END_TIMEOUT	10000 /* ms */

/* CRC(concat(spi_cmd, data)) */
static gboolean
fu_wacom_usb_module_bluetooth_id9_calculate_crc32(GByteArray *buf,
						  GInputStream *stream,
						  guint32 *crc,
						  GError **error)
{
	g_autoptr(GBytes) blob = g_bytes_new(buf->data, buf->len);
	g_autoptr(GInputStream) composite_stream = fu_composite_input_stream_new();
	if (!fu_composite_input_stream_add_bytes(FU_COMPOSITE_INPUT_STREAM(composite_stream),
						 blob,
						 error))
		return FALSE;
	if (!fu_composite_input_stream_add_stream(FU_COMPOSITE_INPUT_STREAM(composite_stream),
						  stream,
						  error))
		return FALSE;
	return fu_input_stream_compute_crc32(composite_stream,
					     FU_CRC_KIND_B32_STANDARD,
					     crc,
					     error);
}

static FuChunk *
fu_wacom_usb_module_bluetooth_id9_get_startcmd(GInputStream *stream,
					       gboolean full_erase,
					       GError **error)
{
	gsize streamsz = 0;
	guint8 command = full_erase ? FU_WACOM_USB_MODULE_BLUETOOTH_ID9_CMD_FULLERASE
				    : FU_WACOM_USB_MODULE_BLUETOOTH_ID9_CMD_NORMAL;
	guint32 crc = ~0;
	g_autoptr(FuStructWacomUsbId9LoaderCmd) st_loader =
	    fu_struct_wacom_usb_id9_loader_cmd_new();
	g_autoptr(FuStructWacomUsbId9SpiCmd) st_spi = fu_struct_wacom_usb_id9_spi_cmd_new();
	g_autoptr(FuStructWacomUsbId9UnknownCmd) st_unknown =
	    fu_struct_wacom_usb_id9_unknown_cmd_new();
	g_autoptr(GBytes) blob = NULL;

	if (!fu_input_stream_size(stream, &streamsz, error))
		return NULL;
	fu_struct_wacom_usb_id9_unknown_cmd_set_size(st_unknown, streamsz);

	fu_struct_wacom_usb_id9_spi_cmd_set_size(st_spi,
						 streamsz +
						     FU_STRUCT_WACOM_USB_ID9_UNKNOWN_CMD_SIZE);
	if (!fu_struct_wacom_usb_id9_spi_cmd_set_data(st_spi, st_unknown, error))
		return NULL;

	fu_struct_wacom_usb_id9_loader_cmd_set_command(st_loader, command);
	fu_struct_wacom_usb_id9_loader_cmd_set_size(st_loader,
						    streamsz +
							FU_STRUCT_WACOM_USB_ID9_SPI_CMD_SIZE);
	if (!fu_wacom_usb_module_bluetooth_id9_calculate_crc32(st_spi->buf, stream, &crc, error))
		return NULL;
	fu_struct_wacom_usb_id9_loader_cmd_set_crc(st_loader, crc);
	if (!fu_struct_wacom_usb_id9_loader_cmd_set_data(st_loader, st_spi, error))
		return NULL;

	if (!fu_struct_wacom_usb_id9_loader_cmd_validate(st_loader->buf->data,
							 st_loader->buf->len,
							 0,
							 error))
		return NULL;

	blob = fu_struct_wacom_usb_id9_loader_cmd_to_bytes(st_loader);
	return fu_chunk_bytes_new(blob);
}

static gboolean
fu_wacom_usb_module_bluetooth_id9_write_block(FuWacomUsbModule *self,
					      guint8 phase,
					      FuChunk *chunk,
					      FuProgress *progress,
					      GError **error)
{
	g_autoptr(GByteArray) buf = g_byte_array_new();
	g_autoptr(GBytes) blob_chunk = NULL;

	fu_byte_array_append_uint8(buf, phase);
	g_byte_array_append(buf, fu_chunk_get_data(chunk), fu_chunk_get_data_sz(chunk));

	blob_chunk = g_bytes_new(buf->data, buf->len);
	return fu_wacom_usb_module_set_feature(self,
					       FU_WACOM_USB_MODULE_COMMAND_DATA,
					       blob_chunk,
					       fu_progress_get_child(progress),
					       FU_WACOM_USB_MODULE_BLUETOOTH_ID9_POLL_INTERVAL,
					       FU_WACOM_USB_MODULE_BLUETOOTH_ID9_DATA_TIMEOUT,
					       error);
}

static gboolean
fu_wacom_usb_module_bluetooth_id9_write_blocks(FuWacomUsbModule *self,
					       guint8 phase,
					       GInputStream *stream,
					       gsize block_len,
					       FuProgress *progress,
					       GError **error)
{
	g_autoptr(FuChunkArray) chunks = NULL;

	chunks = fu_chunk_array_new_from_stream(stream,
						FU_CHUNK_ADDR_OFFSET_NONE,
						FU_CHUNK_PAGESZ_NONE,
						block_len,
						error);
	if (chunks == NULL)
		return FALSE;

	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, fu_chunk_array_length(chunks));
	for (guint i = 0; i < fu_chunk_array_length(chunks); i++) {
		g_autoptr(FuChunk) chk = NULL;

		/* prepare chunk */
		chk = fu_chunk_array_index(chunks, i, error);
		if (chk == NULL)
			return FALSE;
		if (!fu_wacom_usb_module_bluetooth_id9_write_block(self,
								   phase,
								   chk,
								   progress,
								   error))
			return FALSE;

		fu_progress_step_done(progress);
	}
	return TRUE;
}

static FuFirmware *
fu_wacom_usb_module_bluetooth_id9_prepare_firmware(FuDevice *device,
						   GInputStream *stream,
						   FuProgress *progress,
						   FuFirmwareParseFlags flags,
						   GError **error)
{
	const guint8 *blob;
	gsize blob_len = 0;
	guint16 loader_len = 0;
	gsize payload_len = 0;
	g_autoptr(GBytes) fw = NULL;
	g_autoptr(GBytes) loader_bytes = NULL;
	g_autoptr(GBytes) payload_bytes = NULL;
	g_autoptr(FuFirmware) firmware = fu_firmware_new();
	g_autoptr(FuFirmware) loader_fw = NULL;
	g_autoptr(FuFirmware) payload_fw = NULL;

	/* convert to blob */
	fw = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error);
	if (fw == NULL)
		return NULL;

	/* The firmware file is formatted as a 2 byte "length" field
	 * followed by <length> bytes of loader code. The remainder
	 * payload is firmware data.
	 */
	blob = g_bytes_get_data(fw, &blob_len);
	if (!fu_memread_uint16_safe(blob, blob_len, 0, &loader_len, G_BIG_ENDIAN, error)) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "invalid firmware size");
		return NULL;
	}

	if (blob_len < 2 || loader_len > blob_len - 2) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_FILE,
				    "invalid firmware loader size");
		return NULL;
	}
	loader_bytes = fu_bytes_new_offset(fw, 2, loader_len, error);
	if (loader_bytes == NULL)
		return NULL;
	loader_fw = fu_firmware_new_from_bytes(loader_bytes);
	fu_firmware_set_id(loader_fw, FU_FIRMWARE_ID_HEADER);
	fu_firmware_add_image_gtype(firmware, FU_TYPE_FIRMWARE);
	if (!fu_firmware_add_image(firmware, loader_fw, error))
		return NULL;

	payload_len = blob_len - 2 - loader_len;
	payload_bytes = fu_bytes_new_offset(fw, 2 + loader_len, payload_len, error);
	if (payload_bytes == NULL)
		return NULL;
	payload_fw = fu_firmware_new_from_bytes(payload_bytes);
	fu_firmware_set_id(payload_fw, FU_FIRMWARE_ID_PAYLOAD);
	if (!fu_firmware_add_image(firmware, payload_fw, error))
		return NULL;

	return g_steal_pointer(&firmware);
}

static gboolean
fu_wacom_usb_module_bluetooth_id9_write_firmware(FuDevice *device,
						 FuFirmware *firmware,
						 FuProgress *progress,
						 FwupdInstallFlags flags,
						 GError **error)
{
	FuWacomUsbModule *self = FU_WACOM_USB_MODULE(device);
	const guint8 buf_start[] = {FU_WACOM_USB_MODULE_BLUETOOTH_ID9_START_NORMAL};
	g_autoptr(GBytes) blob_start = g_bytes_new_static(buf_start, sizeof(buf_start));
	g_autoptr(GInputStream) stream_loader = NULL;
	g_autoptr(GInputStream) stream_payload = NULL;
	g_autoptr(FuChunk) cmd = NULL;

	/* get firmware images */
	stream_loader = fu_firmware_get_image_by_id_stream(firmware, FU_FIRMWARE_ID_HEADER, error);
	if (stream_loader == NULL)
		return FALSE;
	stream_payload =
	    fu_firmware_get_image_by_id_stream(firmware, FU_FIRMWARE_ID_PAYLOAD, error);
	if (stream_payload == NULL)
		return FALSE;
	cmd = fu_wacom_usb_module_bluetooth_id9_get_startcmd(stream_payload, FALSE, error);
	if (cmd == NULL)
		return FALSE;

	/* progress */
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 22, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 67, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, NULL);

	/* start */
	if (!fu_wacom_usb_module_set_feature(self,
					     FU_WACOM_USB_MODULE_COMMAND_START,
					     blob_start,
					     fu_progress_get_child(progress),
					     FU_WACOM_USB_MODULE_BLUETOOTH_ID9_POLL_INTERVAL,
					     FU_WACOM_USB_MODULE_BLUETOOTH_ID9_START_TIMEOUT,
					     error))
		return FALSE;
	fu_progress_step_done(progress);

	/* transfer flash programmer to device RAM */
	if (!fu_wacom_usb_module_bluetooth_id9_write_blocks(
		self,
		FU_WACOM_USB_MODULE_BLUETOOTH_ID9_LOADER_RAM,
		stream_loader,
		FU_WACOM_USB_MODULE_BLUETOOTH_ID9_PAYLOAD_SZ,
		fu_progress_get_child(progress),
		error))
		return FALSE;
	fu_progress_step_done(progress);

	/* send "flash start" command to programer */
	if (!fu_wacom_usb_module_bluetooth_id9_write_block(
		self,
		FU_WACOM_USB_MODULE_BLUETOOTH_ID9_LOADER_BEGIN,
		cmd,
		progress,
		error))
		return FALSE;

	/* transfer payload for programming */
	if (!fu_wacom_usb_module_bluetooth_id9_write_blocks(
		self,
		FU_WACOM_USB_MODULE_BLUETOOTH_ID9_LOADER_DATA,
		stream_payload,
		FU_WACOM_USB_MODULE_BLUETOOTH_ID9_PAYLOAD_SZ,
		fu_progress_get_child(progress),
		error))
		return FALSE;
	fu_progress_step_done(progress);

	/* end */
	if (!fu_wacom_usb_module_set_feature(self,
					     FU_WACOM_USB_MODULE_COMMAND_END,
					     NULL,
					     fu_progress_get_child(progress),
					     FU_WACOM_USB_MODULE_BLUETOOTH_ID9_POLL_INTERVAL,
					     FU_WACOM_USB_MODULE_BLUETOOTH_ID9_END_TIMEOUT,
					     error))
		return FALSE;
	fu_progress_step_done(progress);

	/* success */
	return TRUE;
}

static void
fu_wacom_usb_module_bluetooth_id9_init(FuWacomUsbModuleBluetoothId9 *self)
{
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_set_install_duration(FU_DEVICE(self), 15);
}

static void
fu_wacom_usb_module_bluetooth_id9_class_init(FuWacomUsbModuleBluetoothId9Class *klass)
{
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
	device_class->write_firmware = fu_wacom_usb_module_bluetooth_id9_write_firmware;
	device_class->prepare_firmware = fu_wacom_usb_module_bluetooth_id9_prepare_firmware;
}

FuWacomUsbModule *
fu_wacom_usb_module_bluetooth_id9_new(FuDevice *proxy)
{
	FuWacomUsbModule *module = NULL;
	module = g_object_new(FU_TYPE_WACOM_USB_MODULE_BLUETOOTH_ID9,
			      "proxy",
			      proxy,
			      "fw-type",
			      FU_WACOM_USB_MODULE_FW_TYPE_BLUETOOTH_ID9,
			      NULL);
	return module;
}
