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

/**
 * SECTION:dfu-device
 * @short_description: Object representing a DFU-capable device
 *
 * This object allows two things:
 *
 *  - Downloading from the host to the device, optionally with
 *    verification using a DFU or DfuSe firmware file.
 *
 *  - Uploading from the device to the host to a DFU or DfuSe firmware
 *    file. The file format is chosen automatically, with DfuSe being
 *    chosen if the device contains more than one target.
 *
 * See also: #DfuTarget, #FuDfuseFirmware
 */

/**
 * FU_QUIRKS_DFU_FLAGS:
 * @key: the USB device ID, e.g. `USB\VID_0763&PID_2806`
 * @value: a string, separated using `|`, e.g. `ignore-polltimeout|no-pid-change`
 *
 * Assigns optional quirks to use for a DFU device which does not follow the
 * DFU 1.0 or 1.1 specification. The list of supported quirks is thus:
 *
 * * `none`:			No device quirks
 * * `attach-extra-reset`:	Device needs resetting twice for attach
 * * `attach-upload-download`:	An upload or download is required for attach
 * * `force-dfu-mode`:		Force DFU mode
 * * `ignore-polltimeout`:	Ignore the device download timeout
 * * `ignore-runtime`:		Device has broken DFU runtime support
 * * `ignore-upload`:		Uploading from the device is broken
 * * `no-dfu-runtime`:		No DFU runtime interface is provided
 * * `no-get-status-upload`:	Do not do GetStatus when uploading
 * * `no-pid-change`:		Accept the same VID:PID when changing modes
 * * `use-any-interface`:	Use any interface for DFU
 * * `use-atmel-avr`:		Device uses the ATMEL bootloader
 * * `use-protocol-zero`:	Fix up the protocol number
 * * `legacy-protocol`:		Use a legacy protocol version
 * * `detach-for-attach`:	Requires a DFU_REQUEST_DETACH to attach
 * * `absent-sector-size`:	In absence of sector size, assume byte
 * * `manifest-poll`:		Requires polling via GetStatus in dfuManifest state
 * * `no-bus-reset-attach`:	Do not require a bus reset to attach to normal
 *
 * Default value: `none`
 *
 * Since: 1.0.1
 */
#define	FU_QUIRKS_DFU_FLAGS			"Flags"

/**
 * FU_QUIRKS_DFU_FORCE_VERSION:
 * @key: the USB device ID, e.g. `USB\VID_0763&PID_2806`
 * @value: the uint16_t DFU version, encoded in base 16, e.g. `0110`
 *
 * Forces a specific DFU version for the hardware device. This is required
 * if the device does not set, or sets incorrectly, items in the DFU functional
 * descriptor.
 *
 * Since: 1.0.1
 */
#define	FU_QUIRKS_DFU_FORCE_VERSION		"DfuForceVersion"

#define DFU_DEVICE_DNLOAD_TIMEOUT_DEFAULT	5	/* ms */

#include "config.h"

#include <string.h>

#include "dfu-common.h"
#include "dfu-device.h"
#include "dfu-target-avr.h"
#include "dfu-target-private.h"
#include "dfu-target-stm.h"

#include "fu-device-locker.h"
#include "fu-dfu-firmware-private.h"
#include "fu-dfuse-firmware.h"
#include "fu-firmware-common.h"

#include "fwupd-error.h"

static void dfu_device_finalize			 (GObject *object);

typedef struct {
	DfuDeviceAttributes	 attributes;
	DfuState		 state;
	DfuStatus		 status;
	GPtrArray		*targets;
	gboolean		 done_upload_or_download;
	gboolean		 claimed_interface;
	gchar			*chip_id;
	guint16			 version;
	guint16			 force_version;
	guint16			 force_transfer_size;
	guint16			 runtime_pid;
	guint16			 runtime_vid;
	guint16			 runtime_release;
	guint16			 transfer_size;
	guint8			 iface_number;
	guint			 dnload_timeout;
	guint			 timeout_ms;
} DfuDevicePrivate;

G_DEFINE_TYPE_WITH_PRIVATE (DfuDevice, dfu_device, FU_TYPE_USB_DEVICE)
#define GET_PRIVATE(o) (dfu_device_get_instance_private (o))

static void	dfu_device_set_state (DfuDevice *device, DfuState state);

static void
dfu_device_to_string (FuDevice *device, guint idt, GString *str)
{
	DfuDevice *self = DFU_DEVICE (device);
	DfuDevicePrivate *priv = GET_PRIVATE (self);
	fu_common_string_append_kv (str, idt, "State", dfu_state_to_string (priv->state));
	fu_common_string_append_kv (str, idt, "Status", dfu_status_to_string (priv->status));
	fu_common_string_append_kb (str, idt, "DoneUploadOrDownload", priv->done_upload_or_download);
	fu_common_string_append_kb (str, idt, "ClaimedInterface", priv->claimed_interface);
	if (priv->chip_id != NULL)
		fu_common_string_append_kv (str, idt, "ChipId", priv->chip_id);
	fu_common_string_append_kx (str, idt, "Version", priv->version);
	fu_common_string_append_kx (str, idt, "ForceVersion", priv->force_version);
	if (priv->force_transfer_size != 0x0) {
		fu_common_string_append_kx (str, idt, "ForceTransferSize",
					    priv->force_transfer_size);
	}
	fu_common_string_append_kx (str, idt, "RuntimePid", priv->runtime_pid);
	fu_common_string_append_kx (str, idt, "RuntimeVid", priv->runtime_vid);
	fu_common_string_append_kx (str, idt, "RuntimeRelease", priv->runtime_release);
	fu_common_string_append_kx (str, idt, "TransferSize", priv->transfer_size);
	fu_common_string_append_kx (str, idt, "IfaceNumber", priv->iface_number);
	fu_common_string_append_kx (str, idt, "DnloadTimeout", priv->dnload_timeout);
	fu_common_string_append_kx (str, idt, "TimeoutMs", priv->timeout_ms);
	for (guint i = 0; i < priv->targets->len; i++) {
		DfuTarget *target = g_ptr_array_index (priv->targets, i);
		dfu_target_to_string (target, idt + 1, str);
	}
}

/**
 * dfu_device_get_transfer_size:
 * @device: a #GUsbDevice
 *
 * Gets the transfer size in bytes.
 *
 * Return value: packet size, or 0 for unknown
 **/
guint16
dfu_device_get_transfer_size (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), 0xffff);
	return priv->transfer_size;
}

/**
 * dfu_device_get_version:
 * @device: a #GUsbDevice
 *
 * Gets the DFU specification version supported by the device.
 *
 * Return value: integer, or 0 for unknown, e.g. %DFU_VERSION_DFU_1_1
 **/
guint16
dfu_device_get_version (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), 0xffff);
	return priv->version;
}

/**
 * dfu_device_get_download_timeout:
 * @device: a #GUsbDevice
 *
 * Gets the download timeout in ms.
 *
 * Return value: delay, or 0 for unknown
 **/
guint
dfu_device_get_download_timeout (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), 0);
	return priv->dnload_timeout;
}

/**
 * dfu_device_set_transfer_size:
 * @device: a #GUsbDevice
 * @transfer_size: maximum packet size
 *
 * Sets the transfer size in bytes.
 **/
void
dfu_device_set_transfer_size (DfuDevice *device, guint16 transfer_size)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_if_fail (DFU_IS_DEVICE (device));
	priv->transfer_size = transfer_size;
}

typedef struct __attribute__((packed)) {
	guint8		bLength;
	guint8		bDescriptorType;
	guint8		bmAttributes;
	guint16		wDetachTimeOut;
	guint16		wTransferSize;
	guint16		bcdDFUVersion;
} DfuFuncDescriptor;

static gboolean
dfu_device_parse_iface_data (DfuDevice *device, GBytes *iface_data, GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	DfuFuncDescriptor desc = { 0x0 };
	const guint8 *buf;
	gsize sz;

	/* parse the functional descriptor */
	buf = g_bytes_get_data (iface_data, &sz);
	if (sz == sizeof(DfuFuncDescriptor)) {
		memcpy (&desc, buf, sz);
	} else if (sz > sizeof(DfuFuncDescriptor)) {
		g_debug ("DFU interface with %" G_GSIZE_FORMAT " bytes vendor data",
			 sz - sizeof(DfuFuncDescriptor));
		memcpy (&desc, buf, sizeof(DfuFuncDescriptor));
	} else if (sz == sizeof(DfuFuncDescriptor) - 2) {
		g_warning ("truncated DFU interface data, no bcdDFUVersion");
		memcpy (&desc, buf, sz);
		desc.bcdDFUVersion = DFU_VERSION_DFU_1_1;
	} else {
		g_autoptr(GString) bufstr = g_string_new (NULL);
		for (gsize i = 0; i < sz; i++)
			g_string_append_printf (bufstr, "%02x ", buf[i]);
		if (bufstr->len > 0)
			g_string_truncate (bufstr, bufstr->len - 1);
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_INVALID_DATA,
			     "interface found, but not the correct length for "
			     "functional data: %" G_GSIZE_FORMAT " bytes: %s",
			     sz, bufstr->str);
		return FALSE;
	}

	/* get transfer size and version */
	priv->transfer_size = GUINT16_FROM_LE (desc.wTransferSize);
	priv->version = GUINT16_FROM_LE (desc.bcdDFUVersion);

	/* ST-specific */
	if (priv->version == DFU_VERSION_DFUSE &&
	    desc.bmAttributes & DFU_DEVICE_ATTRIBUTE_CAN_ACCELERATE)
		priv->transfer_size = 0x1000;

	/* get attributes about the DFU operation */
	priv->attributes = desc.bmAttributes;
	return TRUE;
}

static void
dfu_device_guess_state_from_iface (DfuDevice *device, GUsbInterface *iface)
{
	/* some devices use the wrong interface */
	if (fu_device_has_custom_flag (FU_DEVICE (device), "force-dfu-mode")) {
		g_debug ("quirking device into DFU mode");
		dfu_device_set_state (device, DFU_STATE_DFU_IDLE);
		return;
	}

	/* runtime */
	if (g_usb_interface_get_protocol (iface) == 0x01) {
		dfu_device_set_state (device, DFU_STATE_APP_IDLE);
		return;
	}

	/* DFU */
	if (g_usb_interface_get_protocol (iface) == 0x02) {
		dfu_device_set_state (device, DFU_STATE_DFU_IDLE);
		return;
	}
	g_warning ("unable to guess initial device state from interface %u",
		   g_usb_interface_get_protocol (iface));
}

static gboolean
dfu_device_add_targets (DfuDevice *device, GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	g_autoptr(GPtrArray) ifaces = NULL;

	/* add all DFU-capable targets */
	ifaces = g_usb_device_get_interfaces (usb_device, error);
	if (ifaces == NULL)
		return FALSE;
	g_ptr_array_set_size (priv->targets, 0);
	for (guint i = 0; i < ifaces->len; i++) {
		GBytes *iface_data = NULL;
		DfuTarget *target;
		g_autoptr(GError) error_local = NULL;

		GUsbInterface *iface = g_ptr_array_index (ifaces, i);

		/* some devices don't use the right class and subclass */
		if (!fu_device_has_custom_flag (FU_DEVICE (device), "use-any-interface")) {
			if (g_usb_interface_get_class (iface) != G_USB_DEVICE_CLASS_APPLICATION_SPECIFIC)
				continue;
			if (g_usb_interface_get_subclass (iface) != 0x01)
				continue;
		}
		/* parse any interface data */
		iface_data = g_usb_interface_get_extra (iface);
		if (g_bytes_get_size (iface_data) > 0) {
			if (!dfu_device_parse_iface_data (device, iface_data, &error_local)) {
				g_warning ("failed to parse interface data for %04x:%04x: %s",
					   g_usb_device_get_vid (usb_device),
					   g_usb_device_get_pid (usb_device),
					   error_local->message);
				continue;
			}
		} else {
			priv->attributes |= DFU_DEVICE_ATTRIBUTE_CAN_DOWNLOAD |
					    DFU_DEVICE_ATTRIBUTE_CAN_UPLOAD;
		}

		/* fix up the version */
		if (priv->force_version > 0)
			priv->version = priv->force_version;
		if (priv->version == DFU_VERSION_DFU_1_0 ||
		    priv->version == DFU_VERSION_DFU_1_1) {
			g_debug ("DFU v1.1");
		} else if (priv->version == DFU_VERSION_ATMEL_AVR) {
			g_debug ("AVR-DFU support");
			priv->version = DFU_VERSION_ATMEL_AVR;
		} else if (priv->version == DFU_VERSION_DFUSE) {
			g_debug ("STM-DFU support");
		} else if (priv->version == 0x0101) {
			g_debug ("DFU v1.1 assumed");
			priv->version = DFU_VERSION_DFU_1_1;
		} else {
			g_warning ("DFU version 0x%04x invalid, v1.1 assumed", priv->version);
			priv->version = DFU_VERSION_DFU_1_1;
		}

		/* set expected protocol */
		if (priv->version == DFU_VERSION_DFUSE) {
			fu_device_set_protocol (FU_DEVICE (device), "com.st.dfuse");
		} else {
			fu_device_set_protocol (FU_DEVICE (device), "org.usb.dfu");
		}

		/* fix up the transfer size */
		if (priv->force_transfer_size != 0x0) {
			priv->transfer_size = priv->force_transfer_size;
			g_debug ("forcing DFU transfer size 0x%04x bytes", priv->transfer_size);
		} else if (priv->transfer_size == 0xffff) {
			priv->transfer_size = 0x0400;
			g_debug ("DFU transfer size unspecified, guessing");
		} else if (priv->transfer_size == 0x0) {
			g_warning ("DFU transfer size invalid, using default");
			priv->transfer_size = 64;
		} else {
			g_debug ("using DFU transfer size 0x%04x bytes",
				 priv->transfer_size);
		}

		/* create a target of the required type */
		switch (priv->version) {
		case DFU_VERSION_DFUSE:
			target = dfu_target_stm_new ();
			break;
		case DFU_VERSION_ATMEL_AVR:
			target = dfu_target_avr_new ();
			break;
		default:
			target = dfu_target_new ();
			break;
		}
		dfu_target_set_device (target, device);
		dfu_target_set_alt_idx (target, g_usb_interface_get_index (iface));
		dfu_target_set_alt_setting (target, g_usb_interface_get_alternate (iface));

		/* add target */
		priv->iface_number = g_usb_interface_get_number (iface);
		g_ptr_array_add (priv->targets, target);
		dfu_device_guess_state_from_iface (device, iface);
	}

	/* save for reset */
	if (priv->state == DFU_STATE_APP_IDLE ||
	    fu_device_has_custom_flag (FU_DEVICE (device), "no-pid-change")) {
		priv->runtime_vid = g_usb_device_get_vid (usb_device);
		priv->runtime_pid = g_usb_device_get_pid (usb_device);
		priv->runtime_release = g_usb_device_get_release (usb_device);
	}

	/* the device has no DFU runtime, so cheat */
	if (priv->targets->len == 0 &&
	    fu_device_has_custom_flag (FU_DEVICE (device), "no-dfu-runtime")) {
		g_debug ("no DFU runtime, so faking device");
		dfu_device_set_state (device, DFU_STATE_APP_IDLE);
		priv->iface_number = 0xff;
		priv->runtime_vid = g_usb_device_get_vid (usb_device);
		priv->runtime_pid = g_usb_device_get_pid (usb_device);
		priv->runtime_release = g_usb_device_get_release (usb_device);
		priv->attributes = DFU_DEVICE_ATTRIBUTE_CAN_DOWNLOAD |
				   DFU_DEVICE_ATTRIBUTE_CAN_UPLOAD;
		return TRUE;
	}

	/* no targets */
	if (priv->targets->len == 0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "no DFU interfaces");
		return FALSE;
	}

	/* the device upload is broken */
	if (fu_device_has_custom_flag (FU_DEVICE (device), "ignore-upload"))
		priv->attributes &= ~DFU_DEVICE_ATTRIBUTE_CAN_UPLOAD;

	return TRUE;
}

/**
 * dfu_device_can_upload:
 * @device: a #GUsbDevice
 *
 * Gets if the device can upload.
 *
 * Return value: %TRUE if the device can upload from device to host
 **/
gboolean
dfu_device_can_upload (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE);
	return (priv->attributes & DFU_DEVICE_ATTRIBUTE_CAN_UPLOAD) > 0;
}

/**
 * dfu_device_can_download:
 * @device: a #GUsbDevice
 *
 * Gets if the device can download.
 *
 * Return value: %TRUE if the device can download from host to device
 **/
gboolean
dfu_device_can_download (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE);
	return (priv->attributes & DFU_DEVICE_ATTRIBUTE_CAN_DOWNLOAD) > 0;
}

/**
 * dfu_device_set_timeout:
 * @device: a #DfuDevice
 * @timeout_ms: the timeout in ms
 *
 * Sets the USB timeout to use when contacting the USB device.
 **/
void
dfu_device_set_timeout (DfuDevice *device, guint timeout_ms)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_if_fail (DFU_IS_DEVICE (device));
	priv->timeout_ms = timeout_ms;
}

/**
 * dfu_device_get_timeout:
 * @device: a #GUsbDevice
 *
 * Gets the device timeout.
 *
 * Return value: enumerated timeout in ms
 **/
guint
dfu_device_get_timeout (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), 0);
	return priv->timeout_ms;
}

/**
 * dfu_device_get_state:
 * @device: a #GUsbDevice
 *
 * Gets the device state.
 *
 * Return value: enumerated state, e.g. %DFU_STATE_DFU_UPLOAD_IDLE
 **/
DfuState
dfu_device_get_state (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), 0);
	return priv->state;
}

/**
 * dfu_device_get_status:
 * @device: a #GUsbDevice
 *
 * Gets the device status.
 *
 * Return value: enumerated status, e.g. %DFU_STATUS_ERR_ADDRESS
 **/
DfuStatus
dfu_device_get_status (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), 0);
	return priv->status;
}

/**
 * dfu_device_has_attribute: (skip)
 * @device: A #DfuDevice
 * @attribute: A #DfuDeviceAttributes, e.g. %DFU_DEVICE_ATTRIBUTE_CAN_DOWNLOAD
 *
 * Returns if an attribute set for the device.
 *
 * Return value: %TRUE if the attribute is set
 **/
gboolean
dfu_device_has_attribute (DfuDevice *device, DfuDeviceAttributes attribute)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE);
	return (priv->attributes & attribute) > 0;
}

/**
 * dfu_device_remove_attribute: (skip)
 * @device: A #DfuDevice
 * @attribute: A #DfuDeviceAttributes, e.g. %DFU_DEVICE_ATTRIBUTE_CAN_DOWNLOAD
 *
 * Removes an attribute from the device.
 **/
void
dfu_device_remove_attribute (DfuDevice *device, DfuDeviceAttributes attribute)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_if_fail (DFU_IS_DEVICE (device));
	priv->attributes &= ~attribute;
}

/**
 * dfu_device_new:
 *
 * Creates a new DFU device object.
 *
 * Return value: a new #DfuDevice
 **/
DfuDevice *
dfu_device_new (GUsbDevice *usb_device)
{
	DfuDevice *device;
	device = g_object_new (DFU_TYPE_DEVICE,
			       "usb-device", usb_device,
			       NULL);
	return device;
}

/**
 * dfu_device_get_targets:
 * @device: a #DfuDevice
 *
 * Gets all the targets for this device.
 *
 * Return value: (transfer none) (element-type DfuTarget): #DfuTarget, or %NULL
 **/
GPtrArray *
dfu_device_get_targets (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), NULL);
	return priv->targets;
}

/**
 * dfu_device_get_target_by_alt_setting:
 * @device: a #DfuDevice
 * @alt_setting: the setting used to find
 * @error: a #GError, or %NULL
 *
 * Gets a target with a specific alternative setting.
 *
 * Return value: (transfer full): a #DfuTarget, or %NULL
 **/
DfuTarget *
dfu_device_get_target_by_alt_setting (DfuDevice *device,
				      guint8 alt_setting,
				      GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);

	g_return_val_if_fail (DFU_IS_DEVICE (device), NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	/* find by ID */
	for (guint i = 0; i < priv->targets->len; i++) {
		DfuTarget *target = g_ptr_array_index (priv->targets, i);
		if (dfu_target_get_alt_setting (target) == alt_setting)
			return g_object_ref (target);
	}

	/* failed */
	g_set_error (error,
		     FWUPD_ERROR,
		     FWUPD_ERROR_NOT_FOUND,
		     "No target with alt-setting %i",
		     alt_setting);
	return NULL;
}

/**
 * dfu_device_get_target_by_alt_name:
 * @device: a #DfuDevice
 * @alt_name: the name used to find
 * @error: a #GError, or %NULL
 *
 * Gets a target with a specific alternative name.
 *
 * Return value: (transfer full): a #DfuTarget, or %NULL
 **/
DfuTarget *
dfu_device_get_target_by_alt_name (DfuDevice *device,
				   const gchar *alt_name,
				   GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);

	g_return_val_if_fail (DFU_IS_DEVICE (device), NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	/* find by ID */
	for (guint i = 0; i < priv->targets->len; i++) {
		DfuTarget *target = g_ptr_array_index (priv->targets, i);
		if (g_strcmp0 (dfu_target_get_alt_name (target, NULL), alt_name) == 0)
			return g_object_ref (target);
	}

	/* failed */
	g_set_error (error,
		     FWUPD_ERROR,
		     FWUPD_ERROR_NOT_FOUND,
		     "No target with alt-name %s",
		     alt_name);
	return NULL;
}

/**
 * dfu_device_get_platform_id:
 * @device: a #DfuDevice
 *
 * Gets the platform ID which normally corresponds to the port in some way.
 *
 * Return value: string or %NULL
 **/
const gchar *
dfu_device_get_platform_id (DfuDevice *device)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	g_return_val_if_fail (DFU_IS_DEVICE (device), NULL);
	return g_usb_device_get_platform_id (usb_device);
}

/**
 * dfu_device_get_runtime_vid:
 * @device: a #DfuDevice
 *
 * Gets the runtime vendor ID.
 *
 * Return value: vendor ID, or 0xffff for unknown
 **/
guint16
dfu_device_get_runtime_vid (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), 0xffff);
	return priv->runtime_vid;
}

/**
 * dfu_device_get_runtime_pid:
 * @device: a #DfuDevice
 *
 * Gets the runtime product ID.
 *
 * Return value: product ID, or 0xffff for unknown
 **/
guint16
dfu_device_get_runtime_pid (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), 0xffff);
	return priv->runtime_pid;
}

/**
 * dfu_device_get_runtime_release:
 * @device: a #DfuDevice
 *
 * Gets the runtime release number in BCD format.
 *
 * Return value: release number, or 0xffff for unknown
 **/
guint16
dfu_device_get_runtime_release (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), 0xffff);
	return priv->runtime_release;
}

const gchar *
dfu_device_get_chip_id (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), NULL);
	return priv->chip_id;
}

void
dfu_device_set_chip_id (DfuDevice *device, const gchar *chip_id)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_if_fail (DFU_IS_DEVICE (device));
	g_debug ("chip ID set to: %s", chip_id);
	priv->chip_id = g_strdup (chip_id);
}

static void
dfu_device_set_state (DfuDevice *device, DfuState state)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	if (priv->state == state)
		return;
	priv->state = state;

	/* set bootloader status */
	if (state == DFU_STATE_APP_IDLE ||
	    state == DFU_STATE_APP_DETACH) {
		fu_device_remove_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
	} else {
		fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
	}

	switch (state) {
	case DFU_STATE_DFU_UPLOAD_IDLE:
		fu_device_set_status (FU_DEVICE (device), FWUPD_STATUS_DEVICE_VERIFY);
		break;
	case DFU_STATE_DFU_DNLOAD_IDLE:
		fu_device_set_status (FU_DEVICE (device), FWUPD_STATUS_DEVICE_WRITE);
		break;
	default:
		break;
	}
}

static void
dfu_device_set_status (DfuDevice *device, DfuStatus status)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	if (priv->status == status)
		return;
	priv->status = status;
}

gboolean
dfu_device_ensure_interface (DfuDevice *device, GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	g_autoptr(GError) error_local = NULL;

	/* already done */
	if (priv->claimed_interface)
		return TRUE;

	/* nothing set */
	if (priv->iface_number == 0xff)
		return TRUE;

	/* claim, without detaching kernel driver */
	if (!g_usb_device_claim_interface (usb_device,
					   (gint) priv->iface_number,
					   G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
					   &error_local)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "cannot claim interface %i: %s",
			     priv->iface_number, error_local->message);
		return FALSE;
	}

	/* success */
	priv->claimed_interface = TRUE;
	return TRUE;
}

/**
 * dfu_device_refresh_and_clear:
 * @device: a #DfuDevice
 * @error: a #GError, or %NULL
 *
 * Refreshes the cached properties on the DFU device. If there are any transers
 * in progress they are cancelled, and if there are any pending errors they are
 * cancelled.
 *
 * Return value: %TRUE for success
 **/
gboolean
dfu_device_refresh_and_clear (DfuDevice *device, GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	if (!dfu_device_refresh (device, error))
		return FALSE;
	switch (priv->state) {
	case DFU_STATE_DFU_UPLOAD_IDLE:
	case DFU_STATE_DFU_DNLOAD_IDLE:
	case DFU_STATE_DFU_DNLOAD_SYNC:
		g_debug ("aborting transfer %s", dfu_status_to_string (priv->status));
		if (!dfu_device_abort (device, error))
			return FALSE;
		break;
	case DFU_STATE_DFU_ERROR:
		g_debug ("clearing error %s", dfu_status_to_string (priv->status));
		if (!dfu_device_clear_status (device, error))
			return FALSE;
		break;
	default:
		break;
	}
	return TRUE;
}

/**
 * dfu_device_refresh:
 * @device: a #DfuDevice
 * @error: a #GError, or %NULL
 *
 * Refreshes the cached properties on the DFU device.
 *
 * Return value: %TRUE for success
 **/
gboolean
dfu_device_refresh (DfuDevice *device, GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	gsize actual_length = 0;
	guint8 buf[6];
	g_autoptr(GError) error_local = NULL;

	g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* no backing USB device */
	if (usb_device == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to refresh: no GUsbDevice for %s",
			     dfu_device_get_platform_id (device));
		return FALSE;
	}

	/* the device has no DFU runtime, so cheat */
	if (priv->state == DFU_STATE_APP_IDLE &&
	    fu_device_has_custom_flag (FU_DEVICE (device), "no-dfu-runtime"))
		return TRUE;

	/* ensure interface is claimed */
	if (!dfu_device_ensure_interface (device, error))
		return FALSE;

	/* Device that cannot communicate via the USB after the
	 * Manifestation phase indicated this limitation to the
	 * host by clearing bmAttributes bit bitManifestationTolerant.
	 * so we assume the operation was successful */
	if (priv->state == DFU_STATE_DFU_MANIFEST &&
	    !(priv->attributes & DFU_DEVICE_ATTRIBUTE_MANIFEST_TOL))
		return TRUE;

	if (!g_usb_device_control_transfer (usb_device,
					    G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
					    G_USB_DEVICE_REQUEST_TYPE_CLASS,
					    G_USB_DEVICE_RECIPIENT_INTERFACE,
					    DFU_REQUEST_GETSTATUS,
					    0,
					    priv->iface_number,
					    buf, sizeof(buf), &actual_length,
					    priv->timeout_ms,
					    NULL, /* cancellable */
					    &error_local)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "cannot get device state: %s",
			     error_local->message);
		return FALSE;
	}
	if (actual_length != 6) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "cannot get device status, invalid size: %04x",
			     (guint) actual_length);
		return FALSE;
	}

	/* some devices use the wrong state value */
	if (fu_device_has_custom_flag (FU_DEVICE (device), "force-dfu-mode") &&
	    dfu_device_get_state (device) != DFU_STATE_DFU_IDLE) {
		g_debug ("quirking device into DFU mode");
		dfu_device_set_state (device, DFU_STATE_DFU_IDLE);
	} else {
		dfu_device_set_state (device, buf[4]);
	}

	/* status or state changed */
	dfu_device_set_status (device, buf[0]);
	if (fu_device_has_custom_flag (FU_DEVICE (device), "ignore-polltimeout")) {
		priv->dnload_timeout = DFU_DEVICE_DNLOAD_TIMEOUT_DEFAULT;
	} else {
		priv->dnload_timeout = buf[1] +
					(((guint32) buf[2]) << 8) +
					(((guint32) buf[3]) << 16);
		if (priv->dnload_timeout == 0) {
			priv->dnload_timeout = DFU_DEVICE_DNLOAD_TIMEOUT_DEFAULT;
			g_debug ("no dnload-timeout, using default of %ums",
				 priv->dnload_timeout);
		}
	}
	g_debug ("refreshed status=%s and state=%s (dnload=%u)",
		 dfu_status_to_string (priv->status),
		 dfu_state_to_string (priv->state),
		 priv->dnload_timeout);
	return TRUE;
}

static gboolean
dfu_device_request_detach (DfuDevice *self, GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (self);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
	const guint16 timeout_reset_ms = 1000;
	g_autoptr(GError) error_local = NULL;

	if (!g_usb_device_control_transfer (usb_device,
					    G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
					    G_USB_DEVICE_REQUEST_TYPE_CLASS,
					    G_USB_DEVICE_RECIPIENT_INTERFACE,
					    DFU_REQUEST_DETACH,
					    timeout_reset_ms,
					    priv->iface_number,
					    NULL, 0, NULL,
					    priv->timeout_ms,
					    NULL, /* cancellable */
					    &error_local)) {
		/* some devices just reboot and stall the endpoint :/ */
		if (g_error_matches (error_local,
				     G_USB_DEVICE_ERROR,
				     G_USB_DEVICE_ERROR_NOT_SUPPORTED) ||
		    g_error_matches (error_local,
				     G_USB_DEVICE_ERROR,
				     G_USB_DEVICE_ERROR_FAILED)) {
			g_debug ("ignoring while detaching: %s", error_local->message);
		} else {
			/* refresh the error code */
			dfu_device_error_fixup (self, &error_local);
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "cannot detach device: %s",
				     error_local->message);
			return FALSE;
		}
	}
	return TRUE;
}

static gboolean
dfu_device_reload (FuDevice *device, GError **error)
{
	DfuDevice *self = DFU_DEVICE (device);
	return dfu_device_refresh_and_clear (self, error);
}

static gboolean
dfu_device_detach (FuDevice *device, GError **error)
{
	DfuDevice *self = DFU_DEVICE (device);
	DfuDevicePrivate *priv = GET_PRIVATE (self);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));

	g_return_val_if_fail (DFU_IS_DEVICE (self), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* already in DFU mode */
	if (!dfu_device_refresh_and_clear (self, error))
		return FALSE;
	if (fu_device_has_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER))
		return TRUE;

	/* no backing USB device */
	if (usb_device == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to detach: no GUsbDevice for %s",
			     dfu_device_get_platform_id (self));
		return FALSE;
	}

	/* the device has no DFU runtime, so cheat */
	if (priv->state == DFU_STATE_APP_IDLE &&
	    fu_device_has_custom_flag (FU_DEVICE (self), "no-dfu-runtime"))
		return TRUE;

	/* ensure interface is claimed */
	if (!dfu_device_ensure_interface (self, error))
		return FALSE;

	/* inform UI there's going to be a detach:attach */
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);
	if (!dfu_device_request_detach (self, error))
		return FALSE;

	/* do a host reset */
	if ((priv->attributes & DFU_DEVICE_ATTRIBUTE_WILL_DETACH) == 0) {
		g_debug ("doing device reset as host will not self-reset");
		if (!dfu_device_reset (self, error))
			return FALSE;
	}

	/* success */
	priv->force_version = 0x0;
	fu_device_set_status (device, FWUPD_STATUS_IDLE);
	fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
	return TRUE;
}

/**
 * dfu_device_abort:
 * @device: a #DfuDevice
 * @error: a #GError, or %NULL
 *
 * Aborts any upload or download in progress.
 *
 * Return value: %TRUE for success
 **/
gboolean
dfu_device_abort (DfuDevice *device, GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	g_autoptr(GError) error_local = NULL;

	g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* no backing USB device */
	if (usb_device == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to abort: no GUsbDevice for %s",
			     dfu_device_get_platform_id (device));
		return FALSE;
	}

	/* the device has no DFU runtime, so cheat */
	if (priv->state == DFU_STATE_APP_IDLE &&
	    fu_device_has_custom_flag (FU_DEVICE (device), "no-dfu-runtime")) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "not supported as no DFU runtime");
		return FALSE;
	}

	/* ensure interface is claimed */
	if (!dfu_device_ensure_interface (device, error))
		return FALSE;

	if (!g_usb_device_control_transfer (usb_device,
					    G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
					    G_USB_DEVICE_REQUEST_TYPE_CLASS,
					    G_USB_DEVICE_RECIPIENT_INTERFACE,
					    DFU_REQUEST_ABORT,
					    0,
					    priv->iface_number,
					    NULL, 0, NULL,
					    priv->timeout_ms,
					    NULL, /* cancellable */
					    &error_local)) {
		/* refresh the error code */
		dfu_device_error_fixup (device, &error_local);
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "cannot abort device: %s",
			     error_local->message);
		return FALSE;
	}

	return TRUE;
}

/**
 * dfu_device_clear_status:
 * @device: a #DfuDevice
 * @error: a #GError, or %NULL
 *
 * Clears any error status on the DFU device.
 *
 * Return value: %TRUE for success
 **/
gboolean
dfu_device_clear_status (DfuDevice *device, GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	g_autoptr(GError) error_local = NULL;

	g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* no backing USB device */
	if (usb_device == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to clear status: no GUsbDevice for %s",
			     dfu_device_get_platform_id (device));
		return FALSE;
	}

	/* the device has no DFU runtime, so cheat */
	if (priv->state == DFU_STATE_APP_IDLE &&
	    fu_device_has_custom_flag (FU_DEVICE (device), "no-dfu-runtime")) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "not supported as no DFU runtime");
		return FALSE;
	}

	/* ensure interface is claimed */
	if (!dfu_device_ensure_interface (device, error))
		return FALSE;

	if (!g_usb_device_control_transfer (usb_device,
					    G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
					    G_USB_DEVICE_REQUEST_TYPE_CLASS,
					    G_USB_DEVICE_RECIPIENT_INTERFACE,
					    DFU_REQUEST_CLRSTATUS,
					    0,
					    priv->iface_number,
					    NULL, 0, NULL,
					    priv->timeout_ms,
					    NULL, /* cancellable */
					    &error_local)) {
		/* refresh the error code */
		dfu_device_error_fixup (device, &error_local);
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "cannot clear status on the device: %s",
			     error_local->message);
		return FALSE;
	}
	return TRUE;
}

/**
 * dfu_device_get_interface:
 * @device: a #DfuDevice
 *
 * Gets the interface number.
 **/
guint8
dfu_device_get_interface (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	g_return_val_if_fail (DFU_IS_DEVICE (device), 0xff);
	return priv->iface_number;
}

/**
 * dfu_device_open:
 * @device: a #DfuDevice
 * @error: a #GError, or %NULL
 *
 * Opens a DFU-capable device.
 *
 * Return value: %TRUE for success
 **/
static gboolean
dfu_device_open (FuDevice *device, GError **error)
{
	DfuDevice *self = DFU_DEVICE (device);
	DfuDevicePrivate *priv = GET_PRIVATE (self);
	GPtrArray *targets = dfu_device_get_targets (self);

	g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

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

	/* the device has no DFU runtime, so cheat */
	if (priv->state == DFU_STATE_APP_IDLE &&
	    fu_device_has_custom_flag (device, "no-dfu-runtime")) {
		dfu_device_set_state (self, DFU_STATE_APP_IDLE);
		priv->status = DFU_STATUS_OK;
	}

	/* GD32VF103 encodes the serial number in UTF-8 (rather than UTF-16)
	 * and also uses the first two bytes as the model identifier */
	if (fu_device_has_custom_flag (FU_DEVICE (device), "gd32")) {
#if G_USB_CHECK_VERSION(0,3,6)
		GUsbDevice *usb_device = fu_usb_device_get_dev (device);
		const guint8 *buf;
		gsize bufsz = 0;
		guint16 langid = G_USB_DEVICE_LANGID_ENGLISH_UNITED_STATES;
		guint8 idx = g_usb_device_get_serial_number_index (usb_device);
		g_autofree gchar *chip_id = NULL;
		g_autofree gchar *serial_str = NULL;
		g_autoptr(GBytes) serial_blob = NULL;
		serial_blob = g_usb_device_get_string_descriptor_bytes (usb_device,
									idx,
									langid,
									error);
		if (serial_blob == NULL)
			return FALSE;
		if (g_getenv ("FWUPD_DFU_VERBOSE") != NULL)
			fu_common_dump_bytes (G_LOG_DOMAIN, "GD32 serial", serial_blob);
		buf = g_bytes_get_data (serial_blob, &bufsz);
		if (bufsz < 2) {
			g_set_error_literal (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_NOT_SUPPORTED,
					     "GD32 serial number invalid");
			return FALSE;
		}

		/* ID is first two bytes */
		chip_id = g_strdup_printf ("%02x%02x", buf[0], buf[1]);
		dfu_device_set_chip_id (self, chip_id);

		/* serial number follows */
		serial_str = g_strndup ((const gchar *) buf + 2, bufsz - 2);
		fu_device_set_serial (FU_DEVICE (device), serial_str);
#else
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "GUsb version %s too old to support GD32, "
			     "fwupd needs to be rebuilt against 0.3.6 or later",
			     g_usb_version_string ());
		return FALSE;
#endif
	}

	/* set up target ready for use */
	for (guint j = 0; j < targets->len; j++) {
		DfuTarget *target = g_ptr_array_index (targets, j);
		if (!dfu_target_setup (target, error))
			return FALSE;
	}

	/* success */
	return TRUE;
}

/**
 * dfu_device_close:
 * @device: a #DfuDevice
 * @error: a #GError, or %NULL
 *
 * Closes a DFU device.
 *
 * Return value: %TRUE for success
 **/
static gboolean
dfu_device_close (FuDevice *device, GError **error)
{
	DfuDevice *self = DFU_DEVICE (device);
	DfuDevicePrivate *priv = GET_PRIVATE (self);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));

	/* release interface */
	if (priv->claimed_interface) {
		g_autoptr(GError) error_local = NULL;
		if (!g_usb_device_release_interface (usb_device,
						     (gint) priv->iface_number,
						     0, &error_local)) {
			g_warning ("failed to release interface: %s",
				   error_local->message);
		}
		priv->claimed_interface = FALSE;
	}

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

static gboolean
dfu_device_probe (FuDevice *device, GError **error)
{
	DfuDevice *self = DFU_DEVICE (device);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));

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

	/* add all the targets */
	if (!dfu_device_add_targets (self, error)) {
		g_prefix_error (error, "%04x:%04x is not supported: ",
				g_usb_device_get_vid (usb_device),
				g_usb_device_get_pid (usb_device));
		return FALSE;
	}

	/* check capabilities */
	if (!dfu_device_can_download (self)) {
		g_warning ("%04x:%04x is missing download capability",
			   g_usb_device_get_vid (usb_device),
			   g_usb_device_get_pid (usb_device));
	}

	/* hardware from Jabra literally reboots if you try to retry a failed
	 * write -- there's no way to avoid blocking the daemon like this... */
	if (fu_device_has_custom_flag (device, "attach-extra-reset"))
		fu_device_sleep_with_progress (device, 10); /* seconds */

	/* success */
	return TRUE;
}

/**
 * dfu_device_reset:
 * @device: a #DfuDevice
 * @error: a #GError, or %NULL
 *
 * Resets the USB device.
 *
 * Return value: %TRUE for success
 **/
gboolean
dfu_device_reset (DfuDevice *device, GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GTimer) timer = g_timer_new ();

	g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* no backing USB device */
	if (usb_device == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to reset: no GUsbDevice for %s",
			     dfu_device_get_platform_id (device));
		return FALSE;
	}

	if (!g_usb_device_reset (usb_device, &error_local)) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "cannot reset USB device: %s [%i]",
			     error_local->message,
			     error_local->code);
		return FALSE;
	}
	g_debug ("reset took %.2lfms", g_timer_elapsed (timer, NULL) * 1000);
	return TRUE;
}

static gboolean
dfu_device_attach (FuDevice *device, GError **error)
{
	DfuDevice *self = DFU_DEVICE (device);
	DfuDevicePrivate *priv = GET_PRIVATE (self);
	g_autoptr(DfuTarget) target = NULL;

	g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* already in runtime mode */
	if (!dfu_device_refresh_and_clear (self, error))
		return FALSE;
	if (!fu_device_has_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER))
		return TRUE;

	/* inform UI there's going to be a re-attach */
	fu_device_set_status (device, FWUPD_STATUS_DEVICE_RESTART);

	/* handle weirdness */
	if (fu_device_has_custom_flag (device, "detach-for-attach")) {
		if (!dfu_device_request_detach (self, error))
			return FALSE;
		fu_device_set_status (device, FWUPD_STATUS_IDLE);
		fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
		return TRUE;
	}

	/* handle m-stack DFU bootloaders */
	if (!priv->done_upload_or_download &&
	    fu_device_has_custom_flag (device, "attach-upload-download")) {
		g_autoptr(GBytes) chunk = NULL;
		g_autoptr(DfuTarget) target_zero = NULL;
		g_debug ("doing dummy upload to work around m-stack quirk");
		target_zero = dfu_device_get_target_by_alt_setting (self, 0, error);
		if (target_zero == NULL)
			return FALSE;
		chunk = dfu_target_upload_chunk (target_zero, 0, 0, error);
		if (chunk == NULL)
			return FALSE;
	}

	/* get default target */
	target = dfu_device_get_target_by_alt_setting (self, 0, error);
	if (target == NULL)
		return FALSE;

	/* normal DFU mode just needs a bus reset */
	if (fu_device_has_custom_flag (device, "no-bus-reset-attach") &&
	    dfu_device_has_attribute (self, DFU_DEVICE_ATTRIBUTE_WILL_DETACH))
		g_debug ("Bus reset is not required. Device will reboot to normal");
	else if (!dfu_target_attach (target, error))
		return FALSE;

	/* success */
	priv->force_version = 0x0;
	fu_device_set_status (device, FWUPD_STATUS_IDLE);
	fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
	return TRUE;
}

static void
dfu_device_percentage_cb (DfuTarget *target, guint percentage, DfuDevice *device)
{
	fu_device_set_progress (FU_DEVICE (device), percentage);
}

static void
dfu_device_action_cb (DfuTarget *target, FwupdStatus action, DfuDevice *device)
{
	fu_device_set_status (FU_DEVICE (device), action);
}

/**
 * dfu_device_upload:
 * @device: a #DfuDevice
 * @flags: flags to use, e.g. %DFU_TARGET_TRANSFER_FLAG_VERIFY
 * @error: a #GError, or %NULL
 *
 * Uploads firmware from the target to the host.
 *
 * Return value: (transfer full): the uploaded firmware, or %NULL for error
 **/
FuFirmware *
dfu_device_upload (DfuDevice *device,
		   DfuTargetTransferFlags flags,
		   GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	g_autoptr(FuFirmware) firmware = NULL;

	/* no backing USB device */
	if (usb_device == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to upload: no GUsbDevice for %s",
			     dfu_device_get_platform_id (device));
		return NULL;
	}

	/* ensure interface is claimed */
	if (!dfu_device_ensure_interface (device, error))
		return NULL;

	/* choose the most appropriate type */
	if (priv->targets->len > 1) {
		firmware = fu_dfuse_firmware_new ();
		g_debug ("switching to DefuSe automatically");
	} else {
		firmware = fu_dfu_firmware_new ();
	}
	fu_dfu_firmware_set_vid (FU_DFU_FIRMWARE (firmware), priv->runtime_vid);
	fu_dfu_firmware_set_pid (FU_DFU_FIRMWARE (firmware), priv->runtime_pid);
	fu_dfu_firmware_set_release (FU_DFU_FIRMWARE (firmware), 0xffff);

	/* upload from each target */
	for (guint i = 0; i < priv->targets->len; i++) {
		DfuTarget *target;
		const gchar *alt_name;
		gulong id1;
		gulong id2;

		/* upload to target and proxy signals */
		target = g_ptr_array_index (priv->targets, i);

		/* ignore some target types */
		alt_name = dfu_target_get_alt_name_for_display (target, NULL);
		if (g_strcmp0 (alt_name, "Option Bytes") == 0) {
			g_debug ("ignoring target %s", alt_name);
			continue;
		}

		id1 = g_signal_connect (target, "percentage-changed",
					G_CALLBACK (dfu_device_percentage_cb), device);
		id2 = g_signal_connect (target, "action-changed",
					G_CALLBACK (dfu_device_action_cb), device);
		if (!dfu_target_upload (target,
					firmware,
					DFU_TARGET_TRANSFER_FLAG_NONE,
					error))
			return NULL;
		g_signal_handler_disconnect (target, id1);
		g_signal_handler_disconnect (target, id2);
	}

	/* do not do the dummy upload for quirked devices */
	priv->done_upload_or_download = TRUE;

	/* success */
	fu_device_set_status (FU_DEVICE (device), FWUPD_STATUS_IDLE);
	return g_object_ref (firmware);
}

static gboolean
dfu_device_id_compatible (guint16 id_file, guint16 id_runtime, guint16 id_dev)
{
	/* file doesn't specify */
	if (id_file == 0xffff)
		return TRUE;

	/* runtime matches */
	if (id_runtime != 0xffff && id_file == id_runtime)
		return TRUE;

	/* bootloader matches */
	if (id_dev != 0xffff && id_file == id_dev)
		return TRUE;

	/* nothing */
	return FALSE;
}

static gboolean
dfu_device_download (DfuDevice *device,
		     FuFirmware *firmware,
		     DfuTargetTransferFlags flags,
		     GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	gboolean ret;
	g_autoptr(GPtrArray) images = NULL;

	/* no backing USB device */
	if (usb_device == NULL) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "failed to download: no GUsbDevice for %s",
			     dfu_device_get_platform_id (device));
		return FALSE;
	}

	/* ensure interface is claimed */
	if (!dfu_device_ensure_interface (device, error))
		return FALSE;

	/* do we allow wildcard VID:PID matches */
	if ((flags & DFU_TARGET_TRANSFER_FLAG_WILDCARD_VID) == 0) {
		if (fu_dfu_firmware_get_vid (FU_DFU_FIRMWARE (firmware)) == 0xffff) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "firmware vendor ID not specified");
			return FALSE;
		}
	}
	if ((flags & DFU_TARGET_TRANSFER_FLAG_WILDCARD_PID) == 0) {
		if (fu_dfu_firmware_get_pid (FU_DFU_FIRMWARE (firmware)) == 0xffff) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "firmware product ID not specified");
			return FALSE;
		}
	}

	/* check vendor matches */
	if (priv->runtime_vid != 0xffff) {
		if (!dfu_device_id_compatible (fu_dfu_firmware_get_vid (FU_DFU_FIRMWARE (firmware)),
					       priv->runtime_vid,
					       fu_usb_device_get_vid (FU_USB_DEVICE (device)))) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "vendor ID incorrect, expected 0x%04x "
				     "got 0x%04x and 0x%04x\n",
				     fu_dfu_firmware_get_vid (FU_DFU_FIRMWARE (firmware)),
				     priv->runtime_vid,
				     fu_usb_device_get_vid (FU_USB_DEVICE (device)));
			return FALSE;
		}
	}

	/* check product matches */
	if (priv->runtime_pid != 0xffff) {
		if (!dfu_device_id_compatible (fu_dfu_firmware_get_pid (FU_DFU_FIRMWARE (firmware)),
					       priv->runtime_pid,
					       fu_usb_device_get_pid (FU_USB_DEVICE (device)))) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "product ID incorrect, expected 0x%04x "
				     "got 0x%04x and 0x%04x",
				     fu_dfu_firmware_get_pid (FU_DFU_FIRMWARE (firmware)),
				     priv->runtime_pid,
				     fu_usb_device_get_pid (FU_USB_DEVICE (device)));
			return FALSE;
		}
	}

	/* download each target */
	images = fu_firmware_get_images (firmware);
	if (images->len == 0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INVALID_FILE,
				     "no images in firmware file");
		return FALSE;
	}
	for (guint i = 0; i < images->len; i++) {
		FuFirmwareImage *image = g_ptr_array_index (images, i);
		DfuTargetTransferFlags flags_local = DFU_TARGET_TRANSFER_FLAG_NONE;
		const gchar *alt_name;
		guint8 alt;
		gulong id1;
		gulong id2;
		g_autoptr(DfuTarget) target_tmp = NULL;
		g_autoptr(GError) error_local = NULL;

		alt = fu_firmware_image_get_idx (image);
		target_tmp = dfu_device_get_target_by_alt_setting (device, alt, error);
		if (target_tmp == NULL)
			return FALSE;

		/* we don't actually need to print this */
		alt_name = dfu_target_get_alt_name (target_tmp, &error_local);
		if (alt_name == NULL) {
			if (g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) {
				alt_name = "unknown";
			} else {
				g_propagate_error (error, g_steal_pointer (&error_local));
				return FALSE;
			}
		}
		g_debug ("downloading to target: %s", alt_name);

		/* download onto target */
		if (flags & DFU_TARGET_TRANSFER_FLAG_VERIFY)
			flags_local = DFU_TARGET_TRANSFER_FLAG_VERIFY;
		if (fu_dfu_firmware_get_version (FU_DFU_FIRMWARE (firmware)) == 0x0)
			flags_local |= DFU_TARGET_TRANSFER_FLAG_ADDR_HEURISTIC;
		id1 = g_signal_connect (target_tmp, "percentage-changed",
					G_CALLBACK (dfu_device_percentage_cb), device);
		id2 = g_signal_connect (target_tmp, "action-changed",
					G_CALLBACK (dfu_device_action_cb), device);
		ret = dfu_target_download (target_tmp,
					   image,
					   flags_local,
					   error);
		g_signal_handler_disconnect (target_tmp, id1);
		g_signal_handler_disconnect (target_tmp, id2);
		if (!ret)
			return FALSE;
	}

	/* do not do the dummy upload for quirked devices */
	priv->done_upload_or_download = TRUE;

	/* success */
	fu_device_set_status (FU_DEVICE (device), FWUPD_STATUS_IDLE);
	return TRUE;
}

void
dfu_device_error_fixup (DfuDevice *device, GError **error)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);

	/* sad panda */
	if (error == NULL)
		return;

	/* not the right error to query */
	if (!g_error_matches (*error,
			      G_USB_DEVICE_ERROR,
			      G_USB_DEVICE_ERROR_NOT_SUPPORTED))
		return;

	/* get the status */
	if (!dfu_device_refresh (device, NULL))
		return;

	/* not in an error state */
	if (priv->state != DFU_STATE_DFU_ERROR)
		return;

	/* prefix the error */
	switch (priv->status) {
	case DFU_STATUS_OK:
		/* ignore */
		break;
	case DFU_STATUS_ERR_VENDOR:
		g_prefix_error (error, "read protection is active: ");
		break;
	default:
		g_prefix_error (error, "[%s,%s]: ",
				dfu_state_to_string (priv->state),
				dfu_status_to_string (priv->status));
		break;
	}
}

static GBytes *
dfu_device_dump_firmware (FuDevice *device, GError **error)
{
	DfuDevice *self = DFU_DEVICE (device);
	g_autoptr(FuFirmware) firmware = NULL;
	g_autoptr(FuDeviceLocker) locker = 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;

	/* get data from hardware */
	g_debug ("uploading from device->host");
	if (!dfu_device_refresh_and_clear (self, error))
		return NULL;
	firmware = dfu_device_upload (self, DFU_TARGET_TRANSFER_FLAG_NONE, error);
	if (firmware == NULL)
		return NULL;

	/* get the checksum */
	return fu_firmware_write (firmware, error);
}

static FuFirmware *
dfu_device_prepare_firmware (FuDevice *device,
			     GBytes *fw,
			     FwupdInstallFlags flags,
			     GError **error)
{
	return fu_firmware_new_from_gtypes (fw, flags, error,
					    FU_TYPE_DFUSE_FIRMWARE,
					    FU_TYPE_DFU_FIRMWARE,
					    FU_TYPE_FIRMWARE,
					    G_TYPE_INVALID);
}

static gboolean
dfu_device_write_firmware (FuDevice *device,
			   FuFirmware *firmware,
			   FwupdInstallFlags flags,
			   GError **error)
{
	DfuDevice *self = DFU_DEVICE (device);
	DfuTargetTransferFlags transfer_flags = DFU_TARGET_TRANSFER_FLAG_VERIFY;

	/* open it */
	if (!dfu_device_refresh_and_clear (self, error))
		return FALSE;
	if (flags & FWUPD_INSTALL_FLAG_IGNORE_VID_PID) {
		transfer_flags |= DFU_TARGET_TRANSFER_FLAG_WILDCARD_VID;
		transfer_flags |= DFU_TARGET_TRANSFER_FLAG_WILDCARD_PID;
	}

	/* hit hardware */
	return dfu_device_download (self, firmware, transfer_flags, error);
}

static gboolean
dfu_device_set_quirk_kv (FuDevice *device,
			 const gchar *key,
			 const gchar *value,
			 GError **error)
{
	DfuDevice *self = DFU_DEVICE (device);
	DfuDevicePrivate *priv = GET_PRIVATE (self);

	if (g_strcmp0 (key, FU_QUIRKS_DFU_FORCE_VERSION) == 0) {
		if (value != NULL) {
			gsize valuesz = strlen (value);
			return fu_firmware_strparse_uint16_safe (value, valuesz, 0,
								 &priv->force_version,
								 error);
		}
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "invalid DFU version");
		return FALSE;
	}
	if (g_strcmp0 (key, "DfuForceTimeout") == 0) {
		guint64 tmp = fu_common_strtoull (value);
		if (tmp < G_MAXUINT) {
			priv->timeout_ms = tmp;
			return TRUE;
		}
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "invalid DFU timeout");
		return FALSE;
	}
	if (g_strcmp0 (key, "DfuForceTransferSize") == 0) {
		guint64 tmp = fu_common_strtoull (value);
		if (tmp < G_MAXUINT16) {
			priv->force_transfer_size = tmp;
			return TRUE;
		}
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "invalid DFU transfer size");
		return FALSE;
	}

	/* failed */
	g_set_error_literal (error,
			     G_IO_ERROR,
			     G_IO_ERROR_NOT_SUPPORTED,
			     "quirk key not supported");
	return FALSE;
}

/**
 * dfu_device_get_attributes_as_string: (skip)
 * @device: a #DfuDevice
 *
 * Gets a string describing the attributes for a device.
 *
 * Return value: a string, possibly empty
 **/
gchar *
dfu_device_get_attributes_as_string (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	GString *str;

	/* just append to a string */
	str = g_string_new ("");
	if (priv->attributes & DFU_DEVICE_ATTRIBUTE_CAN_DOWNLOAD)
		g_string_append_printf (str, "can-download|");
	if (priv->attributes & DFU_DEVICE_ATTRIBUTE_CAN_UPLOAD)
		g_string_append_printf (str, "can-upload|");
	if (priv->attributes & DFU_DEVICE_ATTRIBUTE_MANIFEST_TOL)
		g_string_append_printf (str, "manifest-tol|");
	if (priv->attributes & DFU_DEVICE_ATTRIBUTE_WILL_DETACH)
		g_string_append_printf (str, "will-detach|");
	if (priv->attributes & DFU_DEVICE_ATTRIBUTE_CAN_ACCELERATE)
		g_string_append_printf (str, "can-accelerate|");

	/* remove trailing pipe */
	g_string_truncate (str, str->len - 1);
	return g_string_free (str, FALSE);
}

static void
dfu_device_finalize (GObject *object)
{
	DfuDevice *device = DFU_DEVICE (object);
	DfuDevicePrivate *priv = GET_PRIVATE (device);

	g_free (priv->chip_id);
	g_ptr_array_unref (priv->targets);

	G_OBJECT_CLASS (dfu_device_parent_class)->finalize (object);
}

static void
dfu_device_class_init (DfuDeviceClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
	klass_device->set_quirk_kv = dfu_device_set_quirk_kv;
	klass_device->to_string = dfu_device_to_string;
	klass_device->dump_firmware = dfu_device_dump_firmware;
	klass_device->write_firmware = dfu_device_write_firmware;
	klass_device->prepare_firmware = dfu_device_prepare_firmware;
	klass_device->attach = dfu_device_attach;
	klass_device->detach = dfu_device_detach;
	klass_device->reload = dfu_device_reload;
	klass_device->open = dfu_device_open;
	klass_device->close = dfu_device_close;
	klass_device->probe = dfu_device_probe;
	object_class->finalize = dfu_device_finalize;
}

static void
dfu_device_init (DfuDevice *device)
{
	DfuDevicePrivate *priv = GET_PRIVATE (device);
	priv->iface_number = 0xff;
	priv->runtime_pid = 0xffff;
	priv->runtime_vid = 0xffff;
	priv->runtime_release = 0xffff;
	priv->state = DFU_STATE_APP_IDLE;
	priv->status = DFU_STATUS_OK;
	priv->targets = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	priv->timeout_ms = 1500;
	priv->transfer_size = 64;
	fu_device_add_icon (FU_DEVICE (device), "drive-harddisk-usb");
	fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_ADD_COUNTERPART_GUIDS);
	fu_device_set_remove_delay (FU_DEVICE (device), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE);
}
