/*
 * 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 <gio/gio.h>

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

struct _FuSynapromFirmware {
	FuFirmware		 parent_instance;
	guint32			 product_id;
};

G_DEFINE_TYPE (FuSynapromFirmware, fu_synaprom_firmware, FU_TYPE_FIRMWARE)

#define FU_SYNAPROM_FIRMWARE_TAG_MFW_HEADER		0x0001
#define FU_SYNAPROM_FIRMWARE_TAG_MFW_PAYLOAD		0x0002
#define FU_SYNAPROM_FIRMWARE_TAG_CFG_HEADER		0x0003
#define FU_SYNAPROM_FIRMWARE_TAG_CFG_PAYLOAD		0x0004

typedef struct __attribute__((packed)) {
	guint16			 tag;
	guint32			 bufsz;
} FuSynapromFirmwareHdr;

/* use only first 12 bit of 16 bits as tag value */
#define FU_SYNAPROM_FIRMWARE_TAG_MAX			0xfff0
#define FU_SYNAPROM_FIRMWARE_SIGSIZE			0x0100

#define FU_SYNAPROM_FIRMWARE_IMAGE_COUNT_MAX		64

static const gchar *
fu_synaprom_firmware_tag_to_string (guint16 tag)
{
	if (tag == FU_SYNAPROM_FIRMWARE_TAG_MFW_HEADER)
		return "mfw-update-header";
	if (tag == FU_SYNAPROM_FIRMWARE_TAG_MFW_PAYLOAD)
		return "mfw-update-payload";
	if (tag == FU_SYNAPROM_FIRMWARE_TAG_CFG_HEADER)
		return "cfg-update-header";
	if (tag == FU_SYNAPROM_FIRMWARE_TAG_CFG_PAYLOAD)
		return "cfg-update-payload";
	return NULL;
}

static void
fu_synaprom_firmware_to_string (FuFirmware *firmware, guint idt, GString *str)
{
	FuSynapromFirmware *self = FU_SYNAPROM_FIRMWARE (firmware);
	fu_common_string_append_kx (str, idt, "ProductId", self->product_id);
}

static gboolean
fu_synaprom_firmware_parse (FuFirmware *firmware,
			    GBytes *fw,
			    guint64 addr_start,
			    guint64 addr_end,
			    FwupdInstallFlags flags,
			    GError **error)
{
	const guint8 *buf;
	gsize bufsz = 0;
	gsize offset = 0;
	guint img_cnt = 0;

	g_return_val_if_fail (fw != NULL, FALSE);

	buf = g_bytes_get_data (fw, &bufsz);

	/* 256 byte signature as footer */
	if (bufsz < FU_SYNAPROM_FIRMWARE_SIGSIZE + sizeof(FuSynapromFirmwareHdr)) {
		g_set_error_literal (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "blob is too small to be firmware");
		return FALSE;
	}
	bufsz -= FU_SYNAPROM_FIRMWARE_SIGSIZE;

	/* parse each chunk */
	while (offset != bufsz) {
		FuSynapromFirmwareHdr header;
		guint32 hdrsz;
		guint32 tag;
		g_autoptr(GBytes) bytes = NULL;
		g_autoptr(FuFirmwareImage) img = NULL;

		/* verify item header */
		memcpy (&header, buf, sizeof(header));
		tag = GUINT16_FROM_LE(header.tag);
		if (tag >= FU_SYNAPROM_FIRMWARE_TAG_MAX) {
			g_set_error (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "tag 0x%04x is too large",
				     tag);
			return FALSE;
		}
		hdrsz = GUINT32_FROM_LE(header.bufsz);
		if (hdrsz == 0) {
			g_set_error (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "empty header for tag 0x%04x",
				     tag);
			return FALSE;
		}
		offset += sizeof(header) + hdrsz;
		if (offset > bufsz) {
			g_set_error (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "data is corrupted 0x%04x > 0x%04x",
				     (guint) offset, (guint) bufsz);
			return FALSE;
		}

		/* move pointer to data */
		buf += sizeof(header);
		bytes = g_bytes_new (buf, hdrsz);
		g_debug ("adding 0x%04x (%s) with size 0x%04x",
			 tag,
			 fu_synaprom_firmware_tag_to_string (tag),
			 hdrsz);
		img = fu_firmware_image_new (bytes);
		fu_firmware_image_set_idx (img, tag);
		fu_firmware_image_set_id (img, fu_synaprom_firmware_tag_to_string (tag));
		fu_firmware_add_image (firmware, img);

		/* sanity check */
		if (img_cnt++ > FU_SYNAPROM_FIRMWARE_IMAGE_COUNT_MAX) {
			g_set_error (error,
				     G_IO_ERROR,
				     G_IO_ERROR_INVALID_DATA,
				     "maximum number of images exceeded, "
				     "maximum is 0x%02x",
				     (guint) FU_SYNAPROM_FIRMWARE_IMAGE_COUNT_MAX);
			return FALSE;
		}

		/* next item */
		buf += hdrsz;
	}
	return TRUE;
}

static GBytes *
fu_synaprom_firmware_write (FuFirmware *firmware, GError **error)
{
	FuSynapromFirmware *self = FU_SYNAPROM_FIRMWARE (firmware);
	GByteArray *blob = g_byte_array_new ();
	g_autoptr(GBytes) payload = NULL;
	FuSynapromFirmwareMfwHeader hdr = {
		.product	= GUINT32_TO_LE(self->product_id),
		.id 		= GUINT32_TO_LE(0xff),
		.buildtime	= GUINT32_TO_LE(0xff),
		.buildnum	= GUINT32_TO_LE(0xff),
		.vmajor		= 10,
		.vminor		= 1,
	};

	/* add header */
	fu_byte_array_append_uint16 (blob,
				     FU_SYNAPROM_FIRMWARE_TAG_MFW_HEADER,
				     G_LITTLE_ENDIAN);
	fu_byte_array_append_uint32 (blob,
				     sizeof(hdr),
				     G_LITTLE_ENDIAN);
	g_byte_array_append (blob, (const guint8 *) &hdr, sizeof(hdr));

	/* add payload */
	payload = fu_firmware_get_image_default_bytes (firmware, error);
	if (payload == NULL)
		return NULL;
	fu_byte_array_append_uint16 (blob,
				     FU_SYNAPROM_FIRMWARE_TAG_MFW_PAYLOAD,
				     G_LITTLE_ENDIAN);
	fu_byte_array_append_uint32 (blob,
				     g_bytes_get_size (payload),
				     G_LITTLE_ENDIAN);
	g_byte_array_append (blob,
			     g_bytes_get_data (payload, NULL),
			     g_bytes_get_size (payload));

	/* add signature */
	for (guint i = 0; i < FU_SYNAPROM_FIRMWARE_SIGSIZE; i++)
		fu_byte_array_append_uint8 (blob, 0xff);
	return g_byte_array_free_to_bytes (blob);
}

static gboolean
fu_synaprom_firmware_build (FuFirmware *firmware, XbNode *n, GError **error)
{
	FuSynapromFirmware *self = FU_SYNAPROM_FIRMWARE (firmware);
	guint64 tmp;

	/* simple properties */
	tmp = xb_node_query_text_as_uint (n, "product_id", NULL);
	if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT32)
		self->product_id = tmp;

	/* success */
	return TRUE;
}

static void
fu_synaprom_firmware_init (FuSynapromFirmware *self)
{
	fu_firmware_add_flag (FU_FIRMWARE (self), FU_FIRMWARE_FLAG_HAS_VID_PID);
}

static void
fu_synaprom_firmware_class_init (FuSynapromFirmwareClass *klass)
{
	FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS (klass);
	klass_firmware->parse = fu_synaprom_firmware_parse;
	klass_firmware->write = fu_synaprom_firmware_write;
	klass_firmware->to_string = fu_synaprom_firmware_to_string;
	klass_firmware->build = fu_synaprom_firmware_build;
}

FuFirmware *
fu_synaprom_firmware_new (void)
{
	return FU_FIRMWARE (g_object_new (FU_TYPE_SYNAPROM_FIRMWARE, NULL));
}
