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

#include "config.h"

#include <string.h>

#include "fu-wac-common.h"
#include "fu-wac-device.h"
#include "fu-wac-module.h"

#define FU_WAC_MODULE_STATUS_OK			    0
#define FU_WAC_MODULE_STATUS_BUSY		    1
#define FU_WAC_MODULE_STATUS_ERR_CRC		    2
#define FU_WAC_MODULE_STATUS_ERR_CMD		    3
#define FU_WAC_MODULE_STATUS_ERR_HW_ACCESS_FAIL	    4
#define FU_WAC_MODULE_STATUS_ERR_FLASH_NO_SUPPORT   5
#define FU_WAC_MODULE_STATUS_ERR_MODE_WRONG	    6
#define FU_WAC_MODULE_STATUS_ERR_MPU_NO_SUPPORT	    7
#define FU_WAC_MODULE_STATUS_ERR_VERSION_NO_SUPPORT 8
#define FU_WAC_MODULE_STATUS_ERR_ERASE		    9
#define FU_WAC_MODULE_STATUS_ERR_WRITE		    10
#define FU_WAC_MODULE_STATUS_ERR_EXIT		    11
#define FU_WAC_MODULE_STATUS_ERR		    12
#define FU_WAC_MODULE_STATUS_ERR_INVALID_OP	    13
#define FU_WAC_MODULE_STATUS_ERR_WRONG_IMAGE	    14

typedef struct {
	GUsbDevice *usb_device;
	guint8 fw_type;
	guint8 command;
	guint8 status;
} FuWacModulePrivate;

G_DEFINE_TYPE_WITH_PRIVATE(FuWacModule, fu_wac_module, FU_TYPE_DEVICE)
#define GET_PRIVATE(o) (fu_wac_module_get_instance_private(o))

enum { PROP_0, PROP_FW_TYPE, PROP_USB_DEVICE, PROP_LAST };

static const gchar *
fu_wac_module_fw_type_to_string(guint8 fw_type)
{
	if (fw_type == FU_WAC_MODULE_FW_TYPE_TOUCH)
		return "touch";
	if (fw_type == FU_WAC_MODULE_FW_TYPE_BLUETOOTH)
		return "bluetooth";
	if (fw_type == FU_WAC_MODULE_FW_TYPE_BLUETOOTH_ID6)
		return "bluetooth-id6";
	if (fw_type == FU_WAC_MODULE_FW_TYPE_EMR_CORRECTION)
		return "emr-correction";
	if (fw_type == FU_WAC_MODULE_FW_TYPE_BLUETOOTH_HID)
		return "bluetooth-hid";
	return NULL;
}

static const gchar *
fu_wac_module_command_to_string(guint8 command)
{
	if (command == FU_WAC_MODULE_COMMAND_START)
		return "start";
	if (command == FU_WAC_MODULE_COMMAND_DATA)
		return "data";
	if (command == FU_WAC_MODULE_COMMAND_END)
		return "end";
	return NULL;
}

static const gchar *
fu_wac_module_status_to_string(guint8 status)
{
	if (status == FU_WAC_MODULE_STATUS_OK)
		return "ok";
	if (status == FU_WAC_MODULE_STATUS_BUSY)
		return "busy";
	if (status == FU_WAC_MODULE_STATUS_ERR_CRC)
		return "err-crc";
	if (status == FU_WAC_MODULE_STATUS_ERR_CMD)
		return "err-cmd";
	if (status == FU_WAC_MODULE_STATUS_ERR_HW_ACCESS_FAIL)
		return "err-hw-access-fail";
	if (status == FU_WAC_MODULE_STATUS_ERR_FLASH_NO_SUPPORT)
		return "err-flash-no-support";
	if (status == FU_WAC_MODULE_STATUS_ERR_MODE_WRONG)
		return "err-mode-wrong";
	if (status == FU_WAC_MODULE_STATUS_ERR_MPU_NO_SUPPORT)
		return "err-mpu-no-support";
	if (status == FU_WAC_MODULE_STATUS_ERR_VERSION_NO_SUPPORT)
		return "erro-version-no-support";
	if (status == FU_WAC_MODULE_STATUS_ERR_ERASE)
		return "err-erase";
	if (status == FU_WAC_MODULE_STATUS_ERR_WRITE)
		return "err-write";
	if (status == FU_WAC_MODULE_STATUS_ERR_EXIT)
		return "err-exit";
	if (status == FU_WAC_MODULE_STATUS_ERR)
		return "err-err";
	if (status == FU_WAC_MODULE_STATUS_ERR_INVALID_OP)
		return "err-invalid-op";
	if (status == FU_WAC_MODULE_STATUS_ERR_WRONG_IMAGE)
		return "err-wrong-image";
	return NULL;
}

static void
fu_wac_module_to_string(FuDevice *device, guint idt, GString *str)
{
	FuWacModule *self = FU_WAC_MODULE(device);
	FuWacModulePrivate *priv = GET_PRIVATE(self);
	fu_common_string_append_kv(str,
				   idt,
				   "FwType",
				   fu_wac_module_fw_type_to_string(priv->fw_type));
	fu_common_string_append_kv(str,
				   idt,
				   "Status",
				   fu_wac_module_status_to_string(priv->status));
	fu_common_string_append_kv(str,
				   idt,
				   "Command",
				   fu_wac_module_command_to_string(priv->command));
}

static gboolean
fu_wac_module_refresh(FuWacModule *self, GError **error)
{
	FuWacDevice *parent_device = FU_WAC_DEVICE(fu_device_get_parent(FU_DEVICE(self)));
	FuWacModulePrivate *priv = GET_PRIVATE(self);
	guint8 buf[] = {[0] = FU_WAC_REPORT_ID_MODULE, [1 ... FU_WAC_PACKET_LEN - 1] = 0xff};

	/* get from hardware */
	if (!fu_wac_device_get_feature_report(parent_device,
					      buf,
					      sizeof(buf),
					      FU_HID_DEVICE_FLAG_ALLOW_TRUNC,
					      error)) {
		g_prefix_error(error, "failed to refresh status: ");
		return FALSE;
	}

	/* check fw type */
	if (priv->fw_type != buf[1]) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "Submodule GetFeature fw_Type invalid "
			    "got 0x%02x expected 0x%02x",
			    (guint)buf[1],
			    (guint)priv->fw_type);
		return FALSE;
	}

	/* current phase and status */
	if (priv->command != buf[2] || priv->status != buf[3]) {
		priv->command = buf[2];
		priv->status = buf[3];
		if (g_getenv("FWUPD_WACOM_VERBOSE") != NULL) {
			g_debug("command: %s, status: %s",
				fu_wac_module_command_to_string(priv->command),
				fu_wac_module_status_to_string(priv->status));
		}
	}

	/* success */
	return TRUE;
}

gboolean
fu_wac_module_set_feature(FuWacModule *self,
			  guint8 command,
			  GBytes *blob, /* optional */
			  FuProgress *progress,
			  guint busy_timeout,
			  GError **error)
{
	FuWacDevice *parent_device = FU_WAC_DEVICE(fu_device_get_parent(FU_DEVICE(self)));
	FuWacModulePrivate *priv = GET_PRIVATE(self);
	const guint8 *data;
	gsize len = 0;
	guint busy_poll_loops = busy_timeout * 100;
	guint8 buf[] = {[0] = FU_WAC_REPORT_ID_MODULE,
			[1] = priv->fw_type,
			[2] = command,
			[3 ... FU_WAC_PACKET_LEN - 1] = 0xff};

	/* sanity check */
	g_return_val_if_fail(FU_IS_WAC_MODULE(self), FALSE);
	g_return_val_if_fail(FU_IS_WAC_DEVICE(parent_device), FALSE);

	/* verify the size of the blob */
	if (blob != NULL) {
		data = g_bytes_get_data(blob, &len);
		if (!fu_memcpy_safe(buf,
				    sizeof(buf),
				    0x03, /* dst */
				    data,
				    len,
				    0x0, /* src */
				    len,
				    error)) {
			g_prefix_error(error, "Submodule blob larger than buffer: ");
			return FALSE;
		}
	}

	/* tell the daemon the current status */
	switch (command) {
	case FU_WAC_MODULE_COMMAND_START:
		fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_ERASE);
		break;
	case FU_WAC_MODULE_COMMAND_DATA:
		fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
		break;
	case FU_WAC_MODULE_COMMAND_END:
		fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_VERIFY);
		break;
	default:
		break;
	}

	/* send to hardware */
	if (!fu_wac_device_set_feature_report(parent_device,
					      buf,
					      len + 3,
					      FU_HID_DEVICE_FLAG_ALLOW_TRUNC,
					      error)) {
		g_prefix_error(error, "failed to set module feature: ");
		return FALSE;
	}

	/* wait for hardware */
	for (guint i = 0; i < busy_poll_loops; i++) {
		if (!fu_wac_module_refresh(self, error))
			return FALSE;
		if (priv->status == FU_WAC_MODULE_STATUS_BUSY) {
			g_usleep(10000); /* 10ms */
			continue;
		}
		if (priv->status == FU_WAC_MODULE_STATUS_OK)
			break;
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "Failed to SetFeature: %s",
			    fu_wac_module_status_to_string(priv->status));
		return FALSE;
	}

	/* too many retries */
	if (priv->status != FU_WAC_MODULE_STATUS_OK) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INTERNAL,
			    "Timed out after %u loops with status %s",
			    busy_poll_loops,
			    fu_wac_module_status_to_string(priv->status));
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_wac_module_cleanup(FuDevice *device, FwupdInstallFlags flags, GError **error)
{
	FuDevice *parent = fu_device_get_parent(device);
	g_autoptr(FuDeviceLocker) locker = fu_device_locker_new(parent, error);
	if (locker == NULL)
		return FALSE;
	return fu_device_cleanup(parent, flags, error);
}

static void
fu_wac_module_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
	FuWacModule *self = FU_WAC_MODULE(object);
	FuWacModulePrivate *priv = GET_PRIVATE(self);
	switch (prop_id) {
	case PROP_FW_TYPE:
		g_value_set_uint(value, priv->fw_type);
		break;
	case PROP_USB_DEVICE:
		g_value_set_object(value, priv->usb_device);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}

static void
fu_wac_module_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
	FuWacModule *self = FU_WAC_MODULE(object);
	FuWacModulePrivate *priv = GET_PRIVATE(self);
	switch (prop_id) {
	case PROP_FW_TYPE:
		priv->fw_type = g_value_get_uint(value);
		break;
	case PROP_USB_DEVICE:
		g_set_object(&priv->usb_device, g_value_get_object(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}

static void
fu_wac_module_init(FuWacModule *self)
{
	fu_device_add_protocol(FU_DEVICE(self), "com.wacom.usb");
	fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_BCD);
	fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE);
}

static void
fu_wac_module_constructed(GObject *object)
{
	FuWacModule *self = FU_WAC_MODULE(object);
	FuWacModulePrivate *priv = GET_PRIVATE(self);
	g_autofree gchar *devid = NULL;
	g_autofree gchar *vendor_id = NULL;

	/* set vendor ID */
	vendor_id = g_strdup_printf("USB:0x%04X", g_usb_device_get_vid(priv->usb_device));
	fu_device_add_vendor_id(FU_DEVICE(self), vendor_id);

	/* set USB physical and logical IDs */
	fu_device_set_physical_id(FU_DEVICE(self), g_usb_device_get_platform_id(priv->usb_device));
	fu_device_set_logical_id(FU_DEVICE(self), fu_wac_module_fw_type_to_string(priv->fw_type));

	/* append the firmware kind to the generated GUID */
	devid = g_strdup_printf("USB\\VID_%04X&PID_%04X-%s",
				g_usb_device_get_vid(priv->usb_device),
				g_usb_device_get_pid(priv->usb_device),
				fu_wac_module_fw_type_to_string(priv->fw_type));
	fu_device_add_instance_id(FU_DEVICE(self), devid);

	G_OBJECT_CLASS(fu_wac_module_parent_class)->constructed(object);
}

static void
fu_wac_module_set_progress(FuDevice *self, FuProgress *progress)
{
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* detach */
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100); /* write */
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* attach */
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0);	/* reload */
}

static void
fu_wac_module_finalize(GObject *object)
{
	FuWacModule *self = FU_WAC_MODULE(object);
	FuWacModulePrivate *priv = GET_PRIVATE(self);
	if (priv->usb_device != NULL)
		g_object_unref(priv->usb_device);
	G_OBJECT_CLASS(fu_wac_module_parent_class)->finalize(object);
}

static void
fu_wac_module_class_init(FuWacModuleClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	GParamSpec *pspec;
	FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);

	/* properties */
	object_class->get_property = fu_wac_module_get_property;
	object_class->set_property = fu_wac_module_set_property;

	/**
	 * FuWacModule:usb-device:
	 *
	 * The parent USB device to use.
	 */
	pspec = g_param_spec_object("usb-device",
				    NULL,
				    NULL,
				    G_USB_TYPE_DEVICE,
				    G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME);
	g_object_class_install_property(object_class, PROP_USB_DEVICE, pspec);

	/**
	 * FuWacModule:fw-type:
	 *
	 * The firmware kind.
	 */
	pspec = g_param_spec_uint("fw-type",
				  NULL,
				  NULL,
				  0,
				  G_MAXUINT,
				  0,
				  G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME);
	g_object_class_install_property(object_class, PROP_FW_TYPE, pspec);

	object_class->constructed = fu_wac_module_constructed;
	object_class->finalize = fu_wac_module_finalize;
	klass_device->to_string = fu_wac_module_to_string;
	klass_device->cleanup = fu_wac_module_cleanup;
	klass_device->set_progress = fu_wac_module_set_progress;
}
