/*
 * Copyright (C) 2020 Benson Leung <bleung@chromium.org>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <string.h>

#include "fu-chunk.h"
#include "fu-cros-ec-usb-device.h"
#include "fu-cros-ec-common.h"
#include "fu-cros-ec-firmware.h"

#define USB_SUBCLASS_GOOGLE_UPDATE	0x53
#define USB_PROTOCOL_GOOGLE_UPDATE	0xff

#define SETUP_RETRY_CNT			5
#define MAX_BLOCK_XFER_RETRIES		10
#define FLUSH_TIMEOUT_MS		10
#define BULK_SEND_TIMEOUT_MS		2000
#define BULK_RECV_TIMEOUT_MS		5000
#define CROS_EC_REMOVE_DELAY_RE_ENUMERATE              20000

#define UPDATE_DONE			0xB007AB1E
#define UPDATE_EXTRA_CMD		0xB007AB1F

enum update_extra_command {
        UPDATE_EXTRA_CMD_IMMEDIATE_RESET = 0,
        UPDATE_EXTRA_CMD_JUMP_TO_RW = 1,
        UPDATE_EXTRA_CMD_STAY_IN_RO = 2,
        UPDATE_EXTRA_CMD_UNLOCK_RW = 3,
        UPDATE_EXTRA_CMD_UNLOCK_ROLLBACK = 4,
        UPDATE_EXTRA_CMD_INJECT_ENTROPY = 5,
        UPDATE_EXTRA_CMD_PAIR_CHALLENGE = 6,
        UPDATE_EXTRA_CMD_TOUCHPAD_INFO = 7,
        UPDATE_EXTRA_CMD_TOUCHPAD_DEBUG = 8,
        UPDATE_EXTRA_CMD_CONSOLE_READ_INIT = 9,
        UPDATE_EXTRA_CMD_CONSOLE_READ_NEXT = 10,
};

struct _FuCrosEcUsbDevice {
	FuUsbDevice			parent_instance;
	guint8				iface_idx; 	/* bInterfaceNumber */
	guint8				ep_num;		/* bEndpointAddress */
	guint16				chunk_len; 	/* wMaxPacketSize */

	struct first_response_pdu	targ;
	guint32				writeable_offset;
	guint16				protocol_version;
	guint16				header_type;
	struct cros_ec_version		version;	/* version of other region */
	struct cros_ec_version		active_version; /* version of active region */
	gchar 				configuration[FU_CROS_EC_STRLEN];
	gboolean			in_bootloader;
};

G_DEFINE_TYPE (FuCrosEcUsbDevice, fu_cros_ec_usb_device, FU_TYPE_USB_DEVICE)

typedef union _START_RESP {
	struct first_response_pdu rpdu;
	guint32 legacy_resp;
} START_RESP;

typedef struct {
	struct update_frame_header ufh;
	GBytes *image_bytes;
	gsize offset;
	gsize payload_size;
} FuCrosEcUsbBlockInfo;

static gboolean
fu_cros_ec_usb_device_get_configuration (FuCrosEcUsbDevice *self,
					 GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
	guint8 index;
	g_autofree gchar *configuration = NULL;

#if G_USB_CHECK_VERSION(0,3,5)
	index = g_usb_device_get_configuration_index (usb_device);
#else
	g_set_error_literal (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "this version of GUsb is not supported");
	return FALSE;
#endif
	configuration = g_usb_device_get_string_descriptor (usb_device,
							    index,
							    error);
	if (configuration == NULL)
		return FALSE;

	if (g_strlcpy (self->configuration, configuration, FU_CROS_EC_STRLEN) == 0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INTERNAL,
				     "empty iConfiguration");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_find_interface (FuUsbDevice *device,
				      GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (device);
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);
	g_autoptr(GPtrArray) intfs = NULL;

	/* based on usb_updater2's find_interfacei() and find_endpoint() */

	intfs = g_usb_device_get_interfaces (usb_device, error);
	if (intfs == NULL)
		return FALSE;
	for (guint i = 0; i < intfs->len; i++) {
		GUsbInterface *intf = g_ptr_array_index (intfs, i);
		if (g_usb_interface_get_class (intf) == 255 &&
		    g_usb_interface_get_subclass (intf) == USB_SUBCLASS_GOOGLE_UPDATE &&
		    g_usb_interface_get_protocol (intf) == USB_PROTOCOL_GOOGLE_UPDATE) {
			GUsbEndpoint *ep;
			g_autoptr(GPtrArray) endpoints = NULL;

			endpoints = g_usb_interface_get_endpoints (intf);
			if (NULL == endpoints || 0 == endpoints->len)
				continue;
			ep = g_ptr_array_index (endpoints, 0);
			self->iface_idx = g_usb_interface_get_number (intf);
			self->ep_num = g_usb_endpoint_get_address (ep) & 0x7f;
			self->chunk_len = g_usb_endpoint_get_maximum_packet_size (ep);

			return TRUE;
		}
	}
	g_set_error_literal (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_FOUND,
			     "no update interface found");
	return FALSE;
}

static gboolean
fu_cros_ec_usb_device_open (FuDevice *device, GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);

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

	if (!g_usb_device_claim_interface (usb_device, self->iface_idx,
					   G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
					   error)) {
		g_prefix_error (error, "failed to claim interface: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_probe (FuDevice *device, GError **error)
{
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);

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

	/* very much like usb_updater2's usb_findit() */

	if (!fu_cros_ec_usb_device_find_interface (FU_USB_DEVICE (device), error)) {
		g_prefix_error (error, "failed to find update interface: ");
		return FALSE;
	}

	if (self->chunk_len == 0) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_INVALID_DATA,
			     "wMaxPacketSize isn't valid: %" G_GUINT16_FORMAT,
			     self->chunk_len);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_do_xfer (FuCrosEcUsbDevice * self, const guint8 *outbuf,
			       gsize outlen, guint8 *inbuf, gsize inlen,
			       gboolean allow_less, gsize *rxed_count,
			       GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
	gsize actual = 0;
	g_autofree guint8 *outbuf_tmp = NULL;

	/* make mutable */
	outbuf_tmp = fu_memdup_safe (outbuf, outlen, error);
	if (outbuf_tmp == NULL)
		return FALSE;

	/* send data out */
	if (outbuf != NULL && outlen > 0) {
		if (!g_usb_device_bulk_transfer (usb_device, self->ep_num,
						 outbuf_tmp, outlen,
						 &actual, BULK_SEND_TIMEOUT_MS,
						 NULL, error)) {
			return FALSE;
		}
		if (actual != outlen) {
			g_set_error (error,
				     G_IO_ERROR,
				     G_IO_ERROR_PARTIAL_INPUT,
				     "only sent %" G_GSIZE_FORMAT "/%"
				     G_GSIZE_FORMAT " bytes",
				     actual, outlen);
			return FALSE;
		}
	}

	/* read reply back */
	if (inbuf != NULL && inlen > 0) {
		actual = 0;
		if (!g_usb_device_bulk_transfer (usb_device,
						 self->ep_num | 0x80,
						 inbuf, inlen,
						 &actual, BULK_RECV_TIMEOUT_MS,
						 NULL, error)) {
			return FALSE;
		}
		if (actual != inlen && !allow_less) {
			g_set_error (error,
				     G_IO_ERROR,
				     G_IO_ERROR_PARTIAL_INPUT,
				     "only received %" G_GSIZE_FORMAT "/%"
				     G_GSIZE_FORMAT " bytes",
				     actual, outlen);
			return FALSE;
		}
	}

	if (rxed_count != NULL)
		*rxed_count = actual;

	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_flush (FuDevice *device, gpointer user_data,
			     GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);
	gsize actual = 0;
	g_autofree guint8 *inbuf = g_malloc0 (self->chunk_len);

	if (g_usb_device_bulk_transfer (usb_device, self->ep_num | 0x80, inbuf,
					self->chunk_len, &actual,
					FLUSH_TIMEOUT_MS, NULL, NULL)) {
		g_debug ("flushing %" G_GSIZE_FORMAT " bytes", actual);
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "flushing %" G_GSIZE_FORMAT " bytes", actual);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_recovery (FuDevice *device, GError **error)
{
	/* flush all data from endpoint to recover in case of error */
	if (!fu_device_retry (device, fu_cros_ec_usb_device_flush,
			      SETUP_RETRY_CNT, NULL, error)) {
		g_prefix_error (error, "failed to flush device to idle state: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

/*
 * Channel TPM extension/vendor command over USB. The payload of the USB frame
 * in this case consists of the 2 byte subcommand code concatenated with the
 * command body. The caller needs to indicate if a response is expected, and
 * if it is - of what maximum size.
 */
static gboolean
fu_cros_ec_usb_ext_cmd (FuDevice *device, guint16 subcommand,
			gpointer cmd_body, gsize body_size,
			gpointer resp, gsize *resp_size,
			gboolean allow_less, GError **error)
{
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);
	guint16 *frame_ptr;
	gsize usb_msg_size = sizeof (struct update_frame_header) +
			     sizeof (subcommand) + body_size;
	g_autofree struct update_frame_header *ufh = g_malloc0 (usb_msg_size);

	ufh->block_size = GUINT32_TO_BE (usb_msg_size);
	ufh->cmd.block_digest = 0;
	ufh->cmd.block_base = GUINT32_TO_BE (UPDATE_EXTRA_CMD);
	frame_ptr = (guint16 *)(ufh + 1);
	*frame_ptr = GUINT16_TO_BE (subcommand);

	if (body_size != 0) {
		gsize offset =  sizeof (struct update_frame_header) + sizeof (subcommand);
		if (!fu_memcpy_safe ((guint8 *) ufh, usb_msg_size, offset,
				     (const guint8 *) cmd_body, body_size,
				     0x0, body_size, error))
			return FALSE;
	}

	return fu_cros_ec_usb_device_do_xfer (self, (const guint8 *)ufh, usb_msg_size,
					      (guint8 *)resp,
					      resp_size != NULL ? *resp_size : 0,
					      TRUE, NULL, error);
}

static gboolean
fu_cros_ec_usb_device_start_request (FuDevice *device, gpointer user_data,
				     GError **error)
{
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);
	guint8 *start_resp = (guint8 *) user_data;
	struct update_frame_header ufh;
	gsize rxed_size = 0;

	memset(&ufh, 0, sizeof (ufh));
	ufh.block_size = GUINT32_TO_BE (sizeof(ufh));
	if (!fu_cros_ec_usb_device_do_xfer (self, (const guint8 *)&ufh, sizeof(ufh),
					    start_resp,
					    sizeof(START_RESP), TRUE,
					    &rxed_size, error))
		return FALSE;

	/* we got something, so check for errors in response */
	if (rxed_size < 8) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_PARTIAL_INPUT,
			     "unexpected response size %" G_GSIZE_FORMAT,
			     rxed_size);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_setup (FuDevice *device, GError **error)
{
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);
	guint32 error_code;
	START_RESP start_resp;
	g_auto(GStrv) config_split = NULL;

	if (!fu_cros_ec_usb_device_recovery (device, error))
		return FALSE;

	/* send start request */
	if (!fu_device_retry (device, fu_cros_ec_usb_device_start_request,
			      SETUP_RETRY_CNT, &start_resp, error)) {
		g_prefix_error (error, "failed to send start request: ");
		return FALSE;
	}

	self->protocol_version = GUINT16_FROM_BE (start_resp.rpdu.protocol_version);

	if (self->protocol_version < 5 || self->protocol_version > 6) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_NOT_SUPPORTED,
			     "unsupported protocol version %d",
			     self->protocol_version);
		return FALSE;
	}
	self->header_type = GUINT16_FROM_BE (start_resp.rpdu.header_type);

	error_code = GUINT32_FROM_BE (start_resp.rpdu.return_value);
	if (error_code != 0) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "target reporting error %u", error_code);
		return FALSE;
	}

	self->writeable_offset = GUINT32_FROM_BE (start_resp.rpdu.common.offset);
	if (!fu_memcpy_safe ((guint8 *) self->targ.common.version,
			     FU_CROS_EC_STRLEN, 0x0,
			     (const guint8 *) start_resp.rpdu.common.version,
			     sizeof(start_resp.rpdu.common.version), 0x0,
			     sizeof(start_resp.rpdu.common.version), error))
		return FALSE;
	self->targ.common.maximum_pdu_size =
		GUINT32_FROM_BE (start_resp.rpdu.common.maximum_pdu_size);
	self->targ.common.flash_protection =
		GUINT32_FROM_BE (start_resp.rpdu.common.flash_protection);
	self->targ.common.min_rollback = GINT32_FROM_BE (start_resp.rpdu.common.min_rollback);
	self->targ.common.key_version = GUINT32_FROM_BE (start_resp.rpdu.common.key_version);

	/* get active version string and running region from iConfiguration */
	if (!fu_cros_ec_usb_device_get_configuration (self, error))
		return FALSE;
	config_split = g_strsplit (self->configuration, ":", 2);
	if (g_strv_length (config_split) < 2) {
		/* no prefix found so fall back to offset */
		self->in_bootloader = self->writeable_offset != 0x0;
		if (!fu_cros_ec_parse_version (self->configuration,
					       &self->active_version, error)) {
			g_prefix_error (error,
					"failed parsing device's version: %32s: ",
					self->configuration);
			return FALSE;
		}
	} else {
		self->in_bootloader = g_strcmp0 ("RO", config_split[0]) == 0;
		if (!fu_cros_ec_parse_version (config_split[1],
					       &self->active_version, error)) {
			g_prefix_error (error,
					"failed parsing device's version: %32s: ",
					config_split[1]);
			return FALSE;
		}
	}

	/* get the other region's version string from targ */
	if (!fu_cros_ec_parse_version (self->targ.common.version,
				       &self->version, error)) {
		g_prefix_error (error,
				"failed parsing device's version: %32s: ",
				self->targ.common.version);
		return FALSE;
	}

	if (self->in_bootloader) {
		fu_device_add_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
		fu_device_set_version (FU_DEVICE (device), self->version.triplet);
		fu_device_set_version_bootloader (FU_DEVICE (device),
						  self->active_version.triplet);
	} else {
		fu_device_remove_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
		fu_device_set_version (FU_DEVICE (device), self->active_version.triplet);
		fu_device_set_version_bootloader (FU_DEVICE (device),
						  self->version.triplet);
	}
	fu_device_add_instance_id (FU_DEVICE (device), self->version.boardname);

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_transfer_block (FuDevice *device, gpointer user_data,
				      GError **error)
{
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);
	FuCrosEcUsbBlockInfo *block_info = (FuCrosEcUsbBlockInfo *) user_data;
	gsize image_size = 0;
	gsize transfer_size = 0;
	guint32 reply = 0;
	g_autoptr(GBytes) block_bytes = NULL;
	g_autoptr(GPtrArray) chunks = NULL;

	g_return_val_if_fail (block_info != NULL, FALSE);

	image_size = g_bytes_get_size (block_info->image_bytes);
	if (block_info->offset + block_info->payload_size > image_size) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_INVALID_DATA,
			     "offset %" G_GSIZE_FORMAT "plus payload_size %"
			     G_GSIZE_FORMAT " exceeds image size %"
			     G_GSIZE_FORMAT,
			     block_info->offset, block_info->payload_size,
			     image_size);
		return FALSE;
	}

	block_bytes = fu_common_bytes_new_offset (block_info->image_bytes,
						  block_info->offset,
						  block_info->payload_size,
						  error);
	if (block_bytes == NULL)
		return FALSE;
	chunks = fu_chunk_array_new_from_bytes (block_bytes,
						0x00,
						0x00,
						self->chunk_len);

	/* first send the header */
	if (!fu_cros_ec_usb_device_do_xfer (self, (const guint8 *)&block_info->ufh,
					    sizeof(struct update_frame_header),
					    NULL,
					    0, FALSE,
					    NULL, error)) {
		/* flush all data from endpoint to recover in case of error */
		if (!fu_cros_ec_usb_device_recovery (device, NULL)) {
			g_debug ("failed to flush to idle");
		}
		g_prefix_error (error, "failed at sending header: ");
		return FALSE;
	}

	/* send the block, chunk by chunk */
	for (guint i = 0; i < chunks->len; i++) {
		FuChunk *chk = g_ptr_array_index (chunks, i);

		if (!fu_cros_ec_usb_device_do_xfer (self,
						    fu_chunk_get_data (chk),
						    fu_chunk_get_data_sz (chk),
						    NULL,
						    0, FALSE,
						    NULL, error)) {
			g_prefix_error (error, "failed at sending chunk: ");

			/* flush all data from endpoint to recover in case of error */
			if (!fu_cros_ec_usb_device_recovery (device, NULL)) {
				g_debug ("failed to flush to idle");
			}
			return FALSE;
		}
	}

	/* get the reply */
	if (!fu_cros_ec_usb_device_do_xfer (self, NULL, 0,
					    (guint8 *)&reply, sizeof (reply),
					    TRUE, &transfer_size, error)) {
		g_prefix_error (error, "failed at reply: ");
		/* flush all data from endpoint to recover in case of error */
		if (!fu_cros_ec_usb_device_recovery (device, NULL)) {
			g_debug ("failed to flush to idle");
		}
		return FALSE;
	}
	if (transfer_size == 0) {
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_FAILED,
				     "zero bytes received for block reply");
		return FALSE;
	}
	if (reply != 0) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_FAILED,
			     "error: status 0x%#x", reply);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_transfer_section (FuDevice *device,
					FuFirmware *firmware,
					FuCrosEcFirmwareSection *section,
					GError **error)
{
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);
	const guint8 * data_ptr = NULL;
	guint32 section_addr = 0;
	gsize data_len = 0;
	gsize offset = 0;
	g_autoptr(GBytes) img_bytes = NULL;

	g_return_val_if_fail (section != NULL, FALSE);

	section_addr = section->offset;
	img_bytes = fu_firmware_get_image_by_idx_bytes (firmware,
							section->image_idx,
							error);
	if (img_bytes == NULL) {
		g_prefix_error (error, "failed to find section image: ");
		return FALSE;
	}

	data_ptr = (const guint8 *)g_bytes_get_data (img_bytes, &data_len);
	if (data_ptr == NULL || data_len != section->size) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_INVALID_DATA,
			     "image and section sizes do not match: image = %"
			     G_GSIZE_FORMAT " bytes vs section size = %"
			     G_GSIZE_FORMAT " bytes",
			     data_len, section->size);
		return FALSE;
	}

	/* smart update: trim trailing bytes */
	while (data_len != 0 && (data_ptr[data_len - 1] == 0xff))
		data_len--;
	g_debug ("trimmed %" G_GSIZE_FORMAT " trailing bytes",
		 section->size - data_len);

	g_debug ("sending 0x%zx bytes to %#x", data_len, section_addr);
	while (data_len > 0) {
		gsize payload_size;
		guint32 block_base;
		FuCrosEcUsbBlockInfo block_info;

		/* prepare the header to prepend to the block */
		block_info.image_bytes = img_bytes;
		payload_size = MIN (data_len,
				    self->targ.common.maximum_pdu_size);
		block_base = GUINT32_TO_BE (section_addr);
		block_info.ufh.block_size = GUINT32_TO_BE (payload_size +
							   sizeof (struct update_frame_header));
		block_info.ufh.cmd.block_base = block_base;
		block_info.ufh.cmd.block_digest = 0;
		block_info.offset = offset;
		block_info.payload_size = payload_size;

		if (!fu_device_retry (device,
				      fu_cros_ec_usb_device_transfer_block,
				      MAX_BLOCK_XFER_RETRIES, &block_info,
				      error)) {
			g_prefix_error (error,
					"failed to transfer block, %"
					G_GSIZE_FORMAT " to go: ", data_len);
			return FALSE;
		}
		data_len -= payload_size;
		offset += payload_size;
		section_addr += payload_size;
	}

	/* success */
	return TRUE;
}

static void
fu_cros_ec_usb_device_send_done (FuDevice *device)
{
	guint32 out = GUINT32_TO_BE (UPDATE_DONE);
	g_autoptr(GError) error_local = NULL;

	/* send stop request, ignoring reply */
	if (!fu_cros_ec_usb_device_do_xfer (FU_CROS_EC_USB_DEVICE (device),
					    (const guint8 *)&out, sizeof (out),
					    (guint8 *)&out, 1,
					    FALSE, NULL, &error_local)) {
		g_debug ("error on transfer of done: %s",
			 error_local->message);
	}
}

static gboolean
fu_cros_ec_usb_device_send_subcommand (FuDevice *device, guint16 subcommand,
				       gpointer cmd_body, gsize body_size,
				       gpointer resp, gsize *resp_size,
				       gboolean allow_less, GError **error)
{
	fu_cros_ec_usb_device_send_done (device);

	if (!fu_cros_ec_usb_ext_cmd (device, subcommand,
				     cmd_body, body_size,
				     resp, resp_size, FALSE, error)) {
		g_prefix_error (error,
				"failed to send subcommand %" G_GUINT16_FORMAT ": ",
				subcommand);
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_reset_to_ro (FuDevice *device, GError **error)
{
	guint8 response;
	guint16	subcommand = UPDATE_EXTRA_CMD_IMMEDIATE_RESET;
	guint8 command_body[2]; /* Max command body size. */
	gsize command_body_size = 0;
	gsize response_size = 1;

	if (fu_device_has_custom_flag (device, "ro-written"))
		fu_device_set_custom_flags (device, "ro-written,rebooting-to-ro");
	else
		fu_device_set_custom_flags (device, "rebooting-to-ro");
	if (!fu_cros_ec_usb_device_send_subcommand  (device, subcommand, command_body,
				     command_body_size, &response,
				     &response_size, FALSE, error)) {
		/* failure here is ok */
		g_clear_error (error);
		return TRUE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_jump_to_rw (FuDevice *device)
{
	guint8 response;
	guint16 subcommand = UPDATE_EXTRA_CMD_JUMP_TO_RW;
	guint8 command_body[2]; /* Max command body size. */
	gsize command_body_size = 0;
	gsize response_size = 1;

	if (!fu_cros_ec_usb_device_send_subcommand (device, subcommand, command_body,
						    command_body_size, &response,
						    &response_size, FALSE, NULL)) {
		/* bail out early here if subcommand failed, which is normal */
		return TRUE;
	}

	/* Jump to rw may not work, so if we've reached here, initiate a
	 * full reset using immediate reset */
	subcommand = UPDATE_EXTRA_CMD_IMMEDIATE_RESET;
	fu_cros_ec_usb_device_send_subcommand (device, subcommand, command_body,
					       command_body_size, &response,
					       &response_size, FALSE, NULL);

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_write_firmware (FuDevice *device,
				      FuFirmware *firmware,
				      FwupdInstallFlags flags,
				      GError **error)
{
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);
	GPtrArray *sections;
	FuCrosEcFirmware *cros_ec_firmware = FU_CROS_EC_FIRMWARE (firmware);
	gint num_txed_sections = 0;

	if (fu_device_has_custom_flag (device, "rebooting-to-ro")) {
		gsize response_size = 1;
		guint8 response;
		guint16 subcommand = UPDATE_EXTRA_CMD_STAY_IN_RO;
		guint8 command_body[2]; /* Max command body size. */
		gsize command_body_size = 0;
		START_RESP start_resp;

		if (!fu_cros_ec_usb_device_send_subcommand  (device, subcommand, command_body,
							     command_body_size, &response,
							     &response_size, FALSE, error)) {
			g_prefix_error (error, "failed to send stay-in-ro subcommand: ");
			return FALSE;
		}

		/* flush all data from endpoint to recover in case of error */
		if (!fu_cros_ec_usb_device_recovery (device, error)) {
			g_prefix_error (error, "failed to flush device to idle state: ");
			return FALSE;
		}

		/* send start request */
		if (!fu_device_retry (device, fu_cros_ec_usb_device_start_request,
				      SETUP_RETRY_CNT, &start_resp, error)) {
			g_prefix_error (error, "failed to send start request: ");
			return FALSE;
		}
	}

	if (fu_device_has_custom_flag (device, "rw-written") && self->in_bootloader) {
		/*
		 * we had previously written to the rw region but somehow
		 * ended back up here while still in bootloader; this is
		 * a transitory state due to the fact that we have to boot
		 * through RO to get to RW. Set another write required to
		 * allow the RO region to auto-jump to RW
		 */
		fu_device_set_custom_flags (device, "special,rw-written");
		fu_device_add_flag (device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED);
		return TRUE;
	}

	sections = fu_cros_ec_firmware_get_sections (cros_ec_firmware);
	if (sections == NULL) {
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "invalid sections");
		return FALSE;
	}

	fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
	for (guint i = 0; i < sections->len; i++) {
		FuCrosEcFirmwareSection *section = g_ptr_array_index (sections, i);

		if (section->ustatus == FU_CROS_EC_FW_NEEDED) {
			if (!fu_cros_ec_usb_device_transfer_section (device,
								     firmware,
								     section,
								     error)) {
				return FALSE;
			}
			num_txed_sections++;

			if (self->in_bootloader) {
				fu_device_set_version (FU_DEVICE (device),
						       section->version.triplet);
			} else {
				fu_device_set_version_bootloader (FU_DEVICE (device),
								  section->version.triplet);
			}
		}
	}
	/* send done */
	fu_cros_ec_usb_device_send_done (device);

	if (num_txed_sections == 0) {
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "no sections transferred");
		return FALSE;
	}

	if (self->in_bootloader) {
		if (fu_device_has_custom_flag (device, "ro-written"))
			fu_device_set_custom_flags (device, "ro-written,rw-written");
		else
			fu_device_set_custom_flags (device, "rw-written");
	} else if (fu_device_has_custom_flag (device, "rw-written")) {
		fu_device_set_custom_flags (device, "ro-written,rw-written");
	} else {
		fu_device_set_custom_flags (device, "ro-written");
	}

	/* logical XOR */
	if (fu_device_has_custom_flag (device, "rw-written") !=
	    fu_device_has_custom_flag (device, "ro-written"))
		fu_device_add_flag (device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED);

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_close (FuDevice *device, GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);

	if (!g_usb_device_release_interface (usb_device, self->iface_idx,
					     G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
					     error)) {
		g_prefix_error (error, "failed to release interface: ");
		return FALSE;
	}

	/* FuUsbDevice->close */
	return FU_DEVICE_CLASS (fu_cros_ec_usb_device_parent_class)->close (device, error);
}

static FuFirmware *
fu_cros_ec_usb_device_prepare_firmware (FuDevice *device,
					GBytes *fw,
					FwupdInstallFlags flags,
					GError **error)
{
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);
	FuCrosEcFirmware *cros_ec_firmware = NULL;
	g_autoptr(FuFirmware) firmware = fu_cros_ec_firmware_new ();

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

	/* pick sections */
	if (!fu_cros_ec_firmware_pick_sections (cros_ec_firmware,
						self->writeable_offset,
						error)) {
		g_prefix_error (error, "failed to pick sections: ");
		return NULL;
	}
	return g_steal_pointer (&firmware);
}

static gboolean
fu_cros_ec_usb_device_attach (FuDevice *device, GError **error)
{
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);

	if (self->in_bootloader && fu_device_has_custom_flag (device, "special")) {
		fu_device_set_remove_delay (device, CROS_EC_REMOVE_DELAY_RE_ENUMERATE);
		fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);
		fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
		return TRUE;
	}

	fu_device_set_remove_delay (device, CROS_EC_REMOVE_DELAY_RE_ENUMERATE);
	if (fu_device_has_custom_flag (device, "ro-written") &&
	    !fu_device_has_custom_flag (device, "rw-written")) {
		if (!fu_cros_ec_usb_device_reset_to_ro (device, error)) {
			return FALSE;
		}
	} else {
		fu_cros_ec_usb_device_jump_to_rw (device);
	}
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);
	fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);

	/* success */
	return TRUE;
}

static gboolean
fu_cros_ec_usb_device_detach (FuDevice *device, GError **error)
{
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);

	if (fu_device_has_custom_flag (device, "rw-written") &&
	    !fu_device_has_custom_flag (device, "ro-written"))
		return TRUE;

	if (self->in_bootloader) {
		g_debug ("skipping immediate reboot in case of already in bootloader");
		/* in RO so skip reboot */
		return TRUE;
	} else if (self->targ.common.flash_protection != 0x0) {
		/* in RW, and RO region is write protected, so jump to RO */
		fu_device_set_custom_flags (device, "ro-written");
		fu_device_set_remove_delay (device, CROS_EC_REMOVE_DELAY_RE_ENUMERATE);
		if (!fu_cros_ec_usb_device_reset_to_ro (device, error))
			return FALSE;

		fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);
		fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
	}

	/* success */
	return TRUE;
}

static void
fu_cros_ec_usb_device_init (FuCrosEcUsbDevice *device)
{
	fu_device_set_protocol (FU_DEVICE (device), "com.google.usb.crosec");
	fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_set_version_format (FU_DEVICE (device), FWUPD_VERSION_FORMAT_TRIPLET);
	fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_DUAL_IMAGE);
}

static void
fu_cros_ec_usb_device_to_string (FuDevice *device, guint idt, GString *str)
{
	FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE (device);
	g_autofree gchar *min_rollback = NULL;

	fu_common_string_append_kv (str, idt, "GitHash", self->version.sha1);
	fu_common_string_append_kb (str, idt, "Dirty",
				    self->version.dirty);
	fu_common_string_append_ku (str, idt, "ProtocolVersion",
				    self->protocol_version);
	fu_common_string_append_ku (str, idt, "HeaderType",
				    self->header_type);
	fu_common_string_append_ku (str, idt, "MaxPDUSize",
				    self->targ.common.maximum_pdu_size);
	fu_common_string_append_kx (str, idt, "FlashProtectionStatus",
				    self->targ.common.flash_protection);
	fu_common_string_append_kv (str, idt, "RawVersion",
				    self->targ.common.version);
	fu_common_string_append_ku (str, idt, "KeyVersion",
				    self->targ.common.key_version);
	min_rollback = g_strdup_printf ("%" G_GINT32_FORMAT,
					self->targ.common.min_rollback);
	fu_common_string_append_kv (str, idt, "MinRollback", min_rollback);
	fu_common_string_append_kx (str, idt, "WriteableOffset",
				    self->writeable_offset);
}

static void
fu_cros_ec_usb_device_class_init (FuCrosEcUsbDeviceClass *klass)
{
	FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
	klass_device->attach = fu_cros_ec_usb_device_attach;
	klass_device->detach = fu_cros_ec_usb_device_detach;
	klass_device->prepare_firmware = fu_cros_ec_usb_device_prepare_firmware;
	klass_device->setup = fu_cros_ec_usb_device_setup;
	klass_device->to_string = fu_cros_ec_usb_device_to_string;
	klass_device->write_firmware = fu_cros_ec_usb_device_write_firmware;
	klass_device->open = fu_cros_ec_usb_device_open;
	klass_device->probe = fu_cros_ec_usb_device_probe;
	klass_device->close = fu_cros_ec_usb_device_close;
}
