/* SPDX-License-Identifier: GPL-2.0-only */

#include <bootstate.h>
#include <cbmem.h>
#include <console/console.h>
#include <cpu/x86/name.h>
#include <cpu/x86/msr.h>
#include <cpu/x86/lapic.h>
#include <acpi/acpi.h>
#include <arch/bert_storage.h>
#include <string.h>
#include <types.h>

/* BERT region management:  Allow the chipset to determine the specific
 * location of the BERT region.  We find that base and size, then manage
 * the allocation of error information within it.
 *
 * Use simple static variables for managing the BERT region.  This is a thin
 * implementation; it is only created and consumed by coreboot, and only in
 * a single stage, and we don't want its information to survive reboot or
 * resume cycles.  If the requirements change, consider using IMD to help
 * manage the space.
 */
static bool bert_region_broken;
static void *bert_region_base;
static size_t bert_region_size;
static size_t bert_region_used;

/* Calculate the remaining space in the BERT region.  This knowledge may help
 * the caller prioritize the information to store.
 */
size_t bert_storage_remaining(void)
{
	return bert_region_broken ? 0 : bert_region_size - bert_region_used;
}

bool bert_errors_present(void)
{
	return !bert_region_broken && bert_region_used;
}

void bert_errors_region(void **start, size_t *size)
{
	if (bert_region_broken) {
		*start = NULL;
		*size = 0;
		return;
	}

	/* No metadata, etc. with our region, so this is easy */
	*start = bert_region_base;
	*size = bert_region_used;
}

static void *bert_allocate_storage(size_t size)
{
	size_t alloc;

	if (bert_region_broken)
		return NULL;
	if (bert_region_used + size > bert_region_size)
		return NULL;

	alloc = bert_region_used;
	bert_region_used += size;

	return (void *)((u8 *)bert_region_base + alloc);
}

/* Generic Error Status:  Each Status represents a unique error event within
 * the BERT errors region.  Each event may have multiple errors associated
 * with it.
 */

/* Find the nth (1-based) Generic Data Structure attached to an Error Status */
static void *acpi_hest_generic_data_nth(
		acpi_generic_error_status_t *status, int num)
{
	acpi_hest_generic_data_v300_t *ptr;
	size_t struct_size;

	if (!num || num > bert_entry_count(status))
		return NULL;

	ptr = (acpi_hest_generic_data_v300_t *)(status + 1);
	while (--num) {
		if (ptr->revision == HEST_GENERIC_ENTRY_V300)
			struct_size = sizeof(acpi_hest_generic_data_v300_t);
		else
			struct_size = sizeof(acpi_hest_generic_data_t);
		ptr = (acpi_hest_generic_data_v300_t *)(
				(u8 *)ptr
				+ ptr->data_length
				+ struct_size);
	}
	return ptr;
}

/* Update data_length for this Error Status, and final Data Entry it contains */
static void revise_error_sizes(acpi_generic_error_status_t *status, size_t size)
{
	acpi_hest_generic_data_v300_t *entry;
	int entries;

	if (!status)
		return;

	entries = bert_entry_count(status);
	entry = acpi_hest_generic_data_nth(status, entries);
	status->data_length += size;
	if (entry)
		entry->data_length += size;
}

/* Create space for a new BERT Generic Error Status Block, by finding the next
 * available slot and moving the ending location.  There is nothing to designate
 * this as another Generic Error Status Block (e.g. no signature); only that it
 * is within the BERT region.
 *
 * It is up to the caller to correctly fill the information, including status
 * and error severity, and to update/maintain data offsets and lengths as
 * entries are added.
 */
static acpi_generic_error_status_t *new_bert_status(void)
{
	acpi_generic_error_status_t *status;

	status = bert_allocate_storage(sizeof(*status));

	if (!status) {
		printk(BIOS_ERR, "New BERT error entry would exceed available region\n");
		return NULL;
	}

	status->error_severity = ACPI_GENERROR_SEV_NONE;
	return status;
}

/* Generic Error Data:  Each Generic Error Status may contain zero or more
 * Generic Error Data structures.  The data structures describe particular
 * error(s) associated with an event.  The definition for the structure is
 * found in the ACPI spec, however the data types and any accompanying data
 * definitions are in the Common Platform Error Record appendix of the UEFI
 * spec.
 */

/* Create space for a new BERT Generic Data Entry.  Update the count and
 * data length in the parent Generic Error Status Block.  Version 0x300 of
 * the structure is used, and the timestamp is filled and marked precise
 * (i.e. assumed close enough for reporting).
 *
 * It is up to the caller to fill the Section Type field and add the Common
 * Platform Error Record type data as appropriate.  In addition, the caller
 * should update the error severity, and may optionally add FRU information
 * or override any existing information.
 */
static acpi_hest_generic_data_v300_t *new_generic_error_entry(
		acpi_generic_error_status_t *status)
{
	acpi_hest_generic_data_v300_t *entry;

	if (bert_entry_count(status) == GENERIC_ERR_STS_ENTRY_COUNT_MAX) {
		printk(BIOS_ERR, "New BERT error would exceed maximum entries\n");
		return NULL;
	}

	entry = bert_allocate_storage(sizeof(*entry));
	if (!entry) {
		printk(BIOS_ERR, "New BERT error entry would exceed available region\n");
		return NULL;
	}

	entry->revision = HEST_GENERIC_ENTRY_V300;

	entry->timestamp = cper_timestamp(CPER_TIMESTAMP_PRECISE);
	entry->validation_bits |= ACPI_GENERROR_VALID_TIMESTAMP;

	status->data_length += sizeof(*entry);
	bert_bump_entry_count(status);

	return entry;
}

/* Find the size of a CPER error section w/o any add-ons */
static size_t sizeof_error_section(guid_t *guid)
{
	if (!guidcmp(guid, &CPER_SEC_PROC_GENERIC_GUID))
		return sizeof(cper_proc_generic_error_section_t);
	else if (!guidcmp(guid, &CPER_SEC_PROC_IA32X64_GUID))
		return sizeof(cper_ia32x64_proc_error_section_t);
	else if (!guidcmp(guid, &CPER_SEC_FW_ERR_REC_REF_GUID))
		return sizeof(cper_fw_err_rec_section_t);
	/* else if ... sizeof(structures not yet defined) */

	printk(BIOS_ERR, "Requested size of unrecognized CPER GUID\n");
	return 0;
}

void *new_cper_fw_error_crashlog(acpi_generic_error_status_t *status, size_t cl_size)
{
	void *cl_data = bert_allocate_storage(cl_size);
	if (!cl_data) {
		printk(BIOS_ERR, "Crashlog entry (size %zu) would exceed available region\n",
			cl_size);
		return NULL;
	}

	revise_error_sizes(status, cl_size);

	return cl_data;
}

/* Helper to append an ACPI Generic Error Data Entry per crashlog data */
acpi_hest_generic_data_v300_t *bert_append_fw_err(acpi_generic_error_status_t *status)
{
	acpi_hest_generic_data_v300_t *entry;
	cper_fw_err_rec_section_t *fw_err;

	entry = bert_append_error_datasection(status, &CPER_SEC_FW_ERR_REC_REF_GUID);
	if (!entry)
		return NULL;

	status->block_status |= GENERIC_ERR_STS_UNCORRECTABLE_VALID;
	status->error_severity = ACPI_GENERROR_SEV_FATAL;
	entry->error_severity = ACPI_GENERROR_SEV_FATAL;

	fw_err = section_of_acpientry(fw_err, entry);

	fw_err->record_type = CRASHLOG_RECORD_TYPE;
	fw_err->revision = CRASHLOG_FW_ERR_REV;
	fw_err->record_id = 0;
	guidcpy(&fw_err->record_guid, &FW_ERR_RECORD_ID_CRASHLOG_GUID);

	return entry;
}

/* Append a new ACPI Generic Error Data Entry plus CPER Error Section to an
 * existing ACPI Generic Error Status Block.  The caller is responsible for
 * the setting the status and entry severity, as well as populating all fields
 * of the error section.
 */
acpi_hest_generic_data_v300_t *bert_append_error_datasection(
		acpi_generic_error_status_t *status, guid_t *guid)
{
	acpi_hest_generic_data_v300_t *entry;
	void *sect;
	size_t sect_size;

	sect_size = sizeof_error_section(guid);
	if (!sect_size)
		return NULL; /* Don't allocate structure if bad GUID passed */

	if (sizeof(*entry) + sect_size > bert_storage_remaining())
		return NULL;

	entry = new_generic_error_entry(status);
	if (!entry)
		return NULL;

	/* error section immediately follows the Generic Error Data Entry */
	sect = bert_allocate_storage(sect_size);
	if (!sect)
		return NULL;

	revise_error_sizes(status, sect_size);

	guidcpy(&entry->section_type, guid);
	return entry;
}

/* Helper to append an ACPI Generic Error Data Entry plus a CPER Processor
 * Generic Error Section.  As many fields are populated as possible for the
 * caller.
 */
acpi_hest_generic_data_v300_t *bert_append_genproc(
		acpi_generic_error_status_t *status)
{
	acpi_hest_generic_data_v300_t *entry;
	cper_proc_generic_error_section_t *ges;

	entry = bert_append_error_datasection(status,
					&CPER_SEC_PROC_GENERIC_GUID);
	if (!entry)
		return NULL;

	status->block_status |= GENERIC_ERR_STS_UNCORRECTABLE_VALID;
	status->error_severity = ACPI_GENERROR_SEV_FATAL;

	entry->error_severity = ACPI_GENERROR_SEV_FATAL;

	ges = section_of_acpientry(ges, entry);

	ges->proc_type = GENPROC_PROCTYPE_IA32X64;
	ges->validation |= GENPROC_VALID_PROC_TYPE;

	ges->cpu_version = cpuid_eax(1);
	ges->validation |= GENPROC_VALID_CPU_VERSION;

	fill_processor_name(ges->cpu_brand_string);
	ges->validation |= GENPROC_VALID_CPU_BRAND;

	ges->proc_id = lapicid();
	ges->validation |= GENPROC_VALID_CPU_ID;

	return entry;
}

/* Add a new IA32/X64 Processor Context Structure (Table 261), following any
 * other contexts, to an existing Processor Error Section (Table 255).  Contexts
 * may only be added after the entire Processor Error Info array has been
 * created.
 *
 * This function fills only the minimal amount of information required to parse
 * or step through the contexts.  The type is filled and PROC_CONTEXT_INFO_NUM
 * is updated.
 *
 * type is one of:
 *   CPER_IA32X64_CTX_UNCL
 *   CPER_IA32X64_CTX_MSR
 *   CPER_IA32X64_CTX_32BIT_EX
 *   CPER_IA32X64_CTX_64BIT_EX
 *   CPER_IA32X64_CTX_FXSAVE
 *   CPER_IA32X64_CTX_32BIT_DBG
 *   CPER_IA32X64_CTX_64BIT_DBG
 *   CPER_IA32X64_CTX_MEMMAPPED
 * num is the number of bytes eventually used to fill the context's register
 *   array, e.g. 4 MSRs * sizeof(msr_t)
 *
 * status and entry data_length values are updated.
 */
cper_ia32x64_context_t *new_cper_ia32x64_ctx(
		acpi_generic_error_status_t *status,
		cper_ia32x64_proc_error_section_t *x86err, int type, int num)
{
	size_t size;
	cper_ia32x64_context_t *ctx;
	static const char * const ctx_names[] = {
			"Unclassified Data",
			"MSR Registers",
			"32-bit Mode Execution",
			"64-bit Mode Execution",
			"FXSAVE",
			"32-bit Mode Debug",
			"64-bit Mode Debug",
			"Memory Mapped"
	};

	if (type > CPER_IA32X64_CTX_MEMMAPPED)
		return NULL;

	if (cper_ia32x64_proc_num_ctxs(x86err) == I32X64SEC_VALID_CTXNUM_MAX) {
		printk(BIOS_ERR, "New IA32X64 %s context entry would exceed max allowable contexts\n",
				ctx_names[type]);
		return NULL;
	}

	size = cper_ia32x64_ctx_sz_bytype(type, num);
	ctx = bert_allocate_storage(size);
	if (!ctx) {
		printk(BIOS_ERR, "New IA32X64 %s context entry would exceed available region\n",
				ctx_names[type]);
		return NULL;
	}

	revise_error_sizes(status, size);

	ctx->type = type;
	ctx->array_size = num;
	cper_bump_ia32x64_ctx_count(x86err);

	return ctx;
}

/* Add a new IA32/X64 Processor Error Information Structure (Table 256),
 * following any other errors, to an existing Processor Error Section
 * (Table 255).  All error structures must be added before any contexts are
 * added.
 *
 * This function fills only the minimal amount of information required to parse
 * or step through the errors.  The type is filled and PROC_ERR_INFO_NUM is
 * updated.
 */
cper_ia32x64_proc_error_info_t *new_cper_ia32x64_check(
		acpi_generic_error_status_t *status,
		cper_ia32x64_proc_error_section_t *x86err,
		enum cper_x86_check_type type)
{
	cper_ia32x64_proc_error_info_t *check;
	static const char * const check_names[] = {
			"cache",
			"TLB",
			"bus",
			"MS"
	};
	const guid_t check_guids[] = {
			X86_PROCESSOR_CACHE_CHK_ERROR_GUID,
			X86_PROCESSOR_TLB_CHK_ERROR_GUID,
			X86_PROCESSOR_BUS_CHK_ERROR_GUID,
			X86_PROCESSOR_MS_CHK_ERROR_GUID
	};

	if (type > X86_PROCESSOR_CHK_MAX)
		return NULL;

	if (cper_ia32x64_proc_num_chks(x86err) == I32X64SEC_VALID_ERRNUM_MAX) {
		printk(BIOS_ERR, "New IA32X64 %s check entry would exceed max allowable errors\n",
				check_names[type]);
		return NULL;
	}

	check = bert_allocate_storage(sizeof(*check));
	if (!check) {
		printk(BIOS_ERR, "New IA32X64 %s check entry would exceed available region\n",
				check_names[type]);
		return NULL;
	}

	revise_error_sizes(status, sizeof(*check));

	guidcpy(&check->type, &check_guids[type]);
	cper_bump_ia32x64_chk_count(x86err);

	return check;
}

/* Helper to append an ACPI Generic Error Data Entry plus a CPER IA32/X64
 * Processor Error Section.  As many fields are populated as possible for the
 * caller.
 */
acpi_hest_generic_data_v300_t *bert_append_ia32x64(
					acpi_generic_error_status_t *status)
{
	acpi_hest_generic_data_v300_t *entry;
	cper_ia32x64_proc_error_section_t *ipe;
	struct cpuid_result id;

	entry = bert_append_error_datasection(status,
					&CPER_SEC_PROC_IA32X64_GUID);
	if (!entry)
		return NULL;

	status->block_status |= GENERIC_ERR_STS_UNCORRECTABLE_VALID;
	status->error_severity = ACPI_GENERROR_SEV_FATAL;

	entry->error_severity = ACPI_GENERROR_SEV_FATAL;

	ipe = section_of_acpientry(ipe, entry);

	ipe->apicid = lapicid();
	ipe->validation |= I32X64SEC_VALID_LAPIC;

	id = cpuid(1);
	ipe->cpuid[0] = id.eax;
	ipe->cpuid[1] = id.ebx;
	ipe->cpuid[2] = id.ecx;
	ipe->cpuid[3] = id.edx;
	ipe->validation |= I32X64SEC_VALID_CPUID;

	return entry;
}

static const char * const generic_error_types[] = {
	"PROCESSOR_GENERIC",
	"PROCESSOR_SPECIFIC_X86",
	"PROCESSOR_SPECIFIC_ARM",
	"PLATFORM_MEMORY",
	"PLATFORM_MEMORY2",
	"PCIE",
	"FW_ERROR_RECORD",
	"PCI_PCIX_BUS",
	"PCI_DEVICE",
	"DMAR_GENERIC",
	"DIRECTED_IO_DMAR",
	"IOMMU_DMAR",
	"UNRECOGNIZED"
};

static const char *generic_error_name(guid_t *guid)
{
	if (!guidcmp(guid, &CPER_SEC_PROC_GENERIC_GUID))
		return generic_error_types[0];
	if (!guidcmp(guid, &CPER_SEC_PROC_IA32X64_GUID))
		return generic_error_types[1];
	if (!guidcmp(guid, &CPER_SEC_PROC_ARM_GUID))
		return generic_error_types[2];
	if (!guidcmp(guid, &CPER_SEC_PLATFORM_MEM_GUID))
		return generic_error_types[3];
	if (!guidcmp(guid, &CPER_SEC_PLATFORM_MEM2_GUID))
		return generic_error_types[4];
	if (!guidcmp(guid, &CPER_SEC_PCIE_GUID))
		return generic_error_types[5];
	if (!guidcmp(guid, &CPER_SEC_FW_ERR_REC_REF_GUID))
		return generic_error_types[6];
	if (!guidcmp(guid, &CPER_SEC_PCI_X_BUS_GUID))
		return generic_error_types[7];
	if (!guidcmp(guid, &CPER_SEC_PCI_DEV_GUID))
		return generic_error_types[8];
	if (!guidcmp(guid, &CPER_SEC_DMAR_GENERIC_GUID))
		return generic_error_types[9];
	if (!guidcmp(guid, &CPER_SEC_DMAR_VT_GUID))
		return generic_error_types[10];
	if (!guidcmp(guid, &CPER_SEC_DMAR_IOMMU_GUID))
		return generic_error_types[11];
	return generic_error_types[12];
}

/* Add a new event to the BERT region.  An event consists of an ACPI Error
 * Status Block, a Generic Error Data Entry, and an associated CPER Error
 * Section.
 */
acpi_generic_error_status_t *bert_new_event(guid_t *guid)
{
	size_t size;
	acpi_generic_error_status_t *status;
	acpi_hest_generic_data_v300_t *entry, *r;

	size = sizeof(*status);
	size += sizeof(*entry);
	size += sizeof_error_section(guid);

	if (size > bert_storage_remaining()) {
		printk(BIOS_ERR, "Not enough BERT region space to add event for type %s\n",
				generic_error_name(guid));
		return NULL;
	}

	status = new_bert_status();
	if (!status)
		return NULL;

	if (!guidcmp(guid, &CPER_SEC_PROC_GENERIC_GUID))
		r = bert_append_genproc(status);
	else if (!guidcmp(guid, &CPER_SEC_PROC_GENERIC_GUID))
		r = bert_append_ia32x64(status);
	else if (!guidcmp(guid, &CPER_SEC_FW_ERR_REC_REF_GUID))
		r = bert_append_fw_err(status);
	/* else if other types not implemented */
	else
		r = NULL;

	if (r)
		return status;
	return NULL;
}

/* Helper to add an MSR context to an existing IA32/X64-type error entry */
cper_ia32x64_context_t *cper_new_ia32x64_context_msr(
		acpi_generic_error_status_t *status,
		cper_ia32x64_proc_error_section_t *x86err, u32 addr, int num)
{
	cper_ia32x64_context_t *ctx;
	int i;
	msr_t *dest;

	ctx = new_cper_ia32x64_ctx(status, x86err, CPER_IA32X64_CTX_MSR, num);
	if (!ctx)
		return NULL;

	/* already filled ctx->type = CPER_IA32X64_CTX_MSR; */
	ctx->msr_addr = addr;
	ctx->array_size = num * sizeof(msr_t);

	dest = (msr_t *)((u8 *)(ctx + 1)); /* point to the Register Array */

	for (i = 0 ; i < num ; i++)
		*(dest + i) = rdmsr(addr + i);
	return ctx;
}

static void bert_reserved_region(void **start, size_t *size)
{
	if (!CONFIG(ACPI_BERT)) {
		*start = NULL;
		*size = 0;
	} else {
		*start = cbmem_add(CBMEM_ID_ACPI_BERT, CONFIG_ACPI_BERT_SIZE);
		*size = CONFIG_ACPI_BERT_SIZE;
	}
	printk(BIOS_INFO, "Reserved BERT region base: %p, size: 0x%zx\n", *start, *size);
}

static void bert_storage_setup(void *unused)
{
	/* Always start with a blank bert region.  Make sure nothing is
	 * maintained across reboots or resumes.
	 */
	bert_region_broken = false;
	bert_region_used = 0;

	bert_reserved_region(&bert_region_base, &bert_region_size);

	if (!bert_region_base || !bert_region_size) {
		printk(BIOS_ERR, "Bug: Can't find/add BERT storage area\n");
		bert_region_broken = true;
		return;
	}

	memset(bert_region_base, 0, bert_region_size);
}

BOOT_STATE_INIT_ENTRY(BS_PRE_DEVICE, BS_ON_EXIT, bert_storage_setup, NULL);
