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

#include "config.h"

#include <fwupdplugin.h>

#include <glib/gi18n.h>
#include <libintl.h>
#include <locale.h>
#include <stdlib.h>
#ifdef HAVE_GIO_UNIX
#include <glib-unix.h>
#endif

#include "fu-context-private.h"
#include "fu-dfu-device.h"
#include "fu-dfu-sector.h"

typedef struct {
	GCancellable *cancellable;
	GPtrArray *cmd_array;
	gboolean force;
	gchar *device_vid_pid;
	guint16 transfer_size;
	FuContext *ctx;
	GUsbContext *usb_context;
} FuDfuTool;

static void
fu_dfu_tool_free(FuDfuTool *self)
{
	if (self == NULL)
		return;
	g_free(self->device_vid_pid);
	if (self->cancellable != NULL)
		g_object_unref(self->cancellable);
	if (self->usb_context != NULL)
		g_object_unref(self->usb_context);
	g_object_unref(self->ctx);
	if (self->cmd_array != NULL)
		g_ptr_array_unref(self->cmd_array);
	g_free(self);
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuDfuTool, fu_dfu_tool_free)
#pragma clang diagnostic pop

typedef gboolean (*FuUtilPrivateCb)(FuDfuTool *util, gchar **values, GError **error);

typedef struct {
	gchar *name;
	gchar *arguments;
	gchar *description;
	FuUtilPrivateCb callback;
} FuUtilItem;

static void
fu_dfu_tool_item_free(FuUtilItem *item)
{
	g_free(item->name);
	g_free(item->arguments);
	g_free(item->description);
	g_free(item);
}

static gint
fu_dfu_tool_sort_command_name_cb(FuUtilItem **item1, FuUtilItem **item2)
{
	return g_strcmp0((*item1)->name, (*item2)->name);
}

static void
fu_dfu_tool_add(GPtrArray *array,
		const gchar *name,
		const gchar *arguments,
		const gchar *description,
		FuUtilPrivateCb callback)
{
	g_auto(GStrv) names = NULL;

	g_return_if_fail(name != NULL);
	g_return_if_fail(description != NULL);
	g_return_if_fail(callback != NULL);

	/* add each one */
	names = g_strsplit(name, ",", -1);
	for (guint i = 0; names[i] != NULL; i++) {
		FuUtilItem *item = g_new0(FuUtilItem, 1);
		item->name = g_strdup(names[i]);
		if (i == 0) {
			item->description = g_strdup(description);
		} else {
			/* TRANSLATORS: this is a command alias, e.g. 'get-devices' */
			item->description = g_strdup_printf(_("Alias to %s"), names[0]);
		}
		item->arguments = g_strdup(arguments);
		item->callback = callback;
		g_ptr_array_add(array, item);
	}
}

static gchar *
fu_dfu_tool_get_descriptions(GPtrArray *array)
{
	gsize len;
	const gsize max_len = 31;
	FuUtilItem *item;
	GString *string;

	/* print each command */
	string = g_string_new("");
	for (guint i = 0; i < array->len; i++) {
		item = g_ptr_array_index(array, i);
		g_string_append(string, "  ");
		g_string_append(string, item->name);
		len = strlen(item->name) + 2;
		if (item->arguments != NULL) {
			g_string_append(string, " ");
			g_string_append(string, item->arguments);
			len += strlen(item->arguments) + 1;
		}
		if (len < max_len) {
			for (guint j = len; j < max_len + 1; j++)
				g_string_append_c(string, ' ');
			g_string_append(string, item->description);
			g_string_append_c(string, '\n');
		} else {
			g_string_append_c(string, '\n');
			for (guint j = 0; j < max_len + 1; j++)
				g_string_append_c(string, ' ');
			g_string_append(string, item->description);
			g_string_append_c(string, '\n');
		}
	}

	/* remove trailing newline */
	if (string->len > 0)
		g_string_set_size(string, string->len - 1);

	return g_string_free(string, FALSE);
}

static gboolean
fu_dfu_tool_run(FuDfuTool *self, const gchar *command, gchar **values, GError **error)
{
	/* find command */
	for (guint i = 0; i < self->cmd_array->len; i++) {
		FuUtilItem *item = g_ptr_array_index(self->cmd_array, i);
		if (g_strcmp0(item->name, command) == 0)
			return item->callback(self, values, error);
	}

	/* not found */
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    /* TRANSLATORS: error message */
			    _("Command not found"));
	return FALSE;
}

static FuDfuDevice *
fu_dfu_tool_get_default_device(FuDfuTool *self, GError **error)
{
	g_autoptr(GPtrArray) devices = NULL;

	/* get all the DFU devices */
	self->usb_context = g_usb_context_new(error);
	if (self->usb_context == NULL)
		return NULL;
	g_usb_context_enumerate(self->usb_context);

	/* we specified it manually */
	if (self->device_vid_pid != NULL) {
		gchar *tmp;
		guint64 pid;
		guint64 vid;
		g_autoptr(GUsbDevice) usb_device = NULL;

		/* parse */
		vid = g_ascii_strtoull(self->device_vid_pid, &tmp, 16);
		if (vid == 0 || vid > G_MAXUINT16) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INTERNAL,
					    "Invalid format of VID:PID");
			return NULL;
		}
		if (tmp[0] != ':') {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INTERNAL,
					    "Invalid format of VID:PID");
			return NULL;
		}
		pid = g_ascii_strtoull(tmp + 1, NULL, 16);
		if (pid == 0 || pid > G_MAXUINT16) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INTERNAL,
					    "Invalid format of VID:PID");
			return NULL;
		}

		/* find device */
		usb_device = g_usb_context_find_by_vid_pid(self->usb_context,
							   (guint16)vid,
							   (guint16)pid,
							   error);
		if (usb_device == NULL) {
			g_prefix_error(error,
				       "no device matches for %04x:%04x: ",
				       (guint)vid,
				       (guint)pid);
			return NULL;
		}
		return fu_dfu_device_new(self->ctx, usb_device);
	}

	/* auto-detect first device */
	devices = g_usb_context_get_devices(self->usb_context);
	for (guint i = 0; i < devices->len; i++) {
		GUsbDevice *usb_device = g_ptr_array_index(devices, i);
		g_autoptr(FuDfuDevice) device = fu_dfu_device_new(self->ctx, usb_device);
		if (fu_device_probe(FU_DEVICE(device), NULL))
			return g_steal_pointer(&device);
	}

	/* failed */
	g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no DFU devices found");
	return NULL;
}

static gboolean
fu_dfu_device_wait_for_replug(FuDfuTool *self, FuDfuDevice *device, guint timeout, GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
	g_autoptr(GUsbDevice) usb_device2 = NULL;
	g_autoptr(GError) error_local = NULL;

	/* close */
	if (!fu_device_close(FU_DEVICE(device), &error_local))
		g_debug("failed to close: %s", error_local->message);

	/* watch the device disappear and re-appear */
	usb_device2 = g_usb_context_wait_for_replug(self->usb_context, usb_device, timeout, error);
	if (usb_device2 == NULL)
		return FALSE;

	/* re-open with new device set */
	fu_usb_device_set_dev(FU_USB_DEVICE(device), usb_device2);
	if (!fu_device_open(FU_DEVICE(device), error))
		return FALSE;
	if (!fu_dfu_device_refresh_and_clear(device, error))
		return FALSE;

	/* success */
	return TRUE;
}

static gboolean
fu_dfu_tool_parse_firmware_from_file(FuFirmware *firmware,
				     GFile *file,
				     FwupdInstallFlags flags,
				     GError **error)
{
	gchar *contents = NULL;
	gsize length = 0;
	g_autoptr(GBytes) bytes = NULL;
	if (!g_file_load_contents(file, NULL, &contents, &length, NULL, error))
		return FALSE;
	bytes = g_bytes_new_take(contents, length);
	return fu_firmware_parse(firmware, bytes, flags, error);
}

static gboolean
fu_dfu_tool_write_firmware_to_file(FuFirmware *firmware, GFile *file, GError **error)
{
	const guint8 *data;
	gsize length = 0;
	g_autoptr(GBytes) bytes = fu_firmware_write(firmware, error);
	if (bytes == NULL)
		return FALSE;
	data = g_bytes_get_data(bytes, &length);
	return g_file_replace_contents(file,
				       (const gchar *)data,
				       length,
				       NULL,
				       FALSE,
				       G_FILE_CREATE_NONE,
				       NULL,
				       NULL, /* cancellable */
				       error);
}

static gboolean
fu_dfu_tool_replace_data(FuDfuTool *self, gchar **values, GError **error)
{
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "Available as `fwupdtool firmware-patch`");
	return FALSE;
}

static void
fu_tool_action_changed_cb(FuProgress *progress, gpointer dummy, FuDfuTool *self)
{
	g_print("%s:\t%u%%\n",
		fwupd_status_to_string(fu_progress_get_status(progress)),
		fu_progress_get_percentage(progress));
}

static gboolean
fu_dfu_tool_read_alt(FuDfuTool *self, gchar **values, GError **error)
{
	FuDfuTargetTransferFlags flags = DFU_TARGET_TRANSFER_FLAG_NONE;
	g_autofree gchar *str_debug = NULL;
	g_autoptr(FuDfuDevice) device = NULL;
	g_autoptr(FuFirmware) firmware = NULL;
	g_autoptr(FuDfuTarget) target = NULL;
	g_autoptr(FuDeviceLocker) locker = NULL;
	g_autoptr(GFile) file = NULL;
	g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC);

	/* check args */
	if (g_strv_length(values) < 2) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "Invalid arguments, expected "
				    "FILENAME DEVICE-ALT-NAME|DEVICE-ALT-ID");
		return FALSE;
	}

	/* open correct device */
	device = fu_dfu_tool_get_default_device(self, error);
	if (device == NULL)
		return FALSE;
	if (self->transfer_size > 0)
		fu_dfu_device_set_transfer_size(device, self->transfer_size);
	locker = fu_device_locker_new(device, error);
	if (locker == NULL)
		return FALSE;
	if (!fu_dfu_device_refresh(device, error))
		return FALSE;

	/* set up progress */
	g_signal_connect(FU_PROGRESS(progress),
			 "status-changed",
			 G_CALLBACK(fu_tool_action_changed_cb),
			 self);
	g_signal_connect(FU_PROGRESS(progress),
			 "percentage-changed",
			 G_CALLBACK(fu_tool_action_changed_cb),
			 self);

	/* APP -> DFU */
	if (!fu_device_has_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
		g_debug("detaching");
		if (!fu_device_detach_full(FU_DEVICE(device), progress, error))
			return FALSE;
		if (!fu_dfu_device_wait_for_replug(self,
						   device,
						   FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE,
						   error))
			return FALSE;
	}

	/* transfer */
	target = fu_dfu_device_get_target_by_alt_name(device, values[1], NULL);
	if (target == NULL) {
		gchar *endptr;
		guint64 tmp = g_ascii_strtoull(values[1], &endptr, 10);
		if (tmp > 0xff || endptr[0] != '\0') {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "Failed to parse alt-setting '%s'",
				    values[1]);
			return FALSE;
		}
		target = fu_dfu_device_get_target_by_alt_setting(device, (guint8)tmp, error);
		if (target == NULL)
			return FALSE;
	}

	/* do transfer */
	firmware = fu_dfuse_firmware_new();
	fu_dfu_firmware_set_vid(FU_DFU_FIRMWARE(firmware), fu_dfu_device_get_runtime_vid(device));
	fu_dfu_firmware_set_pid(FU_DFU_FIRMWARE(firmware), fu_dfu_device_get_runtime_pid(device));
	if (!fu_dfu_target_upload(target, firmware, progress, flags, error))
		return FALSE;

	/* do host reset */
	if (!fu_device_attach_full(FU_DEVICE(device), progress, error))
		return FALSE;
	if (!fu_dfu_device_wait_for_replug(self,
					   device,
					   FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE,
					   error))
		return FALSE;

	/* save file */
	file = g_file_new_for_path(values[0]);
	if (!fu_dfu_tool_write_firmware_to_file(firmware, file, error))
		return FALSE;

	/* print the new object */
	str_debug = fu_firmware_to_string(firmware);
	g_debug("DFU: %s", str_debug);

	/* success */
	g_print("Successfully uploaded from device\n");
	return TRUE;
}

static gboolean
fu_dfu_tool_read(FuDfuTool *self, gchar **values, GError **error)
{
	FuDfuTargetTransferFlags flags = DFU_TARGET_TRANSFER_FLAG_NONE;
	g_autofree gchar *str_debug = NULL;
	g_autoptr(FuDfuDevice) device = NULL;
	g_autoptr(FuFirmware) firmware = NULL;
	g_autoptr(FuDeviceLocker) locker = NULL;
	g_autoptr(GFile) file = NULL;
	g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC);

	/* check args */
	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "Invalid arguments, expected FILENAME");
		return FALSE;
	}

	/* open correct device */
	device = fu_dfu_tool_get_default_device(self, error);
	if (device == NULL)
		return FALSE;
	locker = fu_device_locker_new(device, error);
	if (locker == NULL)
		return FALSE;
	if (!fu_dfu_device_refresh(device, error))
		return FALSE;

	/* APP -> DFU */
	if (!fu_device_has_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
		if (!fu_device_detach_full(FU_DEVICE(device), progress, error))
			return FALSE;
		if (!fu_dfu_device_wait_for_replug(self,
						   device,
						   FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE,
						   error)) {
			return FALSE;
		}
	}

	/* transfer */
	g_signal_connect(FU_PROGRESS(progress),
			 "status-changed",
			 G_CALLBACK(fu_tool_action_changed_cb),
			 self);
	g_signal_connect(FU_PROGRESS(progress),
			 "percentage-changed",
			 G_CALLBACK(fu_tool_action_changed_cb),
			 self);
	firmware = fu_dfu_device_upload(device, progress, flags, error);
	if (firmware == NULL)
		return FALSE;

	/* do host reset */
	if (!fu_device_attach_full(FU_DEVICE(device), progress, error))
		return FALSE;
	if (!fu_dfu_device_wait_for_replug(self,
					   device,
					   FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE,
					   error))
		return FALSE;

	/* save file */
	file = g_file_new_for_path(values[0]);
	if (!fu_dfu_tool_write_firmware_to_file(firmware, file, error))
		return FALSE;

	/* print the new object */
	str_debug = fu_firmware_to_string(firmware);
	g_debug("DFU: %s", str_debug);

	/* success */
	g_print("successfully uploaded from device\n");
	return TRUE;
}

static gboolean
fu_dfu_tool_write_alt(FuDfuTool *self, gchar **values, GError **error)
{
	FuDfuTargetTransferFlags flags = DFU_TARGET_TRANSFER_FLAG_VERIFY;
	g_autofree gchar *str_debug = NULL;
	g_autoptr(FuDfuDevice) device = NULL;
	g_autoptr(FuFirmware) firmware = NULL;
	g_autoptr(FuFirmware) image = NULL;
	g_autoptr(FuDfuTarget) target = NULL;
	g_autoptr(FuDeviceLocker) locker = NULL;
	g_autoptr(GFile) file = NULL;
	g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC);

	/* check args */
	if (g_strv_length(values) < 2) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "Invalid arguments, expected "
				    "FILENAME DEVICE-ALT-NAME|DEVICE-ALT-ID "
				    "[IMAGE-ALT-NAME|IMAGE-ALT-ID]");
		return FALSE;
	}

	/* open file */
	firmware = fu_dfuse_firmware_new();
	file = g_file_new_for_path(values[0]);
	if (!fu_dfu_tool_parse_firmware_from_file(firmware, file, FWUPD_INSTALL_FLAG_NONE, error))
		return FALSE;

	/* open correct device */
	device = fu_dfu_tool_get_default_device(self, error);
	if (device == NULL)
		return FALSE;
	if (self->transfer_size > 0)
		fu_dfu_device_set_transfer_size(device, self->transfer_size);
	locker = fu_device_locker_new(device, error);
	if (locker == NULL)
		return FALSE;
	if (!fu_dfu_device_refresh(device, error))
		return FALSE;

	/* set up progress */
	g_signal_connect(FU_PROGRESS(progress),
			 "status-changed",
			 G_CALLBACK(fu_tool_action_changed_cb),
			 self);
	g_signal_connect(FU_PROGRESS(progress),
			 "percentage-changed",
			 G_CALLBACK(fu_tool_action_changed_cb),
			 self);

	/* APP -> DFU */
	if (!fu_device_has_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
		g_debug("detaching");
		if (!fu_device_detach_full(FU_DEVICE(device), progress, error))
			return FALSE;
		if (!fu_dfu_device_wait_for_replug(self, device, 5000, error))
			return FALSE;
	}

	/* print the new object */
	str_debug = fu_firmware_to_string(firmware);
	g_debug("DFU: %s", str_debug);

	/* get correct target on device */
	target = fu_dfu_device_get_target_by_alt_name(device, values[1], NULL);
	if (target == NULL) {
		gchar *endptr;
		guint64 tmp = g_ascii_strtoull(values[1], &endptr, 10);
		if (tmp > 0xff || endptr[0] != '\0') {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "Failed to parse alt-setting '%s'",
				    values[1]);
			return FALSE;
		}
		target = fu_dfu_device_get_target_by_alt_setting(device, (guint8)tmp, error);
		if (target == NULL)
			return FALSE;
	}

	/* allow overriding the firmware alt-setting */
	if (g_strv_length(values) > 2) {
		image = fu_firmware_get_image_by_id(firmware, values[2], NULL);
		if (image == NULL) {
			gchar *endptr;
			guint64 tmp = g_ascii_strtoull(values[2], &endptr, 10);
			if (tmp > 0xff || endptr[0] != '\0') {
				g_set_error(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INTERNAL,
					    "Failed to parse image alt-setting '%s'",
					    values[2]);
				return FALSE;
			}
			image = fu_firmware_get_image_by_idx(firmware, tmp, error);
			if (image == NULL)
				return FALSE;
		}
	} else {
		g_print("WARNING: Using default firmware image\n");
		image = fu_firmware_get_image_by_id(firmware, NULL, error);
		if (image == NULL)
			return FALSE;
	}

	/* transfer */
	if (!fu_dfu_target_download(target, image, progress, flags, error))
		return FALSE;

	/* do host reset */
	if (!fu_device_attach_full(FU_DEVICE(device), progress, error))
		return FALSE;
	if (!fu_dfu_device_wait_for_replug(self,
					   device,
					   FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE,
					   error))
		return FALSE;

	/* success */
	g_print("Successfully downloaded to device\n");
	return TRUE;
}

static gboolean
fu_dfu_tool_write(FuDfuTool *self, gchar **values, GError **error)
{
	FwupdInstallFlags flags = FWUPD_INSTALL_FLAG_NO_SEARCH;
	g_autoptr(FuDfuDevice) device = NULL;
	g_autoptr(GBytes) fw = NULL;
	g_autoptr(FuDeviceLocker) locker = NULL;
	g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC);

	/* check args */
	if (g_strv_length(values) < 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "Invalid arguments, expected FILENAME");
		return FALSE;
	}

	/* open file */
	fw = fu_bytes_get_contents(values[0], error);
	if (fw == NULL)
		return FALSE;

	/* open correct device */
	device = fu_dfu_tool_get_default_device(self, error);
	if (device == NULL)
		return FALSE;
	locker = fu_device_locker_new(device, error);
	if (locker == NULL)
		return FALSE;
	if (!fu_dfu_device_refresh(device, error))
		return FALSE;

	/* APP -> DFU */
	if (!fu_device_has_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
		if (!fu_device_detach_full(FU_DEVICE(device), progress, error))
			return FALSE;
		if (!fu_dfu_device_wait_for_replug(self,
						   device,
						   fu_device_get_remove_delay(FU_DEVICE(device)),
						   error))
			return FALSE;
	}

	/* allow wildcards */
	if (self->force) {
		flags |= FWUPD_INSTALL_FLAG_IGNORE_VID_PID;
		flags |= FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM;
	}

	/* transfer */
	g_signal_connect(FU_PROGRESS(progress),
			 "status-changed",
			 G_CALLBACK(fu_tool_action_changed_cb),
			 self);
	g_signal_connect(FU_PROGRESS(progress),
			 "percentage-changed",
			 G_CALLBACK(fu_tool_action_changed_cb),
			 self);
	if (!fu_device_write_firmware(FU_DEVICE(device), fw, progress, flags, error))
		return FALSE;

	/* do host reset */
	if (!fu_device_attach_full(FU_DEVICE(device), progress, error))
		return FALSE;

	if (fu_device_has_private_flag(FU_DEVICE(device), FU_DFU_DEVICE_FLAG_MANIFEST_TOL)) {
		if (!fu_dfu_device_wait_for_replug(self,
						   device,
						   fu_device_get_remove_delay(FU_DEVICE(device)),
						   error))
			return FALSE;
	}

	/* success */
	g_print("%u bytes successfully downloaded to device\n", (guint)g_bytes_get_size(fw));
	return TRUE;
}

#ifdef HAVE_GIO_UNIX
static gboolean
fu_dfu_tool_sigint_cb(gpointer user_data)
{
	FuDfuTool *self = (FuDfuTool *)user_data;
	g_debug("Handling SIGINT");
	g_cancellable_cancel(self->cancellable);
	return FALSE;
}
#endif

int
main(int argc, char *argv[])
{
	gboolean ret;
	gboolean verbose = FALSE;
	gboolean version = FALSE;
	g_autofree gchar *cmd_descriptions = NULL;
	g_autoptr(FuDfuTool) self = g_new0(FuDfuTool, 1);
	g_autoptr(GError) error = NULL;
	g_autoptr(GOptionContext) context = NULL;
	const GOptionEntry options[] = {
	    {"version", '\0', 0, G_OPTION_ARG_NONE, &version, _("Print the version number"), NULL},
	    {"verbose",
	     'v',
	     0,
	     G_OPTION_ARG_NONE,
	     &verbose,
	     N_("Print verbose debug statements"),
	     NULL},
	    {"device",
	     'd',
	     0,
	     G_OPTION_ARG_STRING,
	     &self->device_vid_pid,
	     N_("Specify Vendor/Product ID(s) of DFU device"),
	     N_("VID:PID")},
	    {"transfer-size",
	     't',
	     0,
	     G_OPTION_ARG_STRING,
	     &self->transfer_size,
	     N_("Specify the number of bytes per USB transfer"),
	     N_("BYTES")},
	    {"force",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->force,
	     N_("Force the action ignoring all warnings"),
	     NULL},
	    {NULL}};

	setlocale(LC_ALL, "");

	bindtextdomain(GETTEXT_PACKAGE, FWUPD_LOCALEDIR);
	bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
	textdomain(GETTEXT_PACKAGE);

	/* add commands */
	self->cmd_array = g_ptr_array_new_with_free_func((GDestroyNotify)fu_dfu_tool_item_free);
	fu_dfu_tool_add(self->cmd_array,
			"read",
			/* TRANSLATORS: command argument: uppercase, spaces->dashes */
			_("FILENAME"),
			/* TRANSLATORS: command description */
			_("Read firmware from device into a file"),
			fu_dfu_tool_read);
	fu_dfu_tool_add(self->cmd_array,
			"read-alt",
			/* TRANSLATORS: command argument: uppercase, spaces->dashes */
			_("FILENAME DEVICE-ALT-NAME|DEVICE-ALT-ID"),
			/* TRANSLATORS: command description */
			_("Read firmware from one partition into a file"),
			fu_dfu_tool_read_alt);
	fu_dfu_tool_add(self->cmd_array,
			"write",
			NULL,
			/* TRANSLATORS: command description */
			_("Write firmware from file into device"),
			fu_dfu_tool_write);
	fu_dfu_tool_add(self->cmd_array,
			"write-alt",
			/* TRANSLATORS: command argument: uppercase, spaces->dashes */
			_("FILENAME DEVICE-ALT-NAME|DEVICE-ALT-ID [IMAGE-ALT-NAME|IMAGE-ALT-ID]"),
			/* TRANSLATORS: command description */
			_("Write firmware from file into one partition"),
			fu_dfu_tool_write_alt);
	fu_dfu_tool_add(self->cmd_array,
			"replace-data",
			NULL,
			/* TRANSLATORS: command description */
			_("Replace data in an existing firmware file"),
			fu_dfu_tool_replace_data);

	/* use quirks */
	self->ctx = fu_context_new();
	if (!fu_context_load_quirks(self->ctx, FU_QUIRKS_LOAD_FLAG_NONE, &error)) {
		/* TRANSLATORS: quirks are device-specific workarounds */
		g_print("%s: %s\n", _("Failed to load quirks"), error->message);
		return EXIT_FAILURE;
	}

	/* do stuff on ctrl+c */
	self->cancellable = g_cancellable_new();
#ifdef HAVE_GIO_UNIX
	g_unix_signal_add_full(G_PRIORITY_DEFAULT, SIGINT, fu_dfu_tool_sigint_cb, self, NULL);
#endif

	/* sort by command name */
	g_ptr_array_sort(self->cmd_array, (GCompareFunc)fu_dfu_tool_sort_command_name_cb);

	/* get a list of the commands */
	context = g_option_context_new(NULL);
	cmd_descriptions = fu_dfu_tool_get_descriptions(self->cmd_array);
	g_option_context_set_summary(context, cmd_descriptions);

	/* TRANSLATORS: DFU stands for device firmware update */
	g_set_application_name(_("DFU Utility"));
	g_option_context_add_main_entries(context, options, NULL);
	ret = g_option_context_parse(context, &argc, &argv, &error);
	if (!ret) {
		/* TRANSLATORS: the user didn't read the man page */
		g_print("%s: %s\n", _("Failed to parse arguments"), error->message);
		return EXIT_FAILURE;
	}

	/* set verbose? */
	if (verbose)
		(void)g_setenv("G_MESSAGES_DEBUG", "all", FALSE);

	/* version */
	if (version) {
		g_print("%s %s\n", PACKAGE_NAME, PACKAGE_VERSION);
		return EXIT_SUCCESS;
	}

	/* run the specified command */
	ret = fu_dfu_tool_run(self, argv[1], (gchar **)&argv[2], &error);
	if (!ret) {
		if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL)) {
			g_autofree gchar *tmp = NULL;
			tmp = g_option_context_get_help(context, TRUE, NULL);
			g_print("%s\n\n%s", error->message, tmp);
		} else {
			g_print("%s\n", error->message);
		}
		return EXIT_FAILURE;
	}

	/* success/ */
	return EXIT_SUCCESS;
}
