/*
 * Copyright 2021 Gaël PORTAY <gael.portay@collabora.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include "fu-genesys-common.h"
#include "fu-genesys-scaler-device.h"
#include "fu-genesys-scaler-firmware.h"
#include "fu-genesys-usbhub-device.h"

/*
 * NOTE: DO NOT ALLOW ANY MORE MAGIC CONSTANTS IN THIS FILE
 * nocheck:magic-inlines=119
 */

#define GENESYS_SCALER_BANK_SIZE (2 * FU_MB)

#define GENESYS_SCALER_MSTAR_READ     0x7a
#define GENESYS_SCALER_MSTAR_WRITE    0x7b
#define GENESYS_SCALER_MSTAR_DATA_OUT 0x7c
#define GENESYS_SCALER_MSTAR_DATA_IN  0x7f

#define GENESYS_SCALER_CMD_DDCCI_FIRMWARE_PACKET_VERSION 0x06

#define GENESYS_SCALER_CMD_DATA_WRITE 0x10
#define GENESYS_SCALER_CMD_DATA_READ  0x11
#define GENESYS_SCALER_CMD_DATA_END   0x12

#define GENESYS_SCALER_INFO 0xA4

#define GENESYS_SCALER_USB_TIMEOUT 5000 /* 5s */

#define FU_SCALER_FLAG_PAUSE_R2_CPU "pause-r2-cpu"
#define FU_SCALER_FLAG_USE_I2C_CH0  "use-i2c-ch0"

typedef struct {
	guint8 req_read;
	guint8 req_write;
} FuGenesysVendorCommand;

typedef struct {
	guint8 stage;
	guint8 model;
	guint8 major;
	guint8 minor;
} FuGenesysScalerFirmwarePacketVersion;

struct _FuGenesysScalerDevice {
	FuDevice parent_instance;
	guint8 level;
	FuGenesysPublicKey public_key;
	guint32 cfi_flash_id;
	FuCfiDevice *cfi_device;
	FuGenesysVendorCommand vc;
	guint32 sector_size;
	guint32 page_size;
	guint32 transfer_size;
	guint16 gpio_out_reg;
	guint16 gpio_en_reg;
	guint8 gpio_val;
};

G_DEFINE_TYPE(FuGenesysScalerDevice, fu_genesys_scaler_device, FU_TYPE_DEVICE)

static gboolean
fu_genesys_scaler_device_enter_serial_debug_mode(FuGenesysScalerDevice *self, GError **error)
{
	FuDevice *proxy;
	/* nocheck:magic */
	guint8 data[] = {0x53, 0x45, 0x52, 0x44, 0x42};

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0001,	  /* value */
					    0x0000,	  /* idx */
					    data,	  /* data */
					    sizeof(data), /* data length */
					    NULL,	  /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error entering Serial Debug Mode: ");
		return FALSE;
	}

	fu_device_sleep(FU_DEVICE(self), 1); /* ms */

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_exit_serial_debug_mode(FuGenesysScalerDevice *self, GError **error)
{
	FuDevice *proxy;
	guint8 data[] = {0x45};

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0001,	  /* value */
					    0x0000,	  /* idx */
					    data,	  /* data */
					    sizeof(data), /* data length */
					    NULL,	  /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error exiting Serial Debug Mode: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_enter_single_step_mode(FuGenesysScalerDevice *self, GError **error)
{
	FuDevice *proxy;
	guint8 data1[] = {0x10, 0xc0, 0xc1, 0x53};
	guint8 data2[] = {0x10, 0x1f, 0xc1, 0x53};

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0001,	   /* value */
					    0x0000,	   /* idx */
					    data1,	   /* data */
					    sizeof(data1), /* data length */
					    NULL,	   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error entering Single Step Mode: ");
		return FALSE;
	}

	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0001,	   /* value */
					    0x0000,	   /* idx */
					    data2,	   /* data */
					    sizeof(data2), /* data length */
					    NULL,	   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error entering Single Step Mode: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_exit_single_step_mode(FuGenesysScalerDevice *self, GError **error)
{
	FuDevice *proxy;
	guint8 data[] = {0x10, 0xc0, 0xc1, 0xff};

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0001,	  /* value */
					    0x0000,	  /* idx */
					    data,	  /* data */
					    sizeof(data), /* data length */
					    NULL,	  /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error exiting Single Step Mode: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_enter_debug_mode(FuGenesysScalerDevice *self, GError **error)
{
	FuDevice *proxy;
	guint8 data[] = {0x10, 0x00, 0x00, 0x00};

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0001,	  /* value */
					    0x0000,	  /* idx */
					    data,	  /* data */
					    sizeof(data), /* data length */
					    NULL,	  /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error entering Debug Mode: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_mst_i2c_bus_ctrl(FuGenesysScalerDevice *self, GError **error)
{
	FuDevice *proxy;
	guint8 data[] = {0x35, 0x71};

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	for (guint i = 0; i < sizeof(data); i++) {
		if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
						    FU_USB_DIRECTION_HOST_TO_DEVICE,
						    FU_USB_REQUEST_TYPE_VENDOR,
						    FU_USB_RECIPIENT_DEVICE,
						    self->vc.req_write,
						    0x0001,	     /* value */
						    0x0000,	     /* idx */
						    &data[i],	     /* data */
						    sizeof(data[i]), /* data length */
						    NULL,	     /* actual length */
						    GENESYS_SCALER_USB_TIMEOUT,
						    NULL,
						    error)) {
			g_prefix_error(error, "error sending i2c bus ctrl 0x%02x: ", data[i]);
			return FALSE;
		}
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_mst_i2c_bus_switch_to_ch0(FuGenesysScalerDevice *self, GError **error)
{
	FuDevice *proxy;
	/* nocheck:magic */
	guint8 data[] = {0x80, 0x82, 0x84, 0x51, 0x7f, 0x37, 0x61};

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	for (guint i = 0; i < sizeof(data); i++) {
		if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
						    FU_USB_DIRECTION_HOST_TO_DEVICE,
						    FU_USB_REQUEST_TYPE_VENDOR,
						    FU_USB_RECIPIENT_DEVICE,
						    self->vc.req_write,
						    0x0001,	     /* value */
						    0x0000,	     /* idx */
						    &data[i],	     /* data */
						    sizeof(data[i]), /* data length */
						    NULL,	     /* actual length */
						    GENESYS_SCALER_USB_TIMEOUT,
						    NULL,
						    error)) {
			g_prefix_error(error, "error sending i2c bus ch0 0x%02x: ", data[i]);
			return FALSE;
		}
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_mst_i2c_bus_switch_to_ch4(FuGenesysScalerDevice *self, GError **error)
{
	FuDevice *proxy;
	/* nocheck:magic */
	guint8 data[] = {0x80, 0x82, 0x85, 0x53, 0x7f};

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	for (guint i = 0; i < sizeof(data); i++) {
		if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
						    FU_USB_DIRECTION_HOST_TO_DEVICE,
						    FU_USB_REQUEST_TYPE_VENDOR,
						    FU_USB_RECIPIENT_DEVICE,
						    self->vc.req_write,
						    0x0001,	     /* value */
						    0x0000,	     /* idx */
						    &data[i],	     /* data */
						    sizeof(data[i]), /* data length */
						    NULL,	     /* actual length */
						    GENESYS_SCALER_USB_TIMEOUT,
						    NULL,
						    error)) {
			g_prefix_error(error, "error sending i2c bus ch4 0x%02x: ", data[i]);
			return FALSE;
		}
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_disable_wp(FuGenesysScalerDevice *self, gboolean disable, GError **error)
{
	FuDevice *proxy;
	guint8 data_out[] = {0x10,
			     0x00 /* gpio_out_reg_h */,
			     0x00 /* gpio_out_reg_l */,
			     0x00 /* gpio_out_val */};
	guint8 data_en[] = {0x10,
			    0x00 /* gpio_en_reg_h */,
			    0x00 /* gpio_en_reg_l */,
			    0x00 /* gpio_en_val */};

	/* disable write protect [output] */

	data_out[1] = (self->gpio_out_reg & 0xff00) >> 8;
	data_out[2] = self->gpio_out_reg & 0x00ff;

	/* read gpio-out register */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0003,		  /* value */
					    0x0000,		  /* idx */
					    data_out,		  /* data */
					    sizeof(data_out) - 1, /* data length */
					    NULL,		  /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error,
			       "error reading GPIO-Out Register 0x%02x%02x: ",
			       data_out[1],
			       data_out[2]);
		return FALSE;
	}

	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_DEVICE_TO_HOST,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_read,
					    0x0003,		 /* value */
					    0x0000,		 /* idx */
					    &data_out[3],	 /* data */
					    sizeof(data_out[3]), /* data length */
					    NULL,		 /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error,
			       "error reading GPIO-Out Register 0x%02x%02x: ",
			       data_out[1],
			       data_out[2]);
		return FALSE;
	}

	if (data_out[3] == 0xff) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "error reading GPIO-Out Register 0x%02x%02x",
			    data_out[1],
			    data_out[2]);
		return FALSE;
	}

	if (disable)
		data_out[3] |= self->gpio_val; /* pull high */
	else
		data_out[3] &= ~self->gpio_val; /* pull low */

	/* write gpio-out register */
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0001,	      /* value */
					    0x0000,	      /* idx */
					    data_out,	      /* data */
					    sizeof(data_out), /* data length */
					    NULL,	      /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error,
			       "error writing GPIO-Out Register 0x%02x%02x=0x%02x: ",
			       data_out[1],
			       data_out[2],
			       data_out[3]);
		return FALSE;
	}

	/* disable write protect [enable] */

	data_en[1] = (self->gpio_en_reg & 0xff00) >> 8;
	data_en[2] = self->gpio_en_reg & 0x00ff;

	/* read gpio-enable register */
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0003,		 /* value */
					    0x0000,		 /* idx */
					    data_en,		 /* data */
					    sizeof(data_en) - 1, /* data length */
					    NULL,		 /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error,
			       "error writing GPIO-Enable Register 0x%02x%02x: ",
			       data_en[1],
			       data_en[2]);
		return FALSE;
	}

	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_DEVICE_TO_HOST,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_read,
					    0x0003,		/* value */
					    0x0000,		/* idx */
					    &data_en[3],	/* data */
					    sizeof(data_en[3]), /* data length */
					    NULL,		/* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error,
			       "error reading GPIO-Out Register 0x%02x%02x: ",
			       data_en[1],
			       data_en[2]);
		return FALSE;
	}

	if (data_en[3] == 0xff) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "error reading GPIO-Enable Register 0x%02x%02x",
			    data_en[1],
			    data_en[2]);
		return FALSE;
	}

	data_en[3] &= ~self->gpio_val;

	/* write gpio-enable register */
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0001,	     /* value */
					    0x0000,	     /* idx */
					    data_en,	     /* data */
					    sizeof(data_en), /* data length */
					    NULL,	     /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error,
			       "error writing GPIO-Enable Register 0x%02x%02x=0x%02x: ",
			       data_en[1],
			       data_en[2],
			       data_en[3]);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_pause_r2_cpu(FuGenesysScalerDevice *self, GError **error)
{
	FuDevice *proxy;
	guint8 data[] = {0x10, 0x00, 0x10, 0x0F, 0xD7, 0x00};

	/*
	 * MST9U only!
	 *
	 * Pause R2 CPU for preventing Scaler entering Power Saving Mode also
	 * need for Disable SPI Flash Write Protect Mode.
	 */
	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0003,	      /* value */
					    0x0000,	      /* idx */
					    data,	      /* data */
					    sizeof(data) - 1, /* data length */
					    NULL,	      /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error,
			       "error reading register 0x%02x%02x%02x%02x%02x: ",
			       data[0],
			       data[1],
			       data[2],
			       data[3],
			       data[4]);
		return FALSE;
	}

	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_DEVICE_TO_HOST,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_read,
					    0x0003,	     /* value */
					    0x0000,	     /* idx */
					    &data[5],	     /* data */
					    sizeof(data[5]), /* data length */
					    NULL,	     /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error,
			       "error reading register 0x%02x%02x%02x%02x%02x: ",
			       data[0],
			       data[1],
			       data[2],
			       data[3],
			       data[4]);
		return FALSE;
	}

	if (data[5] == 0xff) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "error reading register 0x%02x%02x%02x%02x%02x",
			    data[0],
			    data[1],
			    data[2],
			    data[3],
			    data[4]);
		return FALSE;
	}

	data[5] |= 0x80;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0003,	  /* value */
					    0x0000,	  /* idx */
					    data,	  /* data */
					    sizeof(data), /* data length */
					    NULL,	  /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error,
			       "error writing register 0x%02x%02x%02x%02x%02x: ",
			       data[0],
			       data[1],
			       data[2],
			       data[3],
			       data[4]);
		return FALSE;
	}

	fu_device_sleep(FU_DEVICE(self), 200); /* ms */

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_set_isp_mode_cb(FuDevice *device, gpointer user_data, GError **error)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device);
	FuDevice *proxy;
	guint8 data[] = {0x4d, 0x53, 0x54, 0x41, 0x52};

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0000,	  /* value */
					    0x0000,	  /* idx */
					    data,	  /* data */
					    sizeof(data), /* data length */
					    NULL,	  /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		return FALSE;
	}
	fu_device_sleep(device, 1); /* ms */

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_enter_isp_mode(FuGenesysScalerDevice *self, GError **error)
{
	/*
	 * Enter ISP mode:
	 *
	 * Note: MStar application note say execute twice to avoid race
	 * condition
	 */

	if (!fu_device_retry_full(FU_DEVICE(self),
				  fu_genesys_scaler_device_set_isp_mode_cb,
				  2,
				  1000 /* 1ms */,
				  NULL,
				  error)) {
		g_prefix_error_literal(error, "error entering ISP mode: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_exit_isp_mode(FuGenesysScalerDevice *self, GError **error)
{
	FuDevice *proxy;
	guint8 data[] = {0x24};

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0000,	  /* value */
					    0x0000,	  /* idx */
					    data,	  /* data */
					    sizeof(data), /* data length */
					    NULL,	  /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error exiting ISP mode: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_detach(FuDevice *device, FuProgress *progress, GError **error)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device);

	/*
	 * Important: do not change the order below; otherwise, unexpected
	 * condition occurs.
	 */

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

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

	if (fu_device_has_private_flag(device, FU_SCALER_FLAG_USE_I2C_CH0))
		if (!fu_genesys_scaler_device_mst_i2c_bus_switch_to_ch0(self, error))
			return FALSE;

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

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

	if (!fu_genesys_scaler_device_disable_wp(self, TRUE, error))
		return FALSE;

	if (fu_device_has_private_flag(device, FU_SCALER_FLAG_PAUSE_R2_CPU)) {
		if (!fu_genesys_scaler_device_mst_i2c_bus_switch_to_ch4(self, error))
			return FALSE;

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

		if (!fu_genesys_scaler_device_pause_r2_cpu(self, error))
			return FALSE;
	}

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

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_attach(FuDevice *device, FuProgress *progress, GError **error)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device);

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

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

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

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_get_level(FuGenesysScalerDevice *self, guint8 *level, GError **error)
{
	FuDevice *proxy;

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_DEVICE_TO_HOST,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    GENESYS_SCALER_INFO,
					    0x0004, /* value */
					    0x0000, /* idx */
					    level,  /* data */
					    1,	    /* data length */
					    NULL,   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error getting level: ");
		return FALSE;
	}
	fu_device_sleep(FU_DEVICE(self), 100); /* ms */

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_get_version(FuGenesysScalerDevice *self,
				     guint8 *buf,
				     guint bufsz,
				     GError **error)
{
	FuDevice *proxy;

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_DEVICE_TO_HOST,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    GENESYS_SCALER_INFO,
					    0x0005, /* value */
					    0x0000, /* idx */
					    buf,    /* data */
					    bufsz,  /* data length */
					    NULL,   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error getting version: ");
		return FALSE;
	}

	fu_device_sleep(FU_DEVICE(self), 100); /* ms */

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_get_public_key(FuGenesysScalerDevice *self,
					guint8 *buf,
					guint bufsz,
					GError **error)
{
	FuDevice *proxy;
	const gsize data_size = 0x20;
	g_autoptr(GPtrArray) chunks = NULL;

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	chunks = fu_chunk_array_mutable_new(buf, bufsz, 0, 0, data_size, error);
	if (chunks == NULL)
		return FALSE;
	for (guint i = 0; i < chunks->len; i++) {
		FuChunk *chk = g_ptr_array_index(chunks, i);

		if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
						    FU_USB_DIRECTION_DEVICE_TO_HOST,
						    FU_USB_REQUEST_TYPE_VENDOR,
						    FU_USB_RECIPIENT_DEVICE,
						    GENESYS_SCALER_INFO,
						    0x0006,			/* value */
						    fu_chunk_get_address(chk),	/* idx */
						    fu_chunk_get_data_out(chk), /* data */
						    fu_chunk_get_data_sz(chk),	/* data length */
						    NULL,			/* actual length */
						    GENESYS_SCALER_USB_TIMEOUT,
						    NULL,
						    error)) {
			g_prefix_error_literal(error, "error getting public key: ");
			return FALSE;
		}

		fu_device_sleep(FU_DEVICE(self), 100); /* ms */
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_read_flash(FuGenesysScalerDevice *self,
				    guint addr,
				    guint8 *buf,
				    guint bufsz,
				    FuProgress *progress,
				    GError **error)
{
	FuDevice *proxy;
	guint8 data1[] = {
	    GENESYS_SCALER_CMD_DATA_WRITE,
	    0x00, /* read data command */
	    (addr & 0x00ff0000) >> 16,
	    (addr & 0x0000ff00) >> 8,
	    (addr & 0x000000ff),
	};
	guint8 data2[] = {
	    GENESYS_SCALER_CMD_DATA_READ,
	};
	guint8 data3[] = {
	    GENESYS_SCALER_CMD_DATA_END,
	};
	g_autoptr(GPtrArray) chunks = NULL;

	if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_READ_DATA, &data1[1], error))
		return FALSE;

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0000,	   /* value */
					    0x0000,	   /* idx */
					    data1,	   /* data */
					    sizeof(data1), /* data length */
					    NULL,	   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error, "error reading flash at 0x%06x: ", addr);
		return FALSE;
	}

	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0000,	   /* value */
					    0x0000,	   /* idx */
					    data2,	   /* data */
					    sizeof(data2), /* data length */
					    NULL,	   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error, "error reading flash at 0x%06x: ", addr);
		return FALSE;
	}

	chunks = fu_chunk_array_mutable_new(buf, bufsz, addr, 0, self->transfer_size, error);
	if (chunks == NULL)
		return FALSE;
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, chunks->len);
	for (guint i = 0; i < chunks->len; i++) {
		FuChunk *chk = g_ptr_array_index(chunks, i);

		if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
						    FU_USB_DIRECTION_DEVICE_TO_HOST,
						    FU_USB_REQUEST_TYPE_VENDOR,
						    FU_USB_RECIPIENT_DEVICE,
						    self->vc.req_read,
						    0x0000,			/* value */
						    0x0000,			/* idx */
						    fu_chunk_get_data_out(chk), /* data */
						    fu_chunk_get_data_sz(chk),	/* data length */
						    NULL,			/* actual length */
						    GENESYS_SCALER_USB_TIMEOUT,
						    NULL,
						    error)) {
			g_prefix_error(error,
				       "error reading flash at 0x%06x: ",
				       (guint)fu_chunk_get_address(chk));
			return FALSE;
		}
		fu_progress_step_done(progress);
	}

	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0000,	   /* value */
					    0x0000,	   /* idx */
					    data3,	   /* data */
					    sizeof(data3), /* data length */
					    NULL,	   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error, "error reading flash at 0x%06x: ", addr);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_wait_flash_control_register_cb(FuDevice *dev,
							gpointer user_data,
							GError **error)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(dev);
	FuDevice *proxy;
	FuGenesysWaitFlashRegisterHelper *helper = user_data;
	guint8 status = 0;

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_DEVICE_TO_HOST,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_read,
					    helper->reg << 8 | 0x04, /* value */
					    0x0000,		     /* idx */
					    &status,		     /* data */
					    sizeof(status),	     /* data length */
					    NULL,		     /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error reading flash control register: ");
		return FALSE;
	}

	if ((status & 0x81) != helper->expected_val) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "wrong value in flash control register");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_flash_control_write_enable(FuGenesysScalerDevice *self, GError **error)
{
	FuDevice *proxy;
	guint8 data1[] = {
	    GENESYS_SCALER_CMD_DATA_WRITE,
	    0x00, /* write enable command */
	};
	guint8 data2[] = {
	    GENESYS_SCALER_CMD_DATA_END,
	};

	if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_WRITE_EN, &data1[1], error))
		return FALSE;

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0000,	   /* value */
					    0x0000,	   /* idx */
					    data1,	   /* data */
					    sizeof(data1), /* data length */
					    NULL,	   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error sending flash control write enable: ");
		return FALSE;
	}

	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0000,	   /* value */
					    0x0000,	   /* idx */
					    data2,	   /* data */
					    sizeof(data2), /* data length */
					    NULL,	   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error sending flash control write enable: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_flash_control_write_status(FuGenesysScalerDevice *self,
						    guint8 status,
						    GError **error)
{
	FuDevice *proxy;
	guint8 data1[] = {
	    GENESYS_SCALER_CMD_DATA_WRITE,
	    0x00, /* write status command */
	    status,
	};
	guint8 data2[] = {
	    GENESYS_SCALER_CMD_DATA_END,
	};

	if (!fu_cfi_device_get_cmd(self->cfi_device,
				   FU_CFI_DEVICE_CMD_WRITE_STATUS,
				   &data1[1],
				   error))
		return FALSE;

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0000,	   /* value */
					    0x0000,	   /* idx */
					    data1,	   /* data */
					    sizeof(data1), /* data length */
					    NULL,	   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error, "error sending flash control write status 0x%02x: ", status);
		return FALSE;
	}

	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0000,	   /* value */
					    0x0000,	   /* idx */
					    data2,	   /* data */
					    sizeof(data2), /* data length */
					    NULL,	   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error, "error sending flash control write status 0x%02x: ", status);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_flash_control_sector_erase(FuGenesysScalerDevice *self,
						    guint addr,
						    GError **error)
{
	FuDevice *proxy;
	FuGenesysWaitFlashRegisterHelper helper = {
	    .reg = 0x00, /* read status command */
	    .expected_val = 0,
	};
	guint8 data1[] = {
	    GENESYS_SCALER_CMD_DATA_WRITE,
	    0x00, /* sector erase command */
	    (addr & 0x00ff0000) >> 16,
	    (addr & 0x0000ff00) >> 8,
	    (addr & 0x000000ff),
	};
	guint8 data2[] = {
	    GENESYS_SCALER_CMD_DATA_END,
	};

	if (!fu_cfi_device_get_cmd(self->cfi_device,
				   FU_CFI_DEVICE_CMD_READ_STATUS,
				   &helper.reg,
				   error))
		return FALSE;

	if (!fu_cfi_device_get_cmd(self->cfi_device,
				   FU_CFI_DEVICE_CMD_SECTOR_ERASE,
				   &data1[1],
				   error))
		return FALSE;

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

	if (!fu_genesys_scaler_device_flash_control_write_status(self, 0x00, error))
		return FALSE;

	/* 5s */
	if (!fu_device_retry_full(FU_DEVICE(self),
				  fu_genesys_scaler_device_wait_flash_control_register_cb,
				  100,
				  50, /* 50ms */
				  &helper,
				  error)) {
		g_prefix_error_literal(error,
				       "error waiting for flash control read status register: ");
		return FALSE;
	}

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

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0000,	   /* value */
					    0x0000,	   /* idx */
					    data1,	   /* data */
					    sizeof(data1), /* data length */
					    NULL,	   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error,
			       "error sending flash control erase at address 0x%06x: ",
			       addr);
		return FALSE;
	}

	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    self->vc.req_write,
					    0x0000,	   /* value */
					    0x0000,	   /* idx */
					    data2,	   /* data */
					    sizeof(data2), /* data length */
					    NULL,	   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error(error,
			       "error sending flash control erase at address 0x%06x: ",
			       addr);
		return FALSE;
	}

	/* 5s */
	if (!fu_device_retry_full(FU_DEVICE(self),
				  fu_genesys_scaler_device_wait_flash_control_register_cb,
				  100,
				  50, /* 50ms */
				  &helper,
				  error)) {
		g_prefix_error_literal(error,
				       "error waiting for flash control read status register: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_erase_flash(FuGenesysScalerDevice *self,
				     guint addr,
				     guint bufsz,
				     FuProgress *progress,
				     GError **error)
{
	g_autoptr(GPtrArray) chunks = NULL;

	chunks = fu_chunk_array_new(NULL, bufsz, addr, 0, self->sector_size, error);
	if (chunks == NULL)
		return FALSE;
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, chunks->len);
	for (guint i = 0; i < chunks->len; i++) {
		FuChunk *chk = g_ptr_array_index(chunks, i);

		if (!fu_genesys_scaler_device_flash_control_sector_erase(self,
									 fu_chunk_get_address(chk),
									 error)) {
			g_prefix_error(error,
				       "error erasing flash at address 0x%06x: ",
				       (guint)fu_chunk_get_address(chk));
			return FALSE;
		}
		fu_progress_step_done(progress);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_flash_control_page_program(FuGenesysScalerDevice *self,
						    guint addr,
						    const guint8 *buf,
						    guint bufsz,
						    FuProgress *progress,
						    GError **error)
{
	FuDevice *proxy;
	FuGenesysWaitFlashRegisterHelper helper = {
	    .reg = 0x00, /* read status command */
	    .expected_val = 0,
	};
	guint8 data1[] = {
	    GENESYS_SCALER_CMD_DATA_WRITE,
	    0x00, /* page program command */
	    (addr & 0x00ff0000) >> 16,
	    (addr & 0x0000ff00) >> 8,
	    (addr & 0x000000ff),
	};
	gsize datasz = 0;
	g_autoptr(GPtrArray) chunks = NULL;
	g_autofree guint8 *data = NULL;

	if (!fu_cfi_device_get_cmd(self->cfi_device,
				   FU_CFI_DEVICE_CMD_READ_STATUS,
				   &helper.reg,
				   error))
		return FALSE;

	if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_PAGE_PROG, &data1[1], error))
		return FALSE;

	datasz = sizeof(data1) + bufsz;
	data = g_malloc0(datasz);
	if (!fu_memcpy_safe(data,
			    datasz,
			    0, /* dst */
			    data1,
			    sizeof(data1),
			    0, /* src */
			    sizeof(data1),
			    error))
		return FALSE;
	if (!fu_memcpy_safe(data,
			    datasz,
			    sizeof(data1), /* dst */
			    buf,
			    bufsz,
			    0, /* src */
			    bufsz,
			    error))
		return FALSE;

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;

	chunks = fu_chunk_array_mutable_new(data,
					    datasz,
					    addr + sizeof(data1),
					    0,
					    self->transfer_size,
					    error);
	if (chunks == NULL)
		return FALSE;
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, chunks->len);
	for (guint i = 0; i < chunks->len; i++) {
		FuChunk *chk = g_ptr_array_index(chunks, i);
		guint16 index = 0x0010 * (i + 1);
		/* last chunk */
		if ((i + 1) == chunks->len)
			index |= 0x0080;

		if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
						    FU_USB_DIRECTION_HOST_TO_DEVICE,
						    FU_USB_REQUEST_TYPE_VENDOR,
						    FU_USB_RECIPIENT_DEVICE,
						    self->vc.req_write,
						    index,			/* value */
						    0x0000,			/* idx */
						    fu_chunk_get_data_out(chk), /* data */
						    fu_chunk_get_data_sz(chk),	/* data length */
						    NULL,			/* actual length */
						    GENESYS_SCALER_USB_TIMEOUT,
						    NULL,
						    error)) {
			g_prefix_error(
			    error,
			    "error sending flash control page program at address 0x%06x: ",
			    (guint)fu_chunk_get_address(chk));
			return FALSE;
		}
		fu_progress_step_done(progress);
	}

	/* 200ms */
	if (!fu_device_retry(FU_DEVICE(self),
			     fu_genesys_scaler_device_wait_flash_control_register_cb,
			     20,
			     &helper,
			     error)) {
		g_prefix_error_literal(error,
				       "error waiting for flash control read status register: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_write_sector(FuGenesysScalerDevice *self,
				      guint addr,
				      const guint8 *buf,
				      guint bufsz,
				      FuProgress *progress,
				      GError **error)
{
	g_autoptr(GPtrArray) chunks = NULL;

	chunks = fu_chunk_array_new(buf, bufsz, addr, 0, self->page_size, error);
	if (chunks == NULL)
		return FALSE;
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, chunks->len);
	for (guint i = 0; i < chunks->len; i++) {
		FuChunk *chk = g_ptr_array_index(chunks, i);

		if (!fu_genesys_scaler_device_flash_control_page_program(
			self,
			fu_chunk_get_address(chk),
			fu_chunk_get_data(chk),
			fu_chunk_get_data_sz(chk),
			fu_progress_get_child(progress),
			error))
			return FALSE;
		fu_progress_step_done(progress);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_write_flash(FuGenesysScalerDevice *self,
				     guint addr,
				     const guint8 *buf,
				     guint bufsz,
				     FuProgress *progress,
				     GError **error)
{
	g_autoptr(GPtrArray) chunks = NULL;

	chunks = fu_chunk_array_new(buf, bufsz, addr, 0, self->sector_size, error);
	if (chunks == NULL)
		return FALSE;
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_set_steps(progress, chunks->len);
	for (guint i = 0; i < chunks->len; i++) {
		FuChunk *chk = g_ptr_array_index(chunks, i);

		if (!fu_genesys_scaler_device_write_sector(self,
							   fu_chunk_get_address(chk),
							   fu_chunk_get_data(chk),
							   fu_chunk_get_data_sz(chk),
							   fu_progress_get_child(progress),
							   error))
			return FALSE;
		fu_progress_step_done(progress);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_get_ddcci_data(FuGenesysScalerDevice *self,
					guint8 cmd,
					guint8 *buf,
					guint bufsz,
					GError **error)
{
	FuDevice *proxy;
	guint8 data[] = {0x6e, 0x51, 0x83, 0xcd, 0x01, 0x00 /* command */, 0x00 /* checksum */};

	data[5] = cmd;
	data[6] = fu_xor8(data, sizeof(data) - 1);

	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self),
					    FU_USB_DIRECTION_HOST_TO_DEVICE,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    GENESYS_SCALER_MSTAR_DATA_OUT,
					    0x0000,	  /* value */
					    0x0000,	  /* idx */
					    data,	  /* data */
					    sizeof(data), /* data length */
					    NULL,	  /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error setting dddci data: ");
		return FALSE;
	}

	fu_device_sleep(FU_DEVICE(self), 100); /* 1ms */

	proxy = fu_device_get_proxy(FU_DEVICE(self), error);
	if (proxy == NULL)
		return FALSE;
	if (!fu_usb_device_control_transfer(FU_USB_DEVICE(proxy),
					    FU_USB_DIRECTION_DEVICE_TO_HOST,
					    FU_USB_REQUEST_TYPE_VENDOR,
					    FU_USB_RECIPIENT_DEVICE,
					    GENESYS_SCALER_MSTAR_DATA_IN,
					    0x0001, /* value */
					    0x0000, /* idx */
					    buf,    /* data */
					    bufsz,  /* data length */
					    NULL,   /* actual length */
					    GENESYS_SCALER_USB_TIMEOUT,
					    NULL,
					    error)) {
		g_prefix_error_literal(error, "error getting dddci data: ");
		return FALSE;
	}

	fu_device_sleep(FU_DEVICE(self), 100); /* ms */

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_get_firmware_packet_version(FuGenesysScalerDevice *self,
						     FuGenesysScalerFirmwarePacketVersion *ver,
						     GError **error)
{
	guint8 buf[0x40] = {0};
	guint8 offset = 4;

	if (!fu_genesys_scaler_device_get_ddcci_data(
		self,
		GENESYS_SCALER_CMD_DDCCI_FIRMWARE_PACKET_VERSION,
		buf,
		sizeof(buf),
		error))
		return FALSE;

	if (buf[0] == 0x6f && buf[1] == 0x6e) {
		gsize len = buf[2] ^ 0x80;
		guint8 checksum = 0;
		guint8 checksum_tmp = 0x0;

		if (len > sizeof(buf) - 3) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "error dddci length too large, got 0x%x, expected <= 0x%zx",
				    (guint)len,
				    sizeof(buf));
			return FALSE;
		}

		buf[0] = 0x50; /* drifted value */
		if (!fu_xor8_safe(buf, sizeof(buf), 0x0, len + 3, &checksum, error))
			return FALSE;
		if (!fu_memread_uint8_safe(buf, sizeof(buf), len + 3, &checksum_tmp, error))
			return FALSE;
		if (checksum_tmp != checksum) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "error dddci checksum mismatch, got 0x%02x, expected 0x%02x",
				    checksum_tmp,
				    checksum);
			return FALSE;
		}

		offset = 7;
	}

	ver->stage = buf[offset];
	ver->model = buf[offset + 1];
	ver->major = buf[offset + 2];
	ver->minor = buf[offset + 3];

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_probe(FuDevice *device, GError **error)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device);
	FuGenesysScalerFirmwarePacketVersion ver;
	guint8 buf[7 + 1] = {0};
	g_autofree gchar *guid = NULL;
	g_autofree gchar *version = NULL;
	g_autofree gchar *panelrev = NULL;

	if (!fu_genesys_scaler_device_get_level(self, &self->level, error))
		return FALSE;

	if (!fu_genesys_scaler_device_get_public_key(self,
						     (guint8 *)&self->public_key,
						     sizeof(self->public_key),
						     error))
		return FALSE;
	if (memcmp(self->public_key.N, "N = ", 4) != 0 ||
	    memcmp(self->public_key.E, "E = ", 4) != 0) {
		fu_dump_raw(G_LOG_DOMAIN,
			    "PublicKey",
			    (const guint8 *)&self->public_key,
			    sizeof(self->public_key));
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_SIGNATURE_INVALID,
				    "invalid public-key");
		return FALSE;
	}
	guid = fwupd_guid_hash_data((const guint8 *)&self->public_key,
				    sizeof(self->public_key),
				    FWUPD_GUID_FLAG_NONE);

	if (!fu_genesys_scaler_device_get_version(self, buf, sizeof(buf), error))
		return FALSE;
	/* ?xIM123; where ? is 0x06 (length?) */
	panelrev = fu_memstrsafe(buf, sizeof(buf), 0x1, 6, error);
	if (panelrev == NULL)
		return FALSE;
	if (!fu_genesys_scaler_device_get_firmware_packet_version(self, &ver, error))
		return FALSE;

	version = g_strdup_printf("%d.%d.%d.%d", ver.stage, ver.model, ver.major, ver.minor);
	fu_device_set_version(device, version);
	fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PLAIN);
	fu_device_set_logical_id(device, "scaler");

	/* add instance ID */
	fu_device_add_instance_str(device, "MSTAR", "TSUM_G");
	fu_device_add_instance_strup(device, "PUBKEY", guid);
	fu_device_add_instance_strup(device, "PANELREV", panelrev);
	if (!fu_device_build_instance_id(device, error, "GENESYS_SCALER", "MSTAR", "PUBKEY", NULL))
		return FALSE;
	if (!fu_device_build_instance_id(device,
					 error,
					 "GENESYS_SCALER",
					 "MSTAR",
					 "PUBKEY",
					 "PANELREV",
					 NULL))
		return FALSE;

	fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE);

	self->vc.req_read = GENESYS_SCALER_MSTAR_READ;
	self->vc.req_write = GENESYS_SCALER_MSTAR_WRITE;
	if (self->level != 1) {
		self->vc.req_read += 3;
		self->vc.req_write += 3;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_setup(FuDevice *device, GError **error)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device);
	guint64 size_min = fu_device_get_firmware_size_max(device);
	guint64 size;
	guint32 sector_size;
	guint32 page_size;
	g_autofree gchar *flash_id = NULL;

	flash_id = g_strdup_printf("%06X", self->cfi_flash_id);
	self->cfi_device = fu_cfi_device_new(FU_DEVICE(self), flash_id);
	if (!fu_device_setup(FU_DEVICE(self->cfi_device), error))
		return FALSE;

	sector_size = fu_cfi_device_get_sector_size(self->cfi_device);
	if (sector_size != 0)
		self->sector_size = sector_size;
	page_size = fu_cfi_device_get_page_size(self->cfi_device);
	if (page_size != 0)
		self->page_size = page_size;

	if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE))
		size_min *= 2;

	size = fu_device_get_firmware_size_max(FU_DEVICE(self->cfi_device));
	if (size != 0 && size < size_min) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "CFI device too small, got 0x%x, expected >= 0x%x",
			    (guint)size,
			    (guint)size_min);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static GBytes *
fu_genesys_scaler_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device);
	gsize size = fu_cfi_device_get_size(self->cfi_device);
	g_autofree guint8 *buf = NULL;
	g_autoptr(FuDeviceLocker) locker = NULL;

	/* progress */
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "detach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 99, NULL);

	/* require detach -> attach */
	locker = fu_device_locker_new_full(device,
					   (FuDeviceLockerFunc)fu_device_detach,
					   (FuDeviceLockerFunc)fu_device_attach,
					   error);
	if (locker == NULL)
		return NULL;
	fu_progress_step_done(progress);

	buf = g_malloc0(size);
	if (!fu_genesys_scaler_device_read_flash(self,
						 0,
						 buf,
						 size,
						 fu_progress_get_child(progress),
						 error))
		return NULL;
	fu_progress_step_done(progress);

	/* success */
	return g_bytes_new_take(g_steal_pointer(&buf), size);
}

static gboolean
fu_genesys_scaler_device_check_firmware(FuDevice *device,
					FuFirmware *firmware,
					FuFirmwareParseFlags flags,
					GError **error)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device);
	g_autoptr(GBytes) blob_payload = NULL;
	g_autoptr(GBytes) blob_public_key = NULL;

	/* check public-key */
	blob_public_key =
	    fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_SIGNATURE, error);
	if (blob_public_key == NULL)
		return FALSE;
	fu_dump_raw(G_LOG_DOMAIN,
		    "PublicKey",
		    g_bytes_get_data(blob_public_key, NULL),
		    g_bytes_get_size(blob_public_key));
	if (memcmp(g_bytes_get_data(blob_public_key, NULL),
		   &self->public_key,
		   sizeof(self->public_key)) != 0 &&
	    (flags & FWUPD_INSTALL_FLAG_FORCE) == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_SIGNATURE_INVALID,
				    "mismatch public-key");
		return FALSE;
	}

	/* check size */
	blob_payload = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_PAYLOAD, error);
	if (blob_payload == NULL)
		return FALSE;
	if (g_bytes_get_size(blob_payload) > fu_device_get_firmware_size_max(device)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "firmware too large, got 0x%x, expected <= 0x%x",
			    (guint)g_bytes_get_size(blob_payload),
			    (guint)fu_device_get_firmware_size_max(device));
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_genesys_scaler_device_write_firmware(FuDevice *device,
					FuFirmware *firmware,
					FuProgress *progress,
					FwupdInstallFlags flags,
					GError **error)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device);
	guint addr = 0;
	gsize size;
	const guint8 *data;
	g_autofree guint8 *buf = NULL;
	g_autoptr(FuFirmware) payload = NULL;
	g_autoptr(GBytes) fw_payload = NULL;

	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 4, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 54, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 42, NULL);

	if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE))
		addr = GENESYS_SCALER_BANK_SIZE;

	payload = fu_firmware_get_image_by_id(firmware, FU_FIRMWARE_ID_PAYLOAD, error);
	if (payload == NULL)
		return FALSE;
	fw_payload = fu_firmware_get_bytes(payload, error);
	if (fw_payload == NULL)
		return FALSE;
	data = g_bytes_get_data(fw_payload, &size);
	if (data == NULL)
		return FALSE;

	if (!fu_genesys_scaler_device_erase_flash(self,
						  addr,
						  size,
						  fu_progress_get_child(progress),
						  error))
		return FALSE;
	fu_progress_step_done(progress);

	if (!fu_genesys_scaler_device_write_flash(self,
						  addr,
						  data,
						  size,
						  fu_progress_get_child(progress),
						  error))
		return FALSE;
	fu_progress_step_done(progress);

	buf = g_malloc0(size);
	if (!fu_genesys_scaler_device_read_flash(self,
						 addr,
						 buf,
						 size,
						 fu_progress_get_child(progress),
						 error))
		return FALSE;
	if (!fu_memcmp_safe(buf, size, 0x0, data, size, 0x0, size, error))
		return FALSE;
	fu_progress_step_done(progress);

	/* success */
	return TRUE;
}

static void
fu_genesys_scaler_device_set_progress(FuDevice *device, FuProgress *progress)
{
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload");
}

static void
fu_genesys_scaler_device_to_string(FuDevice *device, guint idt, GString *str)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device);
	gchar public_key_e[6 + 1] = {0};
	gchar public_key_n[0x200 + 1] = {0};
	g_autoptr(GError) error_local_e = NULL;
	g_autoptr(GError) error_local_n = NULL;

	fwupd_codec_string_append_hex(str, idt, "Level", self->level);
	if (fu_memcpy_safe((guint8 *)public_key_e,
			   sizeof(public_key_e),
			   0, /* dst */
			   (const guint8 *)&self->public_key,
			   sizeof(self->public_key),
			   sizeof(self->public_key) - 2 - (sizeof(public_key_e) - 1), /* src */
			   sizeof(public_key_e) - 1,
			   &error_local_e)) {
		fwupd_codec_string_append(str, idt, "PublicKeyE", public_key_e);
	} else {
		g_debug("ignoring public-key parameter E: %s", error_local_e->message);
	}
	if (fu_memcpy_safe((guint8 *)public_key_n,
			   sizeof(public_key_n),
			   0, /* dst */
			   (const guint8 *)&self->public_key,
			   sizeof(self->public_key),
			   4, /* src */
			   sizeof(public_key_n) - 1,
			   &error_local_n)) {
		fwupd_codec_string_append(str, idt, "PublicKeyN", public_key_n);
	} else {
		g_debug("ignoring public-key parameter N: %s", error_local_n->message);
	}
	fwupd_codec_string_append_hex(str, idt, "ReadRequestRead", self->vc.req_read);
	fwupd_codec_string_append_hex(str, idt, "WriteRequest", self->vc.req_write);
	fwupd_codec_string_append_hex(str, idt, "SectorSize", self->sector_size);
	fwupd_codec_string_append_hex(str, idt, "PageSize", self->page_size);
	fwupd_codec_string_append_hex(str, idt, "TransferSize", self->transfer_size);
	fwupd_codec_string_append_hex(str, idt, "GpioOutputRegister", self->gpio_out_reg);
	fwupd_codec_string_append_hex(str, idt, "GpioEnableRegister", self->gpio_en_reg);
	fwupd_codec_string_append_hex(str, idt, "GpioValue", self->gpio_val);
	fwupd_codec_string_append_hex(str, idt, "CfiFlashId", self->cfi_flash_id);
}

static gboolean
fu_genesys_scaler_device_set_quirk_kv(FuDevice *device,
				      const gchar *key,
				      const gchar *value,
				      GError **error)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device);
	guint64 tmp;

	if (g_strcmp0(key, "GenesysScalerDeviceTransferSize") == 0) {
		if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error))
			return FALSE;
		self->transfer_size = tmp;

		/* success */
		return TRUE;
	}
	if (g_strcmp0(key, "GenesysScalerGpioOutputRegister") == 0) {
		if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error))
			return FALSE;
		self->gpio_out_reg = tmp;

		/* success */
		return TRUE;
	}
	if (g_strcmp0(key, "GenesysScalerGpioEnableRegister") == 0) {
		if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error))
			return FALSE;
		self->gpio_en_reg = tmp;

		/* success */
		return TRUE;
	}
	if (g_strcmp0(key, "GenesysScalerGpioValue") == 0) {
		if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error))
			return FALSE;
		self->gpio_val = tmp;

		/* success */
		return TRUE;
	}
	if (g_strcmp0(key, "GenesysScalerCfiFlashId") == 0) {
		if (!fu_strtoull(value, &tmp, 0, 0x00ffffffU, FU_INTEGER_BASE_AUTO, error))
			return FALSE;
		self->cfi_flash_id = tmp;

		/* success */
		return TRUE;
	}

	/* failure */
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "quirk key not supported");
	return FALSE;
}

static void
fu_genesys_scaler_device_init(FuGenesysScalerDevice *self)
{
	fu_device_set_vendor(FU_DEVICE(self), "MStar");
	fu_device_set_name(FU_DEVICE(self), "TSUMG");
	fu_device_add_protocol(FU_DEVICE(self), "com.mstarsemi.scaler");
	fu_device_retry_set_delay(FU_DEVICE(self), 10); /* 10ms */
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE);
	fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN);
	fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY);
	fu_device_register_private_flag(FU_DEVICE(self), FU_SCALER_FLAG_PAUSE_R2_CPU);
	fu_device_register_private_flag(FU_DEVICE(self), FU_SCALER_FLAG_USE_I2C_CH0);
	fu_device_set_install_duration(FU_DEVICE(self), 730); /* 12min 10s */
	fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_GENESYS_SCALER_FIRMWARE);
	fu_device_set_proxy_gtype(FU_DEVICE(self), FU_TYPE_GENESYS_USBHUB_DEVICE);

	self->sector_size = 4 * FU_KB;
	self->page_size = 256;
	self->transfer_size = 64;
	fu_device_set_firmware_size(FU_DEVICE(self), GENESYS_SCALER_BANK_SIZE); /* 2MB */
}

static void
fu_genesys_scaler_device_finalize(GObject *object)
{
	FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(object);
	if (self->cfi_device != NULL)
		g_object_unref(self->cfi_device);
	G_OBJECT_CLASS(fu_genesys_scaler_device_parent_class)->finalize(object);
}

static void
fu_genesys_scaler_device_class_init(FuGenesysScalerDeviceClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
	object_class->finalize = fu_genesys_scaler_device_finalize;
	device_class->probe = fu_genesys_scaler_device_probe;
	device_class->setup = fu_genesys_scaler_device_setup;
	device_class->dump_firmware = fu_genesys_scaler_device_dump_firmware;
	device_class->check_firmware = fu_genesys_scaler_device_check_firmware;
	device_class->write_firmware = fu_genesys_scaler_device_write_firmware;
	device_class->set_progress = fu_genesys_scaler_device_set_progress;
	device_class->detach = fu_genesys_scaler_device_detach;
	device_class->attach = fu_genesys_scaler_device_attach;
	device_class->to_string = fu_genesys_scaler_device_to_string;
	device_class->set_quirk_kv = fu_genesys_scaler_device_set_quirk_kv;
}

FuGenesysScalerDevice *
fu_genesys_scaler_device_new(FuDevice *proxy)
{
	FuGenesysScalerDevice *device = NULL;
	device = g_object_new(FU_TYPE_GENESYS_SCALER_DEVICE, "proxy", proxy, NULL);
	return device;
}
