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

/**
 * SECTION:dfu-target
 * @short_description: Object representing a DFU-capable target
 *
 * This object allows uploading and downloading an image onto a
 * specific DFU-capable target.
 *
 * You only need to use this in preference to #DfuDevice if you only
 * want to update one target on the device. Most users will want to
 * update all the targets on the device at the same time.
 *
 * See also: #DfuDevice, #DfuImage
 */

#include "config.h"

#include <string.h>
#include <math.h>

#include "dfu-common.h"
#include "dfu-device-private.h"
#include "dfu-sector-private.h"
#include "dfu-target-private.h"

#include "fwupd-error.h"

static void dfu_target_finalize			 (GObject *object);

typedef struct {
	DfuDevice		*device;		/* not refcounted */
	DfuCipherKind		 cipher_kind;
	gboolean		 done_setup;
	guint8			 alt_setting;
	guint8			 alt_idx;
	gchar			*alt_name;
	gchar			*alt_name_for_display;
	GPtrArray		*sectors;		/* of DfuSector */
	guint			 old_percentage;
	FwupdStatus		 old_action;
} DfuTargetPrivate;

enum {
	SIGNAL_PERCENTAGE_CHANGED,
	SIGNAL_ACTION_CHANGED,
	SIGNAL_LAST
};

static guint signals [SIGNAL_LAST] = { 0 };

G_DEFINE_TYPE_WITH_PRIVATE (DfuTarget, dfu_target, G_TYPE_OBJECT)
#define GET_PRIVATE(o) (dfu_target_get_instance_private (o))

static void
dfu_target_class_init (DfuTargetClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	/**
	 * DfuTarget::percentage-changed:
	 * @device: the #DfuTarget instance that emitted the signal
	 * @percentage: the new percentage
	 *
	 * The ::percentage-changed signal is emitted when the percentage changes.
	 **/
	signals [SIGNAL_PERCENTAGE_CHANGED] =
		g_signal_new ("percentage-changed",
			      G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (DfuTargetClass, percentage_changed),
			      NULL, NULL, g_cclosure_marshal_VOID__UINT,
			      G_TYPE_NONE, 1, G_TYPE_UINT);

	/**
	 * DfuTarget::action-changed:
	 * @device: the #DfuTarget instance that emitted the signal
	 * @action: the new FwupdStatus
	 *
	 * The ::action-changed signal is emitted when the high level action changes.
	 **/
	signals [SIGNAL_ACTION_CHANGED] =
		g_signal_new ("action-changed",
			      G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (DfuTargetClass, action_changed),
			      NULL, NULL, g_cclosure_marshal_VOID__UINT,
			      G_TYPE_NONE, 1, G_TYPE_UINT);

	object_class->finalize = dfu_target_finalize;
}

static void
dfu_target_init (DfuTarget *target)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	priv->sectors = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	priv->old_percentage = G_MAXUINT;
	priv->old_action = FWUPD_STATUS_IDLE;
}

static void
dfu_target_finalize (GObject *object)
{
	DfuTarget *target = DFU_TARGET (object);
	DfuTargetPrivate *priv = GET_PRIVATE (target);

	g_free (priv->alt_name);
	g_free (priv->alt_name_for_display);
	g_ptr_array_unref (priv->sectors);

	/* we no longer care */
	if (priv->device != NULL) {
		g_object_remove_weak_pointer (G_OBJECT (priv->device),
					      (gpointer *) &priv->device);
	}

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

static gchar *
dfu_target_sectors_to_string (DfuTarget *target)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	GString *str = g_string_new ("");
	for (guint i = 0; i < priv->sectors->len; i++) {
		DfuSector *sector = g_ptr_array_index (priv->sectors, i);
		g_autofree gchar *tmp = dfu_sector_to_string (sector);
		g_string_append_printf (str, "%s\n", tmp);
	}
	if (str->len > 0)
		g_string_truncate (str, str->len - 1);
	return g_string_free (str, FALSE);
}

DfuSector *
dfu_target_get_sector_for_addr (DfuTarget *target, guint32 addr)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);

	for (guint i = 0; i < priv->sectors->len; i++) {
		DfuSector *sector = g_ptr_array_index (priv->sectors, i);
		if (addr < dfu_sector_get_address (sector))
			continue;
		if (addr > dfu_sector_get_address (sector) +
				dfu_sector_get_size (sector))
			continue;
		return sector;
	}
	return NULL;
}

static gboolean
dfu_target_parse_sector (DfuTarget *target,
			 const gchar *dfuse_sector_id,
			 guint32 *addr,
			 guint16 zone,
			 guint16 number,
			 GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	DfuSectorCap cap = DFU_SECTOR_CAP_NONE;
	gchar *tmp;
	guint32 addr_offset = 0;
	guint64 nr_sectors;
	guint64 sector_size;

	/* parse # of sectors */
	nr_sectors = g_ascii_strtoull (dfuse_sector_id, &tmp, 10);
	if (nr_sectors > 999) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "Invalid number of sectors: %s",
			     dfuse_sector_id);
		return FALSE;
	}

	/* check this is the delimiter */
	if (tmp[0] != '*') {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "Invalid sector ID: %s",
			     dfuse_sector_id);
		return FALSE;
	}

	/* parse sector size */
	sector_size = g_ascii_strtoull (tmp + 1, &tmp, 10);
	if (sector_size > 999) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "Invalid sector size: %s",
			     dfuse_sector_id);
		return FALSE;
	}

	/* get multiplier */
	switch (tmp[0]) {
	case 'B':		/* byte */
	case ' ':		/* byte, ST reference bootloader :/ */
		break;
	case 'K':		/* Kilo */
		sector_size *= 0x400;
		break;
	case 'M':		/* Mega */
		sector_size *= 0x100000	;
		break;
	default:
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "Invalid sector multiplier: %s",
			     tmp);
		return FALSE;
	}

	/* get sector type */
	switch (tmp[1]) {
	case 'a':
		cap = DFU_SECTOR_CAP_READABLE;
		break;
	case 'b':
		cap = DFU_SECTOR_CAP_ERASEABLE;
		break;
	case 'c':
		cap = DFU_SECTOR_CAP_READABLE |
		      DFU_SECTOR_CAP_ERASEABLE;
		break;
	case 'd':
		cap = DFU_SECTOR_CAP_WRITEABLE;
		break;
	case 'e':
		cap = DFU_SECTOR_CAP_READABLE |
		      DFU_SECTOR_CAP_WRITEABLE;
		break;
	case 'f':
		cap = DFU_SECTOR_CAP_ERASEABLE |
		      DFU_SECTOR_CAP_WRITEABLE;
		break;
	case 'g':
		cap = DFU_SECTOR_CAP_READABLE |
		      DFU_SECTOR_CAP_ERASEABLE |
		      DFU_SECTOR_CAP_WRITEABLE;
		break;
	default:
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "Invalid sector type: %s",
			     tmp);
		return FALSE;
	}

	/* add all the sectors */
	for (guint i = 0; i < nr_sectors; i++) {
		DfuSector *sector;
		sector = dfu_sector_new (*addr + addr_offset,
					 (guint32) sector_size,
					 (guint32) ((nr_sectors * sector_size) - addr_offset),
					 zone,
					 number,
					 cap);
		g_ptr_array_add (priv->sectors, sector);
		addr_offset += dfu_sector_get_size (sector);
	}

	/* update for next sector */
	*addr += addr_offset;
	return TRUE;
}

gboolean
dfu_target_parse_sectors (DfuTarget *target, const gchar *alt_name, GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	g_autofree gchar *str_debug = NULL;
	g_auto(GStrv) zones = NULL;

	/* not set */
	if (alt_name == NULL)
		return TRUE;

	/* do we have any hint for the cipher */
	if (g_strstr_len (alt_name, -1, "|XTEA") != NULL)
		priv->cipher_kind = DFU_CIPHER_KIND_XTEA;

	/* From the Neo Freerunner */
	if (g_str_has_prefix (alt_name, "RAM 0x")) {
		DfuSector *sector;
		guint64 addr_tmp;
		addr_tmp = g_ascii_strtoull (alt_name + 6, NULL, 16);
		if (addr_tmp == 0 || addr_tmp > G_MAXUINT32)
			return FALSE;
		g_debug ("RAM description, so parsing");
		sector = dfu_sector_new ((guint32) addr_tmp,
					 0x0, /* size */
					 0x0, /* size_left */
					 0x0, /* zone */
					 0x0, /* number */
					 DFU_SECTOR_CAP_READABLE |
					 DFU_SECTOR_CAP_WRITEABLE);
		g_ptr_array_add (priv->sectors, sector);
	}

	/* not a DfuSe alternative name */
	if (alt_name[0] != '@')
		return TRUE;

	/* clear any existing zones */
	g_ptr_array_set_size (priv->sectors, 0);

	/* parse zones */
	zones = g_strsplit (alt_name, "/", -1);
	g_free (priv->alt_name_for_display);
	priv->alt_name_for_display = g_strdup (g_strchomp (zones[0] + 1));
	for (guint i = 1; zones[i] != NULL; i += 2) {
		guint32 addr;
		guint64 addr_tmp;
		g_auto(GStrv) sectors = NULL;

		/* parse address */
		if (!g_str_has_prefix (zones[i], "0x")) {
			g_set_error_literal (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_NOT_SUPPORTED,
					     "No sector address");
			return FALSE;
		}
		addr_tmp = g_ascii_strtoull (zones[i] + 2, NULL, 16);
		if (addr_tmp > G_MAXUINT32) {
			g_set_error_literal (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_NOT_SUPPORTED,
					     "Sector address too large");
			return FALSE;
		}
		addr = (guint32) addr_tmp;

		/* no sectors?! */
		if (zones[i+1] == NULL) {
			g_set_error_literal (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_NOT_SUPPORTED,
					     "No sector section");
			return FALSE;
		}

		/* parse sectors */
		sectors = g_strsplit (zones[i+1], ",", -1);
		for (guint16 j = 0; sectors[j] != NULL; j++) {
			if (!dfu_target_parse_sector (target,
						      sectors[j],
						      &addr,
						      (i - 1) / 2, j,
						      error)) {
				g_prefix_error (error,
						"Failed to parse: '%s': ",
						sectors[j]);
				return FALSE;
			}
		}
	}

	/* success */
	str_debug = dfu_target_sectors_to_string (target);
	g_debug ("%s", str_debug);
	return TRUE;
}

/**
 * dfu_target_new: (skip)
 *
 * Creates a new DFU target, which represents an alt-setting on a
 * DFU-capable device.
 *
 * Return value: a #DfuTarget
 **/
DfuTarget *
dfu_target_new (void)
{
	DfuTarget *target;
	target = g_object_new (DFU_TYPE_TARGET, NULL);
	return target;
}

/**
 * dfu_target_get_sectors:
 * @target: a #GUsbDevice
 *
 * Gets the sectors exported by the target.
 *
 * Return value: (transfer none) (element-type DfuSector): sectors
 **/
GPtrArray *
dfu_target_get_sectors (DfuTarget *target)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	g_return_val_if_fail (DFU_IS_TARGET (target), NULL);
	return priv->sectors;
}

/**
 * dfu_target_get_sector_default:
 * @target: a #GUsbDevice
 *
 * Gets the default (first) sector exported by the target.
 *
 * Return value: (transfer none): a #DfuSector, or %NULL
 **/
DfuSector *
dfu_target_get_sector_default (DfuTarget *target)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	g_return_val_if_fail (DFU_IS_TARGET (target), NULL);
	if (priv->sectors->len == 0)
		return NULL;
	return DFU_SECTOR (g_ptr_array_index (priv->sectors, 0));
}

/**
 * dfu_target_status_to_error_msg:
 * @status: a #DfuStatus, e.g. %DFU_STATUS_ERR_ERASE
 *
 * Converts an enumerated value to an error description.
 *
 * Return value: a string
 **/
static const gchar *
dfu_target_status_to_error_msg (DfuStatus status)
{
	if (status == DFU_STATUS_OK)
		return "No error condition is present";
	if (status == DFU_STATUS_ERR_TARGET)
		return "Firmware is not for designed this device";
	if (status == DFU_STATUS_ERR_FILE)
		return "Firmware is for this device but fails verification";
	if (status == DFU_STATUS_ERR_WRITE)
		return "Device is unable to write memory";
	if (status == DFU_STATUS_ERR_ERASE)
		return "Memory erase function failed";
	if (status == DFU_STATUS_ERR_CHECK_ERASED)
		return "Memory erase check failed";
	if (status == DFU_STATUS_ERR_PROG)
		return "Program memory function failed";
	if (status == DFU_STATUS_ERR_VERIFY)
		return "Programmed memory failed verification";
	if (status == DFU_STATUS_ERR_ADDRESS)
		return "Cannot program memory due to address out of range";
	if (status == DFU_STATUS_ERR_NOTDONE)
		return "Received zero-length download but data is incomplete";
	if (status == DFU_STATUS_ERR_FIRMWARE)
		return "Device firmware is corrupt";
	if (status == DFU_STATUS_ERR_VENDOR)
		return "Vendor-specific error";
	if (status == DFU_STATUS_ERR_USBR)
		return "Device detected unexpected USB reset signaling";
	if (status == DFU_STATUS_ERR_POR)
		return "Device detected unexpected power on reset";
	if (status == DFU_STATUS_ERR_UNKNOWN)
		return "Something unexpected went wrong";
	if (status == DFU_STATUS_ERR_STALLDPKT)
		return "Device stalled an unexpected request";
	return NULL;
}

gboolean
dfu_target_check_status (DfuTarget *target, GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	DfuStatus status;

	/* get the status */
	if (!dfu_device_refresh (priv->device, error))
		return FALSE;

	/* wait for dfuDNBUSY to not be set */
	if (dfu_device_get_version (priv->device) == DFU_VERSION_DFUSE) {
		while (dfu_device_get_state (priv->device) == DFU_STATE_DFU_DNBUSY) {
			g_debug ("waiting for DFU_STATE_DFU_DNBUSY to clear");
			g_usleep (dfu_device_get_download_timeout (priv->device) * 1000);
			if (!dfu_device_refresh (priv->device, error))
				return FALSE;
		}
	}

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

	/* STM32-specific long errors */
	status = dfu_device_get_status (priv->device);
	if (dfu_device_get_version (priv->device) == DFU_VERSION_DFUSE) {
		if (status == DFU_STATUS_ERR_VENDOR) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "Read protection is active");
			return FALSE;
		}
		if (status == DFU_STATUS_ERR_TARGET) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "Address is wrong or unsupported");
			return FALSE;
		}
	}

	/* use a proper error description */
	g_set_error_literal (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     dfu_target_status_to_error_msg (status));
	return FALSE;
}

/**
 * dfu_target_use_alt_setting:
 * @target: a #DfuTarget
 * @error: a #GError, or %NULL
 *
 * Opens a DFU-capable target.
 *
 * Return value: %TRUE for success
 **/
static gboolean
dfu_target_use_alt_setting (DfuTarget *target, GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (priv->device));
	g_autoptr(GError) error_local = NULL;

	g_return_val_if_fail (DFU_IS_TARGET (target), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

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

	/* use the correct setting */
	if (!dfu_device_is_runtime (priv->device)) {
		if (!g_usb_device_set_interface_alt (usb_device,
						     (gint) dfu_device_get_interface (priv->device),
						     (gint) priv->alt_setting,
						     &error_local)) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "cannot set alternate setting 0x%02x on interface %i: %s",
				     priv->alt_setting,
				     dfu_device_get_interface (priv->device),
				     error_local->message);
			return FALSE;
		}
	}

	return TRUE;
}

void
dfu_target_set_alt_name (DfuTarget *target, const gchar *alt_name)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	g_free (priv->alt_name);
	priv->alt_name = g_strdup (alt_name);
}

void
dfu_target_set_device (DfuTarget *target, DfuDevice *device)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	g_set_object (&priv->device, device);

	/* if we try to ref the target and destroy the device */
	g_object_add_weak_pointer (G_OBJECT (priv->device),
				   (gpointer *) &priv->device);
}

/**
 * dfu_target_setup:
 * @target: a #DfuTarget
 * @error: a #GError, or %NULL
 *
 * Opens a DFU-capable target.
 *
 * Return value: %TRUE for success
 **/
gboolean
dfu_target_setup (DfuTarget *target, GError **error)
{
	DfuTargetClass *klass = DFU_TARGET_GET_CLASS (target);
	DfuTargetPrivate *priv = GET_PRIVATE (target);

	g_return_val_if_fail (DFU_IS_TARGET (target), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

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

	/* superclassed */
	if (klass->setup != NULL) {
		if (!klass->setup (target, error))
			return FALSE;
	}

	/* get string */
	if (priv->alt_idx != 0x00 && priv->alt_name == NULL) {
		GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (priv->device));
		priv->alt_name =
			g_usb_device_get_string_descriptor (usb_device,
							    priv->alt_idx,
							    NULL);
	}

	/* parse the DfuSe format according to UM0424 */
	if (!dfu_target_parse_sectors (target,
				       priv->alt_name,
				       error))
		return FALSE;

	/* add a dummy entry */
	if (priv->sectors->len == 0) {
		DfuSector *sector;
		sector = dfu_sector_new (0x0, /* addr */
					 0x0, /* size */
					 0x0, /* size_left */
					 0x0, /* zone */
					 0x0, /* number */
					 DFU_SECTOR_CAP_READABLE |
					 DFU_SECTOR_CAP_WRITEABLE);
		g_debug ("no UM0424 sector description in %s", priv->alt_name);
		g_ptr_array_add (priv->sectors, sector);
	}

	priv->done_setup = TRUE;
	return TRUE;
}

/**
 * dfu_target_mass_erase:
 * @target: a #DfuTarget
 * @error: a #GError, or %NULL
 *
 * Mass erases the device clearing all SRAM and EEPROM memory.
 *
 * IMPORTANT: This only works on STM32 devices from ST and AVR32 devices from Atmel.
 *
 * Return value: %TRUE for success
 **/
gboolean
dfu_target_mass_erase (DfuTarget *target, GError **error)
{
	DfuTargetClass *klass = DFU_TARGET_GET_CLASS (target);
	if (!dfu_target_setup (target, error))
		return FALSE;
	if (klass->mass_erase == NULL) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "mass erase not supported");
		return FALSE;
	}
	return klass->mass_erase (target, error);
}

gboolean
dfu_target_download_chunk (DfuTarget *target, guint16 index, GBytes *bytes, GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (priv->device));
	g_autoptr(GError) error_local = NULL;
	gsize actual_length;

	/* low level packet debugging */
	if (g_getenv ("FWUPD_DFU_VERBOSE") != NULL) {
		gsize sz = 0;
		const guint8 *data = g_bytes_get_data (bytes, &sz);
		for (gsize i = 0; i < sz; i++)
			g_print ("Message: m[%" G_GSIZE_FORMAT "] = 0x%02x\n", i, (guint) data[i]);
	}

	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_DNLOAD,
					    index,
					    dfu_device_get_interface (priv->device),
					    (guint8 *) g_bytes_get_data (bytes, NULL),
					    g_bytes_get_size (bytes),
					    &actual_length,
					    dfu_device_get_timeout (priv->device),
					    NULL,
					    &error_local)) {
		/* refresh the error code */
		dfu_device_error_fixup (priv->device, &error_local);
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "cannot download data: %s",
			     error_local->message);
		return FALSE;
	}

	/* for STM32 devices, the action only occurs when we do GetStatus */
	if (dfu_device_get_version (priv->device) == DFU_VERSION_DFUSE) {
		if (!dfu_device_refresh (priv->device, error))
			return FALSE;
	}

	/* wait for the device to write contents to the EEPROM */
	if (g_bytes_get_size (bytes) == 0 &&
	    dfu_device_get_download_timeout (priv->device) > 0) {
		dfu_target_set_action (target, FWUPD_STATUS_IDLE);
		dfu_target_set_action (target, FWUPD_STATUS_DEVICE_BUSY);
	}
	if (dfu_device_get_download_timeout (priv->device) > 0) {
		g_debug ("sleeping for %ums…",
			 dfu_device_get_download_timeout (priv->device));
		g_usleep (dfu_device_get_download_timeout (priv->device) * 1000);
	}

	/* find out if the write was successful */
	if (!dfu_device_refresh (priv->device, error))
		return FALSE;

	g_assert (actual_length == g_bytes_get_size (bytes));
	return TRUE;
}

GBytes *
dfu_target_upload_chunk (DfuTarget *target, guint16 index, gsize buf_sz, GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (priv->device));
	g_autoptr(GError) error_local = NULL;
	guint8 *buf;
	gsize actual_length;

	/* unset */
	if (buf_sz == 0)
		buf_sz = (gsize) dfu_device_get_transfer_size (priv->device);

	buf = g_new0 (guint8, buf_sz);
	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_UPLOAD,
					    index,
					    dfu_device_get_interface (priv->device),
					    buf, buf_sz,
					    &actual_length,
					    dfu_device_get_timeout (priv->device),
					    NULL,
					    &error_local)) {
		/* refresh the error code */
		dfu_device_error_fixup (priv->device, &error_local);
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NOT_SUPPORTED,
			     "cannot upload data: %s",
			     error_local->message);
		return NULL;
	}

	/* low level packet debugging */
	if (g_getenv ("FWUPD_DFU_VERBOSE") != NULL) {
		for (gsize i = 0; i < actual_length; i++)
			g_print ("Message: r[%" G_GSIZE_FORMAT "] = 0x%02x\n", i, (guint) buf[i]);
	}

	return g_bytes_new_take (buf, actual_length);
}

void
dfu_target_set_alt_idx (DfuTarget *target, guint8 alt_idx)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	priv->alt_idx = alt_idx;
}

void
dfu_target_set_alt_setting (DfuTarget *target, guint8 alt_setting)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	priv->alt_setting = alt_setting;
}

void
dfu_target_set_action (DfuTarget *target, FwupdStatus action)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);

	/* unchanged */
	if (priv->old_action == action)
		return;
	if (priv->old_action != FWUPD_STATUS_IDLE &&
	    action != FWUPD_STATUS_IDLE) {
		g_debug ("ignoring action %s as %s already set and not idle",
			 fwupd_status_to_string (action),
			 fwupd_status_to_string (priv->old_action));
		return;
	}
	g_debug ("setting action %s", fwupd_status_to_string (action));
	g_signal_emit (target, signals[SIGNAL_ACTION_CHANGED], 0, action);
	priv->old_action = action;
}

DfuDevice *
dfu_target_get_device (DfuTarget *target)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	return priv->device;
}

void
dfu_target_set_percentage_raw (DfuTarget *target, guint percentage)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	if (percentage == priv->old_percentage)
		return;
	g_debug ("setting percentage %u%% of %s",
		 percentage, fwupd_status_to_string (priv->old_action));
	g_signal_emit (target,
		       signals[SIGNAL_PERCENTAGE_CHANGED],
		       0, percentage);
	priv->old_percentage = percentage;
}

void
dfu_target_set_percentage (DfuTarget *target, guint value, guint total)
{
	guint percentage;

	g_return_if_fail (total > 0);

	percentage = (value * 100) / total;
	if (percentage >= 100)
		return;
	dfu_target_set_percentage_raw (target, percentage);
}

gboolean
dfu_target_attach (DfuTarget *target, GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	DfuTargetClass *klass = DFU_TARGET_GET_CLASS (target);

	/* ensure populated */
	if (!dfu_target_setup (target, error))
		return FALSE;

	/* implemented as part of a superclass */
	if (klass->attach != NULL)
		return klass->attach (target, error);

	/* normal DFU mode just needs a bus reset */
	return dfu_device_reset (priv->device, error);
}

static DfuElement *
dfu_target_upload_element_dfu (DfuTarget *target,
			       guint32 address,
			       gsize expected_size,
			       gsize maximum_size,
			       GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	DfuElement *element = NULL;
	GBytes *chunk_tmp;
	guint32 offset = 0;
	guint percentage_size = expected_size > 0 ? expected_size : maximum_size;
	gsize total_size = 0;
	guint16 transfer_size = dfu_device_get_transfer_size (priv->device);
	g_autoptr(GBytes) contents = NULL;
	g_autoptr(GPtrArray) chunks = NULL;

	/* update UI */
	dfu_target_set_action (target, FWUPD_STATUS_DEVICE_READ);

	/* get all the chunks from the hardware */
	chunks = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref);
	for (guint16 idx = 0; idx < G_MAXUINT16; idx++) {
		guint32 chunk_size;

		/* read chunk of data */
		chunk_tmp = dfu_target_upload_chunk (target,
						     idx,
						     0, /* device transfer size */
						     error);
		if (chunk_tmp == NULL)
			return NULL;

		/* keep a sum of all the chunks */
		chunk_size = (guint32) g_bytes_get_size (chunk_tmp);
		total_size += chunk_size;
		offset += chunk_size;

		/* add to array */
		g_debug ("got #%04x chunk of size %" G_GUINT32_FORMAT,
			 idx, chunk_size);
		g_ptr_array_add (chunks, chunk_tmp);

		/* update UI */
		if (chunk_size > 0)
			dfu_target_set_percentage (target, total_size, percentage_size);

		/* detect short write as EOF */
		if (chunk_size < transfer_size)
			break;
	}

	/* check final size */
	if (expected_size > 0) {
		if (total_size != expected_size) {
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INVALID_FILE,
				     "invalid size, got %" G_GSIZE_FORMAT ", "
				     "expected %" G_GSIZE_FORMAT ,
				     total_size, expected_size);
			return NULL;
		}
	}

	/* done */
	dfu_target_set_percentage_raw (target, 100);
	dfu_target_set_action (target, FWUPD_STATUS_IDLE);

	/* create new image */
	contents = dfu_utils_bytes_join_array (chunks);
	element = dfu_element_new ();
	dfu_element_set_contents (element, contents);
	return element;
}

static DfuElement *
dfu_target_upload_element (DfuTarget *target,
			   guint32 address,
			   gsize expected_size,
			   gsize maximum_size,
			   GError **error)
{
	DfuTargetClass *klass = DFU_TARGET_GET_CLASS (target);

	/* implemented as part of a superclass */
	if (klass->upload_element != NULL) {
		return klass->upload_element (target, address, expected_size,
					      maximum_size, error);
	}
	return dfu_target_upload_element_dfu (target,
					      address,
					      expected_size,
					      maximum_size,
					      error);
}

static guint32
dfu_target_get_size_of_zone (DfuTarget *target, guint16 zone)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	guint32 len = 0;
	for (guint i = 0; i < priv->sectors->len; i++) {
		DfuSector *sector = g_ptr_array_index (priv->sectors, i);
		if (dfu_sector_get_zone (sector) != zone)
			continue;
		len += dfu_sector_get_size (sector);
	}
	return len;
}

/**
 * dfu_target_upload:
 * @target: a #DfuTarget
 * @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 image, or %NULL for error
 **/
DfuImage *
dfu_target_upload (DfuTarget *target,
		   DfuTargetTransferFlags flags,
		   GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	DfuSector *sector;
	guint16 zone_cur;
	guint32 zone_size = 0;
	guint32 zone_last = G_MAXUINT;
	g_autoptr(DfuImage) image = NULL;

	g_return_val_if_fail (DFU_IS_TARGET (target), NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	/* ensure populated */
	if (!dfu_target_setup (target, error))
		return NULL;

	/* can the target do this? */
	if (!dfu_device_can_upload (priv->device)) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "target cannot do uploading");
		return NULL;
	}

	/* use correct alt */
	if (!dfu_target_use_alt_setting (target, error))
		return NULL;

	/* no open?! */
	if (priv->sectors->len == 0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "no sectors defined for target");
		return NULL;
	}

	/* create a new image */
	image = dfu_image_new ();
	dfu_image_set_name (image, priv->alt_name);
	dfu_image_set_alt_setting (image, priv->alt_setting);

	/* get all the sectors for the device */
	for (guint i = 0; i < priv->sectors->len; i++) {
		g_autoptr(DfuElement) element = NULL;

		/* only upload to the start of any zone:sector */
		sector = g_ptr_array_index (priv->sectors, i);
		zone_cur = dfu_sector_get_zone (sector);
		if (zone_cur == zone_last)
			continue;

		/* get the size of the entire continuous zone */
		zone_size = dfu_target_get_size_of_zone (target, zone_cur);
		zone_last = zone_cur;

		/* get the first element from the hardware */
		g_debug ("starting upload from 0x%08x (0x%04x)",
			 dfu_sector_get_address (sector),
			 zone_size);
		element = dfu_target_upload_element (target,
						     dfu_sector_get_address (sector),
						     0,		/* expected */
						     zone_size,	/* maximum */
						     error);
		if (element == NULL)
			return NULL;

		/* this element was uploaded okay */
		dfu_image_add_element (image, element);
	}

	/* success */
	return g_object_ref (image);
}

static gchar *
_g_bytes_compare_verbose (GBytes *bytes1, GBytes *bytes2)
{
	const guint8 *data1;
	const guint8 *data2;
	gsize length1;
	gsize length2;

	data1 = g_bytes_get_data (bytes1, &length1);
	data2 = g_bytes_get_data (bytes2, &length2);

	/* not the same length */
	if (length1 != length2) {
		return g_strdup_printf ("got %" G_GSIZE_FORMAT " bytes, "
					"expected %" G_GSIZE_FORMAT,
					length1, length2);
	}

	/* return 00 01 02 03 */
	for (guint i = 0; i < length1; i++) {
		if (data1[i] != data2[i]) {
			return g_strdup_printf ("got 0x%02x, expected 0x%02x @ 0x%04x",
						data1[i], data2[i], i);
		}
	}
	return NULL;
}

static gboolean
dfu_target_download_element_dfu (DfuTarget *target,
				 DfuElement *element,
				 DfuTargetTransferFlags flags,
				 GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	GBytes *bytes;
	guint16 nr_chunks;
	guint16 transfer_size = dfu_device_get_transfer_size (priv->device);

	/* round up as we have to transfer incomplete blocks */
	bytes = dfu_element_get_contents (element);
	nr_chunks = (guint) ceil ((gdouble) g_bytes_get_size (bytes) /
				  (gdouble) transfer_size);
	if (nr_chunks == 0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INVALID_FILE,
				     "zero-length firmware");
		return FALSE;
	}
	dfu_target_set_action (target, FWUPD_STATUS_DEVICE_WRITE);
	for (guint16 i = 0; i < nr_chunks + 1; i++) {
		gsize length;
		guint32 offset;
		g_autoptr(GBytes) bytes_tmp = NULL;

		/* caclulate the offset into the element data */
		offset = i * transfer_size;

		/* we have to write one final zero-sized chunk for EOF */
		if (i < nr_chunks) {
			length = g_bytes_get_size (bytes) - offset;
			if (length > transfer_size)
				length = transfer_size;
			bytes_tmp = g_bytes_new_from_bytes (bytes, offset, length);
		} else {
			bytes_tmp = g_bytes_new (NULL, 0);
		}
		g_debug ("writing #%04x chunk of size %" G_GSIZE_FORMAT,
			 i, g_bytes_get_size (bytes_tmp));
		if (!dfu_target_download_chunk (target, i, bytes_tmp, error))
			return FALSE;

		/* update UI */
		dfu_target_set_percentage (target, offset, g_bytes_get_size (bytes));
	}

	/* done */
	dfu_target_set_percentage_raw (target, 100);
	dfu_target_set_action (target, FWUPD_STATUS_IDLE);

	/* success */
	return TRUE;
}

static gboolean
dfu_target_download_element (DfuTarget *target,
			     DfuElement *element,
			     DfuTargetTransferFlags flags,
			     GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	DfuTargetClass *klass = DFU_TARGET_GET_CLASS (target);

	/* implemented as part of a superclass */
	if (klass->download_element != NULL) {
		if (!klass->download_element (target, element, flags, error))
			return FALSE;
	} else {
		if (!dfu_target_download_element_dfu (target,
						      element,
						      flags,
						      error))
			return FALSE;
	}

	/* verify */
	if (flags & DFU_TARGET_TRANSFER_FLAG_VERIFY &&
	    dfu_device_has_attribute (priv->device, DFU_DEVICE_ATTRIBUTE_CAN_UPLOAD)) {
		GBytes *bytes;
		GBytes *bytes_tmp;
		g_autoptr(DfuElement) element_tmp = NULL;
		dfu_target_set_action (target, FWUPD_STATUS_DEVICE_VERIFY);
		bytes = dfu_element_get_contents (element);
		element_tmp = dfu_target_upload_element (target,
							 dfu_element_get_address (element),
							 g_bytes_get_size (bytes),
							 g_bytes_get_size (bytes),
							 error);
		if (element_tmp == NULL)
			return FALSE;
		bytes_tmp = dfu_element_get_contents (element_tmp);
		if (g_bytes_compare (bytes_tmp, bytes) != 0) {
			g_autofree gchar *bytes_cmp_str = NULL;
			bytes_cmp_str = _g_bytes_compare_verbose (bytes_tmp, bytes);
			g_set_error (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_READ,
				     "verify failed: %s",
				     bytes_cmp_str);
			return FALSE;
		}
		dfu_target_set_action (target, FWUPD_STATUS_IDLE);
	}

	return TRUE;
}

/**
 * dfu_target_download:
 * @target: a #DfuTarget
 * @image: a #DfuImage
 * @flags: flags to use, e.g. %DFU_TARGET_TRANSFER_FLAG_VERIFY
 * @error: a #GError, or %NULL
 *
 * Downloads firmware from the host to the target, optionally verifying
 * the transfer.
 *
 * Return value: %TRUE for success
 **/
gboolean
dfu_target_download (DfuTarget *target, DfuImage *image,
		     DfuTargetTransferFlags flags, GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	GPtrArray *elements;
	gboolean ret;

	g_return_val_if_fail (DFU_IS_TARGET (target), FALSE);
	g_return_val_if_fail (DFU_IS_IMAGE (image), FALSE);
	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

	/* ensure populated */
	if (!dfu_target_setup (target, error))
		return FALSE;

	/* can the target do this? */
	if (!dfu_device_can_download (priv->device)) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_SUPPORTED,
				     "target cannot do downloading");
		return FALSE;
	}

	/* use correct alt */
	if (!dfu_target_use_alt_setting (target, error))
		return FALSE;

	/* download all elements in the image to the device */
	elements = dfu_image_get_elements (image);
	if (elements->len == 0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INVALID_FILE,
				     "no image elements");
		return FALSE;
	}
	for (guint i = 0; i < elements->len; i++) {
		DfuElement *element = dfu_image_get_element (image, (guint8) i);
		g_debug ("downloading element at 0x%04x",
			 dfu_element_get_address (element));

		/* auto-detect missing firmware address -- this assumes
		 * that the first target is the main program memory and that
		 * there is only one element in the firmware file */
		if (flags & DFU_TARGET_TRANSFER_FLAG_ADDR_HEURISTIC &&
		    dfu_element_get_address (element) == 0x0 &&
		    elements->len == 1 &&
		    priv->sectors->len > 0) {
			DfuSector *sector = g_ptr_array_index (priv->sectors, 0);
			g_debug ("fixing up firmware address from 0x0 to 0x%x",
				 dfu_sector_get_address (sector));
			dfu_element_set_address (element, dfu_sector_get_address (sector));
		}

		/* download to device */
		ret = dfu_target_download_element (target,
						   element,
						   flags,
						   error);
		if (!ret)
			return FALSE;
	}

	/* success */
	return TRUE;
}

/**
 * dfu_target_get_alt_setting:
 * @target: a #DfuTarget
 *
 * Gets the alternate setting to use for this interface.
 *
 * Return value: the alternative setting, typically zero
 **/
guint8
dfu_target_get_alt_setting (DfuTarget *target)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	g_return_val_if_fail (DFU_IS_TARGET (target), 0xff);
	return priv->alt_setting;
}

/**
 * dfu_target_get_alt_name:
 * @target: a #DfuTarget
 * @error: a #GError, or %NULL
 *
 * Gets the alternate setting name to use for this interface.
 *
 * Return value: the alternative setting name, typically %NULL
 **/
const gchar *
dfu_target_get_alt_name (DfuTarget *target, GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	g_return_val_if_fail (DFU_IS_TARGET (target), NULL);

	/* ensure populated */
	if (!dfu_target_setup (target, error))
		return NULL;

	/* nothing */
	if (priv->alt_name == NULL) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_FOUND,
				     "no alt-name");
		return NULL;
	}

	return priv->alt_name;
}

/**
 * dfu_target_get_alt_name_for_display:
 * @target: a #DfuTarget
 * @error: a #GError, or %NULL
 *
 * Gets the alternate setting name to use for this interface that can be
 * shown on the display.
 *
 * Return value: the alternative setting name
 **/
const gchar *
dfu_target_get_alt_name_for_display (DfuTarget *target, GError **error)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	g_return_val_if_fail (DFU_IS_TARGET (target), NULL);

	/* ensure populated */
	if (!dfu_target_setup (target, error))
		return NULL;

	/* nothing */
	if (priv->alt_name_for_display == NULL) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_NOT_FOUND,
				     "no alt-name for display");
		return NULL;
	}

	return priv->alt_name_for_display;
}

/**
 * dfu_target_get_cipher_kind:
 * @target: a #DfuTarget
 *
 * Gets the cipher used for data sent to this interface.
 *
 * Return value: the cipher, typically %DFU_CIPHER_KIND_NONE
 **/
DfuCipherKind
dfu_target_get_cipher_kind (DfuTarget *target)
{
	DfuTargetPrivate *priv = GET_PRIVATE (target);
	g_return_val_if_fail (DFU_IS_TARGET (target), 0);
	return priv->cipher_kind;
}
