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

#include "config.h"

#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#include <fcntl.h>
#include <glib/gstdio.h>
#include <linux/i2c-dev.h>

#include "fu-realtek-mst-device.h"

/* firmware debug address */
#define I2C_ADDR_DEBUG 0x35
/* programming address */
#define I2C_ADDR_ISP 0x4a

/* some kind of operation attribute bits */
#define REG_CMD_ATTR 0x60
/* write set to begin executing, cleared when done */
#define CMD_ERASE_BUSY 0x01

/* 24-bit address for commands */
#define REG_CMD_ADDR_HI	 0x64
#define REG_CMD_ADDR_MID 0x65
#define REG_CMD_ADDR_LO	 0x66

/* register for erase commands */
#define REG_ERASE_OPCODE	0x61
#define CMD_OPCODE_ERASE_SECTOR 0x20
#define CMD_OPCODE_ERASE_BLOCK	0xD8

/* register for read commands */
#define REG_READ_OPCODE 0x6A
#define CMD_OPCODE_READ 0x03

/* register for write commands */
#define REG_WRITE_OPCODE 0x6D
#define CMD_OPCODE_WRITE 0x02

/* mode register address */
#define REG_MCU_MODE 0x6F
/* when bit is set in mode register, ISP mode is active */
#define MCU_MODE_ISP (1 << 7)
/* write set to begin write, reset by device when complete */
#define MCU_MODE_WRITE_BUSY (1 << 5)
/* when bit is clear, write buffer contains data */
#define MCU_MODE_WRITE_BUF (1 << 4)

/* write data into write buffer */
#define REG_WRITE_FIFO 0x70
/* number of bytes to write minus 1 (0xff means 256 bytes) */
#define REG_WRITE_LEN 0x71

/* Indirect registers allow access to registers with 16-bit addresses. Write
 * 0x9F to the LO register, then the top byte of the address to HI, the
 * bottom byte of the address to LO, then read or write HI to read or write
 * the value of the target register. */
#define REG_INDIRECT_LO 0xF4
#define REG_INDIRECT_HI 0xF5

/* GPIO configuration/access registers */
#define REG_GPIO88_CONFIG 0x104F
#define REG_GPIO88_VALUE  0xFE3F

/* flash chip properties */
#define FLASH_SIZE	  0x100000
#define FLASH_SECTOR_SIZE 4096
#define FLASH_BLOCK_SIZE  65536

/* MST flash layout */
#define FLASH_USER1_ADDR 0x10000
#define FLASH_FLAG1_ADDR 0xfe304
#define FLASH_USER2_ADDR 0x80000
#define FLASH_FLAG2_ADDR 0xff304
#define FLASH_USER_SIZE	 0x70000

enum dual_bank_mode {
	DUAL_BANK_USER_ONLY = 0,
	DUAL_BANK_DIFF = 1,
	DUAL_BANK_COPY = 2,
	DUAL_BANK_USER_ONLY_FLAG = 3,
	DUAL_BANK_MAX_VALUE = 3,
};

enum flash_bank {
	FLASH_BANK_BOOT = 0,
	FLASH_BANK_USER1 = 1,
	FLASH_BANK_USER2 = 2,
	FLASH_BANK_MAX_VALUE = 2,
	FLASH_BANK_INVALID = 255,
};

struct dual_bank_info {
	gboolean is_enabled;
	enum dual_bank_mode mode;
	enum flash_bank active_bank;
	guint8 user1_version[2];
	guint8 user2_version[2];
};

struct _FuRealtekMstDevice {
	FuI2cDevice parent_instance;
	gchar *dp_aux_dev_name;
	gchar *dp_card_kernel_name;
	enum flash_bank active_bank;
};

G_DEFINE_TYPE(FuRealtekMstDevice, fu_realtek_mst_device, FU_TYPE_I2C_DEVICE)

#define FU_REALTEK_MST_DEVICE_IOCTL_TIMEOUT 5000 /* ms */

static gboolean
fu_realtek_mst_device_set_quirk_kv(FuDevice *device,
				   const gchar *key,
				   const gchar *value,
				   GError **error)
{
	FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device);

	if (g_strcmp0(key, "RealtekMstDpAuxName") == 0) {
		self->dp_aux_dev_name = g_strdup(value);
	} else if (g_strcmp0(key, "RealtekMstDrmCardKernelName") == 0) {
		self->dp_card_kernel_name = g_strdup(value);
	} else {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "unsupported quirk key: %s",
			    key);
		return FALSE;
	}
	return TRUE;
}

static FuUdevDevice *
locate_i2c_bus(const GPtrArray *i2c_devices)
{
	for (guint i = 0; i < i2c_devices->len; i++) {
		FuUdevDevice *i2c_device = g_ptr_array_index(i2c_devices, i);
		FuUdevDevice *bus_device;
		g_autoptr(GPtrArray) i2c_buses =
		    fu_udev_device_get_children_with_subsystem(i2c_device, "i2c-dev");

		if (i2c_buses->len == 0) {
			g_debug("no i2c-dev found under %s",
				fu_udev_device_get_sysfs_path(i2c_device));
			continue;
		}
		if (i2c_buses->len > 1) {
			g_debug("ignoring %u additional i2c-dev under %s",
				i2c_buses->len - 1,
				fu_udev_device_get_sysfs_path(i2c_device));
		}

		bus_device = g_object_ref(g_ptr_array_index(i2c_buses, 0));
		g_debug("found I2C bus at %s, using this device",
			fu_udev_device_get_sysfs_path(bus_device));
		return bus_device;
	}
	return NULL;
}

static gboolean
fu_realtek_mst_device_use_aux_dev(FuRealtekMstDevice *self, GError **error)
{
	g_autoptr(GUdevClient) udev_client = g_udev_client_new(NULL);
	g_autoptr(GUdevEnumerator) udev_enumerator = g_udev_enumerator_new(udev_client);
	g_autoptr(GList) matches = NULL;
	FuUdevDevice *bus_device = NULL;

	g_udev_enumerator_add_match_subsystem(udev_enumerator, "drm_dp_aux_dev");
	g_udev_enumerator_add_match_sysfs_attr(udev_enumerator, "name", self->dp_aux_dev_name);
	matches = g_udev_enumerator_execute(udev_enumerator);

	/* from a drm_dp_aux_dev with the given name, locate its sibling i2c
	 * device and in turn the i2c-dev under that representing the actual
	 * I2C bus that runs over DPDDC on the port represented by the
	 * drm_dp_aux_dev */
	for (GList *element = matches; element != NULL; element = element->next) {
		g_autoptr(FuUdevDevice) device = NULL;
		g_autoptr(GPtrArray) i2c_devices = NULL;

		device = fu_udev_device_new(fu_device_get_context(FU_DEVICE(self)), element->data);
		if (bus_device != NULL) {
			g_debug("ignoring additional aux device %s",
				fu_udev_device_get_sysfs_path(device));
			continue;
		}

		i2c_devices = fu_udev_device_get_siblings_with_subsystem(device, "i2c");
		bus_device = locate_i2c_bus(i2c_devices);
	}

	if (bus_device == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "did not find an i2c-dev associated with DP aux \"%s\"",
			    self->dp_aux_dev_name);
		return FALSE;
	}
	fu_udev_device_set_dev(FU_UDEV_DEVICE(self), fu_udev_device_get_dev(bus_device));
	return TRUE;
}

static gboolean
fu_realtek_mst_device_use_drm_card(FuRealtekMstDevice *self, GError **error)
{
	g_autoptr(GUdevClient) udev_client = g_udev_client_new(NULL);
	g_autoptr(GUdevEnumerator) enumerator = g_udev_enumerator_new(udev_client);
	g_autoptr(GList) drm_devices = NULL;
	g_autoptr(FuUdevDevice) bus_device = NULL;

	/* from a drm device with the given name, find an i2c device under it
	 * and in turn an i2c-dev device representing the DPDDC bus */
	g_debug("search for DRM device with name %s", self->dp_card_kernel_name);
	g_udev_enumerator_add_match_subsystem(enumerator, "drm");
	g_udev_enumerator_add_match_name(enumerator, self->dp_card_kernel_name);
	drm_devices = g_udev_enumerator_execute(enumerator);
	for (GList *element = drm_devices; element != NULL; element = element->next) {
		g_autoptr(FuUdevDevice) drm_device = NULL;
		g_autoptr(GPtrArray) i2c_devices = NULL;

		drm_device =
		    fu_udev_device_new(fu_device_get_context(FU_DEVICE(self)), element->data);
		if (bus_device != NULL) {
			g_debug("ignoring additional drm device %s",
				fu_udev_device_get_sysfs_path(drm_device));
			continue;
		}

		i2c_devices = fu_udev_device_get_children_with_subsystem(drm_device, "i2c");
		bus_device = locate_i2c_bus(i2c_devices);
	}

	if (bus_device == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_FOUND,
			    "did not find an i2c-dev associated with drm device %s",
			    self->dp_card_kernel_name);
		return FALSE;
	}
	fu_udev_device_set_dev(FU_UDEV_DEVICE(self), fu_udev_device_get_dev(bus_device));
	return TRUE;
}

static gboolean
mst_ensure_device_address(FuRealtekMstDevice *self, guint8 address, GError **error)
{
	return fu_udev_device_ioctl(FU_UDEV_DEVICE(self),
				    I2C_SLAVE,
				    (guint8 *)(guintptr)address,
				    NULL,
				    FU_REALTEK_MST_DEVICE_IOCTL_TIMEOUT,
				    error);
}

/** Write a value to a device register */
static gboolean
mst_write_register(FuRealtekMstDevice *self, guint8 address, guint8 value, GError **error)
{
	const guint8 command[] = {address, value};
	return fu_i2c_device_write(FU_I2C_DEVICE(self), command, sizeof(command), error);
}

static gboolean
mst_write_register_multi(FuRealtekMstDevice *self,
			 guint8 address,
			 const guint8 *data,
			 gsize count,
			 GError **error)
{
	g_autofree guint8 *command = g_malloc0(count + 1);
	memcpy(command + 1, data, count);
	command[0] = address;
	return fu_i2c_device_write(FU_I2C_DEVICE(self), command, count + 1, error);
}

/** Read a register from the device */
static gboolean
mst_read_register(FuRealtekMstDevice *self, guint8 address, guint8 *value, GError **error)
{
	if (!fu_i2c_device_write(FU_I2C_DEVICE(self), &address, 0x1, error))
		return FALSE;
	return fu_i2c_device_read(FU_I2C_DEVICE(self), value, 0x1, error);
}

static gboolean
mst_set_indirect_address(FuRealtekMstDevice *self, guint16 address, GError **error)
{
	if (!mst_write_register(self, REG_INDIRECT_LO, 0x9F, error))
		return FALSE;
	if (!mst_write_register(self, REG_INDIRECT_HI, address >> 8, error))
		return FALSE;
	return mst_write_register(self, REG_INDIRECT_LO, address, error);
}

static gboolean
mst_read_register_indirect(FuRealtekMstDevice *self, guint16 address, guint8 *value, GError **error)
{
	if (!mst_set_indirect_address(self, address, error))
		return FALSE;
	return mst_read_register(self, REG_INDIRECT_HI, value, error);
}

static gboolean
mst_write_register_indirect(FuRealtekMstDevice *self, guint16 address, guint8 value, GError **error)
{
	if (!mst_set_indirect_address(self, address, error))
		return FALSE;
	return mst_write_register(self, REG_INDIRECT_HI, value, error);
}

/**
 * Wait until a device register reads an expected value.
 *
 * Waiting up to @timeout_seconds, poll the given @address for the read value
 * bitwise-ANDed with @mask to be equal to @expected.
 *
 * Returns an error if the timeout expires or in case of an I/O error.
 */
static gboolean
mst_poll_register(FuRealtekMstDevice *self,
		  guint8 address,
		  guint8 mask,
		  guint8 expected,
		  guint timeout_seconds,
		  GError **error)
{
	guint8 value;
	g_autoptr(GTimer) timer = g_timer_new();

	if (!mst_read_register(self, address, &value, error))
		return FALSE;
	while ((value & mask) != expected && g_timer_elapsed(timer, NULL) <= timeout_seconds) {
		fu_device_sleep(FU_DEVICE(self), 1); /* ms */
		if (!mst_read_register(self, address, &value, error))
			return FALSE;
	}
	if ((value & mask) == expected)
		return TRUE;

	g_set_error(error,
		    G_IO_ERROR,
		    G_IO_ERROR_TIMED_OUT,
		    "register %x still reads %x after %us, wanted %x (mask %x)",
		    address,
		    value,
		    timeout_seconds,
		    expected,
		    mask);
	return FALSE;
}

static gboolean
mst_set_gpio88(FuRealtekMstDevice *self, gboolean level, GError **error)
{
	guint8 value;

	/* ensure pin is configured as push-pull GPIO */
	if (!mst_read_register_indirect(self, REG_GPIO88_CONFIG, &value, error))
		return FALSE;
	if (!mst_write_register_indirect(self, REG_GPIO88_CONFIG, (value & 0xF0) | 1, error))
		return FALSE;

	/* set output level */
	g_debug("set pin 88 = %d", level);
	if (!mst_read_register_indirect(self, REG_GPIO88_VALUE, &value, error))
		return FALSE;
	return mst_write_register_indirect(self,
					   REG_GPIO88_VALUE,
					   (value & 0xFE) | (level != FALSE),
					   error);
}

static gboolean
fu_realtek_mst_device_probe(FuDevice *device, GError **error)
{
	FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device);
	FuContext *context = fu_device_get_context(device);

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

	/* add custom instance ID and load matching 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;

	/* having loaded quirks, check this device is supported */
	if (self->dp_aux_dev_name != NULL) {
		if (!fu_realtek_mst_device_use_aux_dev(self, error))
			return FALSE;
	} else if (self->dp_card_kernel_name != NULL) {
		if (!fu_realtek_mst_device_use_drm_card(self, error))
			return FALSE;
	} else {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "one of RealtekMstDpAuxName or RealtekMstDrmCardKernelName"
				    " must be specified");
		return FALSE;
	}

	/* locate its sibling i2c device and use that instead */

	/* success */
	return TRUE;
}

static gboolean
fu_realtek_mst_device_get_dual_bank_info(FuRealtekMstDevice *self,
					 struct dual_bank_info *info,
					 GError **error)
{
	const guint8 request[] = {0x01};
	guint8 response[11] = {0x0};

	if (!mst_ensure_device_address(self, I2C_ADDR_DEBUG, error))
		return FALSE;

	/* switch to DDCCI mode */
	if (!mst_write_register(self, 0xca, 0x09, error))
		return FALSE;

	/* wait for mode switch to complete */
	fu_device_sleep(FU_DEVICE(self), 200); /* ms */

	/* request dual bank state and read back */
	if (!fu_i2c_device_write(FU_I2C_DEVICE(self), request, sizeof(request), error))
		return FALSE;
	if (!fu_i2c_device_read(FU_I2C_DEVICE(self), response, sizeof(response), error))
		return FALSE;

	if (response[0] != 0xca || response[1] != 9) {
		/* unexpected response code or length usually means the current
		 * firmware doesn't support dual-bank mode at all */
		g_debug("unexpected response code %#x, length %d", response[0], response[1]);
		info->is_enabled = FALSE;
		return TRUE;
	}

	/* enable flag, assume anything other than 1 is unsupported */
	if (response[2] != 1) {
		info->is_enabled = FALSE;
		return TRUE;
	}
	info->is_enabled = TRUE;

	info->mode = response[3];
	if (info->mode > DUAL_BANK_MAX_VALUE) {
		g_debug("unexpected dual bank mode value %#x", info->mode);
		info->is_enabled = FALSE;
		return TRUE;
	}

	info->active_bank = response[4];
	if (info->active_bank > FLASH_BANK_MAX_VALUE) {
		g_debug("unexpected active flash bank value %#x", info->active_bank);
		info->is_enabled = FALSE;
		return TRUE;
	}

	info->user1_version[0] = response[5];
	info->user1_version[1] = response[6];
	info->user2_version[0] = response[7];
	info->user2_version[1] = response[8];
	/* last two bytes of response are reserved */
	return TRUE;
}

static gboolean
fu_realtek_mst_device_probe_version(FuDevice *device, GError **error)
{
	FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device);
	struct dual_bank_info info = {0x0};
	guint8 *active_version;
	g_autofree gchar *version_str = NULL;

	/* ensure probed state is cleared in case of error */
	fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE);
	self->active_bank = FLASH_BANK_INVALID;
	fu_device_set_version(device, NULL);

	if (!fu_realtek_mst_device_get_dual_bank_info(FU_REALTEK_MST_DEVICE(self), &info, error))
		return FALSE;

	if (!info.is_enabled) {
		fu_device_inhibit(device, "dual-bank", "Dual-bank mode is not enabled");
		return TRUE;
	}
	if (info.mode != DUAL_BANK_DIFF) {
		fu_device_inhibit(device, "dual-bank", "Can only update from dual-bank-diff mode");
		return TRUE;
	}
	/* dual-bank mode seems to be fully supported, so we can update
	 * regardless of the active bank- if it's FLASH_BANK_BOOT, updating is
	 * possible even if the current version is unknown */
	fu_device_add_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE);
	fu_device_uninhibit(device, "dual-bank");

	g_info("device is currently running from bank %u", info.active_bank);
	g_return_val_if_fail(info.active_bank <= FLASH_BANK_MAX_VALUE, FALSE);
	self->active_bank = info.active_bank;

	g_info("firmware version reports user1 %d.%d, user2 %d.%d",
	       info.user1_version[0],
	       info.user1_version[1],
	       info.user2_version[0],
	       info.user2_version[1]);
	if (info.active_bank == FLASH_BANK_USER1)
		active_version = info.user1_version;
	else if (info.active_bank == FLASH_BANK_USER2)
		active_version = info.user2_version;
	else
		/* only user bank versions are reported, can't tell otherwise */
		return TRUE;

	version_str = g_strdup_printf("%u.%u", active_version[0], active_version[1]);
	fu_device_set_version(FU_DEVICE(self), version_str);
	return TRUE;
}

static gboolean
flash_iface_read(FuRealtekMstDevice *self,
		 guint32 address,
		 guint8 *buf,
		 const gsize buf_size,
		 FuProgress *progress,
		 GError **error)
{
	gsize bytes_read = 0;
	guint8 byte;
	const guint8 req[] = {0x70};

	g_return_val_if_fail(address < FLASH_SIZE, FALSE);
	g_return_val_if_fail(buf_size <= FLASH_SIZE, FALSE);

	g_debug("read %#" G_GSIZE_MODIFIER "x bytes from %#08x", buf_size, address);

	/* read must start one byte prior to the desired address and ignore the
	 * first byte of data, since the first read value is unpredictable */
	address = (address - 1) & 0xFFFFFF;
	if (!mst_write_register(self, REG_CMD_ADDR_HI, address >> 16, error))
		return FALSE;
	if (!mst_write_register(self, REG_CMD_ADDR_MID, address >> 8, error))
		return FALSE;
	if (!mst_write_register(self, REG_CMD_ADDR_LO, address, error))
		return FALSE;
	if (!mst_write_register(self, REG_READ_OPCODE, CMD_OPCODE_READ, error))
		return FALSE;

	/* ignore first byte of data */
	if (!fu_i2c_device_write(FU_I2C_DEVICE(self), req, sizeof(req), error))
		return FALSE;
	if (!fu_i2c_device_read(FU_I2C_DEVICE(self), &byte, 0x1, error))
		return FALSE;

	while (bytes_read < buf_size) {
		/* read up to 256 bytes in one transaction */
		gsize read_len = buf_size - bytes_read;
		if (read_len > 256)
			read_len = 256;

		if (!fu_i2c_device_read(FU_I2C_DEVICE(self), buf + bytes_read, read_len, error))
			return FALSE;

		bytes_read += read_len;
		fu_progress_set_percentage_full(progress, bytes_read, buf_size);
	}
	return TRUE;
}

static gboolean
flash_iface_erase_sector(FuRealtekMstDevice *self, guint32 address, GError **error)
{
	/* address must be 4k-aligned */
	g_return_val_if_fail((address & 0xFFF) == 0, FALSE);
	g_debug("sector erase %#08x-%#08x", address, address + FLASH_SECTOR_SIZE);

	/* sector address */
	if (!mst_write_register(self, REG_CMD_ADDR_HI, address >> 16, error))
		return FALSE;
	if (!mst_write_register(self, REG_CMD_ADDR_MID, address >> 8, error))
		return FALSE;
	if (!mst_write_register(self, REG_CMD_ADDR_LO, address, error))
		return FALSE;
	/* command type + WREN */
	if (!mst_write_register(self, REG_CMD_ATTR, 0xB8, error))
		return FALSE;
	/* sector erase opcode */
	if (!mst_write_register(self, REG_ERASE_OPCODE, CMD_OPCODE_ERASE_SECTOR, error))
		return FALSE;
	/* begin operation and wait for completion */
	if (!mst_write_register(self, REG_CMD_ATTR, 0xB8 | CMD_ERASE_BUSY, error))
		return FALSE;
	return mst_poll_register(self, REG_CMD_ATTR, CMD_ERASE_BUSY, 0, 10, error);
}
static gboolean
flash_iface_erase_block(FuRealtekMstDevice *self, guint32 address, GError **error)
{
	/* address must be 64k-aligned */
	g_return_val_if_fail((address & 0xFFFF) == 0, FALSE);
	g_debug("block erase %#08x-%#08x", address, address + FLASH_BLOCK_SIZE);

	/* block address */
	if (!mst_write_register(self, REG_CMD_ADDR_HI, address >> 16, error))
		return FALSE;
	if (!mst_write_register(self, REG_CMD_ADDR_MID, 0, error))
		return FALSE;
	if (!mst_write_register(self, REG_CMD_ADDR_LO, 0, error))
		return FALSE;
	/* command type + WREN */
	if (!mst_write_register(self, REG_CMD_ATTR, 0xB8, error))
		return FALSE;
	/* block erase opcode */
	if (!mst_write_register(self, REG_ERASE_OPCODE, CMD_OPCODE_ERASE_BLOCK, error))
		return FALSE;
	/* begin operation and wait for completion */
	if (!mst_write_register(self, REG_CMD_ATTR, 0xB8 | CMD_ERASE_BUSY, error))
		return FALSE;
	return mst_poll_register(self, REG_CMD_ATTR, CMD_ERASE_BUSY, 0, 10, error);
}

static gboolean
flash_iface_write(FuRealtekMstDevice *self,
		  guint32 address,
		  GBytes *data,
		  FuProgress *progress,
		  GError **error)
{
	gsize total_size = g_bytes_get_size(data);
	g_autoptr(FuChunkArray) chunks = fu_chunk_array_new_from_bytes(data, address, 256);

	g_debug("write %#" G_GSIZE_MODIFIER "x bytes at %#08x", total_size, address);
	for (guint i = 0; i < fu_chunk_array_length(chunks); i++) {
		g_autoptr(FuChunk) chunk = fu_chunk_array_index(chunks, i);
		guint32 chunk_address = fu_chunk_get_address(chunk);
		guint32 chunk_size = fu_chunk_get_data_sz(chunk);

		/* write opcode */
		if (!mst_write_register(self, REG_WRITE_OPCODE, CMD_OPCODE_WRITE, error))
			return FALSE;
		/* write length */
		if (!mst_write_register(self, REG_WRITE_LEN, chunk_size - 1, error))
			return FALSE;
		/* target address */
		if (!mst_write_register(self, REG_CMD_ADDR_HI, chunk_address >> 16, error))
			return FALSE;
		if (!mst_write_register(self, REG_CMD_ADDR_MID, chunk_address >> 8, error))
			return FALSE;
		if (!mst_write_register(self, REG_CMD_ADDR_LO, chunk_address, error))
			return FALSE;
		/* ensure write buffer is empty */
		if (!mst_poll_register(self,
				       REG_MCU_MODE,
				       MCU_MODE_WRITE_BUF,
				       MCU_MODE_WRITE_BUF,
				       10,
				       error)) {
			g_prefix_error(error, "failed waiting for write buffer to clear: ");
			return FALSE;
		}
		/* write data into FIFO */
		if (!mst_write_register_multi(self,
					      REG_WRITE_FIFO,
					      fu_chunk_get_data(chunk),
					      chunk_size,
					      error))
			return FALSE;
		/* begin operation and wait for completion */
		if (!mst_write_register(self,
					REG_MCU_MODE,
					MCU_MODE_ISP | MCU_MODE_WRITE_BUSY,
					error))
			return FALSE;
		if (!mst_poll_register(self, REG_MCU_MODE, MCU_MODE_WRITE_BUSY, 0, 10, error)) {
			g_prefix_error(error,
				       "timed out waiting for write at %#x to complete: ",
				       address);
			return FALSE;
		}
		fu_progress_set_percentage_full(progress, i + 1, fu_chunk_array_length(chunks));
	}

	return TRUE;
}

static gboolean
fu_realtek_mst_device_detach(FuDevice *device, FuProgress *progress, GError **error)
{
	FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device);

	if (!mst_ensure_device_address(self, I2C_ADDR_ISP, error))
		return FALSE;

	/* Switch to programming mode (stops regular operation) */
	if (!mst_write_register(self, REG_MCU_MODE, MCU_MODE_ISP, error))
		return FALSE;
	g_debug("wait for ISP mode ready");
	if (!mst_poll_register(self, REG_MCU_MODE, MCU_MODE_ISP, MCU_MODE_ISP, 60, error))
		return FALSE;

	/* magic value makes the MCU clock run faster than normal; this both
	 * helps programming performance and fixes flakiness where register
	 * writes sometimes get nacked for no apparent reason */
	if (!mst_write_register_indirect(self, 0x06A0, 0x74, error))
		return FALSE;

	fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);

	/* Disable hardware write protect, assuming Flash ~WP is connected to
	 * device pin 88, a GPIO. */
	return mst_set_gpio88(self, 1, error);
}

static gboolean
fu_realtek_mst_device_write_firmware(FuDevice *device,
				     FuFirmware *firmware,
				     FuProgress *progress,
				     FwupdInstallFlags flags,
				     GError **error)
{
	FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device);
	/* write an inactive bank: USER2 if USER1 is active, otherwise USER1
	 * (including if the boot bank is active) */
	guint32 base_addr = self->active_bank == FLASH_BANK_USER1 ? FLASH_USER2_ADDR
								  : FLASH_USER1_ADDR;
	guint32 flag_addr = self->active_bank == FLASH_BANK_USER1 ? FLASH_FLAG2_ADDR
								  : FLASH_FLAG1_ADDR;
	GBytes *firmware_bytes = fu_firmware_get_bytes(firmware, error);
	const guint8 flag_data[] = {0xaa, 0xaa, 0xaa, 0xff, 0xff};
	g_autofree guint8 *readback_buf = g_malloc0(FLASH_USER_SIZE);

	g_return_val_if_fail(g_bytes_get_size(firmware_bytes) == FLASH_USER_SIZE, FALSE);

	/* 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, 20, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 70, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 9, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "flag");

	if (!mst_ensure_device_address(self, I2C_ADDR_ISP, error))
		return FALSE;

	/* erase old image */
	g_debug("erase old image from %#x", base_addr);
	for (guint32 offset = 0; offset < FLASH_USER_SIZE; offset += FLASH_BLOCK_SIZE) {
		if (!flash_iface_erase_block(self, base_addr + offset, error))
			return FALSE;
		fu_progress_set_percentage_full(fu_progress_get_child(progress),
						offset + FLASH_BLOCK_SIZE,
						FLASH_USER_SIZE);
	}
	fu_progress_step_done(progress);

	/* write new image */
	g_debug("write new image to %#x", base_addr);
	if (!flash_iface_write(self,
			       base_addr,
			       firmware_bytes,
			       fu_progress_get_child(progress),
			       error))
		return FALSE;
	fu_progress_step_done(progress);

	/* verify */
	if (!flash_iface_read(self,
			      base_addr,
			      readback_buf,
			      FLASH_USER_SIZE,
			      fu_progress_get_child(progress),
			      error))
		return FALSE;
	if (memcmp(g_bytes_get_data(firmware_bytes, NULL), readback_buf, FLASH_USER_SIZE) != 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_WRITE,
			    "flash contents after write do not match firmware image");
		return FALSE;
	}
	fu_progress_step_done(progress);

	/* Erase old flag and write new one. The MST appears to modify the
	 * flag value once booted, so we always write the same value here and
	 * it picks up what we've updated. */
	if (!flash_iface_erase_sector(self, flag_addr & ~(FLASH_SECTOR_SIZE - 1), error))
		return FALSE;
	if (!flash_iface_write(self,
			       flag_addr,
			       g_bytes_new_static(flag_data, sizeof(flag_data)),
			       fu_progress_get_child(progress),
			       error))
		return FALSE;
	fu_progress_step_done(progress);

	/* success */
	return TRUE;
}

static FuFirmware *
fu_realtek_mst_device_read_firmware(FuDevice *device, FuProgress *progress, GError **error)
{
	FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device);
	guint32 bank_address;
	g_autofree guint8 *image_bytes = NULL;

	if (self->active_bank == FLASH_BANK_USER1)
		bank_address = FLASH_USER1_ADDR;
	else if (self->active_bank == FLASH_BANK_USER2)
		bank_address = FLASH_USER2_ADDR;
	else {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "cannot read firmware from bank %u",
			    self->active_bank);
		return NULL;
	}

	image_bytes = g_malloc0(FLASH_USER_SIZE);
	if (!mst_ensure_device_address(self, I2C_ADDR_ISP, error))
		return NULL;
	if (!flash_iface_read(self, bank_address, image_bytes, FLASH_USER_SIZE, progress, error))
		return NULL;
	return fu_firmware_new_from_bytes(
	    g_bytes_new_take(g_steal_pointer(&image_bytes), FLASH_USER_SIZE));
}

static GBytes *
fu_realtek_mst_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error)
{
	FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device);
	g_autofree guint8 *flash_contents = g_malloc0(FLASH_SIZE);

	if (!mst_ensure_device_address(self, I2C_ADDR_ISP, error))
		return NULL;
	fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ);
	if (!flash_iface_read(self, 0, flash_contents, FLASH_SIZE, progress, error))
		return NULL;
	fu_progress_set_status(progress, FWUPD_STATUS_IDLE);

	return g_bytes_new_take(g_steal_pointer(&flash_contents), FLASH_SIZE);
}

static gboolean
fu_realtek_mst_device_attach(FuDevice *device, FuProgress *progress, GError **error)
{
	FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device);
	guint8 value;

	if (!mst_ensure_device_address(self, I2C_ADDR_ISP, error))
		return FALSE;

	/* re-enable hardware write protect via GPIO */
	if (!mst_set_gpio88(self, 0, error))
		return FALSE;

	if (!mst_read_register(self, REG_MCU_MODE, &value, error))
		return FALSE;
	if ((value & MCU_MODE_ISP) != 0) {
		g_autoptr(GError) error_local = NULL;

		g_debug("resetting device to exit ISP mode");

		/* Set register EE bit 2 to request reset. This write can fail
		 * spuriously, so we ignore the write result and verify the device is
		 * no longer in programming mode after giving it time to reset. */
		if (!mst_read_register(self, 0xEE, &value, error))
			return FALSE;
		if (!mst_write_register(self, 0xEE, value | 2, &error_local)) {
			g_debug("write spuriously failed, ignoring: %s", error_local->message);
		}

		/* allow device some time to reset */
		fu_device_sleep(device, 1000); /* ms */

		/* verify device has exited programming mode and actually reset */
		if (!mst_read_register(self, REG_MCU_MODE, &value, error))
			return FALSE;
		if ((value & MCU_MODE_ISP) == MCU_MODE_ISP) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NEEDS_USER_ACTION,
					    "device failed to reset when requested");
			fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN);
			return FALSE;
		}
	} else {
		g_debug("device is already in normal mode");
	}

	fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
	return TRUE;
}

static void
fu_realtek_mst_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_realtek_mst_device_init(FuRealtekMstDevice *self)
{
	self->active_bank = FLASH_BANK_INVALID;

	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL);
	fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);
	fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_NO_GENERIC_GUIDS);
	fu_device_add_protocol(FU_DEVICE(self), "com.realtek.rtd2142");
	fu_device_set_vendor(FU_DEVICE(self), "Realtek");
	fu_device_add_vendor_id(FU_DEVICE(self), "PCI:0x10EC");
	fu_device_set_summary(FU_DEVICE(self), "DisplayPort MST hub");
	fu_device_add_icon(FU_DEVICE(self), "video-display");
	fu_device_set_firmware_size(FU_DEVICE(self), FLASH_USER_SIZE);
}

static void
fu_realtek_mst_device_finalize(GObject *object)
{
	FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(object);
	g_free(self->dp_aux_dev_name);
	g_free(self->dp_card_kernel_name);
	G_OBJECT_CLASS(fu_realtek_mst_device_parent_class)->finalize(object);
}

static void
fu_realtek_mst_device_class_init(FuRealtekMstDeviceClass *klass)
{
	FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
	GObjectClass *klass_object = G_OBJECT_CLASS(klass);

	klass_object->finalize = fu_realtek_mst_device_finalize;
	klass_device->probe = fu_realtek_mst_device_probe;
	klass_device->set_quirk_kv = fu_realtek_mst_device_set_quirk_kv;
	klass_device->setup = fu_realtek_mst_device_probe_version;
	klass_device->detach = fu_realtek_mst_device_detach;
	klass_device->attach = fu_realtek_mst_device_attach;
	klass_device->write_firmware = fu_realtek_mst_device_write_firmware;
	klass_device->reload = fu_realtek_mst_device_probe_version;
	/* read active image */
	klass_device->read_firmware = fu_realtek_mst_device_read_firmware;
	/* dump whole flash */
	klass_device->dump_firmware = fu_realtek_mst_device_dump_firmware;
	klass_device->set_progress = fu_realtek_mst_device_set_progress;
}
