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

#include "config.h"

#include "fu-io-channel.h"

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

struct _FuSynapticsRmiPs2Device {
	FuSynapticsRmiDevice	 parent_instance;
	FuIOChannel		*io_channel;
};

G_DEFINE_TYPE (FuSynapticsRmiPs2Device, fu_synaptics_rmi_ps2_device, FU_TYPE_SYNAPTICS_RMI_DEVICE)

enum EPS2DataPortCommand {
	edpAuxFullRMIBackDoor		= 0x7F,
	edpAuxAccessModeByte1		= 0xE0,
	edpAuxAccessModeByte2		= 0xE1,
	edpAuxIBMReadSecondaryID	= 0xE1,
	edpAuxSetScaling1To1		= 0xE6,
	edpAuxSetScaling2To1		= 0xE7,
	edpAuxSetResolution		= 0xE8,
	edpAuxStatusRequest		= 0xE9,
	edpAuxSetStreamMode		= 0xEA,
	edpAuxReadData			= 0xEB,
	edpAuxResetWrapMode		= 0xEC,
	edpAuxSetWrapMode		= 0xEE,
	edpAuxSetRemoteMode		= 0xF0,
	edpAuxReadDeviceType		= 0xF2,
	edpAuxSetSampleRate		= 0xF3,
	edpAuxEnable			= 0xF4,
	edpAuxDisable			= 0xF5,
	edpAuxSetDefault		= 0xF6,
	edpAuxResend			= 0xFE,
	edpAuxReset			= 0xFF,
};

typedef enum {
	esdrTouchPad			= 0x47,
	esdrStyk			= 0x46,
	esdrControlBar			= 0x44,
	esdrRGBControlBar		= 0x43,
} ESynapticsDeviceResponse;

enum EStatusRequestSequence {
	esrIdentifySynaptics		= 0x00,
	esrReadTouchPadModes		= 0x01,
	esrReadModeByte			= 0x01,
	esrReadEdgeMargins		= 0x02,
	esrReadCapabilities		= 0x02,
	esrReadModelID			= 0x03,
	esrReadCompilationDate		= 0x04,
	esrReadSerialNumberPrefix	= 0x06,
	esrReadSerialNumberSuffix	= 0x07,
	esrReadResolutions		= 0x08,
	esrReadExtraCapabilities1	= 0x09,
	esrReadExtraCapabilities2	= 0x0A,
	esrReadExtraCapabilities3	= 0x0B,
	esrReadExtraCapabilities4	= 0x0C,
	esrReadExtraCapabilities5	= 0x0D,
	esrReadCoordinates		= 0x0D,
	esrReadExtraCapabilities6	= 0x0E,
	esrReadExtraCapabilities7	= 0x0F,
};

enum EPS2DataPortStatus {
	edpsAcknowledge			= 0xFA,
	edpsError			= 0xFC,
	edpsResend			= 0xFE,
	edpsTimeOut			= 0x100
};

enum ESetSampleRateSequence {
	essrSetModeByte1		= 0x0A,
	essrSetModeByte2		= 0x14,
	essrSetModeByte3		= 0x28,
	essrSetModeByte4		= 0x3C,
	essrSetDeluxeModeByte1		= 0x0A,
	essrSetDeluxeModeByte2		= 0x3C,
	essrSetDeluxeModeByte3		= 0xC8,
	essrFastRecalibrate		= 0x50,
	essrPassThroughCommandTunnel	= 0x28
};

enum EDeviceType {
	edtUnknown,
	edtTouchPad,
};

enum EStickDeviceType {
	esdtNone			= 0,
	esdtIBM,
	esdtJYTSyna			= 5,
	esdtSynaptics			= 6,
	esdtUnknown			= 0xFFFFFFFF
};

static gboolean
fu_synaptics_rmi_ps2_device_read_ack (FuSynapticsRmiPs2Device *self,
				      guint8 *pbuf,
				      GError **error)
{
	for(guint i = 0 ; i < 60; i++) {
		g_autoptr(GError) error_local = NULL;
		if (!fu_io_channel_read_raw (self->io_channel, pbuf, 0x1,
					     NULL, 10,
					     FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO,
					     &error_local)) {
			if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) {
				g_warning ("read timed out: %u", i);
				g_usleep (30);
				continue;
			}
			g_propagate_error (error, g_steal_pointer (&error_local));
			return FALSE;
		}
		return TRUE;
	}
	g_set_error (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, "read timed out");
	return FALSE;
}

/* read a single byte from the touchpad */
static gboolean
fu_synaptics_rmi_ps2_device_read_byte (FuSynapticsRmiPs2Device *self,
				       guint8 *pbuf,
				       guint timeout,
				       GError **error)
{
	g_return_val_if_fail (timeout > 0, FALSE);
	return fu_io_channel_read_raw (self->io_channel, pbuf, 0x1,
				       NULL, timeout,
				       FU_IO_CHANNEL_FLAG_NONE,
				       error);
}

/* write a single byte to the touchpad and the read the acknowledge */
static gboolean
fu_synaptics_rmi_ps2_device_write_byte (FuSynapticsRmiPs2Device *self,
					guint8 buf,
					guint timeout,
					FuSynapticsRmiDeviceFlags flags,
					GError **error)
{
	gboolean do_write = TRUE;
	g_return_val_if_fail (timeout > 0, FALSE);
	for (guint i = 0; ; i++) {
		guint8 res = 0;
		g_autoptr(GError) error_local = NULL;
		if (do_write) {
			if (!fu_io_channel_write_raw (self->io_channel,
						      &buf, sizeof(buf),
						      timeout,
						      FU_IO_CHANNEL_FLAG_FLUSH_INPUT |
						      FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO,
						      error))
				return FALSE;
		}
		do_write = FALSE;

		for (;;) {
			/* attempt to read acknowledge... */
			if (!fu_synaptics_rmi_ps2_device_read_ack (self, &res, &error_local)) {
				if (i > 3) {
					g_propagate_prefixed_error (error,
							    g_steal_pointer (&error_local),
							    "read ack failed: ");
					return FALSE;
				}
				g_warning ("read ack failed: %s, retrying", error_local->message);
				break;
			}
			if (res == edpsAcknowledge)
				return TRUE;
			if (res == edpsResend) {
				do_write = TRUE;
				g_debug ("resend");
				g_usleep (G_USEC_PER_SEC);
				break;
			}
			if (res == edpsError) {
				do_write = TRUE;
				g_debug ("error");
				g_usleep (1000 * 10);
				break;
			}
			g_debug ("other response: 0x%x", res);
			g_usleep (1000 * 10);
		}
		if (i >= 3) {
			if (flags & FU_SYNAPTICS_RMI_DEVICE_FLAG_ALLOW_FAILURE) {
				/* just break without error return because FW
				 * will not return ACK for commands like RESET */
				break;
			}
			g_set_error_literal (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_NOT_SUPPORTED,
					     "cannot write byte after retries");
			return FALSE;
		}
	}

	/* success */
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_set_resolution_sequence (FuSynapticsRmiPs2Device *self,
						     guint8 arg,
						     gboolean send_e6s,
						     GError **error)
{
	/* send set scaling twice if send_e6s */
	for (gint i = send_e6s ? 2 : 1; i > 0; --i) {
		if (!fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxSetScaling1To1, 50,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     error))
			return FALSE;
	}
	for (gint i = 3; i >= 0; --i) {
		guint8 ucTwoBitArg = (arg >> (i * 2)) & 0x3;
		if (!fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxSetResolution, 50,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     error)) {
			return FALSE;
		}
		if (!fu_synaptics_rmi_ps2_device_write_byte (self, ucTwoBitArg, 50,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     error))
			return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_status_request_sequence (FuSynapticsRmiPs2Device *self,
						     guint8 ucArgument,
						     guint32 *buf,
						     GError **error)
{
	gboolean success = FALSE;

	/* allow 3 retries */
	for (guint i = 0; i < 3; ++i) {
		g_autoptr(GError) error_local = NULL;
		if (!fu_synaptics_rmi_ps2_device_set_resolution_sequence (self,
									  ucArgument,
									  FALSE,
									  &error_local)) {
			g_debug ("failed set try #%u: %s",
				 i, error_local->message);
			continue;
		}
		if (!fu_synaptics_rmi_ps2_device_write_byte (self,
							     edpAuxStatusRequest,
							     10,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     &error_local)) {
			g_debug ("failed write try #%u: %s",
				 i, error_local->message);
			continue;
		}
		success = TRUE;
		break;
	}
	if (success == FALSE) {
		g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "failed");
		return FALSE;
	}

	/* read the response from the status request */
	for (gint i = 0; i < 3; ++i) {
		guint8 tmp = 0x0;
		if (!fu_synaptics_rmi_ps2_device_read_byte (self, &tmp, 10, error)) {
			g_prefix_error (error, "failed to read byte: ");
			return FALSE;
		}
		*buf = ((*buf) << 8) | tmp;
	}

	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_sample_rate_sequence (FuSynapticsRmiPs2Device *self,
						  guint8 param,
						  guint8 arg,
						  gboolean send_e6s,
						  GError **error)
{
	/* allow 3 retries */
	for (guint i = 0; ; i++) {
		g_autoptr(GError) error_local = NULL;
		if (i > 0) {
			/* always send two E6s when retrying */
			send_e6s = TRUE;
		}
		if (!fu_synaptics_rmi_ps2_device_set_resolution_sequence (self, arg, send_e6s, &error_local) ||
		    !fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxSetSampleRate, 50,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     &error_local) ||
		    !fu_synaptics_rmi_ps2_device_write_byte (self, param, 50,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     &error_local)) {
			if (i > 3) {
				g_propagate_error (error, g_steal_pointer (&error_local));
				return FALSE;
			}
			g_warning ("failed, will retry: %s", error_local->message);
			continue;
		}
		break;
	}
	/* success */
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_detect_synaptics_styk (FuSynapticsRmiPs2Device *self,
						   gboolean *result,
						   GError **error)
{
	guint8 buf;
	if (!fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxIBMReadSecondaryID, 10,
						     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
						     error)) {
		g_prefix_error (error, "failed to write IBMReadSecondaryID(0xE1): ");
		return FALSE;
	}
	if (!fu_synaptics_rmi_ps2_device_read_byte (self, &buf, 10, error)) {
		g_prefix_error (error, "failed to receive IBMReadSecondaryID: ");
		return FALSE;
	}
	if (buf == esdtJYTSyna || buf == esdtSynaptics)
		*result = TRUE;
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_query_build_id (FuSynapticsRmiDevice *rmi_device,
					    guint32 *build_id,
					    GError **error)
{
	FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE (rmi_device);
	guint32 buf = 0;
	gboolean is_synaptics_styk = FALSE;
	ESynapticsDeviceResponse esdr;

	if (!fu_synaptics_rmi_ps2_device_status_request_sequence (self,
								  esrIdentifySynaptics,
								  &buf,
								  error)) {
		g_prefix_error (error, "failed to request IdentifySynaptics: ");
		return FALSE;
	}
	g_debug ("identify Synaptics response = 0x%x", buf);

	esdr = (buf & 0xFF00) >> 8;
	if (!fu_synaptics_rmi_ps2_device_detect_synaptics_styk (self,
								&is_synaptics_styk,
								error)) {
		g_prefix_error (error, "failed to detect Synaptics styk: ");
		return FALSE;
	}
	fu_synaptics_rmi_device_set_iepmode (rmi_device, FALSE);
	if (esdr == esdrTouchPad || is_synaptics_styk) {
		/* Get the firmware id from the Extra Capabilities 2 Byte
		 * The firmware id is located in bits 0 - 23 */
		if (!fu_synaptics_rmi_ps2_device_status_request_sequence (self,
									  esrReadExtraCapabilities2,
									  build_id,
									  error)) {
			g_prefix_error (error, "failed to read extraCapabilities2: ");
			return FALSE;
		}
	}
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_query_product_sub_id (FuSynapticsRmiDevice *rmi_device,
						  guint8 *sub_id,
						  GError **error)
{
	FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE (rmi_device);
	guint32 buf = 0;
	if (!fu_synaptics_rmi_ps2_device_status_request_sequence (self, esrReadCapabilities, &buf, error)) {
		g_prefix_error (error, "failed to status_request_sequence read esrReadCapabilities: ");
		return FALSE;
	}
	*sub_id = (buf >> 8) & 0xFF;
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_enter_iep_mode (FuSynapticsRmiDevice *rmi_device,
					    GError **error)
{
	FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE (rmi_device);

	/* disable stream */
	if (!fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxDisable, 50,
						     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
						     error)) {
		g_prefix_error (error, "failed to disable stream mode: ");
		return FALSE;
	}

	/* enable RMI mode */
	if (!fu_synaptics_rmi_ps2_device_sample_rate_sequence (self,
							       essrSetModeByte2,
							       edpAuxFullRMIBackDoor,
							       FALSE,
							       error)) {
		g_prefix_error (error, "failed to enter RMI mode: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_write_rmi_register (FuSynapticsRmiPs2Device *self,
						guint8 addr,
						const guint8 *buf,
						guint8 buflen,
						guint timeout,
						FuSynapticsRmiDeviceFlags flags,
						GError **error)
{
	g_return_val_if_fail (timeout > 0, FALSE);

	if (!fu_synaptics_rmi_device_enter_iep_mode (FU_SYNAPTICS_RMI_DEVICE (self), error))
		return FALSE;
	if (!fu_synaptics_rmi_ps2_device_write_byte (self,
						     edpAuxSetScaling2To1,
						     timeout,
						     flags,
						     error)) {
		g_prefix_error (error, "failed to edpAuxSetScaling2To1: ");
		return FALSE;
	}
	if (!fu_synaptics_rmi_ps2_device_write_byte (self,
						     edpAuxSetSampleRate,
						     timeout,
						     flags,
						     error)) {
		g_prefix_error (error, "failed to edpAuxSetSampleRate: ");
		return FALSE;
	}
	if (!fu_synaptics_rmi_ps2_device_write_byte (self,
						     addr,
						     timeout,
						     flags,
						     error)) {
		g_prefix_error (error, "failed to write address: ");
		return FALSE;
	}
	for (guint8 i = 0; i < buflen; i++) {
		if (!fu_synaptics_rmi_ps2_device_write_byte (self,
							     edpAuxSetSampleRate,
							     timeout,
							     flags,
							     error)) {
			g_prefix_error (error, "failed to set byte %u: ", i);
			return FALSE;
		}
		if (!fu_synaptics_rmi_ps2_device_write_byte (self,
							     buf[i],
							     timeout,
							     flags,
							     error)) {
			g_prefix_error (error, "failed to write byte %u: ", i);
			return FALSE;
		}
	}

	/* success */
	g_usleep (1000 * 20);
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_read_rmi_register (FuSynapticsRmiPs2Device *self,
					       guint8 addr,
					       guint8 *buf,
					       GError **error)
{
	g_return_val_if_fail (buf != NULL, FALSE);

	if (!fu_synaptics_rmi_device_enter_iep_mode (FU_SYNAPTICS_RMI_DEVICE (self), error))
		return FALSE;
	for (guint retries = 0; ; retries++) {
		g_autoptr(GError) error_local = NULL;
		if (!fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxSetScaling2To1, 50,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     error) ||
		    !fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxSetSampleRate, 50,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     error) ||
		    !fu_synaptics_rmi_ps2_device_write_byte (self, addr, 50,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     error) ||
		    !fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxStatusRequest, 50,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     error)) {
			g_prefix_error (error, "failed to write command in Read RMI register: ");
			return FALSE;
		}
		if (!fu_synaptics_rmi_ps2_device_read_byte (self, buf, 10, &error_local)) {
			if (retries++ > 2) {
				g_propagate_prefixed_error (error,
							    g_steal_pointer (&error_local),
							    "failed to read byte @0x%x after %u retries: ",
							    addr, retries);
				return FALSE;
			}
			g_debug ("failed to read byte @0x%x: %s",
				 addr,
				 error_local->message);
			continue;
		}

		/* success */
		break;
	}

	/* success */
	g_usleep (1000 * 20);
	return TRUE;
}

static GByteArray *
fu_synaptics_rmi_ps2_device_read_rmi_packet_register (FuSynapticsRmiPs2Device *self,
						      guint8 addr,
						      guint req_sz,
						      GError **error)
{
	g_autoptr(GByteArray) buf = g_byte_array_new ();

	if (!fu_synaptics_rmi_ps2_device_enter_iep_mode (FU_SYNAPTICS_RMI_DEVICE (self), error))
		return NULL;
	if (!fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxSetScaling2To1, 50,
						     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
						     error) ||
	    !fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxSetSampleRate, 50,
						     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
						     error) ||
	    !fu_synaptics_rmi_ps2_device_write_byte (self, addr,
						     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
						     FALSE, error) ||
	    !fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxStatusRequest,
						     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
						     FALSE, error)) {
		g_prefix_error (error, "failed to write command in Read RMI Packet Register: ");
		return NULL;
	}
	for (guint i = 0; i < req_sz; ++i) {
		guint8 tmp = 0;
		if (!fu_synaptics_rmi_ps2_device_read_byte (self, &tmp, 10, error)) {
			g_prefix_error (error, "failed to read byte %u: ", i);
			return NULL;
		}
		fu_byte_array_append_uint8 (buf, tmp);
	}

	g_usleep (1000 * 20);
	return g_steal_pointer (&buf);
}

static gboolean
fu_synaptics_rmi_ps2_device_query_status (FuSynapticsRmiDevice *rmi_device,
					  GError **error)
{
	FuSynapticsRmiFunction *f34;
	g_debug ("ps2 query status");
	f34 = fu_synaptics_rmi_device_get_function (rmi_device, 0x34, error);
	if (f34 == NULL)
		return FALSE;
	if (f34->function_version == 0x0 ||
	    f34->function_version == 0x1) {
		return fu_synaptics_rmi_v5_device_query_status (rmi_device, error);
	}
	if (f34->function_version == 0x2) {
		return fu_synaptics_rmi_v7_device_query_status (rmi_device, error);
	}
	g_set_error (error,
		     FWUPD_ERROR,
		     FWUPD_ERROR_NOT_SUPPORTED,
		     "f34 function version 0x%02x unsupported",
		     f34->function_version);
	return FALSE;
}

static gboolean
fu_synaptics_rmi_ps2_device_set_page (FuSynapticsRmiDevice *rmi_device,
				      guint8 page,
				      GError **error)
{
	FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE (rmi_device);
	if (!fu_synaptics_rmi_ps2_device_write_rmi_register (self,
							     RMI_DEVICE_PAGE_SELECT_REGISTER,
							     &page,
							     1,
							     20,
							     FALSE,
							     error)) {
		g_prefix_error (error, "failed to write page %u: ", page);
		return FALSE;
	}
	return TRUE;
}

static GByteArray *
fu_synaptics_rmi_ps2_device_read (FuSynapticsRmiDevice *rmi_device,
				  guint16 addr,
				  gsize req_sz,
				  GError **error)
{
	FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE (rmi_device);
	g_autoptr(GByteArray) buf = NULL;
	g_autofree gchar *dump = NULL;

	if (!fu_synaptics_rmi_device_set_page (rmi_device,
					       addr >> 8,
					       error)) {
		g_prefix_error (error, "failed to set RMI page:");
		return NULL;
	}

	for (guint retries = 0; ; retries++) {
		buf = g_byte_array_new ();
		for (guint i = 0; i < req_sz; i++) {
			guint8 tmp = 0x0;
			if (!fu_synaptics_rmi_ps2_device_read_rmi_register (self,
									    (guint8) ((addr & 0x00FF) + i),
									    &tmp,
									    error)) {
				g_prefix_error (error,
						"failed register read 0x%x: ",
						addr + i);
				return NULL;
			}
			fu_byte_array_append_uint8 (buf, tmp);
		}
		if (buf->len != req_sz) {
			g_debug ("buf->len(%u) != req_sz(%u)", buf->len, (guint) req_sz);
			if (retries++ > 2) {
				g_set_error (error,
					     G_IO_ERROR, G_IO_ERROR_FAILED,
					     "buffer length did not match: %u vs %u",
					     buf->len, (guint) req_sz);
				return NULL;
			}
			continue;
		}
		break;
	}
	dump = g_strdup_printf ("R %x", addr);
	if (g_getenv ("FWUPD_SYNAPTICS_RMI_VERBOSE") != NULL) {
		fu_common_dump_full (G_LOG_DOMAIN, dump,
				     buf->data, buf->len,
				     80, FU_DUMP_FLAGS_NONE);
	}
	return g_steal_pointer (&buf);
}

static GByteArray *
fu_synaptics_rmi_ps2_device_read_packet_register (FuSynapticsRmiDevice *rmi_device,
						  guint16 addr,
						  gsize req_sz,
						  GError **error)
{
	FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE (rmi_device);
	g_autoptr(GByteArray) buf = NULL;

	if (!fu_synaptics_rmi_device_set_page (rmi_device,
					       addr >> 8,
					       error)) {
		g_prefix_error (error, "failed to set RMI page:");
		return NULL;
	}

	buf = fu_synaptics_rmi_ps2_device_read_rmi_packet_register (self,
								    addr,
								    req_sz,
								    error);
	if (buf == NULL) {
		g_prefix_error (error,
				"failed packet register read %x: ",
				addr);
		return NULL;
	}

	if (g_getenv ("FWUPD_SYNAPTICS_RMI_VERBOSE") != NULL) {
		g_autofree gchar *dump = g_strdup_printf ("R %x", addr);
		fu_common_dump_full (G_LOG_DOMAIN, dump,
				     buf->data, buf->len,
				     80, FU_DUMP_FLAGS_NONE);
	}
	return g_steal_pointer (&buf);
}

static gboolean
fu_synaptics_rmi_ps2_device_write (FuSynapticsRmiDevice *rmi_device,
				   guint16 addr,
				   GByteArray *req,
				   FuSynapticsRmiDeviceFlags flags,
				   GError **error)
{
	FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE (rmi_device);
	if (!fu_synaptics_rmi_device_set_page (rmi_device,
					       addr >> 8,
					       error)) {
		g_prefix_error (error, "failed to set RMI page: ");
		return FALSE;
	}
	if (!fu_synaptics_rmi_ps2_device_write_rmi_register (self,
							     addr & 0x00FF,
							     req->data,
							     req->len,
							     1000, /* timeout */
							     flags,
							     error)) {
		g_prefix_error (error,
				"failed to write register %x: ",
				addr);
		return FALSE;
	}
	if (g_getenv ("FWUPD_SYNAPTICS_RMI_VERBOSE") != NULL) {
		g_autofree gchar *str = g_strdup_printf ("W %x", addr);
		fu_common_dump_full (G_LOG_DOMAIN, str,
				     req->data, req->len,
				     80, FU_DUMP_FLAGS_NONE);
	}
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_write_bus_select (FuSynapticsRmiDevice *rmi_device,
					      guint8 bus,
					      GError **error)
{
	g_autoptr(GByteArray) req = g_byte_array_new ();
	fu_byte_array_append_uint8 (req, bus);
	if (!fu_synaptics_rmi_ps2_device_write (rmi_device,
						RMI_DEVICE_BUS_SELECT_REGISTER,
						req,
						FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
						error)) {
		g_prefix_error (error, "failed to write rmi register %u: ", bus);
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_probe (FuDevice *device, GError **error)
{
	/* FuUdevDevice->probe */
	if (!FU_DEVICE_CLASS (fu_synaptics_rmi_ps2_device_parent_class)->probe (device, error))
		return FALSE;

	/* psmouse is the usual mode, but serio is needed for update */
	if (g_strcmp0 (fu_udev_device_get_driver (FU_UDEV_DEVICE (device)), "serio_raw") == 0) {
		fu_device_add_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
	} else {
		fu_device_remove_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
	}

	/* set the physical ID */
	return fu_udev_device_set_physical_id (FU_UDEV_DEVICE (device), "platform", error);
}

static gboolean
fu_synaptics_rmi_ps2_device_open (FuDevice *device, GError **error)
{
	FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE (device);
	guint8 buf[2] = { 0x0 };

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

	/* create channel */
	self->io_channel = fu_io_channel_unix_new (fu_udev_device_get_fd (FU_UDEV_DEVICE (device)));

	/* in serio_raw mode */
	if (fu_device_has_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {

		/* clear out any data in the serio_raw queue */
		for(guint i = 0; i < 0xffff; i++) {
			guint8 tmp = 0;
			if (!fu_synaptics_rmi_ps2_device_read_byte (self, &tmp, 20, NULL))
				break;
		}

		/* send reset -- may take 300-500ms */
		if (!fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxReset, 600,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     error)) {
			g_prefix_error (error, "failed to reset: ");
			return FALSE;
		}

		/* read the 0xAA 0x00 announcing the touchpad is ready */
		if (!fu_synaptics_rmi_ps2_device_read_byte(self, &buf[0], 500, error) ||
		    !fu_synaptics_rmi_ps2_device_read_byte(self, &buf[1], 500, error)) {
			g_prefix_error (error, "failed to read 0xAA00: ");
			return FALSE;
		}
		if (buf[0] != 0xAA || buf[1] != 0x00) {
			g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
				     "failed to read 0xAA00, got 0x%02X%02X: ",
				     buf[0], buf[1]);
			return FALSE;
		}

		/* disable the device so that it stops reporting finger data */
		if (!fu_synaptics_rmi_ps2_device_write_byte (self, edpAuxDisable, 50,
							     FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE,
							     error)) {
			g_prefix_error (error, "failed to disable stream mode: ");
			return FALSE;
		}
	}

	/* success */
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_close (FuDevice *device, GError **error)
{
	FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE (device);
	fu_udev_device_set_fd (FU_UDEV_DEVICE (device), -1);
	g_clear_object (&self->io_channel);

	/* FuUdevDevice->close */
	return FU_DEVICE_CLASS (fu_synaptics_rmi_ps2_device_parent_class)->close (device, error);}

static gboolean
fu_synaptics_rmi_ps2_device_detach (FuDevice *device, GError **error)
{
	FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE (device);
	FuSynapticsRmiFunction *f34;

	/* sanity check */
	if (fu_device_has_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
		g_debug ("already in bootloader mode, skipping");
		return TRUE;
	}

	/* put in serio_raw mode so that we can do register writes */
	if (!fu_udev_device_write_sysfs (FU_UDEV_DEVICE (device),
					 "drvctl", "serio_raw", error)) {
		g_prefix_error (error, "failed to write to drvctl: ");
		return FALSE;
	}

	/* rescan device */
	if (!fu_device_close (device, error))
		return FALSE;
	if (!fu_device_rescan (device, error))
		return FALSE;
	if (!fu_device_open (device, error))
		return FALSE;

	f34 = fu_synaptics_rmi_device_get_function (self, 0x34, error);
	if (f34 == NULL)
		return FALSE;
	if (f34->function_version == 0x0 ||
	    f34->function_version == 0x1) {
		if (!fu_synaptics_rmi_v5_device_detach (device, error))
			return FALSE;
	} else if (f34->function_version == 0x2) {
		if (!fu_synaptics_rmi_v7_device_detach (device, error))
			return FALSE;
	} else {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "f34 function version 0x%02x unsupported",
			     f34->function_version);
		return FALSE;
	}

	if (!fu_synaptics_rmi_device_enter_iep_mode (self, error)) {
		return FALSE;
	}

	if (!fu_synaptics_rmi_ps2_device_query_status (self, error)) {
		g_prefix_error (error, "failed to query status after detach: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_synaptics_rmi_ps2_device_setup (FuDevice *device, GError **error)
{
	/* we can only scan the PDT in serio_raw mode */
	if (!fu_device_has_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER))
		return TRUE;
	return FU_DEVICE_CLASS (fu_synaptics_rmi_ps2_device_parent_class)->setup (device, error);
}

static gboolean
fu_synaptics_rmi_ps2_device_attach (FuDevice *device, GError **error)
{
	FuSynapticsRmiDevice *rmi_device = FU_SYNAPTICS_RMI_DEVICE (device);

	/* sanity check */
	if (!fu_device_has_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
		g_debug ("already in runtime mode, skipping");
		return TRUE;
	}

	/* Set iepmode before reset device forcibly because of FW requirement */
	fu_synaptics_rmi_device_set_iepmode (rmi_device, FALSE);

	/* delay after writing */
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);
	fu_device_sleep_with_progress (device, 2);

	/* reset device */
	if (!fu_synaptics_rmi_device_enter_iep_mode (rmi_device, error))
		return FALSE;
	if (!fu_synaptics_rmi_device_reset (rmi_device, error)) {
		g_prefix_error (error, "failed to reset device: ");
		return FALSE;
	}

	/* delay after reset */
	fu_device_sleep_with_progress (device, 5);

	/* back to psmouse */
	if (!fu_udev_device_write_sysfs (FU_UDEV_DEVICE (device),
					 "drvctl", "psmouse", error)) {
		g_prefix_error (error, "failed to write to drvctl: ");
		return FALSE;
	}

	/* rescan device */
	return fu_device_rescan (device, error);
}

static void
fu_synaptics_rmi_ps2_device_init (FuSynapticsRmiPs2Device *self)
{
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_INTERNAL);
	fu_device_set_name (FU_DEVICE (self), "TouchStyk");
	fu_device_set_vendor (FU_DEVICE (self), "Synaptics");
	fu_device_add_vendor_id (FU_DEVICE (self), "HIDRAW:0x06CB");
	fu_synaptics_rmi_device_set_max_page (FU_SYNAPTICS_RMI_DEVICE (self), 0x1);
	fu_udev_device_set_flags (FU_UDEV_DEVICE (self),
				  FU_UDEV_DEVICE_FLAG_OPEN_READ |
				  FU_UDEV_DEVICE_FLAG_OPEN_WRITE);
}

static gboolean
fu_synaptics_rmi_ps2_device_wait_for_attr (FuSynapticsRmiDevice *rmi_device,
					   guint8 source_mask,
					   guint timeout_ms,
					   GError **error)
{
	g_usleep (1000 * timeout_ms);
	return TRUE;
}

static void
fu_synaptics_rmi_ps2_device_class_init (FuSynapticsRmiPs2DeviceClass *klass)
{
	FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
	FuSynapticsRmiDeviceClass *klass_rmi = FU_SYNAPTICS_RMI_DEVICE_CLASS (klass);
	klass_device->attach = fu_synaptics_rmi_ps2_device_attach;
	klass_device->detach = fu_synaptics_rmi_ps2_device_detach;
	klass_device->setup = fu_synaptics_rmi_ps2_device_setup;
	klass_device->probe = fu_synaptics_rmi_ps2_device_probe;
	klass_device->open = fu_synaptics_rmi_ps2_device_open;
	klass_device->close = fu_synaptics_rmi_ps2_device_close;
	klass_rmi->read = fu_synaptics_rmi_ps2_device_read;
	klass_rmi->write = fu_synaptics_rmi_ps2_device_write;
	klass_rmi->set_page = fu_synaptics_rmi_ps2_device_set_page;
	klass_rmi->query_status = fu_synaptics_rmi_ps2_device_query_status;
	klass_rmi->query_build_id = fu_synaptics_rmi_ps2_device_query_build_id;
	klass_rmi->query_product_sub_id = fu_synaptics_rmi_ps2_device_query_product_sub_id;
	klass_rmi->wait_for_attr = fu_synaptics_rmi_ps2_device_wait_for_attr;
	klass_rmi->enter_iep_mode = fu_synaptics_rmi_ps2_device_enter_iep_mode;
	klass_rmi->write_bus_select = fu_synaptics_rmi_ps2_device_write_bus_select;
	klass_rmi->read_packet_register = fu_synaptics_rmi_ps2_device_read_packet_register;
}
