// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/*
 * Copyright (C) 2012-2014, 2021, 2024-2025 Intel Corporation
 * Copyright (C) 2015 Intel Deutschland GmbH
 */
#include <linux/firmware.h>

#include "iwl-trans.h"
#include "xvt.h"
#include "iwl-nvm-utils.h"
#include "iwl-nvm-parse.h"
#include "iwl-prph.h"
#include "fw-api.h"

/* Default NVM size to read */
#define IWL_NVM_DEFAULT_CHUNK_SIZE	(2*1024)
#define IWL_MAX_NVM_SECTION_SIZE	7000

#define NVM_WRITE_OPCODE 1

enum wkp_nvm_offsets {
	/* NVM HW-Section offset (in words) definitions */
	HW_ADDR = 0x15,
};

enum ext_nvm_offsets {
	/* NVM HW-Section offset (in words) definitions */
	MAC_ADDRESS_OVERRIDE_EXT_NVM = 1,
};

/*
 * prepare the NVM host command w/ the pointers to the nvm buffer
 * and send it to fw
 */
static int iwl_nvm_write_chunk(struct iwl_xvt *xvt, u16 section,
			       u16 offset, u16 length, const u8 *data)
{
	struct iwl_nvm_access_cmd nvm_access_cmd = {
		.offset = cpu_to_le16(offset),
		.length = cpu_to_le16(length),
		.type = cpu_to_le16(section),
		.op_code = NVM_WRITE_OPCODE,
	};
	struct iwl_host_cmd cmd = {
		.id = NVM_ACCESS_CMD,
		.len = { sizeof(struct iwl_nvm_access_cmd), length },
		.flags = CMD_SEND_IN_RFKILL,
		.data = { &nvm_access_cmd, data },
		/* data may come from vmalloc, so use _DUP */
		.dataflags = { 0, IWL_HCMD_DFL_DUP },
	};

	return iwl_xvt_send_cmd(xvt, &cmd);
}

static int iwl_nvm_write_section(struct iwl_xvt *xvt, u16 section,
				 const u8 *data, u16 length)
{
	int offset = 0;

	/* copy data in chunks of 2k (and remainder if any) */

	while (offset < length) {
		int chunk_size, ret;

		chunk_size = min(IWL_NVM_DEFAULT_CHUNK_SIZE,
				 length - offset);

		ret = iwl_nvm_write_chunk(xvt, section, offset,
					  chunk_size, data + offset);
		if (ret < 0)
			return ret;

		offset += chunk_size;
	}

	return 0;
}

#define MAX_NVM_FILE_LEN	16384

/*
 * HOW TO CREATE THE NVM FILE FORMAT:
 * ------------------------------
 * 1. create hex file, format:
 *      3800 -> header
 *      0000 -> header
 *      5a40 -> data
 *
 *   rev - 6 bit (word1)
 *   len - 10 bit (word1)
 *   id - 4 bit (word2)
 *   rsv - 12 bit (word2)
 *
 * 2. flip 8bits with 8 bits per line to get the right NVM file format
 *
 * 3. create binary file from the hex file
 *
 * 4. save as "iNVM_xxx.bin" under /lib/firmware
 */
static int iwl_xvt_load_external_nvm(struct iwl_xvt *xvt)
{
	int ret, section_size;
	u16 section_id;
	const struct firmware *fw_entry;
	const struct {
		__le16 word1;
		__le16 word2;
		u8 data[];
	} *file_sec;
	const u8 *eof;
	const __le32 *dword_buff;
	const u8 *hw_addr;

#define NVM_WORD1_LEN(x) (8 * (x & 0x03FF))
#define NVM_WORD2_ID(x) (x >> 12)
#define EXT_NVM_WORD2_LEN(x) (2 * (((x) & 0xFF) << 8 | (x) >> 8))
#define EXT_NVM_WORD1_ID(x) ((x) >> 4)
#define NVM_HEADER_0	(0x2A504C54)
#define NVM_HEADER_1	(0x4E564D2A)
#define NVM_HEADER_SIZE	(4 * sizeof(u32))

	/*
	 * Obtain NVM image via request_firmware. Since we already used
	 * request_firmware_nowait() for the firmware binary load and only
	 * get here after that we assume the NVM request can be satisfied
	 * synchronously.
	 */
	ret = request_firmware(&fw_entry, iwlwifi_mod_params.nvm_file,
			       xvt->trans->dev);
	if (ret) {
		IWL_WARN(xvt, "WARNING: %s isn't available %d\n",
			 iwlwifi_mod_params.nvm_file, ret);
		return 0;
	}

	IWL_INFO(xvt, "Loaded NVM file %s (%zu bytes)\n",
		 iwlwifi_mod_params.nvm_file, fw_entry->size);

	if (fw_entry->size > MAX_NVM_FILE_LEN) {
		IWL_ERR(xvt, "NVM file too large\n");
		ret = -EINVAL;
		goto out;
	}

	eof = fw_entry->data + fw_entry->size;
	dword_buff = (const __le32 *)fw_entry->data;

	/* some NVM file will contain a header.
	 * The header is identified by 2 dwords header as follows:
	 * dword[0] = 0x2A504C54
	 * dword[1] = 0x4E564D2A
	 *
	 * This header must be skipped when providing the NVM data to the FW.
	 */
	if (fw_entry->size > NVM_HEADER_SIZE &&
	    dword_buff[0] == cpu_to_le32(NVM_HEADER_0) &&
	    dword_buff[1] == cpu_to_le32(NVM_HEADER_1)) {
		file_sec = (const void *)(fw_entry->data + NVM_HEADER_SIZE);
		IWL_INFO(xvt, "NVM Version %08X\n", le32_to_cpu(dword_buff[2]));
		IWL_INFO(xvt, "NVM Manufacturing date %08X\n",
			 le32_to_cpu(dword_buff[3]));
	} else {
		file_sec = (const void *)fw_entry->data;
	}

	while (true) {
		if (file_sec->data > eof) {
			IWL_ERR(xvt,
				"ERROR - NVM file too short for section header\n");
			ret = -EINVAL;
			break;
		}

		/* check for EOF marker */
		if (!file_sec->word1 && !file_sec->word2) {
			ret = 0;
			break;
		}

		if (xvt->trans->cfg->nvm_type != IWL_NVM_EXT) {
			section_size =
				2 * NVM_WORD1_LEN(le16_to_cpu(file_sec->word1));
			section_id = NVM_WORD2_ID(le16_to_cpu(file_sec->word2));
		} else {
			section_size = 2 * EXT_NVM_WORD2_LEN(
						le16_to_cpu(file_sec->word2));
			section_id = EXT_NVM_WORD1_ID(
						le16_to_cpu(file_sec->word1));
		}

		if (section_size > IWL_MAX_NVM_SECTION_SIZE) {
			IWL_ERR(xvt, "ERROR - section too large (%d)\n",
				section_size);
			ret = -EINVAL;
			break;
		}

		if (!section_size) {
			IWL_ERR(xvt, "ERROR - section empty\n");
			ret = -EINVAL;
			break;
		}

		if (file_sec->data + section_size > eof) {
			IWL_ERR(xvt,
				"ERROR - NVM file too short for section (%d bytes)\n",
				section_size);
			ret = -EINVAL;
			break;
		}

		if (section_id == xvt->trans->mac_cfg->base->nvm_hw_section_num) {
			hw_addr = (const u8 *)((const __le16 *)file_sec->data +
						HW_ADDR);

			/* The byte order is little endian 16 bit, meaning 214365 */
			xvt->nvm_hw_addr[0] = hw_addr[1];
			xvt->nvm_hw_addr[1] = hw_addr[0];
			xvt->nvm_hw_addr[2] = hw_addr[3];
			xvt->nvm_hw_addr[3] = hw_addr[2];
			xvt->nvm_hw_addr[4] = hw_addr[5];
			xvt->nvm_hw_addr[5] = hw_addr[4];
		}
		if (section_id == NVM_SECTION_TYPE_MAC_OVERRIDE) {
			xvt->is_nvm_mac_override = true;
			hw_addr = (const u8 *)((const __le16 *)file_sec->data +
				   MAC_ADDRESS_OVERRIDE_EXT_NVM);

			/*
			 * Store the MAC address from MAO section.
			 * No byte swapping is required in MAO section.
			 */
			memcpy(xvt->nvm_hw_addr, hw_addr, ETH_ALEN);
		}

		ret = iwl_nvm_write_section(xvt, section_id, file_sec->data,
					    section_size);
		if (ret < 0) {
			IWL_ERR(xvt, "iwl_mvm_send_cmd failed: %d\n", ret);
			break;
		}

		/* advance to the next section */
		file_sec = (const void *)(file_sec->data + section_size);
	}
out:
	release_firmware(fw_entry);
	return ret;
}

int iwl_xvt_nvm_init(struct iwl_xvt *xvt)
{
	int ret;

	xvt->is_nvm_mac_override = false;

	/* load external NVM if configured */
	if (iwlwifi_mod_params.nvm_file) {
		/* move to External NVM flow */
		ret = iwl_xvt_load_external_nvm(xvt);
		if (ret)
			return ret;
	}

	return 0;
}
