// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * (C) Copyright 2023 Heinrich Schuchardt <heinrich.schuchardt@canonical.com>
 */

#define LOG_CATEGORY UCLASS_QFW

#include <bloblist.h>
#include <efi_loader.h>
#include <errno.h>
#include <log.h>
#include <malloc.h>
#include <mapmem.h>
#include <qfw.h>
#include <smbios.h>
#include <tables_csum.h>
#include <linux/sizes.h>
#include <asm/global_data.h>
#include <linux/err.h>

DECLARE_GLOBAL_DATA_PTR;

/**
 * qfw_load_smbios_table() - load a QEMU firmware file
 *
 * @dev:	QEMU firmware device
 * @size:	parameter to return the size of the loaded table
 * @name:	name of the table to load
 * Return:	address of the loaded table, NULL on error
 */
static void *qfw_load_smbios_table(struct udevice *dev, uint32_t *size,
				   char *name)
{
	struct fw_file *file;
	struct bios_linker_entry *table;

	file = qfw_find_file(dev, name);
	if (!file) {
		log_debug("Can't find %s\n", name);
		return NULL;
	}

	*size = be32_to_cpu(file->cfg.size);

	table = malloc(*size);
	if (!table) {
		log_err("Out of memory\n");
		return NULL;
	}

	qfw_read_entry(dev, be16_to_cpu(file->cfg.select), *size, table);

	return table;
}

/**
 * qfw_parse_smbios_anchor() - parse QEMU's SMBIOS anchor
 *
 * @dev:	QEMU firmware device
 * @entry:	SMBIOS 3 structure to be filled from QEMU's anchor
 * Return:	0 for success, -ve on error
 */
static int qfw_parse_smbios_anchor(struct udevice *dev,
				   struct smbios3_entry *entry)
{
	void *table;
	uint32_t size;
	struct smbios_entry *entry2;
	struct smbios3_entry *entry3;
	const char smbios_sig[] = "_SM_";
	const char smbios3_sig[] = "_SM3_";
	int ret = 0;

	table = qfw_load_smbios_table(dev, &size, "etc/smbios/smbios-anchor");
	if (!table)
		return -ENOMEM;
	if (!memcmp(table, smbios3_sig, sizeof(smbios3_sig) - 1)) {
		entry3 = table;
		if (entry3->length != sizeof(struct smbios3_entry)) {
			ret = -ENOENT;
			goto out;
		}
		memcpy(entry, entry3, sizeof(struct smbios3_entry));
	} else if (!memcmp(table, smbios_sig, sizeof(smbios_sig) - 1)) {
		entry2 = table;
		if (entry2->length != sizeof(struct smbios_entry)) {
			ret = -ENOENT;
			goto out;
		}
		memset(entry, 0, sizeof(struct smbios3_entry));
		memcpy(entry, smbios3_sig, sizeof(smbios3_sig));
		entry->length = sizeof(struct smbios3_entry);
		entry->major_ver = entry2->major_ver;
		entry->minor_ver = entry2->minor_ver;
		entry->table_maximum_size = entry2->struct_table_length;
	} else {
		ret = -ENOENT;
		goto out;
	}
	ret = 0;
out:
	free(table);

	return ret;
}

/**
 * qfw_write_smbios_tables() - copy SMBIOS tables from QEMU
 *
 * @addr:	address of target buffer
 * Return:	0 for success, -ve on error
 */
ulong write_smbios_table(ulong addr)
{
	int ret;
	struct udevice *dev;
	struct smbios3_entry *entry = (void *)addr;
	void *table;
	uint32_t table_size;

	ret = qfw_get_dev(&dev);
	if (ret) {
		log_err("No QEMU firmware device\n");
		return ret;
	}

	ret = qfw_read_firmware_list(dev);
	if (ret) {
		log_err("Can't read firmware file list\n");
		return ret;
	}

	ret = qfw_parse_smbios_anchor(dev, entry);
	if (ret) {
		log_debug("Can't parse anchor\n");
		return ret;
	}

	addr += entry->length;
	entry->struct_table_address = (uintptr_t)addr;
	entry->checksum = 0;
	entry->checksum = table_compute_checksum(entry,
						 sizeof(struct smbios3_entry));

	table = qfw_load_smbios_table(dev, &table_size,
				      "etc/smbios/smbios-tables");
	memcpy((void *)addr, table, table_size);
	free(table);

	return addr + table_size;
}

#ifndef CONFIG_X86
/**
 * qfw_evt_write_smbios_tables() - event handler for copying QEMU SMBIOS tables
 *
 * Return:	0 on success, -ve on error (only out of memory)
 */
static int qfw_evt_write_smbios_tables(void)
{
	ulong addr, end;
	void *ptr;

	/*
	 * TODO:
	 * This size is currently hard coded in lib/efi_loader/efi_smbios.c.
	 * We need a field in global data for the size.
	 */
	uint32_t size = SZ_4K;

	log_debug("qfw_evt_write_smbios_tables bloblist\n");
	/* Reserve 4K for SMBIOS tables, aligned to a 4K boundary */
	ptr = bloblist_add(BLOBLISTT_SMBIOS_TABLES, size, 12);
	if (!ptr)
		return log_msg_ret("bloblist", -ENOBUFS);

	addr = map_to_sysmem(ptr);

	/* Generate SMBIOS tables */
	end = write_smbios_table(addr);
	if (IS_ERR_VALUE(end)) {
		log_warning("SMBIOS: Failed to write (err=%dE)\n", (int)end);
	} else {
		if (end - addr > size)
			return -ENOMEM;
		log_debug("SMBIOS tables copied from QEMU\n");
	}

	gd_set_smbios_start(addr);

	return 0;
}
EVENT_SPY_SIMPLE(EVT_LAST_STAGE_INIT, qfw_evt_write_smbios_tables);
#endif /* !X86 */
