/*
 * Copyright (C) 2012 Andrew Duggan
 * Copyright (C) 2012 Synaptics Inc.
 * Copyright (C) 2019 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include "fu-synaptics-rmi-common.h"
#include "fu-synaptics-rmi-firmware.h"
#include "fu-synaptics-rmi-ps2-device.h"
#include "fu-synaptics-rmi-v5-device.h"
#include "fu-synaptics-rmi-v6-device.h"
#include "fu-synaptics-rmi-v7-device.h"

#define RMI_DEVICE_PAGE_SIZE	       0x100
#define RMI_DEVICE_PAGE_SCAN_START     0x00e9
#define RMI_DEVICE_PAGE_SCAN_END       0x0005
#define RMI_DEVICE_F01_BASIC_QUERY_LEN 11

#define RMI_DEVICE_F01_LTS_RESERVED_SIZE 19

#define RMI_DEVICE_F01_QRY1_HAS_LTS	  (1 << 2)
#define RMI_DEVICE_F01_QRY1_HAS_SENSOR_ID (1 << 3)
#define RMI_DEVICE_F01_QRY1_HAS_PROPS_2	  (1 << 7)

#define RMI_DEVICE_F01_QRY42_DS4_QUERIES   (1 << 0)
#define RMI_DEVICE_F01_QRY43_01_PACKAGE_ID (1 << 0)
#define RMI_DEVICE_F01_QRY43_01_BUILD_ID   (1 << 1)

#define RMI_F34_COMMAND_MASK 0x0f
#define RMI_F34_STATUS_MASK  0x07
#define RMI_F34_STATUS_SHIFT 4
#define RMI_F34_ENABLED_MASK 0x80

#define RMI_F34_COMMAND_V1_MASK 0x3f
#define RMI_F34_STATUS_V1_MASK	0x3f
#define RMI_F34_ENABLED_V1_MASK 0x80

#define RMI_F01_CMD_DEVICE_RESET       1
#define RMI_F01_DEFAULT_RESET_DELAY_MS 100

typedef struct {
	FuSynapticsRmiFlash flash;
	GPtrArray *functions;
	FuSynapticsRmiFunction *f01;
	FuSynapticsRmiFunction *f34;
	guint8 current_page;
	guint16 sig_size; /* 0x0 for non-secure update */
	guint8 max_page;
	gboolean in_iep_mode;
} FuSynapticsRmiDevicePrivate;

G_DEFINE_TYPE_WITH_PRIVATE(FuSynapticsRmiDevice, fu_synaptics_rmi_device, FU_TYPE_UDEV_DEVICE)

#define GET_PRIVATE(o) (fu_synaptics_rmi_device_get_instance_private(o))

FuSynapticsRmiFlash *
fu_synaptics_rmi_device_get_flash(FuSynapticsRmiDevice *self)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	return &priv->flash;
}

static void
fu_synaptics_rmi_flash_to_string(FuSynapticsRmiFlash *flash, guint idt, GString *str)
{
	if (flash->bootloader_id[0] != 0x0) {
		g_autofree gchar *tmp =
		    g_strdup_printf("%02x.%02x", flash->bootloader_id[0], flash->bootloader_id[1]);
		fu_string_append(str, idt, "BootloaderId", tmp);
	}
	fu_string_append_kx(str, idt, "BlockSize", flash->block_size);
	fu_string_append_kx(str, idt, "BlockCountFw", flash->block_count_fw);
	fu_string_append_kx(str, idt, "BlockCountCfg", flash->block_count_cfg);
	fu_string_append_kx(str, idt, "FlashConfigLength", flash->config_length);
	fu_string_append_kx(str, idt, "PayloadLength", flash->payload_length);
	fu_string_append_kx(str, idt, "BuildID", flash->build_id);
}

static void
fu_synaptics_rmi_device_to_string(FuDevice *device, guint idt, GString *str)
{
	FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device);
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);

	/* FuUdevDevice->to_string */
	FU_DEVICE_CLASS(fu_synaptics_rmi_device_parent_class)->to_string(device, idt, str);

	fu_string_append_kx(str, idt, "CurrentPage", priv->current_page);
	fu_string_append_kx(str, idt, "InIepMode", priv->in_iep_mode);
	fu_string_append_kx(str, idt, "MaxPage", priv->max_page);
	fu_string_append_kx(str, idt, "SigSize", priv->sig_size);
	if (priv->f34 != NULL) {
		fu_string_append_kx(str, idt, "BlVer", priv->f34->function_version + 0x5);
	}
	fu_synaptics_rmi_flash_to_string(&priv->flash, idt, str);
}

FuSynapticsRmiFunction *
fu_synaptics_rmi_device_get_function(FuSynapticsRmiDevice *self,
				     guint8 function_number,
				     GError **error)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	if (priv->functions->len == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "no RMI functions, perhaps read the PDT?");
		return NULL;
	}
	for (guint i = 0; i < priv->functions->len; i++) {
		FuSynapticsRmiFunction *func = g_ptr_array_index(priv->functions, i);
		if (func->function_number == function_number)
			return func;
	}
	g_set_error(error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_INTERNAL,
		    "failed to get RMI function 0x%02x",
		    function_number);
	return NULL;
}

GByteArray *
fu_synaptics_rmi_device_read(FuSynapticsRmiDevice *self, guint16 addr, gsize req_sz, GError **error)
{
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self);
	return klass_rmi->read(self, addr, req_sz, error);
}

GByteArray *
fu_synaptics_rmi_device_read_packet_register(FuSynapticsRmiDevice *self,
					     guint16 addr,
					     gsize req_sz,
					     GError **error)
{
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self);
	if (klass_rmi->read_packet_register == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "packet register reads not supported");
		return NULL;
	}
	return klass_rmi->read_packet_register(self, addr, req_sz, error);
}

gboolean
fu_synaptics_rmi_device_write(FuSynapticsRmiDevice *self,
			      guint16 addr,
			      GByteArray *req,
			      FuSynapticsRmiDeviceFlags flags,
			      GError **error)
{
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self);
	return klass_rmi->write(self, addr, req, flags, error);
}

gboolean
fu_synaptics_rmi_device_set_page(FuSynapticsRmiDevice *self, guint8 page, GError **error)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self);
	if (priv->current_page == page)
		return TRUE;
	if (!klass_rmi->set_page(self, page, error))
		return FALSE;
	priv->current_page = page;
	return TRUE;
}

void
fu_synaptics_rmi_device_set_iepmode(FuSynapticsRmiDevice *self, gboolean iepmode)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	priv->in_iep_mode = iepmode;
}

gboolean
fu_synaptics_rmi_device_get_iepmode(FuSynapticsRmiDevice *self)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	return priv->in_iep_mode;
}

gboolean
fu_synaptics_rmi_device_write_bus_select(FuSynapticsRmiDevice *self, guint8 bus, GError **error)
{
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self);
	if (klass_rmi->write_bus_select == NULL)
		return TRUE;
	return klass_rmi->write_bus_select(self, bus, error);
}

gboolean
fu_synaptics_rmi_device_reset(FuSynapticsRmiDevice *self, GError **error)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	g_autoptr(GByteArray) req = g_byte_array_new();

	fu_byte_array_append_uint8(req, RMI_F01_CMD_DEVICE_RESET);
	if (!fu_synaptics_rmi_device_write(self,
					   priv->f01->command_base,
					   req,
					   FU_SYNAPTICS_RMI_DEVICE_FLAG_ALLOW_FAILURE,
					   error))
		return FALSE;
	fu_device_sleep(FU_DEVICE(self), RMI_F01_DEFAULT_RESET_DELAY_MS);
	return TRUE;
}

static gboolean
fu_synaptics_rmi_device_scan_pdt(FuSynapticsRmiDevice *self, GError **error)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	guint interrupt_count = 0;

	/* clear old list */
	g_ptr_array_set_size(priv->functions, 0);

	/* scan pages */
	for (guint page = 0; page < priv->max_page; page++) {
		gboolean found = FALSE;
		guint32 page_start = RMI_DEVICE_PAGE_SIZE * page;
		guint32 pdt_start = page_start + RMI_DEVICE_PAGE_SCAN_START;
		guint32 pdt_end = page_start + RMI_DEVICE_PAGE_SCAN_END;

		/* set page */
		if (!fu_synaptics_rmi_device_set_page(self, page, error))
			return FALSE;

		/* read out functions */
		for (guint addr = pdt_start; addr >= pdt_end; addr -= RMI_DEVICE_PDT_ENTRY_SIZE) {
			g_autofree FuSynapticsRmiFunction *func = NULL;
			g_autoptr(GByteArray) res = NULL;
			res = fu_synaptics_rmi_device_read(self,
							   addr,
							   RMI_DEVICE_PDT_ENTRY_SIZE,
							   error);
			if (res == NULL) {
				g_prefix_error(error,
					       "failed to read page %u PDT entry @ 0x%04x: ",
					       page,
					       addr);
				return FALSE;
			}
			func = fu_synaptics_rmi_function_parse(res,
							       page_start,
							       interrupt_count,
							       error);
			if (func == NULL)
				return FALSE;
			if (func->function_number == 0)
				break;
			interrupt_count += func->interrupt_source_count;
			g_ptr_array_add(priv->functions, g_steal_pointer(&func));
			found = TRUE;
		}
		if (!found)
			break;
	}

	/* for debug */
	for (guint i = 0; i < priv->functions->len; i++) {
		FuSynapticsRmiFunction *func = g_ptr_array_index(priv->functions, i);
		g_debug("PDT-%02u fn:0x%02x vr:%d sc:%d ms:0x%x "
			"db:0x%02x cb:0x%02x cm:0x%02x qb:0x%02x",
			i,
			func->function_number,
			func->function_version,
			func->interrupt_source_count,
			func->interrupt_mask,
			func->data_base,
			func->control_base,
			func->command_base,
			func->query_base);
	}

	/* success */
	return TRUE;
}

void
fu_synaptics_rmi_device_set_sig_size(FuSynapticsRmiDevice *self, guint16 sig_size)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	priv->sig_size = sig_size;
}

guint16
fu_synaptics_rmi_device_get_sig_size(FuSynapticsRmiDevice *self)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	return priv->sig_size;
}

void
fu_synaptics_rmi_device_set_max_page(FuSynapticsRmiDevice *self, guint8 max_page)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	priv->max_page = max_page;
}

guint8
fu_synaptics_rmi_device_get_max_page(FuSynapticsRmiDevice *self)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	return priv->max_page;
}

static void
fu_synaptics_rmi_device_set_product_id(FuSynapticsRmiDevice *self, const gchar *product_id)
{
	g_autofree gchar *instance_id = NULL;
	g_auto(GStrv) product_id_split = g_strsplit(product_id, "-", 2);

	/* use the product ID as an instance ID */
	instance_id = g_strdup_printf("SYNAPTICS_RMI\\%s", product_id);
	fu_device_add_instance_id(FU_DEVICE(self), instance_id);

	/* also add the product ID without the sub-number */
	if (g_strv_length(product_id_split) == 2) {
		g_autofree gchar *instance_id_major = NULL;
		instance_id_major = g_strdup_printf("SYNAPTICS_RMI\\%s", product_id_split[0]);
		fu_device_add_instance_id(FU_DEVICE(self), instance_id_major);
	}
}

static gboolean
fu_synaptics_rmi_device_query_status(FuSynapticsRmiDevice *self, GError **error)
{
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self);
	return klass_rmi->query_status(self, error);
}

static gboolean
fu_synaptics_rmi_device_query_build_id(FuSynapticsRmiDevice *self,
				       guint32 *build_id,
				       GError **error)
{
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self);
	if (klass_rmi->query_build_id == NULL)
		return TRUE;
	return klass_rmi->query_build_id(self, build_id, error);
}

static gboolean
fu_synaptics_rmi_device_query_product_sub_id(FuSynapticsRmiDevice *self,
					     guint8 *product_sub_id,
					     GError **error)
{
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self);
	if (klass_rmi->query_product_sub_id == NULL)
		return TRUE;
	return klass_rmi->query_product_sub_id(self, product_sub_id, error);
}

static gboolean
fu_synaptics_rmi_device_setup(FuDevice *device, GError **error)
{
	FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device);
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	guint16 addr;
	guint16 prod_info_addr;
	guint8 ds4_query_length = 0;
	guint8 product_sub_id = 0;
	gboolean has_build_id_query = FALSE;
	gboolean has_dds4_queries = FALSE;
	gboolean has_lts;
	gboolean has_package_id_query = FALSE;
	gboolean has_query42;
	gboolean has_sensor_id;
	g_autofree gchar *bl_ver = NULL;
	g_autofree gchar *fw_ver = NULL;
	g_autofree gchar *product_id = NULL;
	g_autoptr(GByteArray) f01_basic = NULL;
	g_autoptr(GByteArray) f01_product_id = NULL;
	g_autoptr(GByteArray) f01_ds4 = NULL;

	/* assume reset */
	priv->in_iep_mode = FALSE;

	/* read PDT */
	if (!fu_synaptics_rmi_device_scan_pdt(self, error))
		return FALSE;
	priv->f01 = fu_synaptics_rmi_device_get_function(self, 0x01, error);
	if (priv->f01 == NULL)
		return FALSE;
	addr = priv->f01->query_base;

	/* set page */
	if (!fu_synaptics_rmi_device_set_page(self, 0, error))
		return FALSE;

	/* force entering iep mode again */
	if (!fu_synaptics_rmi_device_enter_iep_mode(self,
						    FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE,
						    error))
		return FALSE;

	f01_basic = fu_synaptics_rmi_device_read(self, addr, RMI_DEVICE_F01_BASIC_QUERY_LEN, error);
	if (f01_basic == NULL) {
		g_prefix_error(error, "failed to read the basic query: ");
		return FALSE;
	}
	has_lts = (f01_basic->data[1] & RMI_DEVICE_F01_QRY1_HAS_LTS) > 0;
	has_sensor_id = (f01_basic->data[1] & RMI_DEVICE_F01_QRY1_HAS_SENSOR_ID) > 0;
	has_query42 = (f01_basic->data[1] & RMI_DEVICE_F01_QRY1_HAS_PROPS_2) > 0;

	/* get the product ID */
	addr += 11;
	f01_product_id = fu_synaptics_rmi_device_read(self, addr, RMI_PRODUCT_ID_LENGTH, error);
	if (f01_product_id == NULL) {
		g_prefix_error(error, "failed to read the product id: ");
		return FALSE;
	}
	if (!fu_synaptics_rmi_device_query_product_sub_id(self, &product_sub_id, error)) {
		g_prefix_error(error, "failed to query product sub id: ");
		return FALSE;
	}
	if (product_sub_id == 0) {
		/* HID */
		product_id = g_strndup((const gchar *)f01_product_id->data, f01_product_id->len);
	} else {
		/* PS/2 */
		g_autofree gchar *tmp = g_strndup((const gchar *)f01_product_id->data, 6);
		product_id = g_strdup_printf("%s-%03d", tmp, product_sub_id);
	}
	if (product_id != NULL)
		fu_synaptics_rmi_device_set_product_id(self, product_id);

	/* force entering iep mode again */
	if (!fu_synaptics_rmi_device_enter_iep_mode(self,
						    FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE,
						    error))
		return FALSE;

	/* skip */
	prod_info_addr = addr + 6;
	addr += 10;
	if (has_lts)
		addr++;
	if (has_sensor_id)
		addr++;
	if (has_lts)
		addr += RMI_DEVICE_F01_LTS_RESERVED_SIZE;

	/* read package ids */
	if (has_query42) {
		g_autoptr(GByteArray) f01_tmp = NULL;
		f01_tmp = fu_synaptics_rmi_device_read(self, addr++, 1, error);
		if (f01_tmp == NULL) {
			g_prefix_error(error, "failed to read query 42: ");
			return FALSE;
		}
		has_dds4_queries = (f01_tmp->data[0] & RMI_DEVICE_F01_QRY42_DS4_QUERIES) > 0;
	}
	if (has_dds4_queries) {
		g_autoptr(GByteArray) f01_tmp = NULL;
		f01_tmp = fu_synaptics_rmi_device_read(self, addr++, 1, error);
		if (f01_tmp == NULL) {
			g_prefix_error(error, "failed to read DS4 query length: ");
			return FALSE;
		}
		ds4_query_length = f01_tmp->data[0];
	}
	f01_ds4 = fu_synaptics_rmi_device_read(self, addr, 0x1, error);
	if (f01_ds4 == NULL) {
		g_prefix_error(error, "failed to read F01 Query43: ");
		return FALSE;
	}
	has_package_id_query = (f01_ds4->data[0] & RMI_DEVICE_F01_QRY43_01_PACKAGE_ID) > 0;
	has_build_id_query = (f01_ds4->data[0] & RMI_DEVICE_F01_QRY43_01_BUILD_ID) > 0;
	addr += ds4_query_length;
	if (has_package_id_query)
		prod_info_addr++;
	if (has_build_id_query) {
		g_autoptr(GByteArray) f01_tmp = NULL;
		guint8 buf32[4] = {0x0};
		f01_tmp = fu_synaptics_rmi_device_read(self, prod_info_addr, 0x3, error);
		if (f01_tmp == NULL) {
			g_prefix_error(error, "failed to read build ID bytes: ");
			return FALSE;
		}
		if (!fu_memcpy_safe(buf32,
				    sizeof(buf32),
				    0x0, /* dst */
				    f01_tmp->data,
				    f01_tmp->len,
				    0x0, /* src */
				    f01_tmp->len,
				    error))
			return FALSE;
		if (!fu_memread_uint32_safe(buf32,
					    sizeof(buf32),
					    0x0,
					    &priv->flash.build_id,
					    G_LITTLE_ENDIAN,
					    error))
			return FALSE;
	}

	/* read build ID, typically only for PS/2 */
	if (!fu_synaptics_rmi_device_query_build_id(self, &priv->flash.build_id, error)) {
		g_prefix_error(error, "failed to query build id: ");
		return FALSE;
	}

	/* get Function34_Query0,1 */
	priv->f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error);
	if (priv->f34 == NULL)
		return FALSE;
	if (priv->f34->function_version == 0x0) {
		if (!fu_synaptics_rmi_v5_device_setup(self, error)) {
			g_prefix_error(error, "failed to do v5 setup: ");
			return FALSE;
		}
	} else if (priv->f34->function_version == 0x1) {
		if (!fu_synaptics_rmi_v6_device_setup(self, error)) {
			g_prefix_error(error, "failed to do v6 setup: ");
			return FALSE;
		}
	} else if (priv->f34->function_version == 0x2) {
		if (!fu_synaptics_rmi_v7_device_setup(self, error)) {
			g_prefix_error(error, "failed to do v7 setup: ");
			return FALSE;
		}
	} else {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "f34 function version 0x%02x unsupported",
			    priv->f34->function_version);
		return FALSE;
	}
	if (!fu_synaptics_rmi_device_query_status(self, error)) {
		g_prefix_error(error, "failed to read bootloader status: ");
		return FALSE;
	}

	/* set versions */
	fw_ver = g_strdup_printf("%u.%u.%u",
				 f01_basic->data[2],
				 f01_basic->data[3],
				 priv->flash.build_id);
	fu_device_set_version(device, fw_ver);
	if (priv->f34->function_version == 0x0 || priv->f34->function_version == 0x1) {
		bl_ver = g_strdup_printf("%c.0.0", priv->flash.bootloader_id[1]);
	} else {
		bl_ver = g_strdup_printf("%u.0.0", priv->flash.bootloader_id[1]);
	}
	fu_device_set_version_bootloader(device, bl_ver);

	/* success */
	return TRUE;
}

static FuFirmware *
fu_synaptics_rmi_device_prepare_firmware(FuDevice *device,
					 GBytes *fw,
					 FwupdInstallFlags flags,
					 GError **error)
{
	FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device);
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	g_autoptr(FuFirmware) firmware = fu_synaptics_rmi_firmware_new();
	g_autoptr(GBytes) bytes_cfg = NULL;
	g_autoptr(GBytes) bytes_bin = NULL;
	gsize size_expected;

	if (!fu_firmware_parse(firmware, fw, flags, error))
		return NULL;

	/* check sizes */
	bytes_bin = fu_firmware_get_image_by_id_bytes(firmware, "ui", error);
	if (bytes_bin == NULL)
		return NULL;
	size_expected = ((gsize)priv->flash.block_count_fw * (gsize)priv->flash.block_size) +
			fu_synaptics_rmi_firmware_get_sig_size(FU_SYNAPTICS_RMI_FIRMWARE(firmware));
	if (g_bytes_get_size(bytes_bin) != size_expected) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "file firmware invalid size 0x%04x, expected 0x%04x",
			    (guint)g_bytes_get_size(bytes_bin),
			    (guint)size_expected);
		return NULL;
	}
	bytes_cfg = fu_firmware_get_image_by_id_bytes(firmware, "config", error);
	if (bytes_cfg == NULL)
		return NULL;
	size_expected = (gsize)priv->flash.block_count_cfg * (gsize)priv->flash.block_size;
	if (g_bytes_get_size(bytes_cfg) != size_expected) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_FILE,
			    "file config invalid size 0x%04x, expected 0x%04x",
			    (guint)g_bytes_get_size(bytes_cfg),
			    (guint)size_expected);
		return NULL;
	}

	return g_steal_pointer(&firmware);
}

static gboolean
fu_synaptics_rmi_device_poll(FuSynapticsRmiDevice *self, GError **error)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	g_autoptr(GByteArray) f34_db = NULL;

	/* get if the last flash read completed successfully */
	f34_db = fu_synaptics_rmi_device_read(self, priv->f34->data_base, 0x1, error);
	if (f34_db == NULL) {
		g_prefix_error(error, "failed to read f34_db: ");
		return FALSE;
	}
	if ((f34_db->data[0] & 0x1f) != 0x0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_WRITE,
			    "flash status invalid: 0x%x",
			    (guint)(f34_db->data[0] & 0x1f));
		return FALSE;
	}

	/* success */
	return TRUE;
}

gboolean
fu_synaptics_rmi_device_poll_wait(FuSynapticsRmiDevice *self, GError **error)
{
	g_autoptr(GError) error_local = NULL;

	/* try to poll every 20ms for up to 400ms */
	for (guint i = 0; i < 20; i++) {
		fu_device_sleep(FU_DEVICE(self), 20);
		g_clear_error(&error_local);
		if (fu_synaptics_rmi_device_poll(self, &error_local))
			return TRUE;
		g_debug("failed: %s", error_local->message);
	}

	/* proxy the last error */
	g_propagate_error(error, g_steal_pointer(&error_local));
	return FALSE;
}

static gboolean
fu_synaptics_rmi_device_wait_for_attr(FuSynapticsRmiDevice *self,
				      guint8 source_mask,
				      guint timeout_ms,
				      GError **error)
{
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self);
	return klass_rmi->wait_for_attr(self, source_mask, timeout_ms, error);
}

gboolean
fu_synaptics_rmi_device_enter_iep_mode(FuSynapticsRmiDevice *self,
				       FuSynapticsRmiDeviceFlags flags,
				       GError **error)
{
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self);
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);

	/* already set */
	if ((flags & FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE) == 0 && priv->in_iep_mode)
		return TRUE;
	if (klass_rmi->enter_iep_mode != NULL) {
		g_debug("enabling RMI iep_mode");
		if (!klass_rmi->enter_iep_mode(self, error)) {
			g_prefix_error(error, "failed to enable RMI iep_mode: ");
			return FALSE;
		}
	}
	priv->in_iep_mode = TRUE;
	return TRUE;
}

gboolean
fu_synaptics_rmi_device_wait_for_idle(FuSynapticsRmiDevice *self,
				      guint timeout_ms,
				      RmiDeviceWaitForIdleFlags flags,
				      GError **error)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	guint8 f34_command;
	guint8 f34_enabled;
	guint8 f34_status;
	g_autoptr(GByteArray) res = NULL;
	g_autoptr(GError) error_local = NULL;

	/* try to get report without requesting */
	if (timeout_ms > 0 && !fu_synaptics_rmi_device_wait_for_attr(self,
								     priv->f34->interrupt_mask,
								     timeout_ms,
								     &error_local)) {
		if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) {
			g_propagate_prefixed_error(error,
						   g_steal_pointer(&error_local),
						   "failed to wait for attr: ");
			return FALSE;
		}
	} else if ((flags & RMI_DEVICE_WAIT_FOR_IDLE_FLAG_REFRESH_F34) == 0) {
		/* device reported idle via an event */
		return TRUE;
	}

	/* if for some reason we are not getting attention reports for HID devices
	 * then we can still continue after the timeout and read F34 status
	 * but if we have to wait for the timeout to elapse every time then this
	 * will be slow */
	if (priv->f34->function_version == 0x1) {
		res = fu_synaptics_rmi_device_read(self, priv->flash.status_addr, 0x2, error);
		if (res == NULL)
			return FALSE;
		f34_command = res->data[0] & RMI_F34_COMMAND_V1_MASK;
		f34_status = res->data[1] & RMI_F34_STATUS_V1_MASK;
		f34_enabled = !!(res->data[1] & RMI_F34_ENABLED_MASK);
	} else {
		res = fu_synaptics_rmi_device_read(self, priv->flash.status_addr, 0x1, error);
		if (res == NULL)
			return FALSE;
		f34_command = res->data[0] & RMI_F34_COMMAND_MASK;
		f34_status = (res->data[0] >> RMI_F34_STATUS_SHIFT) & RMI_F34_STATUS_MASK;
		f34_enabled = !!(res->data[0] & RMI_F34_ENABLED_MASK);
	}

	/* PS/2 */
	if (FU_IS_SYNAPTICS_RMI_PS2_DEVICE(self)) {
		if (f34_command == 0) {
			g_debug("F34 zero as PS/2");
			return TRUE;
		}
	}

	/* is idle */
	if (f34_status == 0x0 && f34_command == 0x0) {
		if (f34_enabled == 0x0) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "idle but enabled unset");
			return FALSE;
		}
		return TRUE;
	}

	/* failed */
	g_set_error(error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_NOT_SUPPORTED,
		    "timed out waiting for idle [cmd:0x%x, sta:0x%x, ena:0x%x]",
		    f34_command,
		    f34_status,
		    f34_enabled);
	return FALSE;
}

gboolean
fu_synaptics_rmi_device_disable_sleep(FuSynapticsRmiDevice *self, GError **error)
{
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self);
	if (klass_rmi->disable_sleep == NULL)
		return TRUE;
	return klass_rmi->disable_sleep(self, error);
}

gboolean
fu_synaptics_rmi_device_write_bootloader_id(FuSynapticsRmiDevice *self, GError **error)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	gint block_data_offset = RMI_F34_BLOCK_DATA_OFFSET;
	g_autoptr(GByteArray) bootloader_id_req = g_byte_array_new();

	if (priv->f34->function_version == 0x1)
		block_data_offset = RMI_F34_BLOCK_DATA_V1_OFFSET;

	/* write bootloader_id into F34_Flash_Data0,1 */
	g_byte_array_append(bootloader_id_req,
			    priv->flash.bootloader_id,
			    sizeof(priv->flash.bootloader_id));
	if (!fu_synaptics_rmi_device_write(self,
					   priv->f34->data_base + block_data_offset,
					   bootloader_id_req,
					   FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
					   error)) {
		g_prefix_error(error, "failed to write bootloader_id: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

gboolean
fu_synaptics_rmi_device_disable_irqs(FuSynapticsRmiDevice *self, GError **error)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	g_autoptr(GByteArray) interrupt_disable_req = g_byte_array_new();

	fu_byte_array_append_uint8(interrupt_disable_req,
				   priv->f34->interrupt_mask | priv->f01->interrupt_mask);
	if (!fu_synaptics_rmi_device_write(self,
					   priv->f01->control_base + 1,
					   interrupt_disable_req,
					   FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
					   error)) {
		g_prefix_error(error, "failed to disable interrupts: ");
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_synaptics_rmi_device_write_firmware(FuDevice *device,
				       FuFirmware *firmware,
				       FuProgress *progress,
				       FwupdInstallFlags flags,
				       GError **error)
{
	FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device);
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	if (priv->f34->function_version == 0x0 || priv->f34->function_version == 0x1) {
		return fu_synaptics_rmi_v5_device_write_firmware(device,
								 firmware,
								 progress,
								 flags,
								 error);
	}
	if (priv->f34->function_version == 0x2) {
		return fu_synaptics_rmi_v7_device_write_firmware(device,
								 firmware,
								 progress,
								 flags,
								 error);
	}
	g_set_error(error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_NOT_SUPPORTED,
		    "f34 function version 0x%02x unsupported",
		    priv->f34->function_version);
	return FALSE;
}

static void
fu_synaptics_rmi_device_init(FuSynapticsRmiDevice *self)
{
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.rmi");
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);
	fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET);
	priv->current_page = 0xfe;
	priv->functions = g_ptr_array_new_with_free_func(g_free);
}

static void
fu_synaptics_rmi_device_finalize(GObject *object)
{
	FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(object);
	FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self);
	g_ptr_array_unref(priv->functions);
	G_OBJECT_CLASS(fu_synaptics_rmi_device_parent_class)->finalize(object);
}

static void
fu_synaptics_rmi_device_class_init(FuSynapticsRmiDeviceClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
	object_class->finalize = fu_synaptics_rmi_device_finalize;
	klass_device->to_string = fu_synaptics_rmi_device_to_string;
	klass_device->prepare_firmware = fu_synaptics_rmi_device_prepare_firmware;
	klass_device->setup = fu_synaptics_rmi_device_setup;
	klass_device->write_firmware = fu_synaptics_rmi_device_write_firmware;
}
