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

#include "config.h"

#include <string.h>

#include "dfu-element.h"
#include "dfu-format-srec.h"
#include "dfu-image.h"

#include "fu-wac-firmware.h"

#include "fwupd-error.h"

typedef struct {
	guint32 addr;
	guint32 sz;
	guint32 prog_start_addr;
} DfuFirmwareWacHeaderRecord;

/**
 * fu_wac_firmware_parse_data:
 * @firmware: a #DfuFirmware
 * @bytes: data to parse
 * @flags: some #DfuFirmwareParseFlags
 * @error: a #GError, or %NULL
 *
 * Unpacks into a firmware object from DfuSe data.
 *
 * Returns: %TRUE for success
 **/
gboolean
fu_wac_firmware_parse_data (DfuFirmware *firmware,
			 GBytes *bytes,
			 DfuFirmwareParseFlags flags,
			 GError **error)
{
	gsize len;
	guint8 *data;
	g_auto(GStrv) lines = NULL;
	g_autoptr(GString) image_buffer = NULL;
	g_autofree gchar *data_str = NULL;
	guint8 images_cnt = 0;
	g_autoptr(GPtrArray) header_infos = g_ptr_array_new_with_free_func (g_free);

	/* check the prefix (BE) */
	data = (guint8 *) g_bytes_get_data (bytes, &len);
	if (memcmp (data, "WACOM", 5) != 0) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INTERNAL,
				     "invalid .wac prefix");
		return FALSE;
	}

	/* parse each line */
	data_str = g_strndup ((const gchar *) data, len);
	lines = g_strsplit (data_str, "\n", -1);
	for (guint i = 0; lines[i] != NULL; i++) {
		g_autofree gchar *cmd = g_strndup (lines[i], 2);

		/* remove windows line endings */
		g_strdelimit (lines[i], "\r", '\0');

		/* Wacom-specific metadata */
		if (g_strcmp0 (cmd, "WA") == 0) {
			guint cmdlen = strlen (lines[i]);

			/* header info record */
			if (memcmp (lines[i] + 2, "COM", 3) == 0) {
				guint8 header_image_cnt = 0;
				if (cmdlen != 40) {
					g_set_error (error,
						     FWUPD_ERROR,
						     FWUPD_ERROR_INTERNAL,
						     "invalid header, got %u bytes",
						     cmdlen);
					return FALSE;
				}
				header_image_cnt = dfu_utils_buffer_parse_uint4 (lines[i] + 5);
				for (guint j = 0; j < header_image_cnt; j++) {
					DfuFirmwareWacHeaderRecord *hdr = g_new0 (DfuFirmwareWacHeaderRecord, 1);
					hdr->addr = dfu_utils_buffer_parse_uint32 (lines[i] + (j * 16) + 6);
					hdr->sz = dfu_utils_buffer_parse_uint32 (lines[i] + (j * 16) + 14);
					g_ptr_array_add (header_infos, hdr);
					g_debug ("header_fw%u_addr: 0x%x", j, hdr->addr);
					g_debug ("header_fw%u_sz:   0x%x", j, hdr->sz);
				}
				continue;
			}

			/* firmware headline record */
			if (cmdlen == 13) {
				DfuFirmwareWacHeaderRecord *hdr;
				guint8 idx = dfu_utils_buffer_parse_uint4 (lines[i] + 2);
				if (idx == 0) {
					g_set_error (error,
						     FWUPD_ERROR,
						     FWUPD_ERROR_INTERNAL,
						     "headline %u invalid",
						     idx);
					return FALSE;
				}
				if (idx > header_infos->len) {
					g_set_error (error,
						     FWUPD_ERROR,
						     FWUPD_ERROR_INTERNAL,
						     "headline %u exceeds header count %u",
						     idx, header_infos->len);
					return FALSE;
				}
				hdr = g_ptr_array_index (header_infos, idx - 1);
				hdr->prog_start_addr = dfu_utils_buffer_parse_uint32 (lines[i] + 3);
				if (hdr->prog_start_addr != hdr->addr) {
					g_set_error (error,
						     FWUPD_ERROR,
						     FWUPD_ERROR_INTERNAL,
						     "programming address 0x%x != "
						     "base address 0x%0x for idx %u",
						     hdr->prog_start_addr,
						     hdr->addr,
						     idx);
					return FALSE;
				}
				g_debug ("programing-start-address: 0x%x", hdr->prog_start_addr);
				continue;
			}

			g_debug ("unknown Wacom-specific metadata");
			continue;
		}

		/* start */
		if (g_strcmp0 (cmd, "S0") == 0) {
			if (image_buffer != NULL) {
				g_set_error_literal (error,
						     FWUPD_ERROR,
						     FWUPD_ERROR_INTERNAL,
						     "duplicate S0 without S7");
				return FALSE;
			}
			image_buffer = g_string_new (NULL);
		}

		/* these are things we want to include in the image */
		if (g_strcmp0 (cmd, "S0") == 0 ||
		    g_strcmp0 (cmd, "S1") == 0 ||
		    g_strcmp0 (cmd, "S2") == 0 ||
		    g_strcmp0 (cmd, "S3") == 0 ||
		    g_strcmp0 (cmd, "S5") == 0 ||
		    g_strcmp0 (cmd, "S7") == 0 ||
		    g_strcmp0 (cmd, "S8") == 0 ||
		    g_strcmp0 (cmd, "S9") == 0) {
			if (image_buffer == NULL) {
				g_set_error (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_INTERNAL,
					     "%s without S0", cmd);
				return FALSE;
			}
			g_string_append_printf (image_buffer, "%s\n", lines[i]);
		}

		/* end */
		if (g_strcmp0 (cmd, "S7") == 0) {
			g_autoptr(GBytes) blob = NULL;
			g_autoptr(DfuImage) image = dfu_image_new ();
			DfuFirmwareWacHeaderRecord *hdr;

			/* get the correct relocated start address */
			if (images_cnt >= header_infos->len) {
				g_set_error (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_INTERNAL,
					     "%s without header", cmd);
				return FALSE;
			}
			hdr = g_ptr_array_index (header_infos, images_cnt);

			if (image_buffer == NULL) {
				g_set_error (error,
					     FWUPD_ERROR,
					     FWUPD_ERROR_INTERNAL,
					     "%s with missing image buffer", cmd);
				return FALSE;
			}

			/* parse SREC file and add as image */
			blob = g_bytes_new (image_buffer->str, image_buffer->len);
			if (!dfu_image_from_srec (image, blob, hdr->addr, flags, error))
				return FALSE;

			/* the alt-setting is used for the firmware index */
			dfu_image_set_alt_setting (image, images_cnt);
			dfu_firmware_add_image (firmware, image);
			images_cnt++;

			/* clear the image buffer */
			g_string_free (image_buffer, TRUE);
			image_buffer = NULL;
		}
	}

	/* verify data is complete */
	if (image_buffer != NULL) {
		g_set_error_literal (error,
				     FWUPD_ERROR,
				     FWUPD_ERROR_INTERNAL,
				     "truncated data: no S7");
		return FALSE;
	}

	/* ensure this matched the header */
	if (header_infos->len != images_cnt) {
		g_set_error (error,
			     FWUPD_ERROR,
			     FWUPD_ERROR_INTERNAL,
			     "not enough images %u for header count %u",
			     images_cnt,
			     header_infos->len);
		return FALSE;
	}

	/* success */
	dfu_firmware_set_format (firmware, DFU_FIRMWARE_FORMAT_SREC);
	return TRUE;
}
