/*
 * Copyright 2021 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include "fu-logitech-hidpp-common.h"
#include "fu-logitech-hidpp-runtime-unifying.h"
#include "fu-logitech-hidpp-struct.h"

struct _FuLogitechHidppRuntimeUnifying {
	FuLogitechHidppRuntime parent_instance;
};

G_DEFINE_TYPE(FuLogitechHidppRuntimeUnifying,
	      fu_logitech_hidpp_runtime_unifying,
	      FU_TYPE_LOGITECH_HIDPP_RUNTIME)
#define GET_PRIVATE(o) (fu_logitech_hidpp_runtime_unifying_get_instance_private(o))

static gboolean
fu_logitech_hidpp_runtime_unifying_detach(FuDevice *device, FuProgress *progress, GError **error)
{
	FuLogitechHidppRuntime *self = FU_LOGITECH_HIDPP_RUNTIME(device);
	g_autoptr(FuStructLogitechHidppMsg) st = fu_struct_logitech_hidpp_msg_new();
	g_autoptr(GError) error_local = NULL;
	const guint8 buf[] = {
	    'I',
	    'C',
	    'P',
	};

	fu_struct_logitech_hidpp_msg_set_report_id(st, FU_LOGITECH_HIDPP_REPORT_ID_SHORT);
	fu_struct_logitech_hidpp_msg_set_device_id(st, FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER);
	fu_struct_logitech_hidpp_msg_set_sub_id(st, FU_LOGITECH_HIDPP_SUBID_SET_REGISTER);
	fu_struct_logitech_hidpp_msg_set_function_id(
	    st,
	    FU_LOGITECH_HIDPP_REGISTER_DEVICE_FIRMWARE_UPDATE_MODE);
	if (!fu_struct_logitech_hidpp_msg_set_data(st, buf, sizeof(buf), error))
		return FALSE;
	if (!fu_logitech_hidpp_send(FU_UDEV_DEVICE(self),
				    st,
				    FU_LOGITECH_HIDPP_VERSION_1,
				    FU_LOGITECH_HIDPP_DEVICE_TIMEOUT_MS * 10,
				    FU_LOGITECH_HIDPP_MSG_FLAG_NON_BLOCKING_IO,
				    &error_local)) {
		if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_WRITE) ||
		    g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) {
			g_debug("failed to detach to bootloader: %s", error_local->message);
		} else {
			g_prefix_error_literal(&error_local, "failed to detach to bootloader: ");
			g_propagate_error(error, g_steal_pointer(&error_local));
			return FALSE;
		}
	}
	fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
	return TRUE;
}

static gboolean
fu_logitech_hidpp_runtime_unifying_setup_internal(FuLogitechHidppRuntime *self, GError **error)
{
	guint8 config[10] = {0};
	g_autofree gchar *version_fw = NULL;

	/* read all 10 bytes of the version register */
	for (guint i = 0x01; i < 0x05; i++) {
		g_autoptr(FuStructLogitechHidppMsg) st_req = fu_struct_logitech_hidpp_msg_new();
		g_autoptr(FuStructLogitechHidppMsg) st_rsp = NULL;
		const guint8 *data;
		gsize datasz = 0;
		const guint8 buf[] = {i};

		/* workaround a bug in the 12.01 firmware, which fails with
		 * INVALID_VALUE when reading MCU1_HW_VERSION */
		if (i == 0x03)
			continue;

		fu_struct_logitech_hidpp_msg_set_report_id(st_req,
							   FU_LOGITECH_HIDPP_REPORT_ID_SHORT);
		fu_struct_logitech_hidpp_msg_set_device_id(st_req,
							   FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER);
		fu_struct_logitech_hidpp_msg_set_sub_id(st_req,
							FU_LOGITECH_HIDPP_SUBID_GET_REGISTER);
		fu_struct_logitech_hidpp_msg_set_function_id(
		    st_req,
		    FU_LOGITECH_HIDPP_REGISTER_DEVICE_FIRMWARE_INFORMATION);
		if (!fu_struct_logitech_hidpp_msg_set_data(st_req, buf, sizeof(buf), error))
			return FALSE;
		st_rsp = fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self),
						    st_req,
						    FU_LOGITECH_HIDPP_VERSION_1,
						    FU_LOGITECH_HIDPP_MSG_FLAG_NONE,
						    error);
		if (st_rsp == NULL) {
			g_prefix_error_literal(error, "failed to read device config: ");
			return FALSE;
		}
		data = fu_struct_logitech_hidpp_msg_get_data(st_rsp, &datasz);
		if (!fu_memcpy_safe(config,
				    sizeof(config),
				    i * 2, /* dst */
				    data,
				    datasz,
				    0x1, /* src */
				    2,
				    error))
			return FALSE;
	}

	/* get firmware version */
	version_fw = fu_logitech_hidpp_format_version("RQR",
						      config[2],
						      config[3],
						      (guint16)config[4] << 8 | config[5]);
	fu_device_set_version(FU_DEVICE(self), version_fw);

	/* get bootloader version */
	if (fu_logitech_hidpp_runtime_get_version_bl_major(self) > 0) {
		g_autofree gchar *version_bl = NULL;
		version_bl = fu_logitech_hidpp_format_version(
		    "BOT",
		    fu_logitech_hidpp_runtime_get_version_bl_major(self),
		    config[8],
		    config[9]);
		fu_device_set_version_bootloader(FU_DEVICE(self), version_bl);

		/* is the USB receiver expecting signed firmware */
		if ((fu_logitech_hidpp_runtime_get_version_bl_major(self) == 0x01 &&
		     config[8] >= 0x04) ||
		    (fu_logitech_hidpp_runtime_get_version_bl_major(self) == 0x03 &&
		     config[8] >= 0x02)) {
			fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD);
			fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifyingsigned");
		}
	}
	if (!fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD)) {
		fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);
		fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifying");
	}

	/* enable HID++ notifications */
	if (!fu_logitech_hidpp_runtime_enable_notifications(self, error)) {
		g_prefix_error_literal(error, "failed to enable notifications: ");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_logitech_hidpp_runtime_unifying_setup(FuDevice *device, GError **error)
{
	FuLogitechHidppRuntime *self = FU_LOGITECH_HIDPP_RUNTIME(device);
	g_autoptr(GError) error_local = NULL;
	for (guint i = 0; i < 5; i++) {
		g_clear_error(&error_local);
		/* HID++1.0 devices have to sleep to allow Solaar to talk to
		 * the device first -- we can't use the SwID as this is a
		 * HID++2.0 feature */
		fu_device_sleep(device, 200); /* ms */
		if (fu_logitech_hidpp_runtime_unifying_setup_internal(self, &error_local))
			return TRUE;
		if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA)) {
			g_propagate_error(error, g_steal_pointer(&error_local));
			return FALSE;
		}
	}
	g_propagate_error(error, g_steal_pointer(&error_local));
	return FALSE;
}

static FuFirmware *
fu_logitech_hidpp_runtime_unifying_prepare_firmware(FuDevice *device,
						    GInputStream *stream,
						    FuProgress *progress,
						    FuFirmwareParseFlags flags,
						    GError **error)
{
	g_autoptr(FuFirmware) firmware = fu_ihex_firmware_new();
	if (!fu_firmware_tokenize(firmware, stream, flags, error))
		return NULL;
	return g_steal_pointer(&firmware);
}

static void
fu_logitech_hidpp_runtime_unifying_set_progress(FuDevice *device, FuProgress *progress)
{
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 70, "write");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 4, "attach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 27, "reload");
}

static void
fu_logitech_hidpp_runtime_unifying_class_init(FuLogitechHidppRuntimeUnifyingClass *klass)
{
	FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);

	device_class->detach = fu_logitech_hidpp_runtime_unifying_detach;
	device_class->setup = fu_logitech_hidpp_runtime_unifying_setup;
	device_class->prepare_firmware = fu_logitech_hidpp_runtime_unifying_prepare_firmware;
	device_class->set_progress = fu_logitech_hidpp_runtime_unifying_set_progress;
}

static void
fu_logitech_hidpp_runtime_unifying_init(FuLogitechHidppRuntimeUnifying *self)
{
}
