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

#include "config.h"

#include <string.h>

#include "fu-chunk.h"

#include "fu-ccgx-common.h"
#include "fu-ccgx-hpi-common.h"
#include "fu-ccgx-hpi-device.h"
#include "fu-ccgx-firmware.h"
#include "fu-ccgx-firmware.h"

struct _FuCcgxHpiDevice
{
	FuUsbDevice		 parent_instance;
	guint8			 inf_num;	/* USB interface number */
	guint8			 scb_index;
	guint16			 silicon_id;
	guint16			 fw_app_type;
	guint8			 hpi_addrsz;	/* hpiv1: 1 byte, hpiv2: 2 byte	*/
	guint8			 num_ports;	/* max number of ports	*/
	FWMode			 fw_mode;
	FWImageType		 fw_image_type;
	guint8			 target_address;
	guint8			 ep_bulk_in;
	guint8			 ep_bulk_out;
	guint8			 ep_intr_in;
	guint32			 flash_row_size;
	guint32			 flash_size;
};

G_DEFINE_TYPE (FuCcgxHpiDevice, fu_ccgx_hpi_device, FU_TYPE_USB_DEVICE)

#define HPI_CMD_REG_READ_WRITE_DELAY_US			10000
#define HPI_CMD_ENTER_FLASH_MODE_DELAY_US		20000
#define HPI_CMD_SETUP_EVENT_WAIT_TIME_MS		200
#define HPI_CMD_SETUP_EVENT_CLEAR_TIME_MS		150
#define HPI_CMD_COMMAND_RESPONSE_TIME_MS		500
#define HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS		30
#define HPI_CMD_RESET_COMPLETE_DELAY_US			150000
#define HPI_CMD_RETRY_DELAY				30 /* ms */
#define HPI_CMD_RESET_RETRY_CNT				3
#define HPI_CMD_ENTER_LEAVE_FLASH_MODE_RETRY_CNT	3
#define HPI_CMD_FLASH_WRITE_RETRY_CNT 			3
#define HPI_CMD_FLASH_READ_RETRY_CNT 			3
#define HPI_CMD_VALIDATE_FW_RETRY_CNT 			3

static void
fu_ccgx_hpi_device_to_string (FuDevice *device, guint idt, GString *str)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	fu_common_string_append_kx (str, idt, "InfNum", self->inf_num);
	fu_common_string_append_kx (str, idt, "ScbIndex", self->scb_index);
	fu_common_string_append_kx (str, idt, "SiliconId", self->silicon_id);
	fu_common_string_append_kx (str, idt, "FwAppType", self->fw_app_type);
	fu_common_string_append_kx (str, idt, "HpiAddrsz", self->hpi_addrsz);
	fu_common_string_append_kx (str, idt, "NumPorts", self->num_ports);
	fu_common_string_append_kv (str, idt, "FWMode",
				    fu_ccgx_fw_mode_to_string (self->fw_mode));
	fu_common_string_append_kv (str, idt, "FwImageType",
				    fu_ccgx_fw_image_type_to_string (self->fw_image_type));
	fu_common_string_append_kx (str, idt, "EpBulkIn", self->ep_bulk_in);
	fu_common_string_append_kx (str, idt, "EpBulkOut", self->ep_bulk_out);
	fu_common_string_append_kx (str, idt, "EpIntrIn", self->ep_intr_in);
	if (self->flash_row_size > 0)
		fu_common_string_append_kx (str, idt, "CcgxFlashRowSize", self->flash_row_size);
	if (self->flash_size > 0)
		fu_common_string_append_kx (str, idt, "CcgxFlashSize", self->flash_size);
}

typedef struct {
	guint8			 mode;
	guint16			 addr;
	guint8			*buf;
	gsize			 bufsz;
} FuCcgxHpiDeviceRetryHelper;

typedef struct {
	guint16			 addr;
	const guint8		*buf;
	gsize			 bufsz;
} FuCcgxHpiFlashWriteRetryHelper;

typedef struct {
	guint16			 addr;
	guint8			*buf;
	gsize			 bufsz;
} FuCcgxHpiFlashReadRetryHelper;

static gboolean
fu_ccgx_hpi_device_i2c_reset_cb (FuDevice *device, gpointer user_data, GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	FuCcgxHpiDeviceRetryHelper *helper = (FuCcgxHpiDeviceRetryHelper *) user_data;
	g_autoptr(GError) error_local = NULL;
	if (!g_usb_device_control_transfer (fu_usb_device_get_dev (FU_USB_DEVICE (self)),
					    G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
					    G_USB_DEVICE_REQUEST_TYPE_VENDOR,
					    G_USB_DEVICE_RECIPIENT_DEVICE,
					    CY_I2C_RESET_CMD,
					    (self->scb_index << CY_SCB_INDEX_POS) | helper->mode,
					    0x0, NULL, 0x0, NULL,
					    FU_CCGX_HPI_WAIT_TIMEOUT,
					    NULL, &error_local)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to reset i2c: %s",
			     error_local->message);
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_check_i2c_status (FuCcgxHpiDevice *self,
				     guint8 mode,
				     GError **error)
{
	guint8 buf[CY_I2C_GET_STATUS_LEN] = { 0x0 };
	g_autoptr(GError) error_local =	NULL;
	if (!g_usb_device_control_transfer (fu_usb_device_get_dev (FU_USB_DEVICE (self)),
					    G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
					    G_USB_DEVICE_REQUEST_TYPE_VENDOR,
					    G_USB_DEVICE_RECIPIENT_DEVICE,
					    CY_I2C_GET_STATUS_CMD,
					    (((guint16) self->scb_index) << CY_SCB_INDEX_POS) | mode,
					    0x0,
					    buf, sizeof(buf),
					    NULL,
					    FU_CCGX_HPI_WAIT_TIMEOUT,
					    NULL,
					    &error_local)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to get i2c status: %s",
			     error_local->message);
		return FALSE;
	}
	if (buf[0] & CY_I2C_ERROR_BIT) {
		if (buf[0] & 0x80) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_WRITE,
				     "i2c status write error: 0x%x",
				     buf[0]);
			return FALSE;
		}
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_READ,
			     "i2c status read error: 0x%x",
			     buf[0]);
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_get_i2c_config (FuCcgxHpiDevice *self,
				   CyI2CConfig *i2c_config,
				   GError **error)
{
	g_autoptr(GError) error_local = NULL;
	if (!g_usb_device_control_transfer (fu_usb_device_get_dev (FU_USB_DEVICE (self)),
					    G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
					    G_USB_DEVICE_REQUEST_TYPE_VENDOR,
					    G_USB_DEVICE_RECIPIENT_DEVICE,
					    CY_I2C_GET_CONFIG_CMD,
					    ((guint16) self->scb_index) << CY_SCB_INDEX_POS,
					    0x0,
					    (guint8 *) i2c_config,
					    sizeof(*i2c_config),
					    NULL,
					    FU_CCGX_HPI_WAIT_TIMEOUT,
					    NULL,
					    &error_local)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "i2c get config error: control xfer: %s",
			     error_local->message);
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_set_i2c_config (FuCcgxHpiDevice *self,
				   CyI2CConfig *i2c_config,
				   GError **error)
{
	g_autoptr(GError) error_local = NULL;
	if (!g_usb_device_control_transfer (fu_usb_device_get_dev (FU_USB_DEVICE (self)),
					    G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
					    G_USB_DEVICE_REQUEST_TYPE_VENDOR,
					    G_USB_DEVICE_RECIPIENT_DEVICE,
					    CY_I2C_SET_CONFIG_CMD,
					    ((guint16) self->scb_index) << CY_SCB_INDEX_POS,
					    0x0,
					    (guint8 *) i2c_config,
					    sizeof(*i2c_config),
					    NULL,
					    FU_CCGX_HPI_WAIT_TIMEOUT,
					    NULL,
					    &error_local)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "i2c set config error: control xfer: %s",
			     error_local->message);
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_wait_for_notify (FuCcgxHpiDevice *self,
				    guint16 *bytes_pending,
				    GError **error)
{
	guint8 buf[CY_I2C_EVENT_NOTIFICATION_LEN] = { 0x0 };
	g_autoptr(GError) error_local = NULL;

	if (!g_usb_device_interrupt_transfer (fu_usb_device_get_dev (FU_USB_DEVICE (self)),
					      self->ep_intr_in,
					      buf, sizeof(buf), NULL,
					      FU_CCGX_HPI_WAIT_TIMEOUT,
					      NULL, &error_local)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to get i2c event: %s",
			     error_local->message);
		return FALSE;
	}

	/* @bytes_pending available on failure */
	if (buf[0] & CY_I2C_ERROR_BIT) {
		if (bytes_pending != NULL) {
			if (!fu_common_read_uint16_safe (buf, sizeof(buf), 0x01,
							 bytes_pending, G_LITTLE_ENDIAN,
							 error))
				return FALSE;
		}
		if (buf[0] & 0x80) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_WRITE,
				     "i2c status write error: 0x%x",
				     buf[0]);
			return FALSE;
		}
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_READ,
			     "i2c status read error: 0x%x",
			     buf[0]);
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_i2c_read (FuCcgxHpiDevice *self,
			     guint8 *buf, gsize bufsz,
			     CyI2CDataConfigBits cfg_bits,
			     GError **error)
{
	guint8 target_address = 0;

	if (!fu_ccgx_hpi_device_check_i2c_status (self, CY_I2C_MODE_READ, error)) {
		g_prefix_error (error, "i2c read error: ");
		return FALSE;
	}
	target_address = (self->target_address & 0x7F) | (self->scb_index << 7);
	if (!g_usb_device_control_transfer (fu_usb_device_get_dev (FU_USB_DEVICE (self)),
					    G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
					    G_USB_DEVICE_REQUEST_TYPE_VENDOR,
					    G_USB_DEVICE_RECIPIENT_DEVICE,
					    CY_I2C_READ_CMD,
					    (((guint16) target_address) << 8) | cfg_bits,
					    bufsz, NULL, 0x0, NULL,
					    FU_CCGX_HPI_WAIT_TIMEOUT, NULL,
					    error)) {
		g_prefix_error (error, "i2c read error: control xfer: ");
		return FALSE;
	}
	if (!g_usb_device_bulk_transfer (fu_usb_device_get_dev (FU_USB_DEVICE (self)),
					 self->ep_bulk_in,
					 buf, bufsz, NULL,
					 FU_CCGX_HPI_WAIT_TIMEOUT,
					 NULL, error)) {
		g_prefix_error (error, "i2c read error: bulk xfer: ");
		return FALSE;
	}

	/* 10 msec delay */
	g_usleep (I2C_READ_WRITE_DELAY_US);
	if (!fu_ccgx_hpi_device_wait_for_notify (self, NULL, error)) {
		g_prefix_error (error, "i2c read error: ");
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_i2c_write (FuCcgxHpiDevice *self,
			      guint8 *buf, gsize bufsz,
			      CyI2CDataConfigBits cfg_bits,
			      GError **error)
{
	guint8 target_address;

	if (!fu_ccgx_hpi_device_check_i2c_status (self, CY_I2C_MODE_WRITE, error)) {
		g_prefix_error (error, "i2c get status error: ");
		return FALSE;
	}
	target_address = (self->target_address & 0x7F) | (self->scb_index << 7);
	if (!g_usb_device_control_transfer (fu_usb_device_get_dev (FU_USB_DEVICE (self)),
					    G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
					    G_USB_DEVICE_REQUEST_TYPE_VENDOR,
					    G_USB_DEVICE_RECIPIENT_DEVICE,
					    CY_I2C_WRITE_CMD,
					    ((guint16) target_address << 8) | (cfg_bits & CY_I2C_DATA_CONFIG_STOP),
					    bufsz, /* idx */
					    NULL, 0x0, NULL,
					    FU_CCGX_HPI_WAIT_TIMEOUT,
					    NULL, error)) {
		g_prefix_error (error, "i2c write error: control xfer: ");
		return FALSE;
	}
	if (!g_usb_device_bulk_transfer (fu_usb_device_get_dev (FU_USB_DEVICE (self)),
					self->ep_bulk_out, buf, bufsz, NULL,
					FU_CCGX_HPI_WAIT_TIMEOUT,
					NULL, error)) {
		g_prefix_error (error, "i2c write error: bulk xfer: ");
		return FALSE;
	}

	/* 10 msec delay */
	g_usleep (I2C_READ_WRITE_DELAY_US);
	if (!fu_ccgx_hpi_device_wait_for_notify (self, NULL, error)) {
		g_prefix_error (error, "i2c wait for notification error: ");
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_i2c_write_no_resp (FuCcgxHpiDevice *self,
				      guint8 *buf, gsize bufsz,
				      CyI2CDataConfigBits cfg_bits,
				      GError **error)
{
	guint8 target_address = 0;
	g_autoptr(GError) error_local = NULL;

	if (!fu_ccgx_hpi_device_check_i2c_status (self, CY_I2C_MODE_WRITE, error)) {
		g_prefix_error (error, "i2c write error: ");
		return FALSE;
	}
	target_address = (self->target_address & 0x7F) | (self->scb_index << 7);
	if (!g_usb_device_control_transfer (fu_usb_device_get_dev (FU_USB_DEVICE (self)),
					    G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
					    G_USB_DEVICE_REQUEST_TYPE_VENDOR,
					    G_USB_DEVICE_RECIPIENT_DEVICE,
					    CY_I2C_WRITE_CMD,
					    ((guint16) target_address << 8) | (cfg_bits & CY_I2C_DATA_CONFIG_STOP),
					    bufsz, NULL, 0x0, NULL,
					    FU_CCGX_HPI_WAIT_TIMEOUT,
					    NULL, error)) {
		g_prefix_error (error, "i2c write error: control xfer: ");
		return FALSE;
	}

	/* device will reboot after this, so txfer will fail */
	if (!g_usb_device_bulk_transfer (fu_usb_device_get_dev (FU_USB_DEVICE (self)),
					 self->ep_bulk_out, buf, bufsz, NULL,
					 FU_CCGX_HPI_WAIT_TIMEOUT,
					 NULL, &error_local)) {
		g_debug ("ignoring i2c write error: bulk xfer: %s",
			 error_local->message);
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_reg_read_cb (FuDevice *device, gpointer user_data, GError **error)
{
	FuCcgxHpiDeviceRetryHelper *helper = (FuCcgxHpiDeviceRetryHelper *) user_data;
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	g_autofree guint8 *bufhw = g_malloc0 (self->hpi_addrsz);

	for (guint32 i = 0; i < self->hpi_addrsz; i++)
		bufhw[i] = (guint8) (helper->addr >> (8 * i));
	if (!fu_ccgx_hpi_device_i2c_write (self, bufhw, self->hpi_addrsz,
					   CY_I2C_DATA_CONFIG_NAK, error)) {
		g_prefix_error (error, "write error: ");
		return FALSE;
	}
	if (!fu_ccgx_hpi_device_i2c_read (self, helper->buf, helper->bufsz,
					  CY_I2C_DATA_CONFIG_STOP | CY_I2C_DATA_CONFIG_NAK,
					  error)) {
		g_prefix_error (error, "read error: ");
		return FALSE;
	}
	g_usleep (HPI_CMD_REG_READ_WRITE_DELAY_US);
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_reg_read (FuCcgxHpiDevice *self,
			     guint16 addr,
			     guint8 *buf,
			     gsize bufsz,
			     GError **error)
{
	FuCcgxHpiDeviceRetryHelper helper = {
		.addr = addr,
		.mode = CY_I2C_MODE_READ,
		.buf = buf,
		.bufsz = bufsz,
	};
	return fu_device_retry (FU_DEVICE (self),
				fu_ccgx_hpi_device_reg_read_cb,
				HPI_CMD_RESET_RETRY_CNT,
				&helper, error);
}

static gboolean
fu_ccgx_hpi_device_reg_write_cb (FuDevice *device, gpointer user_data, GError **error)
{
	FuCcgxHpiDeviceRetryHelper *helper = (FuCcgxHpiDeviceRetryHelper *) user_data;
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	g_autofree guint8 *bufhw = g_malloc0 (helper->bufsz + self->hpi_addrsz);

	for (guint32 i = 0; i < self->hpi_addrsz; i++)
		bufhw[i] = (guint8) (helper->addr >> (8*i));
	memcpy (&bufhw[self->hpi_addrsz], helper->buf, helper->bufsz);
	if (!fu_ccgx_hpi_device_i2c_write (self, bufhw, helper->bufsz + self->hpi_addrsz,
					   CY_I2C_DATA_CONFIG_STOP | CY_I2C_DATA_CONFIG_NAK,
					   error)) {
		g_prefix_error (error, "reg write error: ");
		return FALSE;
	}
	g_usleep (HPI_CMD_REG_READ_WRITE_DELAY_US);
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_reg_write (FuCcgxHpiDevice *self,
			      guint16 addr,
			      const guint8 *buf,
			      gsize bufsz,
			      GError **error)
{
	FuCcgxHpiDeviceRetryHelper helper = {
		.addr = addr,
		.mode = CY_I2C_MODE_WRITE,
		.buf = (guint8 *) buf,
		.bufsz = bufsz,
	};
	return fu_device_retry (FU_DEVICE (self),
				fu_ccgx_hpi_device_reg_write_cb,
				HPI_CMD_RESET_RETRY_CNT,
				&helper, error);
}

static gboolean
fu_ccgx_hpi_device_reg_write_no_resp (FuCcgxHpiDevice *self,
				      guint16 addr,
				      guint8 *buf,
				      guint16 bufsz,
				      GError **error)
{
	g_autofree guint8 *bufhw = g_malloc0 (bufsz + self->hpi_addrsz);
	for (guint32 i = 0; i < self->hpi_addrsz; i++)
		bufhw[i] = (guint8) (addr >> (8 * i));
	memcpy (&bufhw[self->hpi_addrsz], buf, bufsz);
	if (!fu_ccgx_hpi_device_i2c_write_no_resp (self, bufhw, bufsz + self->hpi_addrsz,
						   CY_I2C_DATA_CONFIG_STOP | CY_I2C_DATA_CONFIG_NAK,
						   error)) {
		g_prefix_error (error, "reg write no-resp error: ");
		return FALSE;
	}
	g_usleep (HPI_CMD_REG_READ_WRITE_DELAY_US);
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_clear_intr (FuCcgxHpiDevice *self,
			       HPIRegSection section,
			       GError **error)
{
	guint8 intr = 0;
	for (guint8 i = 0; i <= self->num_ports; i++) {
		if (i == section || section == HPI_REG_SECTION_ALL)
			intr |= 1 << i;
	}
	if (!fu_ccgx_hpi_device_reg_write (self, HPI_DEV_REG_INTR_ADDR,
					   &intr, sizeof(intr),
					   error)) {
		g_prefix_error (error, "failed to clear interrupt: ");
		return FALSE;
	}
	return TRUE;
}

static guint16
fu_ccgx_hpi_device_reg_addr_gen (guint8 section, guint8 part, guint8 addr)
{
	return (((guint16) section) << 12) | (((guint16) part) << 8) | addr;
}

static gboolean
fu_ccgx_hpi_device_read_event_reg (FuCcgxHpiDevice *self,
				   HPIRegSection section,
				   HPIEvent *event,
				   GError **error)
{

	if (section != HPI_REG_SECTION_DEV) {
		guint16 reg_addr;
		guint8 buf[4] = { 0x0 };

		/* first read the response register */
		reg_addr = fu_ccgx_hpi_device_reg_addr_gen (section,
							    HPI_REG_PART_PDDATA_READ,
							    0);
		if (!fu_ccgx_hpi_device_reg_read (self,
						  reg_addr,
						  buf, sizeof(buf),
						  error)) {
			g_prefix_error (error, "read response reg error: ");
			return FALSE;
		}

		/* byte 1 is reserved and should read as zero */
		buf[1] = 0;
		memcpy ((guint8 *) event, buf, sizeof(buf));
		if (event->event_length != 0) {
			reg_addr = fu_ccgx_hpi_device_reg_addr_gen (section,
								    HPI_REG_PART_PDDATA_READ,
								    sizeof(buf));
			if (!fu_ccgx_hpi_device_reg_read (self,
							  reg_addr,
							  event->event_data,
							  event->event_length,
							  error)) {
				g_prefix_error (error, "read event data error: ");
				return FALSE;
			}
		}
	} else {
		guint8 buf[2] = { 0x0 };
		if (!fu_ccgx_hpi_device_reg_read (self,
						  CY_PD_REG_RESPONSE_ADDR,
						  buf, sizeof(buf),
						  error)) {
			g_prefix_error (error, "read response reg error: ");
			return FALSE;
		}
		event->event_code   = buf[0];
		event->event_length = buf[1];
		if (event->event_length != 0) {
			/* read the data memory */
			if (!fu_ccgx_hpi_device_reg_read (self,
							  CY_PD_REG_BOOTDATA_MEMORY_ADDR,
							  event->event_data,
							  event->event_length,
							  error)) {
				g_prefix_error (error, "read event data error: ");
				return FALSE;
			}
		}
	}

	/* success */
	return fu_ccgx_hpi_device_clear_intr (self, section, error);
}

static gboolean
fu_ccgx_hpi_device_app_read_intr_reg (FuCcgxHpiDevice *self,
				      HPIRegSection section,
				      HPIEvent *event_array,
				      guint8 *event_count,
				      GError **error)
{
	guint16 reg_addr;
	guint8 event_count_tmp = 0;
	guint8 intr_reg = 0;

	reg_addr = fu_ccgx_hpi_device_reg_addr_gen (HPI_REG_SECTION_DEV,
						    HPI_REG_PART_REG,
						    HPI_DEV_REG_INTR_ADDR);
	if (!fu_ccgx_hpi_device_reg_read (self,
					  reg_addr,
					  &intr_reg,
					  sizeof(intr_reg),
					  error)) {
		g_prefix_error (error, "read intr reg error: ");
		return FALSE;
	}

	/* device section will not come here */
	for (guint8 i = 0; i <= self->num_ports; i++) {
		/* check if this section is needed */
		if (section == i || section == HPI_REG_SECTION_ALL) {
			/* check whether this section has any event/response */
			if ((1 << i) & intr_reg) {
				if (!fu_ccgx_hpi_device_read_event_reg (self,
									section,
									&event_array[i],
									error)) {
					g_prefix_error (error, "read event error: ");
					return FALSE;
				}
				event_count_tmp++;
			}
		}
	}
	if (event_count != NULL)
		*event_count = event_count_tmp;
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_wait_for_event (FuCcgxHpiDevice *self,
				   HPIRegSection section,
				   HPIEvent *event_array,
				   guint32 timeout_ms,
				   GError **error)
{
	guint8 event_count = 0;
	g_autoptr(GTimer) start_time = g_timer_new ();
	do {
		if (!fu_ccgx_hpi_device_app_read_intr_reg (self,
							   section,
							   event_array,
							   &event_count,
							   error))
			return FALSE;
		if (event_count > 0)
			return TRUE;
	} while (g_timer_elapsed (start_time, NULL) * 1000.f <= timeout_ms);

	/* timed out */
	g_set_error (error,
		     G_IO_ERROR,
		     G_IO_ERROR_TIMED_OUT,
		     "failed to wait for event in %ums",
		     timeout_ms);
	return FALSE;
}

static gboolean
fu_ccgx_hpi_device_get_event (FuCcgxHpiDevice *self,
			      HPIRegSection reg_section,
			      CyPDResp *event,
			      guint32 io_timeout,
			      GError **error )
{
	HPIEvent event_array[HPI_REG_SECTION_ALL + 1] = { 0x0 };
	if (!fu_ccgx_hpi_device_wait_for_event (self,
						reg_section,
						event_array,
						io_timeout,
						error)) {
		g_prefix_error (error, "failed to get event: ");
		return FALSE;
	}
	*event = event_array[reg_section].event_code;
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_clear_all_events (FuCcgxHpiDevice *self,
				     guint32 io_timeout,
				     GError **error)
{
	HPIEvent event_array[HPI_REG_SECTION_ALL + 1] = { 0x0 };
	if (io_timeout == 0) {
		return fu_ccgx_hpi_device_app_read_intr_reg (self,
							     HPI_REG_SECTION_ALL,
							     event_array,
							     NULL,
							     error);
	}
	for (guint8 i = 0; i < self->num_ports; i++) {
		g_autoptr(GError) error_local = NULL;
		if (!fu_ccgx_hpi_device_wait_for_event (self, i, event_array,
							io_timeout, &error_local)) {
			if (!g_error_matches (error_local,
					      G_IO_ERROR,
					      G_IO_ERROR_TIMED_OUT)) {
				g_propagate_prefixed_error (error,
							    g_steal_pointer (&error_local),
							    "failed to clear events: ");
				return FALSE;
			}
		}
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_validate_fw_cb (FuDevice *device, gpointer user_data, GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	guint8 *fw_index = (guint8 *) user_data;
	CyPDResp hpi_event = 0;

	g_return_val_if_fail (fw_index != NULL, FALSE);
	if (!fu_ccgx_hpi_device_clear_all_events (self,
						  HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS,
						  error))
		return FALSE;

	if (!fu_ccgx_hpi_device_reg_write (self,
					   CY_PD_REG_VALIDATE_FW_ADDR,
					   fw_index, 1,
					   error)) {
		g_prefix_error (error, "validate fw error: ");
		return FALSE;
	}
	if (!fu_ccgx_hpi_device_get_event (self, HPI_REG_SECTION_DEV,
					   &hpi_event,
					   HPI_CMD_COMMAND_RESPONSE_TIME_MS,
					   error)) {
		g_prefix_error (error, "validate fw resp error: ");
		return FALSE;
	}
	if (hpi_event != CY_PD_RESP_SUCCESS) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "validate failed: %s [0x%x]",
			     fu_ccgx_pd_resp_to_string (hpi_event),
			     hpi_event);
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_validate_fw (FuCcgxHpiDevice *self, guint8 fw_index, GError **error)
{
	return fu_device_retry (FU_DEVICE (self),
			        fu_ccgx_hpi_validate_fw_cb,
			        HPI_CMD_VALIDATE_FW_RETRY_CNT,
			        &fw_index, error);
}

static gboolean
fu_ccgx_hpi_enter_flash_mode_cb (FuDevice *device, gpointer user_data, GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	CyPDResp hpi_event = 0;
	guint8 buf[] = { CY_PD_ENTER_FLASHING_MODE_CMD_SIG };

	if (!fu_ccgx_hpi_device_clear_all_events (self,
						  HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS,
						  error))
		return FALSE;
	if (!fu_ccgx_hpi_device_reg_write (self,
					   CY_PD_REG_ENTER_FLASH_MODE_ADDR,
					   buf, sizeof(buf),
					   error)) {
		g_prefix_error (error, "enter flash mode error: ");
		return FALSE;
	}
	if (!fu_ccgx_hpi_device_get_event (self,
					   HPI_REG_SECTION_DEV,
					   &hpi_event,
					   HPI_CMD_COMMAND_RESPONSE_TIME_MS,
					   error)) {
		g_prefix_error (error, "enter flash mode resp error: ");
		return FALSE;
	}
	if (hpi_event != CY_PD_RESP_SUCCESS) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "enter flash failed: %s [0x%x]",
			     fu_ccgx_pd_resp_to_string (hpi_event),
			     hpi_event);
		return FALSE;
	}

	/* wait 10 msec */
	g_usleep (HPI_CMD_ENTER_FLASH_MODE_DELAY_US);
	return TRUE;
}

static gboolean
fu_ccgx_hpi_enter_flash_mode (FuCcgxHpiDevice *self, GError **error)
{
	return fu_device_retry (FU_DEVICE (self),
			        fu_ccgx_hpi_enter_flash_mode_cb,
			        HPI_CMD_ENTER_LEAVE_FLASH_MODE_RETRY_CNT,
			        NULL, error);
}

static gboolean
fu_ccgx_hpi_leave_flash_mode_cb (FuDevice *device, gpointer user_data, GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	CyPDResp hpi_event = 0;
	guint8 buf = { 0x0 };

	if (!fu_ccgx_hpi_device_clear_all_events (self,
						  HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS,
						  error))
		return FALSE;

	if (!fu_ccgx_hpi_device_reg_write (self,
					   CY_PD_REG_ENTER_FLASH_MODE_ADDR,
					   &buf, sizeof(buf),
					   error)) {
		g_prefix_error (error, "leave flash mode error: ");
		return FALSE;
	}
	if (!fu_ccgx_hpi_device_get_event (self,
					   HPI_REG_SECTION_DEV,
					   &hpi_event,
					   HPI_CMD_COMMAND_RESPONSE_TIME_MS,
					   error)) {
		g_prefix_error (error, "leave flash mode resp error: ");
		return FALSE;
	}
	if (hpi_event != CY_PD_RESP_SUCCESS) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "leave flash mode failed: %s [0x%x]",
			     fu_ccgx_pd_resp_to_string (hpi_event),
			     hpi_event);
		return FALSE;
	}

	/* wait 10 msec */
	g_usleep (HPI_CMD_ENTER_FLASH_MODE_DELAY_US);
	return TRUE;
}

static gboolean
fu_ccgx_hpi_leave_flash_mode (FuCcgxHpiDevice *self, GError **error)
{
	return fu_device_retry (FU_DEVICE (self),
			        fu_ccgx_hpi_leave_flash_mode_cb,
			        HPI_CMD_ENTER_LEAVE_FLASH_MODE_RETRY_CNT,
			        NULL, error);
}

static gboolean
fu_ccgx_hpi_write_flash_cb (FuDevice *device, gpointer user_data, GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	FuCcgxHpiFlashWriteRetryHelper *helper = (FuCcgxHpiFlashWriteRetryHelper *) user_data;
	CyPDResp hpi_event = 0;
	guint16 addr_tmp = 0;
	guint8 bufhw[] = {
		CY_PD_FLASH_READ_WRITE_CMD_SIG,
		CY_PD_REG_FLASH_ROW_WRITE_CMD,
		helper->addr & 0xFF,
		helper->addr >> 8,
	};

	if (!fu_ccgx_hpi_device_clear_all_events (self,
						  HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS,
						  error))
		return FALSE;

	/* write data to memory */
	addr_tmp = self->hpi_addrsz > 1 ? HPI_DEV_REG_FLASH_MEM : CY_PD_REG_BOOTDATA_MEMORY_ADDR;
	if (!fu_ccgx_hpi_device_reg_write (self, addr_tmp, helper->buf, helper->bufsz, error)) {
		g_prefix_error (error, "write buf to memory error: ");
		return FALSE;
	}
	if (!fu_ccgx_hpi_device_reg_write (self, CY_PD_REG_FLASH_READ_WRITE_ADDR,
					   bufhw, sizeof(bufhw), error)) {
		g_prefix_error (error, "write flash error: ");
		return FALSE;
	}

	/* wait until flash is written */
	if (!fu_ccgx_hpi_device_get_event (self,
					   HPI_REG_SECTION_DEV,
					   &hpi_event,
					   HPI_CMD_COMMAND_RESPONSE_TIME_MS,
					   error)) {
		g_prefix_error (error, "write flash resp error: ");
		return FALSE;
	}
	if (hpi_event != CY_PD_RESP_SUCCESS) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "write flash failed: %s [0x%x]",
			     fu_ccgx_pd_resp_to_string (hpi_event),
			     hpi_event);
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_write_flash (FuCcgxHpiDevice *self,
			 guint16 addr,
			 const guint8 *buf,
			 guint16 bufsz,
			 GError **error)
{
	FuCcgxHpiFlashWriteRetryHelper helper = {
		.addr = addr,
		.buf = buf,
		.bufsz = bufsz,
	};
	return fu_device_retry (FU_DEVICE (self),
			        fu_ccgx_hpi_write_flash_cb,
			        HPI_CMD_FLASH_WRITE_RETRY_CNT,
			        &helper, error);
}

static gboolean
fu_ccgx_hpi_read_flash_cb (FuDevice *device, gpointer user_data, GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	FuCcgxHpiFlashReadRetryHelper *helper = (FuCcgxHpiFlashReadRetryHelper *) user_data;
	CyPDResp hpi_event = 0;
	guint16 addr_tmp;
	guint8 bufhw[] = {
		CY_PD_FLASH_READ_WRITE_CMD_SIG,
		CY_PD_REG_FLASH_ROW_READ_CMD,
		helper->addr & 0xFF,
		helper->addr >> 8,
	};

	/* set address */
	if (!fu_ccgx_hpi_device_clear_all_events (self,
						  HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS,
						  error))
		return FALSE;
	if (!fu_ccgx_hpi_device_reg_write (self, CY_PD_REG_FLASH_READ_WRITE_ADDR,
					   bufhw, sizeof(bufhw), error)) {
		g_prefix_error (error, "read flash error: ");
		return FALSE;
	}

	/* wait until flash is read */
	if (!fu_ccgx_hpi_device_get_event (self,
					   HPI_REG_SECTION_DEV,
					   &hpi_event,
					   HPI_CMD_COMMAND_RESPONSE_TIME_MS,
					   error)) {
		g_prefix_error (error, "read flash resp error: ");
		return FALSE;
	}
	if (hpi_event != CY_PD_RESP_FLASH_DATA_AVAILABLE) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "read flash failed: %s [0x%x]",
			     fu_ccgx_pd_resp_to_string (hpi_event),
			     hpi_event);
		return FALSE;
	}
	addr_tmp = self->hpi_addrsz > 1 ? HPI_DEV_REG_FLASH_MEM : CY_PD_REG_BOOTDATA_MEMORY_ADDR;
	if (!fu_ccgx_hpi_device_reg_read (self, addr_tmp, helper->buf, helper->bufsz, error)) {
		g_prefix_error (error, "read data from memory error: ");
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_read_flash (FuCcgxHpiDevice *self,
			guint16 addr,
			guint8 *buf,
			guint16 bufsz,
			GError **error)
{
	FuCcgxHpiFlashReadRetryHelper helper = {
		.addr = addr,
		.buf = buf,
		.bufsz = bufsz,
	};
	return fu_device_retry (FU_DEVICE (self),
			        fu_ccgx_hpi_read_flash_cb,
			        HPI_CMD_FLASH_READ_RETRY_CNT,
			        &helper, error);
}

static gboolean
fu_ccgx_hpi_device_detach (FuDevice *device, GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	guint8 buf[] = {
		CY_PD_JUMP_TO_ALT_FW_CMD_SIG,
	};

	/* not required */
	if (fu_device_has_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER) ||
	    self->fw_image_type == FW_IMAGE_TYPE_DUAL_SYMMETRIC)
		return TRUE;

	/* jump to Alt FW */
	if (!fu_ccgx_hpi_device_clear_all_events (self,
						  HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS,
						  error))
		return FALSE;
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);
	if (!fu_ccgx_hpi_device_reg_write (self,
					   CY_PD_JUMP_TO_BOOT_REG_ADDR,
					   buf, sizeof(buf),
					   error)) {
		g_prefix_error (error, "jump to alt mode error: ");
		return FALSE;
	}

	/* sym not required */
	fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);

	/* success */
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_attach (FuDevice *device, GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	guint8 buf[] = {
		CY_PD_DEVICE_RESET_CMD_SIG,
		CY_PD_REG_RESET_DEVICE_CMD,
	};
	if (!fu_ccgx_hpi_device_clear_all_events (self,
						  HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS,
						  error))
		return FALSE;
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);
	if (!fu_ccgx_hpi_device_reg_write_no_resp (self,
						   CY_PD_REG_RESET_ADDR,
						   buf, sizeof(buf),
						   error)) {
		g_prefix_error (error, "reset device error: ");
		return FALSE;
	}
	fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
	return TRUE;
}

static FuFirmware *
fu_ccgx_hpi_device_prepare_firmware (FuDevice *device,
				     GBytes *fw,
				     FwupdInstallFlags flags,
				     GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	FWMode fw_mode;
	guint16 fw_app_type;
	guint16 fw_silicon_id;
	g_autoptr(FuFirmware) firmware = fu_ccgx_firmware_new ();

	/* parse all images */
	if (!fu_firmware_parse (firmware, fw, flags, error))
		return NULL;

	/* check the silicon ID */
	fw_silicon_id = fu_ccgx_firmware_get_silicon_id (FU_CCGX_FIRMWARE (firmware));
	if (fw_silicon_id != self->silicon_id) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "silicon id mismatch, expected 0x%x, got 0x%x",
			     self->silicon_id, fw_silicon_id);
		return NULL;
	}
	if ((flags & FWUPD_INSTALL_FLAG_IGNORE_VID_PID) == 0) {
		fw_app_type = fu_ccgx_firmware_get_app_type (FU_CCGX_FIRMWARE (firmware));
		if (fw_app_type != self->fw_app_type) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "app type mismatch, expected 0x%x, got 0x%x",
				     self->fw_app_type, fw_app_type);
			return NULL;
		}
	}
	fw_mode = fu_ccgx_firmware_get_fw_mode (FU_CCGX_FIRMWARE (firmware));
	if (fw_mode != fu_ccgx_fw_mode_get_alternate (self->fw_mode)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "FWMode mismatch, expected %s, got %s",
			     fu_ccgx_fw_mode_to_string (fu_ccgx_fw_mode_get_alternate (self->fw_mode)),
			     fu_ccgx_fw_mode_to_string (fw_mode));
		return NULL;
	}
	return g_steal_pointer (&firmware);
}

static gboolean
fu_ccgx_hpi_get_metadata_offset (FuCcgxHpiDevice *self,
				 FWMode fw_mode,
				 guint32 *addr,
				 guint32 *offset,
				 GError **error)
{
	guint32 addr_max;

	/* sanity check */
	if (self->flash_row_size == 0x0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "unset support row size");
		return FALSE;
	}

	/* get the row offset for the flash size */
	addr_max = self->flash_size / self->flash_row_size;
	if (self->flash_row_size == 128) {
		*offset = HPI_META_DATA_OFFSET_ROW_128;
	} else if (self->flash_row_size == 256) {
		*offset = HPI_META_DATA_OFFSET_ROW_256;
	} else {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "unsupported support row size: 0x%x",
			     self->flash_row_size);
		return FALSE;
	}

	/* get the row offset in the flash */
	switch (fw_mode) {
	case FW_MODE_FW1:
		*addr = addr_max - 1;
		break;
	case FW_MODE_FW2:
		*addr = addr_max - 2;
		break;
	default:
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "boot recovery not supported");
		return FALSE;
	}
	return TRUE;
}

/* this will only work after fu_ccgx_hpi_enter_flash_mode() has been used */
static gboolean
fu_ccgx_hpi_load_metadata (FuCcgxHpiDevice *self,
			   FWMode fw_mode,
			   CCGxMetaData *metadata,
			   GError **error)
{
	guint32 addr = 0x0;
	guint32 md_offset = 0x0;
	g_autofree guint8 *buf = NULL;

	/* read flash at correct address */
	if (!fu_ccgx_hpi_get_metadata_offset (self, fw_mode, &addr, &md_offset, error))
		return FALSE;
	buf = g_malloc0 (self->flash_row_size);
	if (!fu_ccgx_hpi_read_flash (self, addr,
				     buf, self->flash_row_size,
				     error)) {
		g_prefix_error (error, "fw metadata read error: ");
		return FALSE;
	}
	return fu_memcpy_safe ((guint8 *) metadata, sizeof(*metadata), 0x0,
			       buf, self->flash_row_size, md_offset,
			       sizeof(metadata), error);
}

/* this will only work after fu_ccgx_hpi_enter_flash_mode() has been used */
static gboolean
fu_ccgx_hpi_save_metadata (FuCcgxHpiDevice *self,
			   FWMode fw_mode,
			   CCGxMetaData *metadata,
			   GError **error)
{
	guint32 addr = 0x0;
	guint32 md_offset = 0x0;
	g_autofree guint8 *buf = NULL;

	/* read entire row of flash at correct address */
	if (!fu_ccgx_hpi_get_metadata_offset (self, fw_mode,
					      &addr, &md_offset,
					      error))
		return FALSE;
	buf = g_malloc0 (self->flash_row_size);
	if (!fu_ccgx_hpi_read_flash (self, addr,
				     buf, self->flash_row_size,
				     error)) {
		g_prefix_error (error, "fw metadata read existing error: ");
		return FALSE;
	}
	if (!fu_memcpy_safe (buf, self->flash_row_size, md_offset,
			     (guint8 *) metadata, sizeof(*metadata), 0x0,
			     sizeof(metadata), error))
		return FALSE;
	if (!fu_ccgx_hpi_write_flash (self, addr,
				      buf, self->flash_row_size,
				      error)) {
		g_prefix_error (error, "fw metadata write error: ");
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_write_firmware (FuDevice *device,
			    FuFirmware *firmware,
			    FwupdInstallFlags flags,
			    GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	CCGxMetaData metadata = { 0x0 };
	GPtrArray *records = fu_ccgx_firmware_get_records (FU_CCGX_FIRMWARE (firmware));
	FWMode fw_mode_alt = fu_ccgx_fw_mode_get_alternate (self->fw_mode);
	g_autoptr(FuDeviceLocker) locker = NULL;

	/* enter flash mode */
	locker = fu_device_locker_new_full (self,
					    (FuDeviceLockerFunc) fu_ccgx_hpi_enter_flash_mode,
					    (FuDeviceLockerFunc) fu_ccgx_hpi_leave_flash_mode,
					    error);
	if (locker == NULL)
		return FALSE;

	/* invalidate metadata for alternate image */
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_READ);
	if (!fu_ccgx_hpi_load_metadata (self, fw_mode_alt, &metadata, error))
		return FALSE;
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_ERASE);
	metadata.metadata_valid = 0x00;
	if (!fu_ccgx_hpi_save_metadata (self, fw_mode_alt, &metadata, error))
		return FALSE;

	/* write new image */
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
	for (guint i = 0; i < records->len; i++) {
		FuCcgxFirmwareRecord *rcd = g_ptr_array_index (records, i);

		/* write chunk */
		if (!fu_ccgx_hpi_write_flash (self, rcd->row_number,
					      g_bytes_get_data (rcd->data, NULL),
					      g_bytes_get_size (rcd->data),
					      error)) {
			g_prefix_error (error, "fw write error @0x%x: ", rcd->row_number);
			return FALSE;
		}

		/* update progress */
		fu_device_set_progress_full (device, (gsize) i, (gsize) records->len - 1);
	}

	/* validate fw */
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_VERIFY);
	if (!fu_ccgx_hpi_validate_fw (self, fw_mode_alt, error)) {
		g_prefix_error (error, "fw validate error: ");
		return FALSE;
	}

	/* this is a good time to leave the flash mode *before* rebooting */
	if (!fu_device_locker_close (locker, error))
		return FALSE;

	/* success */
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_ensure_silicon_id (FuCcgxHpiDevice *self, GError **error)
{
	guint8 buf[2] = { 0x0 };
	g_autofree gchar *instance_id = NULL;

	if (!fu_ccgx_hpi_device_reg_read (self, CY_PD_SILICON_ID,
					  buf, sizeof(buf), error)) {
		g_prefix_error (error, "get silicon id error: ");
		return FALSE;
	}
	if (!fu_common_read_uint16_safe (buf, sizeof(buf),
					0x0, &self->silicon_id,
					G_LITTLE_ENDIAN, error))
		return FALSE;

	/* add quirks */
	instance_id = g_strdup_printf ("CCGX\\SID_%X", self->silicon_id);
	fu_device_add_instance_id_full (FU_DEVICE (self),
					instance_id,
					FU_DEVICE_INSTANCE_FLAG_ONLY_QUIRKS);

	/* sanity check */
	if (self->flash_row_size == 0x0 ||
	    self->flash_size == 0x0 ||
	    self->flash_size % self->flash_row_size != 0) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "invalid row size for Instance ID %s: 0x%x/0x%x",
			     instance_id,
			     self->flash_row_size,
			     self->flash_size);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static void
fu_ccgx_hpi_device_set_version_raw (FuCcgxHpiDevice *self, guint32 version_raw)
{
	g_autofree gchar *version = fu_ccgx_version_to_string (version_raw);
	fu_device_set_version (FU_DEVICE (self), version);
	fu_device_set_version_raw (FU_DEVICE (self), version_raw);
}

static void
fu_ccgx_hpi_device_setup_with_fw_mode (FuCcgxHpiDevice *self)
{
	fu_device_set_logical_id (FU_DEVICE (self),
				  fu_ccgx_fw_mode_to_string (self->fw_mode));
}

static void
fu_ccgx_hpi_device_setup_with_app_type (FuCcgxHpiDevice *self)
{
	if (self->silicon_id != 0x0 && self->fw_app_type != 0x0) {
		g_autofree gchar *instance_id1 = NULL;
		g_autofree gchar *instance_id2 = NULL;

		/* we get fw_image_type from the quirk */
		instance_id1 = g_strdup_printf ("USB\\VID_%04X&PID_%04X&SID_%04X&APP_%04X",
						fu_usb_device_get_vid (FU_USB_DEVICE (self)),
						fu_usb_device_get_pid (FU_USB_DEVICE (self)),
						self->silicon_id,
						self->fw_app_type);
		fu_device_add_instance_id (FU_DEVICE (self), instance_id1);
		instance_id2 = g_strdup_printf ("USB\\VID_%04X&PID_%04X&SID_%04X&APP_%04X&MODE_%s",
						fu_usb_device_get_vid (FU_USB_DEVICE (self)),
						fu_usb_device_get_pid (FU_USB_DEVICE (self)),
						self->silicon_id,
						self->fw_app_type,
						fu_ccgx_fw_mode_to_string (self->fw_mode));
		fu_device_add_instance_id (FU_DEVICE (self), instance_id2);
	}
}

static gboolean
fu_ccgx_hpi_device_setup (FuDevice *device, GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	CyI2CConfig i2c_config = { 0x0 };
	guint32 hpi_event = 0;
	guint8 mode = 0;
	g_autoptr(GError) error_local = NULL;

	/* set the new config */
	if (!fu_ccgx_hpi_device_get_i2c_config (self, &i2c_config, error)) {
		g_prefix_error (error, "get config error: ");
		return FALSE;
	}
	i2c_config.frequency = FU_CCGX_HPI_FREQ;
	i2c_config.is_initiator = TRUE;
	i2c_config.is_msb_first = TRUE;
	if (!fu_ccgx_hpi_device_set_i2c_config (self, &i2c_config, error)) {
		g_prefix_error (error, "set config error: ");
		return FALSE;
	}
	if (!fu_ccgx_hpi_device_reg_read (self, CY_PD_REG_DEVICE_MODE_ADDR,
					  &mode, 1, error)) {
		g_prefix_error (error, "get device mode error: ");
		return FALSE;
	}
	self->hpi_addrsz = mode & 0x80 ? 2 : 1;
	self->num_ports = (mode >> 2) & 0x03 ? 2 : 1;
	self->fw_mode = (FWMode) (mode & 0x03);
	fu_ccgx_hpi_device_setup_with_fw_mode (self);

	/* get silicon ID */
	if (!fu_ccgx_hpi_device_ensure_silicon_id (self, error))
		return FALSE;

	/* get correct version if not in boot mode */
	if (self->fw_mode != FW_MODE_BOOT) {
		guint16 bufsz;
		guint32 versions[FW_MODE_LAST] = { 0x0 };
		guint8 bufver[HPI_DEVICE_VERSION_SIZE_HPIV2] = { 0x0 };

		bufsz = self->hpi_addrsz == 1 ? HPI_DEVICE_VERSION_SIZE_HPIV1 :
						HPI_DEVICE_VERSION_SIZE_HPIV2;
		if (!fu_ccgx_hpi_device_reg_read (self,
						  CY_PD_GET_VERSION,
						  bufver, bufsz,
						  error)) {
			g_prefix_error (error, "get version error: ");
			return FALSE;
		}

		/* fw1 */
		if (!fu_common_read_uint32_safe (bufver, sizeof(bufver),
						 0x0c, &versions[FW_MODE_FW1],
						 G_LITTLE_ENDIAN, error))
			return FALSE;

		/* fw2 */
		if (!fu_common_read_uint32_safe (bufver, sizeof(bufver),
						 0x14, &versions[FW_MODE_FW2],
						 G_LITTLE_ENDIAN, error))
			return FALSE;

		/* add GUIDs that are specific to the firmware app type */
		self->fw_app_type = versions[self->fw_mode] & 0xffff;
		fu_ccgx_hpi_device_setup_with_app_type (self);

		/* if running in bootloader force an upgrade to any version */
		if (fu_device_has_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
			fu_ccgx_hpi_device_set_version_raw (self, 0x0);
		} else {
			fu_ccgx_hpi_device_set_version_raw (self, versions[self->fw_mode]);
		}
	}

	/* not supported in boot mode */
	if (self->fw_mode == FW_MODE_BOOT) {
		fu_device_remove_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
	} else {
		fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
	}

	/* if we are coming back from reset, wait for hardware to settle */
	if (!fu_ccgx_hpi_device_get_event (self,
					   HPI_REG_SECTION_DEV,
					   &hpi_event,
					   HPI_CMD_SETUP_EVENT_WAIT_TIME_MS,
					   &error_local)) {
		if (!g_error_matches (error_local,
				      G_IO_ERROR,
				      G_IO_ERROR_TIMED_OUT)) {
			g_propagate_error (error, g_steal_pointer (&error_local));
			return FALSE;
		}
	} else {
		if (hpi_event == CY_PD_RESP_RESET_COMPLETE)
			g_usleep (HPI_CMD_RESET_COMPLETE_DELAY_US);
	}

	/* start with no events in the queue */
	return fu_ccgx_hpi_device_clear_all_events (self,
						    HPI_CMD_SETUP_EVENT_CLEAR_TIME_MS,
						    error);
}

static gboolean
fu_ccgx_hpi_device_set_quirk_kv (FuDevice *device,
				 const gchar *key,
				 const gchar *value,
				 GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	if (g_strcmp0 (key, "SiliconId") == 0) {
		guint64 tmp = fu_common_strtoull (value);
		if (tmp < G_MAXUINT16) {
			self->silicon_id = tmp;
			return TRUE;
		}
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "invalid SiliconId");
		return FALSE;
	}
	if (g_strcmp0 (key, "CcgxFlashRowSize") == 0) {
		guint64 tmp = fu_common_strtoull (value);
		if (tmp < G_MAXUINT32) {
			self->flash_row_size = tmp;
			return TRUE;
		}
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "invalid CcgxFlashRowSize");
		return FALSE;
	}
	if (g_strcmp0 (key, "CcgxFlashSize") == 0) {
		guint64 tmp = fu_common_strtoull (value);
		if (tmp < G_MAXUINT32) {
			self->flash_size = tmp;
			return TRUE;
		}
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "invalid CcgxFlashSize");
		return FALSE;
	}
	if (g_strcmp0 (key, "CcgxImageKind") == 0) {
		self->fw_image_type = fu_ccgx_fw_image_type_from_string (value);
		if (self->fw_image_type != FW_IMAGE_TYPE_UNKNOWN)
			return TRUE;
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "invalid CcgxImageKind");
		return FALSE;
	}
	g_set_error_literal (error,
			     G_IO_ERROR,
			     G_IO_ERROR_NOT_SUPPORTED,
			     "no supported");
	return FALSE;
}

static gboolean
fu_ccgx_hpi_device_open (FuDevice *device, GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	g_autoptr(GError) error_local = NULL;

	/* FuUsbDevice->open */
	if (!FU_DEVICE_CLASS (fu_ccgx_hpi_device_parent_class)->open (device, error))
		return FALSE;

	if (!g_usb_device_claim_interface (fu_usb_device_get_dev (FU_USB_DEVICE (device)),
					   self->inf_num,
					   G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
					   &error_local)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "cannot claim interface: %s",
			     error_local->message);
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_ccgx_hpi_device_close (FuDevice *device, GError **error)
{
	FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE (device);
	g_autoptr(GError) error_local = NULL;

	/* do not close handle when device restarts */
	if (fu_device_get_status (device) == FWUPD_STATUS_DEVICE_RESTART)
		return TRUE;
	if (!g_usb_device_release_interface (fu_usb_device_get_dev (FU_USB_DEVICE (device)),
					     self->inf_num,
					     G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
					     &error_local)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "cannot release interface: %s",
			     error_local->message);
		return FALSE;
	}
	/* FuUsbDevice->close */
	return FU_DEVICE_CLASS (fu_ccgx_hpi_device_parent_class)->close (device, error);
}

static void
fu_ccgx_hpi_device_init (FuCcgxHpiDevice *self)
{
	self->inf_num = 0x0;
	self->hpi_addrsz = 1;
	self->num_ports = 1;
	self->target_address = PD_I2C_TARGET_ADDRESS;
	self->ep_bulk_out = PD_I2C_USB_EP_BULK_OUT;
	self->ep_bulk_in = PD_I2C_USB_EP_BULK_IN;
	self->ep_intr_in = PD_I2C_USB_EP_INTR_IN;
	fu_device_add_protocol (FU_DEVICE (self), "com.cypress.ccgx");
	fu_device_set_version_format (FU_DEVICE (self), FWUPD_VERSION_FORMAT_TRIPLET);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_REQUIRE_AC);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_DUAL_IMAGE);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_SELF_RECOVERY);
	fu_device_add_internal_flag (FU_DEVICE (self), FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID);
	fu_device_retry_set_delay (FU_DEVICE (self), HPI_CMD_RETRY_DELAY);

	/* we can recover the I²C link using reset */
	fu_device_retry_add_recovery (FU_DEVICE (self),
				      FWUPD_ERROR,
				      FWUPD_ERROR_READ,
				      fu_ccgx_hpi_device_i2c_reset_cb);
	fu_device_retry_add_recovery (FU_DEVICE (self),
				      FWUPD_ERROR,
				      FWUPD_ERROR_WRITE,
				      fu_ccgx_hpi_device_i2c_reset_cb);

	/* this might not be true for future hardware */
	if (self->inf_num > 0)
		self->scb_index = 1;
}

static void
fu_ccgx_hpi_device_class_init (FuCcgxHpiDeviceClass *klass)
{
	FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
	klass_device->to_string = fu_ccgx_hpi_device_to_string;
	klass_device->write_firmware = fu_ccgx_hpi_write_firmware;
	klass_device->prepare_firmware = fu_ccgx_hpi_device_prepare_firmware;
	klass_device->detach = fu_ccgx_hpi_device_detach;
	klass_device->attach = fu_ccgx_hpi_device_attach;
	klass_device->setup = fu_ccgx_hpi_device_setup;
	klass_device->set_quirk_kv = fu_ccgx_hpi_device_set_quirk_kv;
	klass_device->open = fu_ccgx_hpi_device_open;
	klass_device->close = fu_ccgx_hpi_device_close;
}
