/*
 * Copyright (C) 2021 Peter Marheine <pmarheine@chromium.org>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <linux/i2c-dev.h>

#include "fu-parade-lspcon-device.h"

/* device registers are split into pages, where
 * each page has its own I2C address */
#define I2C_ADDR_PAGE2	 0x4A
#define REG_ADDR_CLT2SPI 0x82
/* FLASH_ADDR_* are the upper 16 bits of the 24-bit flash address that gets
 * mapped into page 7. Writing 0x01, 0x42 will map the 256 bytes from 0x420100
 * into page 7. */
#define REG_ADDR_FLASH_ADDR_LO 0x8E
#define REG_ADDR_FLASH_ADDR_HI 0x8F
/* 16-deep SPI write and read buffer FIFOs */
#define REG_ADDR_WR_FIFO 0x90
#define REG_ADDR_RD_FIFO 0x91
/* Low nibble is write operation length, high nibble for read commands.
 * Reset to 0 after command completion. */
#define REG_ADDR_SPI_LEN 0x92

#define REG_ADDR_SPI_CTL 0x93
/* set to do a write-only transaction */
#define SPI_CTL_NOREAD 0x04
/* set to begin executing command */
#define SPI_CTL_TRIGGER 0x01

/* operation status fields: set to 1 when operation begins, 2 when command has been
 * sent, reset to 0 when command completed */
#define REG_ADDR_SPI_STATUS 0x9e
/* byte programming */
#define SPI_STATUS_BP_MASK 0x03
/* sector erase */
#define SPI_STATUS_SE_MASK 0x0C
/* chip erase */
#define SPI_STATUS_CE_MASK 0x30

/* write WR_PROTECT_DISABLE to permit flash write operations */
#define REG_ADDR_WR_PROTECT 0xB3
#define WR_PROTECT_DISABLE  0x10

/* MPU control register */
#define REG_ADDR_MPU 0xBC

/* write a magic sequence to this register to enable writes to
 * mapped memory via page 7, or anything else to disable */
#define REG_ADDR_MAP_WRITE 0xDA

#define I2C_ADDR_PAGE5		  0x4D
#define REG_ADDR_ACTIVE_PARTITION 0x0E

#define I2C_ADDR_PAGE7 0x4F

#define FLASH_BLOCK_SIZE 0x10000

#define FU_PARADE_LSPCON_DEVICE_IOCTL_TIMEOUT 5000 /* ms */

/*
 * user1: 0x10000 - 0x20000
 * user2: 0x20000 - 0x30000
 * flag:  0x00002 - 0x00004
 */
struct _FuParadeLspconDevice {
	FuI2cDevice parent_instance;
	guint8 active_partition;
	gchar *aux_device_name;
};

G_DEFINE_TYPE(FuParadeLspconDevice, fu_parade_lspcon_device, FU_TYPE_I2C_DEVICE)

static void
fu_parade_lspcon_device_init(FuParadeLspconDevice *self)
{
	FuDevice *device = FU_DEVICE(self);
	fu_device_set_vendor(device, "Parade Technologies");
	fu_device_add_vendor_id(device, "PCI:0x1AF8");
	fu_device_add_protocol(device, "com.paradetech.ps176");
	fu_device_add_icon(device, "video-display");
	fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL);
	fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE);
	fu_device_add_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE);
	fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);
	fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_NO_GENERIC_GUIDS);
	fu_device_set_firmware_size(device, 0x10000);
	fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PAIR);
}

static void
fu_parade_lspcon_device_finalize(GObject *object)
{
	FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(object);
	g_free(self->aux_device_name);
	G_OBJECT_CLASS(fu_parade_lspcon_device_parent_class)->finalize(object);
}

static gboolean
fu_parade_lspcon_device_set_quirk_kv(FuDevice *device,
				     const gchar *key,
				     const gchar *value,
				     GError **error)
{
	FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);

	if (g_strcmp0(key, "ParadeLspconAuxDeviceName") == 0) {
		self->aux_device_name = g_strdup(value);
		return TRUE;
	}
	return FU_DEVICE_CLASS(fu_parade_lspcon_device_parent_class)
	    ->set_quirk_kv(device, key, value, error);
}

static gboolean
fu_parade_lspcon_device_probe(FuDevice *device, GError **error)
{
	FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
	FuContext *context = fu_device_get_context(device);

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

	/* custom instance IDs to get device quirks */
	fu_device_add_instance_str(device,
				   "FAMILY",
				   fu_context_get_hwid_value(context, FU_HWIDS_KEY_FAMILY));
	if (!fu_device_build_instance_id(device, error, "I2C", "NAME", "FAMILY", NULL))
		return FALSE;

	/* should know which aux device over which we read DPCD version */
	if (self->aux_device_name == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "ParadeLspconAuxDeviceName must be specified");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_parade_lspcon_ensure_i2c_address(FuParadeLspconDevice *self, guint8 address, GError **error)
{
	if (!fu_udev_device_ioctl(FU_UDEV_DEVICE(self),
				  I2C_SLAVE,
				  (guint8 *)(guintptr)address,
				  NULL,
				  FU_PARADE_LSPCON_DEVICE_IOCTL_TIMEOUT,
				  error)) {
		g_prefix_error(error, "failed to set I2C address: ");
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_parade_lspcon_device_open(FuDevice *device, GError **error)
{
	if (!FU_DEVICE_CLASS(fu_parade_lspcon_device_parent_class)->open(device, error))
		return FALSE;

	/* general assumption is that page 2 is selected: code that uses another address
	 * should use an address guard to ensure it gets reset */
	return fu_parade_lspcon_ensure_i2c_address(FU_PARADE_LSPCON_DEVICE(device),
						   I2C_ADDR_PAGE2,
						   error);
}

/**
 * creates a scope in which the device's target I2C address is something
 * other than page 2, and resets it to page 2 when the scope is left.
 */
typedef struct {
	FuParadeLspconDevice *device;
} FuParadeLspconI2cAddressGuard;

static FuParadeLspconI2cAddressGuard *
fu_parade_lspcon_i2c_address_guard_new(FuParadeLspconDevice *self,
				       guint8 new_address,
				       GError **error)
{
	FuParadeLspconI2cAddressGuard *out;

	if (!fu_parade_lspcon_ensure_i2c_address(self, new_address, error))
		return NULL;
	out = g_new0(FuParadeLspconI2cAddressGuard, 1);
	out->device = self;
	return out;
}

static void
fu_parade_lspcon_i2c_address_guard_free(FuParadeLspconI2cAddressGuard *guard)
{
	g_autoptr(GError) error_local = NULL;
	if (!fu_parade_lspcon_ensure_i2c_address(guard->device, I2C_ADDR_PAGE2, &error_local)) {
		g_warning("failed to set page2 back: %s", error_local->message);
	}
	g_free(guard);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuParadeLspconI2cAddressGuard,
			      fu_parade_lspcon_i2c_address_guard_free);

static gboolean
fu_parade_lspcon_write_register(FuParadeLspconDevice *self,
				guint8 register_addr,
				guint8 value,
				GError **error)
{
	guint8 transaction[] = {register_addr, value};
	return fu_i2c_device_write(FU_I2C_DEVICE(self), transaction, sizeof(transaction), error);
}

static gboolean
fu_parade_lspcon_read_register(FuParadeLspconDevice *self,
			       guint8 register_addr,
			       guint8 *value,
			       GError **error)
{
	FuI2cDevice *i2c_device = FU_I2C_DEVICE(self);
	if (!fu_i2c_device_write(i2c_device, &register_addr, 0x1, error))
		return FALSE;
	return fu_i2c_device_read(i2c_device, value, 0x1, error);
}

/* map the page containing the given address into page 7 */
static gboolean
fu_parade_lspcon_map_page(FuParadeLspconDevice *self, guint32 address, GError **error)
{
	if (!fu_parade_lspcon_write_register(self, REG_ADDR_FLASH_ADDR_HI, address >> 16, error))
		return FALSE;
	return fu_parade_lspcon_write_register(self, REG_ADDR_FLASH_ADDR_LO, address >> 8, error);
}

/* wait until the specified register masked with mask reads the expected
 * value, up to 10 seconds */
static gboolean
fu_parade_lspcon_poll_register(FuParadeLspconDevice *self,
			       guint8 register_address,
			       guint8 mask,
			       guint8 expected,
			       GError **error)
{
	guint8 value;
	g_autoptr(GTimer) timer = g_timer_new();

	do {
		if (!fu_parade_lspcon_read_register(self, register_address, &value, error))
			return FALSE;
		if ((value & mask) == expected)
			return TRUE;
	} while (g_timer_elapsed(timer, NULL) <= 10.0);

	g_set_error(error,
		    G_IO_ERROR,
		    G_IO_ERROR_TIMED_OUT,
		    "register %x did not read %x (mask %x) within 10 seconds: read %x",
		    register_address,
		    expected,
		    mask,
		    value);
	return FALSE;
}

static gboolean
fu_parade_lspcon_flash_read(FuParadeLspconDevice *self,
			    guint32 base_address,
			    guint8 *data,
			    const gsize len,
			    FuProgress *progress,
			    GError **error)
{
	FuI2cDevice *i2c_device = FU_I2C_DEVICE(self);
	gsize offset = 0;

	while (offset < len) {
		/* page 7 reads always start from the base of the mapped window- we'll
		 * read the whole page then pull out the parts we care about, using the
		 * full page everywhere except possibly in the first and last reads */
		guint8 page_data[256] = {0x0};
		guint8 page_data_start = base_address & 0xFF;
		gsize page_data_take = MIN((gssize)len, 256 - page_data_start);
		g_autoptr(FuParadeLspconI2cAddressGuard) guard = NULL;

		if (!fu_parade_lspcon_map_page(self, base_address, error))
			return FALSE;
		guard = fu_parade_lspcon_i2c_address_guard_new(self, I2C_ADDR_PAGE7, error);
		if (guard == NULL)
			return FALSE;
		if (!fu_i2c_device_read(i2c_device, page_data, 256, error))
			return FALSE;

		if (!fu_memcpy_safe(data,
				    len,
				    offset,
				    page_data,
				    sizeof(page_data),
				    page_data_start,
				    page_data_take,
				    error))
			return FALSE;
		base_address += page_data_take;
		offset += page_data_take;

		fu_progress_set_percentage_full(progress, offset, len);
	}

	return TRUE;
}

static gboolean
fu_parade_lspcon_flash_transmit_command(FuParadeLspconDevice *self,
					const guint8 *command,
					gsize command_len,
					GError **error)
{
	/* write length field is 4 bits wide */
	g_return_val_if_fail(command_len > 0 && command_len <= 16, FALSE);

	/* fill transmit buffer */
	for (gsize i = 0; i < command_len; i++) {
		if (!fu_parade_lspcon_write_register(self, REG_ADDR_WR_FIFO, command[i], error))
			return FALSE;
	}

	/* set command length */
	if (!fu_parade_lspcon_write_register(self, REG_ADDR_SPI_LEN, command_len - 1, error))
		return FALSE;

	/* execute operation */
	return fu_parade_lspcon_write_register(self,
					       REG_ADDR_SPI_CTL,
					       SPI_CTL_NOREAD | SPI_CTL_TRIGGER,
					       error);
}

/*
 * set the flash Write Enable Latch, permitting the next program, erase or
 * status register write operation.
 */
static gboolean
fu_parade_lspcon_flash_enable_write(FuParadeLspconDevice *self, GError **error)
{
	const guint8 write_enable[] = {0x06};
	return fu_parade_lspcon_flash_transmit_command(self,
						       write_enable,
						       sizeof(write_enable),
						       error);
}

static gboolean
fu_parade_lspcon_flash_read_status(FuParadeLspconDevice *self, guint8 *value, GError **error)
{
	if (!fu_parade_lspcon_write_register(self, REG_ADDR_WR_FIFO, 0x05, error))
		return FALSE;
	if (!fu_parade_lspcon_write_register(self, REG_ADDR_SPI_LEN, 0, error))
		return FALSE;
	if (!fu_parade_lspcon_write_register(self, REG_ADDR_SPI_CTL, SPI_CTL_TRIGGER, error))
		return FALSE;

	/* wait for command completion */
	if (!fu_parade_lspcon_poll_register(self, REG_ADDR_SPI_CTL, SPI_CTL_TRIGGER, 0, error))
		return FALSE;

	/* read SR value */
	return fu_parade_lspcon_read_register(self, REG_ADDR_RD_FIFO, value, error);
}

/* poll the flash status register for operation completion */
static gboolean
fu_parade_lspcon_flash_wait_ready(FuParadeLspconDevice *self, GError **error)
{
	g_autoptr(GTimer) timer = g_timer_new();

	do {
		guint8 status_register;
		if (!fu_parade_lspcon_flash_read_status(self, &status_register, error))
			return FALSE;

		/* BUSY bit clears on completion */
		if ((status_register & 1) == 0)
			return TRUE;

		/* flash operations generally take between 1ms and 4s; polling
		 * at 1000 Hz is still quite responsive and not overly slow */
		fu_device_sleep(FU_DEVICE(self), 1); /* ms */
	} while (g_timer_elapsed(timer, NULL) <= 10.0);

	g_set_error_literal(error,
			    G_IO_ERROR,
			    G_IO_ERROR_TIMED_OUT,
			    "flash did not become ready within 10 seconds");
	return FALSE;
}

static gboolean
fu_parade_lspcon_flash_write(FuParadeLspconDevice *self,
			     guint32 base_address,
			     GBytes *data,
			     FuProgress *progress,
			     GError **error)
{
	FuI2cDevice *i2c_device = FU_I2C_DEVICE(self);
	const guint8 unlock_writes[] = {0xaa, 0x55, 0x50, 0x41, 0x52, 0x44};
	gsize data_len = g_bytes_get_size(data);
	g_autoptr(FuChunkArray) chunks = NULL;

	/* address must be 256-byte aligned */
	g_return_val_if_fail((base_address & 0xFF) == 0, FALSE);
	g_debug("flash write %" G_GSIZE_FORMAT " bytes at %#x",
		g_bytes_get_size(data),
		base_address);

	/* unlock map writes by writing the magic sequence */
	for (gsize i = 0; i < sizeof(unlock_writes); i++) {
		if (!fu_parade_lspcon_write_register(self,
						     REG_ADDR_MAP_WRITE,
						     unlock_writes[i],
						     error))
			return FALSE;
	}

	/* reset clt2SPI, required before write */
	if (!fu_parade_lspcon_write_register(self, REG_ADDR_CLT2SPI, 0x20, error))
		return FALSE;
	fu_device_sleep(FU_DEVICE(self), 100); /* ms */
	if (!fu_parade_lspcon_write_register(self, REG_ADDR_CLT2SPI, 0, error))
		return FALSE;

	chunks = fu_chunk_array_new_from_bytes(data, base_address, 256);
	for (gsize i = 0; i < fu_chunk_array_length(chunks); i++) {
		g_autoptr(FuChunk) chunk = fu_chunk_array_index(chunks, i);
		guint32 address = fu_chunk_get_address(chunk);
		guint32 chunk_size = fu_chunk_get_data_sz(chunk);
		guint8 write_data[257] = {0x0};
		g_autoptr(FuParadeLspconI2cAddressGuard) guard = NULL;

		/* map target address range in page 7 */
		if (!fu_parade_lspcon_map_page(self, address, error))
			return FALSE;

		/* write data to page 7 memory window */
		guard = fu_parade_lspcon_i2c_address_guard_new(self, I2C_ADDR_PAGE7, error);
		if (guard == NULL)
			return FALSE;

		/* page write is prefixed with an offset:
		 * we always start from offset 0 */
		if (!fu_memcpy_safe(write_data,
				    sizeof(write_data),
				    1,
				    fu_chunk_get_data(chunk),
				    chunk_size,
				    0,
				    chunk_size,
				    error))
			return FALSE;

		if (!fu_i2c_device_write(i2c_device, write_data, chunk_size + 1, error))
			return FALSE;

		fu_progress_set_percentage_full(progress, address - base_address, data_len);
	}

	/* re-lock map writes */
	return fu_parade_lspcon_write_register(self, REG_ADDR_MAP_WRITE, 0, error);
}

static gboolean
fu_parade_lspcon_flash_erase_block(FuParadeLspconDevice *self,
				   guint32 base_address,
				   guint32 size,
				   GError **error)
{
	const guint8 block_erase[] = {0xd8, base_address >> 16, base_address >> 8, base_address};

	/* address must be block-aligned */
	g_return_val_if_fail((base_address & (FLASH_BLOCK_SIZE - 1)) == 0, FALSE);
	g_return_val_if_fail(size == FLASH_BLOCK_SIZE, FALSE);

	g_debug("flash erase block at %#x", base_address);

	if (!fu_parade_lspcon_flash_enable_write(self, error))
		return FALSE;

	if (!fu_parade_lspcon_flash_transmit_command(self, block_erase, sizeof(block_erase), error))
		return FALSE;

	/* wait for command completion */
	if (!fu_parade_lspcon_poll_register(self,
					    REG_ADDR_SPI_STATUS,
					    SPI_STATUS_SE_MASK,
					    0,
					    error))
		return FALSE;

	/* wait for flash to complete erase */
	return fu_parade_lspcon_flash_wait_ready(self, error);
}

static gboolean
fu_parade_lspcon_probe_active_flash_partition(FuParadeLspconDevice *self,
					      guint8 *partition,
					      GError **error)
{
	guint8 data = 0x0;
	g_autoptr(FuParadeLspconI2cAddressGuard) guard = NULL;

	/* read currently-running flash partition number */
	guard = fu_parade_lspcon_i2c_address_guard_new(self, I2C_ADDR_PAGE5, error);
	if (guard == NULL)
		return FALSE;
	if (!fu_parade_lspcon_read_register(self, REG_ADDR_ACTIVE_PARTITION, &data, error))
		return FALSE;

	*partition = data;
	return TRUE;
}

static gboolean
fu_parade_lspcon_device_reload(FuDevice *device, GError **error)
{
	FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
	guint32 oui;
	guint8 version_buf[2] = {0x0};
	g_autofree gchar *version = NULL;
	g_autofree gchar *oui_string = NULL;
	g_autolist(GUdevDevice) aux_devices = NULL;
	g_autoptr(FuDeviceLocker) aux_device_locker = NULL;
	g_autoptr(FuUdevDevice) aux_device = NULL;
	g_autoptr(GUdevClient) udev_client = g_udev_client_new(NULL);
	g_autoptr(GUdevEnumerator) enumerator = g_udev_enumerator_new(udev_client);

	/* determine active partition for flashing later */
	if (!fu_parade_lspcon_probe_active_flash_partition(self, &self->active_partition, error))
		return FALSE;
	g_info("device reports running from partition %d", self->active_partition);
	if (self->active_partition < 1 || self->active_partition > 3) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "unexpected active flash partition: %d",
			    self->active_partition);
		return FALSE;
	}

	/* find the drm_dp_aux_dev specified by quirks that is connected to the
	 * LSPCON, in order to read DPCD from it */
	if (self->aux_device_name == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no DP aux device specified, unable to query LSPCON");
		return FALSE;
	}
	g_udev_enumerator_add_match_subsystem(enumerator, "drm_dp_aux_dev");
	g_udev_enumerator_add_match_sysfs_attr(enumerator, "name", self->aux_device_name);
	aux_devices = g_udev_enumerator_execute(enumerator);
	if (aux_devices == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "failed to locate a DP aux device named \"%s\"",
			    self->aux_device_name);
		return FALSE;
	}
	if (g_list_length(aux_devices) > 1) {
		g_list_free_full(aux_devices, g_object_unref);
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "found multiple DP aux devices with name \"%s\"",
			    self->aux_device_name);
		return FALSE;
	}
	aux_device = g_object_new(FU_TYPE_DPAUX_DEVICE,
				  "context",
				  fu_device_get_context(device),
				  "udev-device",
				  aux_devices->data,
				  NULL);
	g_debug("using aux dev %s", fu_udev_device_get_sysfs_path(aux_device));

	/* the following open() requires the device have IDs set */
	if (!fu_udev_device_set_physical_id(aux_device, "drm_dp_aux_dev", error))
		return FALSE;

	/* open device to read version from DPCD */
	aux_device_locker = fu_device_locker_new(aux_device, error);
	if (aux_device_locker == NULL)
		return FALSE;

	/* DPCD address 00500-00502: device OUI */
	oui = fu_dpaux_device_get_dpcd_ieee_oui(FU_DPAUX_DEVICE(aux_device));
	if (oui != 0x001CF8) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "device OUI %06X does not match expected value for Paradetech",
			    oui);
		return FALSE;
	}
	oui_string = g_strdup_printf("OUI:%06X", oui);
	fu_device_add_vendor_id(device, oui_string);

	/* DPCD address 0x50A, 0x50B: branch device firmware
	 * major and minor revision */
	if (!fu_udev_device_pread(aux_device, 0x50a, version_buf, sizeof(version_buf), error))
		return FALSE;
	version = g_strdup_printf("%d.%d", version_buf[0], version_buf[1]);
	fu_device_set_version(device, version);
	return TRUE;
}

static gboolean
fu_parade_lspcon_device_write_firmware(FuDevice *device,
				       FuFirmware *firmware,
				       FuProgress *progress,
				       FwupdInstallFlags flags,
				       GError **error)
{
	FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
	const guint8 write_sr_volatile[] = {0x50};
	const guint8 write_sr_disable_bp[] = {
	    0x01, /* write SR */
	    0x80, /* write protect follows /WP signal, no block protection */
	    0x00};
	const guint8 write_sr_enable_bp[] = {0x01, 0x8c, 0x00};
	/* if the boot partition is active we could flash either, but prefer
	 * the first */
	const guint8 target_partition = self->active_partition == 1 ? 2 : 1;
	const guint32 target_address = target_partition << 16;
	const guint8 flag_data[] = {0x55, 0xaa, target_partition, 1 - target_partition};
	const guint8 *buf;
	gsize bufsz;
	g_autofree guint8 *readback_buf = NULL;
	g_autoptr(GBytes) blob_fw = NULL;
	g_autoptr(GBytes) flag_data_bytes = NULL;

	/* progress */
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 5, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 70, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 25, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 3, "device-write-boot");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 2, "device-verify-boot");

	blob_fw = fu_firmware_get_bytes(firmware, error);
	if (blob_fw == NULL)
		return FALSE;

	buf = g_bytes_get_data(blob_fw, &bufsz);
	if (bufsz != FLASH_BLOCK_SIZE) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "invalid image size %#" G_GSIZE_MODIFIER "x, expected %#x",
			    bufsz,
			    (unsigned)FLASH_BLOCK_SIZE);
		return FALSE;
	}

	/* deassert flash /WP */
	if (!fu_parade_lspcon_write_register(self, REG_ADDR_WR_PROTECT, WR_PROTECT_DISABLE, error))
		return FALSE;

	/* disable flash protection until next power-off */
	if (!fu_parade_lspcon_flash_transmit_command(self,
						     write_sr_volatile,
						     sizeof(write_sr_volatile),
						     error))
		return FALSE;
	if (!fu_parade_lspcon_flash_transmit_command(self,
						     write_sr_disable_bp,
						     sizeof(write_sr_disable_bp),
						     error))
		return FALSE;

	/* wait for SR write to complete */
	if (!fu_parade_lspcon_flash_wait_ready(self, error))
		return FALSE;

	/* erase entire target partition (one flash block) */
	if (!fu_parade_lspcon_flash_erase_block(self, target_address, bufsz, error)) {
		g_prefix_error(error, "failed to erase flash partition %d: ", target_partition);
		return FALSE;
	}
	fu_progress_step_done(progress);

	/* write image */
	if (!fu_parade_lspcon_flash_write(self,
					  target_address,
					  blob_fw,
					  fu_progress_get_child(progress),
					  error)) {
		g_prefix_error(error,
			       "failed to write firmware to partition %d: ",
			       target_partition);
		return FALSE;
	}
	fu_progress_step_done(progress);

	/* read back written image to verify */
	readback_buf = g_malloc0(bufsz);
	if (!fu_parade_lspcon_flash_read(self,
					 target_address,
					 readback_buf,
					 bufsz,
					 fu_progress_get_child(progress),
					 error))
		return FALSE;
	if (!fu_memcmp_safe(buf, bufsz, 0x0, readback_buf, bufsz, 0x0, bufsz, error)) {
		g_prefix_error(error, "flash contents do not match: ");
		return FALSE;
	}
	fu_progress_step_done(progress);

	/* erase flag partition */
	if (!fu_parade_lspcon_flash_erase_block(self, 0, FLASH_BLOCK_SIZE, error))
		return FALSE;

	/* write flag indicating device should boot the target partition */
	fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
	flag_data_bytes = g_bytes_new_static(flag_data, sizeof(flag_data));
	if (!fu_parade_lspcon_flash_write(self,
					  0,
					  flag_data_bytes,
					  fu_progress_get_child(progress),
					  error))
		return FALSE;
	fu_progress_step_done(progress);

	/* verify flag partition */
	if (!fu_parade_lspcon_flash_read(self,
					 0,
					 readback_buf,
					 sizeof(flag_data),
					 fu_progress_get_child(progress),
					 error))
		return FALSE;
	if (!fu_memcmp_safe(flag_data,
			    sizeof(flag_data),
			    0x0,
			    readback_buf,
			    bufsz,
			    0x0,
			    MIN(sizeof(flag_data), bufsz),
			    error)) {
		g_prefix_error(error, "flag partition contents do not match: ");
		return FALSE;
	}

	/* re-enable flash protection */
	if (!fu_parade_lspcon_flash_transmit_command(self,
						     write_sr_volatile,
						     sizeof(write_sr_volatile),
						     error))
		return FALSE;
	if (!fu_parade_lspcon_flash_transmit_command(self,
						     write_sr_enable_bp,
						     sizeof(write_sr_enable_bp),
						     error))
		return FALSE;
	fu_progress_step_done(progress);

	/* reassert /WP to flash */
	return fu_parade_lspcon_write_register(self, REG_ADDR_WR_PROTECT, 0, error);
}

static gboolean
fu_parade_lspcon_set_mpu_running(FuParadeLspconDevice *self, gboolean running, GError **error)
{
	/* reset */
	if (!fu_parade_lspcon_write_register(self, REG_ADDR_MPU, 0xc0, error))
		return FALSE;

	/* release reset, set MPU active or not */
	return fu_parade_lspcon_write_register(self, REG_ADDR_MPU, running ? 0 : 0x40, error);
}

static gboolean
fu_parade_lspcon_device_detach(FuDevice *device, FuProgress *progress, GError **error)
{
	FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
	return fu_parade_lspcon_set_mpu_running(self, FALSE, error);
}

static gboolean
fu_parade_lspcon_device_attach(FuDevice *device, FuProgress *progress, GError **error)
{
	FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
	return fu_parade_lspcon_set_mpu_running(self, TRUE, error);
}

static GBytes *
fu_parade_lspcon_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error)
{
	FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device);
	g_autofree guint8 *data = g_malloc0(FLASH_BLOCK_SIZE);

	if (!fu_parade_lspcon_flash_read(self,
					 self->active_partition * FLASH_BLOCK_SIZE,
					 data,
					 FLASH_BLOCK_SIZE,
					 progress,
					 error))
		return NULL;
	return g_bytes_new_take(g_steal_pointer(&data), FLASH_BLOCK_SIZE);
}

static void
fu_parade_lspcon_device_set_progress(FuDevice *self, FuProgress *progress)
{
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload");
}

static void
fu_parade_lspcon_device_class_init(FuParadeLspconDeviceClass *klass)
{
	FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
	GObjectClass *klass_object = G_OBJECT_CLASS(klass);

	klass_object->finalize = fu_parade_lspcon_device_finalize;
	klass_device->set_quirk_kv = fu_parade_lspcon_device_set_quirk_kv;
	klass_device->probe = fu_parade_lspcon_device_probe;
	klass_device->setup = fu_parade_lspcon_device_reload;
	klass_device->open = fu_parade_lspcon_device_open;
	klass_device->reload = fu_parade_lspcon_device_reload;
	klass_device->detach = fu_parade_lspcon_device_detach;
	klass_device->write_firmware = fu_parade_lspcon_device_write_firmware;
	klass_device->attach = fu_parade_lspcon_device_attach;
	klass_device->dump_firmware = fu_parade_lspcon_device_dump_firmware;
	klass_device->set_progress = fu_parade_lspcon_device_set_progress;
}
