/*
 * Copyright 2018 Richard Hughes <richard@hughsie.com>
 * Copyright 2021 Jason Gerecke <killertofu@gmail.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-id6.h"
#include "fu-wacom-usb-struct.h"

struct _FuWacomUsbModuleBluetoothId6 {
	FuWacomUsbModule parent_instance;
};

G_DEFINE_TYPE(FuWacomUsbModuleBluetoothId6,
	      fu_wacom_usb_module_bluetooth_id6,
	      FU_TYPE_WACOM_USB_MODULE)

#define FU_WACOM_USB_MODULE_BLUETOOTH_ID6_CRC8_POLYNOMIAL 0x31
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID6_PAYLOAD_SZ	  256
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID6_START_NORMAL	  0x00
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID6_START_FULLERASE 0xFE

#define FU_WACOM_USB_MODULE_BLUETOOTH_ID6_START_TIMEOUT 60000 /* ms */
#define FU_WACOM_USB_MODULE_BLUETOOTH_ID6_END_TIMEOUT	60000 /* ms */

static guint8
fu_wacom_usb_module_bluetooth_id6_reverse_bits(guint8 value)
{
	guint8 reverse = 0;

	for (gint i = 0; i < 8; i++) {
		reverse <<= 1;
		reverse |= (value & 0x01);
		value >>= 1;
	}
	return reverse;
}

/* this doesn't appear to be any kind of standard CRC-8 */
static guint8
fu_wacom_usb_module_bluetooth_id6_calculate_crc(const guint8 *buf, gsize bufsz)
{
	const guint8 polynomial = FU_WACOM_USB_MODULE_BLUETOOTH_ID6_CRC8_POLYNOMIAL;
	guint32 crc = 0x00;
	for (gsize j = bufsz; j > 0; j--) {
		crc ^= (*(buf++) << 8);
		for (guint32 i = 8; i; i--) {
			if (crc & 0x8000)
				crc ^= ((polynomial | 0x100) << 7);
			crc <<= 1;
		}
	}
	crc = (guint8)(crc >> 8);
	return fu_wacom_usb_module_bluetooth_id6_reverse_bits(crc);
}

static gboolean
fu_wacom_usb_module_bluetooth_id6_write_blob(FuWacomUsbModule *self,
					     GInputStream *stream,
					     FuProgress *progress,
					     GError **error)
{
	g_autoptr(FuChunkArray) chunks =
	    fu_chunk_array_new_from_stream(stream,
					   FU_CHUNK_ADDR_OFFSET_NONE,
					   FU_CHUNK_PAGESZ_NONE,
					   FU_WACOM_USB_MODULE_BLUETOOTH_ID6_PAYLOAD_SZ,
					   error);
	if (chunks == NULL)
		return FALSE;

	/* progress */
	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;
		guint8 buf[FU_WACOM_USB_MODULE_BLUETOOTH_ID6_PAYLOAD_SZ + 7] = {0x00, 0x01, 0xFF};
		g_autoptr(GBytes) blob_chunk = NULL;

		/* prepare chunk */
		chk = fu_chunk_array_index(chunks, i, error);
		if (chk == NULL)
			return FALSE;

		/* build data packet */
		fu_memwrite_uint32(buf + 0x3, 0x0, G_LITTLE_ENDIAN); /* addr, always zero */
		memcpy(buf + 0x7,				     /* nocheck:blocked */
		       fu_chunk_get_data(chk),
		       fu_chunk_get_data_sz(chk));
		buf[2] = fu_wacom_usb_module_bluetooth_id6_calculate_crc(
		    buf + 0x7,
		    FU_WACOM_USB_MODULE_BLUETOOTH_ID6_PAYLOAD_SZ); /* include 0xFF for the possibly
								incomplete last chunk */
		blob_chunk = g_bytes_new(buf, sizeof(buf));
		g_debug("writing block %u of %u", i, fu_chunk_array_length(chunks) - 1);
		if (!fu_wacom_usb_module_set_feature(self,
						     FU_WACOM_USB_MODULE_COMMAND_DATA,
						     blob_chunk,
						     fu_progress_get_child(progress),
						     FU_WACOM_USB_MODULE_POLL_INTERVAL,
						     FU_WACOM_USB_MODULE_DATA_TIMEOUT,
						     error)) {
			g_prefix_error(error,
				       "failed to write block %u of %u: ",
				       i,
				       fu_chunk_array_length(chunks) - 1);
			return FALSE;
		}
		fu_progress_step_done(progress);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_wacom_usb_module_bluetooth_id6_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_ID6_START_NORMAL};
	g_autoptr(GBytes) blob_start = g_bytes_new_static(buf_start, sizeof(buf_start));
	g_autoptr(GInputStream) stream = NULL;

	/* progress */
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 8, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 59, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 33, NULL);

	/* get default image */
	stream = fu_firmware_get_stream(firmware, error);
	if (stream == NULL) {
		g_prefix_error_literal(error, "failed to get stream: ");
		return FALSE;
	}

	/* start, which will erase the module */
	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_POLL_INTERVAL,
					     FU_WACOM_USB_MODULE_BLUETOOTH_ID6_START_TIMEOUT,
					     error)) {
		g_prefix_error_literal(error, "failed to erase: ");
		return FALSE;
	}
	fu_progress_step_done(progress);

	/* data */
	if (!fu_wacom_usb_module_bluetooth_id6_write_blob(self,
							  stream,
							  fu_progress_get_child(progress),
							  error)) {
		g_prefix_error_literal(error, "failed to write: ");
		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_POLL_INTERVAL,
					     FU_WACOM_USB_MODULE_BLUETOOTH_ID6_END_TIMEOUT,
					     error)) {
		g_prefix_error_literal(error, "failed to end: ");
		return FALSE;
	}
	fu_progress_step_done(progress);

	/* success */
	return TRUE;
}

static void
fu_wacom_usb_module_bluetooth_id6_init(FuWacomUsbModuleBluetoothId6 *self)
{
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_set_install_duration(FU_DEVICE(self), 120);
	fu_device_set_remove_delay(FU_DEVICE(self), 300000);
}

static void
fu_wacom_usb_module_bluetooth_id6_class_init(FuWacomUsbModuleBluetoothId6Class *klass)
{
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
	device_class->write_firmware = fu_wacom_usb_module_bluetooth_id6_write_firmware;
}

FuWacomUsbModule *
fu_wacom_usb_module_bluetooth_id6_new(FuDevice *proxy)
{
	FuWacomUsbModule *module = NULL;
	module = g_object_new(FU_TYPE_WACOM_USB_MODULE_BLUETOOTH_ID6,
			      "proxy",
			      proxy,
			      "fw-type",
			      FU_WACOM_USB_MODULE_FW_TYPE_BLUETOOTH_ID6,
			      NULL);
	return module;
}
