/*
 * Copyright (C) 2019 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2021 Jeremy Soller <jeremy@system76.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include "fu-system76-launch-device.h"

#define SYSTEM76_LAUNCH_CMD_VERSION	3
#define SYSTEM76_LAUNCH_CMD_RESET	6
#define SYSTEM76_LAUNCH_TIMEOUT		1000

struct _FuSystem76LaunchDevice {
	FuUsbDevice			 parent_instance;
};

G_DEFINE_TYPE (FuSystem76LaunchDevice, fu_system76_launch_device, FU_TYPE_USB_DEVICE)

static gboolean
fu_system76_launch_device_command (FuDevice *device, guint8 *data, gsize len, GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	const guint8 ep_in = 0x82;
	const guint8 ep_out = 0x03;
	gsize actual_len = 0;

	/* send command */
	if (!g_usb_device_interrupt_transfer (usb_device,
					      ep_out,
					      data,
					      len,
					      &actual_len,
					      SYSTEM76_LAUNCH_TIMEOUT,
					      NULL,
					      error)) {
		g_prefix_error (error, "failed to send command: ");
		return FALSE;
	}
	if (actual_len < len) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_INVALID_DATA,
			     "command truncated: sent %" G_GSIZE_FORMAT " bytes",
			     actual_len);
		return FALSE;
	}

	/* receive response */
	if (!g_usb_device_interrupt_transfer (usb_device,
					      ep_in,
					      data,
					      len,
					      &actual_len,
					      SYSTEM76_LAUNCH_TIMEOUT,
					      NULL,
					      error)) {
		g_prefix_error (error, "failed to read response: ");
		return FALSE;
	}
	if (actual_len < len) {
		g_set_error (error,
			     G_IO_ERROR,
			     G_IO_ERROR_INVALID_DATA,
			     "response truncated: received %" G_GSIZE_FORMAT " bytes",
			     actual_len);
		return FALSE;
	}

	return TRUE;
}

static gboolean
fu_system76_launch_device_setup (FuDevice *device, GError **error)
{
	guint8 data[32] = { 0 };
	g_autofree gchar *version = NULL;

	/* execute version command */
	data[0] = SYSTEM76_LAUNCH_CMD_VERSION;
	if (!fu_system76_launch_device_command (device, data, sizeof(data), error)) {
		g_prefix_error (error, "failed to execute version command: ");
		return FALSE;
	}

	version = g_strdup_printf ("%s", &data[2]);
	fu_device_set_version (device, version);

	return TRUE;
}

static gboolean
fu_system76_launch_device_detach (FuDevice *device, GError **error)
{
	guint8 data[32] = { 0 };

	/* execute reset command */
	data[0] = SYSTEM76_LAUNCH_CMD_RESET;
	if (!fu_system76_launch_device_command (device, data, sizeof(data), error)) {
		g_prefix_error (error, "failed to execute reset command: ");
		return FALSE;
	}

	/* prompt for unlock if reset was blocked */
	if (data[1] != 0) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_NEEDS_USER_ACTION,
			     "To ensure you have physical access, %s needs to be manually unlocked. "
			     "Please press Fn+Esc to unlock and re-run the update.",
			     fu_device_get_name (device));
		return FALSE;
	}

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

	return TRUE;
}

static gboolean
fu_system76_launch_device_open (FuDevice *device, GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	const guint8 iface_idx = 0x01;

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

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

	return TRUE;
}

static gboolean
fu_system76_launch_device_close (FuDevice *device, GError **error)
{
	GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
	const guint8 iface_idx = 0x01;

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

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

static void
fu_system76_launch_device_init (FuSystem76LaunchDevice *self)
{
	fu_device_set_remove_delay (FU_DEVICE (self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_ADD_COUNTERPART_GUIDS);
	fu_device_add_internal_flag (FU_DEVICE (self), FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID);
	fu_device_set_version_format (FU_DEVICE (self), FWUPD_VERSION_FORMAT_PLAIN);
	fu_device_add_protocol (FU_DEVICE (self), "org.usb.dfu");
	fu_device_retry_set_delay (FU_DEVICE (self), 100);
}

static void
fu_system76_launch_device_class_init (FuSystem76LaunchDeviceClass *klass)
{
	FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
	klass_device->setup = fu_system76_launch_device_setup;
	klass_device->detach = fu_system76_launch_device_detach;
	klass_device->open = fu_system76_launch_device_open;
	klass_device->close = fu_system76_launch_device_close;
}
