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

#include "config.h"

#include <string.h>

#include "fu-synaprom-common.h"
#include "fu-synaprom-config.h"
#include "fu-synaprom-firmware.h"

struct _FuSynapromConfig {
	FuDevice parent_instance;
	guint32 configid1; /* config ID1 */
	guint32 configid2; /* config ID2 */
};

/* Iotas can exceed the size of available RAM in the part.
 * In order to allow the host to read them the IOTA_FIND command supports
 * transferring iotas with multiple commands */
typedef struct __attribute__((packed)) {
	guint16 itype;	  /* type of iotas to find */
	guint16 flags;	  /* flags, see below */
	guint8 maxniotas; /* maximum number of iotas to return, 0 = unlimited */
	guint8 firstidx;  /* first index of iotas to return */
	guint8 dummy[2];
	guint32 offset; /* byte offset of data to return */
	guint32 nbytes; /* maximum number of bytes to return */
} FuSynapromCmdIotaFind;

/* this is followed by a chain of iotas, as follows */
typedef struct __attribute__((packed)) {
	guint16 status;
	guint32 fullsize;
	guint16 nbytes;
	guint16 itype;
} FuSynapromReplyIotaFindHdr;

/* this iota contains the configuration id and version */
typedef struct __attribute__((packed)) {
	guint32 config_id1; /* YYMMDD */
	guint32 config_id2; /* HHMMSS */
	guint16 version;
	guint16 unused[3];
} FuSynapromIotaConfigVersion;

/* le */
typedef struct __attribute__((packed)) {
	guint32 product;
	guint32 id1;	 /* verification ID */
	guint32 id2;	 /* verification ID */
	guint16 version; /* config version */
	guint8 unused[2];
} FuSynapromFirmwareCfgHeader;

#define FU_SYNAPROM_CMD_IOTA_FIND_FLAGS_ALLIOTAS 0x0001	     /* itype ignored*/
#define FU_SYNAPROM_CMD_IOTA_FIND_FLAGS_READMAX	 0x0002	     /* nbytes ignored */
#define FU_SYNAPROM_MAX_IOTA_READ_SIZE		 (64 * 1024) /* max size of iota data returned */

#define FU_SYNAPROM_IOTA_ITYPE_CONFIG_VERSION 0x0009 /* Configuration id and version */

G_DEFINE_TYPE(FuSynapromConfig, fu_synaprom_config, FU_TYPE_DEVICE)

static gboolean
fu_synaprom_config_setup(FuDevice *device, GError **error)
{
	FuDevice *parent = fu_device_get_parent(device);
	FuSynapromConfig *self = FU_SYNAPROM_CONFIG(device);
	FuSynapromCmdIotaFind cmd = {0x0};
	FuSynapromIotaConfigVersion cfg;
	FuSynapromReplyIotaFindHdr hdr;
	g_autofree gchar *version = NULL;
	g_autoptr(GByteArray) reply = NULL;
	g_autoptr(GByteArray) request = NULL;
	g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC);
	g_autofree gchar *devid = NULL;

	/* get IOTA */
	cmd.itype = GUINT16_TO_LE((guint16)FU_SYNAPROM_IOTA_ITYPE_CONFIG_VERSION);
	cmd.flags = GUINT16_TO_LE((guint16)FU_SYNAPROM_CMD_IOTA_FIND_FLAGS_READMAX);
	request = fu_synaprom_request_new(FU_SYNAPROM_CMD_IOTA_FIND, &cmd, sizeof(cmd));
	reply = fu_synaprom_reply_new(sizeof(FuSynapromReplyIotaFindHdr) +
				      FU_SYNAPROM_MAX_IOTA_READ_SIZE);
	if (!fu_synaprom_device_cmd_send(FU_SYNAPROM_DEVICE(parent),
					 request,
					 reply,
					 progress,
					 5000,
					 error))
		return FALSE;
	if (reply->len < sizeof(hdr) + sizeof(cfg)) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_NOT_SUPPORTED,
			    "CFG return data invalid size: 0x%04x",
			    reply->len);
		return FALSE;
	}
	memcpy(&hdr, reply->data, sizeof(hdr));
	if (GUINT32_FROM_LE(hdr.itype) != FU_SYNAPROM_IOTA_ITYPE_CONFIG_VERSION) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_NOT_SUPPORTED,
			    "CFG iota had invalid itype: 0x%04x",
			    GUINT32_FROM_LE(hdr.itype));
		return FALSE;
	}
	if (!fu_memcpy_safe((guint8 *)&cfg,
			    sizeof(cfg),
			    0x0, /* dst */
			    reply->data,
			    reply->len,
			    sizeof(hdr), /* src */
			    sizeof(cfg),
			    error))
		return FALSE;
	self->configid1 = GUINT32_FROM_LE(cfg.config_id1);
	self->configid2 = GUINT32_FROM_LE(cfg.config_id2);
	g_debug("id1=%u, id2=%u, ver=%u",
		self->configid1,
		self->configid2,
		GUINT16_FROM_LE(cfg.version));

	/* append the configid to the generated GUID */
	devid = g_strdup_printf("USB\\VID_%04X&PID_%04X&CFG1_%u&CFG2_%u",
				fu_usb_device_get_vid(FU_USB_DEVICE(parent)),
				fu_usb_device_get_pid(FU_USB_DEVICE(parent)),
				self->configid1,
				self->configid2);
	fu_device_add_instance_id(FU_DEVICE(self), devid);

	/* no downgrades are allowed */
	version = g_strdup_printf("%04u", GUINT16_FROM_LE(cfg.version));
	fu_device_set_version(FU_DEVICE(self), version);
	fu_device_set_version_lowest(FU_DEVICE(self), version);
	return TRUE;
}

static FuFirmware *
fu_synaprom_config_prepare_firmware(FuDevice *device,
				    GBytes *fw,
				    FwupdInstallFlags flags,
				    GError **error)
{
	FuSynapromConfig *self = FU_SYNAPROM_CONFIG(device);
	FuSynapromFirmwareCfgHeader hdr;
	g_autoptr(GBytes) blob = NULL;
	g_autoptr(FuFirmware) firmware = fu_synaprom_firmware_new();
	guint32 product;
	guint32 id1;
	guint32 id2;

	/* parse the firmware */
	if (!fu_firmware_parse(firmware, fw, flags, error))
		return NULL;

	/* check the update header product and version */
	blob = fu_firmware_get_image_by_id_bytes(firmware, "cfg-update-header", error);
	if (blob == NULL)
		return NULL;
	if (g_bytes_get_size(blob) != sizeof(hdr)) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_INVALID_DATA,
				    "CFG metadata is invalid");
		return NULL;
	}
	memcpy(&hdr, g_bytes_get_data(blob, NULL), sizeof(hdr));
	product = GUINT32_FROM_LE(hdr.product);
	if (product != FU_SYNAPROM_PRODUCT_PROMETHEUS) {
		if (flags & FWUPD_INSTALL_FLAG_IGNORE_VID_PID) {
			g_warning("CFG metadata not compatible, "
				  "got 0x%02x expected 0x%02x",
				  product,
				  (guint)FU_SYNAPROM_PRODUCT_PROMETHEUS);
		} else {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_NOT_SUPPORTED,
				    "CFG metadata not compatible, "
				    "got 0x%02x expected 0x%02x",
				    product,
				    (guint)FU_SYNAPROM_PRODUCT_PROMETHEUS);
			return NULL;
		}
	}
	id1 = GUINT32_FROM_LE(hdr.id1);
	id2 = GUINT32_FROM_LE(hdr.id2);
	if (id1 != self->configid1 || id2 != self->configid2) {
		if (flags & FWUPD_INSTALL_FLAG_IGNORE_VID_PID) {
			g_warning("CFG version not compatible, "
				  "got %u:%u expected %u:%u",
				  id1,
				  id2,
				  self->configid1,
				  self->configid2);
		} else {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_NOT_SUPPORTED,
				    "CFG version not compatible, "
				    "got %u:%u expected %u:%u",
				    id1,
				    id2,
				    self->configid1,
				    self->configid2);
			return NULL;
		}
	}

	/* success */
	return g_steal_pointer(&firmware);
}

static gboolean
fu_synaprom_config_write_firmware(FuDevice *device,
				  FuFirmware *firmware,
				  FuProgress *progress,
				  FwupdInstallFlags flags,
				  GError **error)
{
	FuDevice *parent = fu_device_get_parent(device);
	g_autoptr(GBytes) fw = NULL;

	/* get default image */
	fw = fu_firmware_get_image_by_id_bytes(firmware, "cfg-update-payload", error);
	if (fw == NULL)
		return FALSE;

	/* I assume the CFG/MFW difference is detected in the device...*/
	return fu_synaprom_device_write_fw(FU_SYNAPROM_DEVICE(parent), fw, progress, error);
}

static void
fu_synaprom_config_init(FuSynapromConfig *self)
{
	fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.prometheus.config");
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE);
	fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_USE_PARENT_FOR_OPEN);
	fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN);
	fu_device_set_logical_id(FU_DEVICE(self), "cfg");
	fu_device_set_name(FU_DEVICE(self), "Prometheus IOTA Config");
	fu_device_set_summary(FU_DEVICE(self), "Fingerprint reader config");
	fu_device_add_icon(FU_DEVICE(self), "touchpad-disabled");
}

static void
fu_synaprom_config_constructed(GObject *obj)
{
	FuSynapromConfig *self = FU_SYNAPROM_CONFIG(obj);
	FuDevice *parent = fu_device_get_parent(FU_DEVICE(self));
	g_autofree gchar *devid = NULL;

	/* append the firmware kind to the generated GUID */
	devid = g_strdup_printf("USB\\VID_%04X&PID_%04X-cfg",
				fu_usb_device_get_vid(FU_USB_DEVICE(parent)),
				fu_usb_device_get_pid(FU_USB_DEVICE(parent)));
	fu_device_add_instance_id(FU_DEVICE(self), devid);

	G_OBJECT_CLASS(fu_synaprom_config_parent_class)->constructed(obj);
}

static gboolean
fu_synaprom_config_attach(FuDevice *device, FuProgress *progress, GError **error)
{
	FuDevice *parent = fu_device_get_parent(device);
	return fu_device_attach_full(parent, progress, error);
}

static gboolean
fu_synaprom_config_detach(FuDevice *device, FuProgress *progress, GError **error)
{
	FuDevice *parent = fu_device_get_parent(device);
	return fu_device_detach_full(parent, progress, error);
}

static void
fu_synaprom_config_class_init(FuSynapromConfigClass *klass)
{
	FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	object_class->constructed = fu_synaprom_config_constructed;
	klass_device->write_firmware = fu_synaprom_config_write_firmware;
	klass_device->prepare_firmware = fu_synaprom_config_prepare_firmware;
	klass_device->setup = fu_synaprom_config_setup;
	klass_device->reload = fu_synaprom_config_setup;
	klass_device->attach = fu_synaprom_config_attach;
	klass_device->detach = fu_synaprom_config_detach;
}

FuSynapromConfig *
fu_synaprom_config_new(FuSynapromDevice *device)
{
	FuSynapromConfig *self;
	self = g_object_new(FU_TYPE_SYNAPROM_CONFIG, "parent", device, NULL);
	return FU_SYNAPROM_CONFIG(self);
}
