/*
 * Copyright (c) 2024 Ambiq Micro Inc. <www.ambiq.com>
 * SPDX-License-Identifier: Apache-2.0
 * Emulate a memory device on MSPI emulator bus
 */
#define DT_DRV_COMPAT zephyr_mspi_emul_flash

#include <zephyr/logging/log.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/emul.h>
#include <zephyr/drivers/mspi.h>
#include <zephyr/drivers/mspi_emul.h>
#include <zephyr/drivers/flash.h>
#include "spi_nor.h"

LOG_MODULE_REGISTER(zephyr_mspi_emul_flash, CONFIG_FLASH_LOG_LEVEL);

/* add else if for other SoC platforms */
#if defined(CONFIG_SOC_POSIX)
typedef struct mspi_timing_cfg mspi_timing_cfg;
typedef enum mspi_timing_param mspi_timing_param;
#endif

struct flash_mspi_emul_device_config {
	uint32_t                            size;
	struct flash_parameters             flash_param;
	struct flash_pages_layout           page_layout;

	struct mspi_dev_id                  dev_id;
	struct mspi_dev_cfg                 tar_dev_cfg;
	struct mspi_xip_cfg                 tar_xip_cfg;
	struct mspi_scramble_cfg            tar_scramble_cfg;

	bool                                sw_multi_periph;
};

struct flash_mspi_emul_device_data {
	const struct device                 *bus;
	struct mspi_dev_cfg                 dev_cfg;
	struct mspi_xip_cfg                 xip_cfg;
	struct mspi_scramble_cfg            scramble_cfg;
	mspi_timing_cfg                     timing_cfg;

	struct mspi_xfer                    xfer;
	struct mspi_xfer_packet             packet;

	struct k_sem                        lock;
	uint8_t                             *mem;
};

/**
 * Acquire the device lock.
 *
 * @param flash MSPI emulation flash device.
 */
static void acquire(const struct device *flash)
{
	const struct flash_mspi_emul_device_config *cfg = flash->config;
	struct flash_mspi_emul_device_data *data = flash->data;

	k_sem_take(&data->lock, K_FOREVER);
	if (cfg->sw_multi_periph) {
		while (mspi_dev_config(data->bus, &cfg->dev_id,
				       MSPI_DEVICE_CONFIG_ALL, &data->dev_cfg)) {
			;
		}
	} else {
		while (mspi_dev_config(data->bus, &cfg->dev_id,
				       MSPI_DEVICE_CONFIG_NONE, NULL)) {
			;
		}
	}
}

/**
 * Release the device lock.
 *
 * @param flash MSPI emulation flash device.
 */
static void release(const struct device *flash)
{
	struct flash_mspi_emul_device_data *data = flash->data;

	while (mspi_get_channel_status(data->bus, 0)) {

	}

	k_sem_give(&data->lock);
}

/**
 * API implementation of emul_mspi_dev_api_transceive transceive.
 *
 * @param target Pointer to MSPI device emulator.
 * @param dev_id Pointer to the device ID structure from a device.
 * @param xfer Pointer to the MSPI transfer started by dev_id.
 *
 * @retval 0 if successful.
 * @retval -ESTALE device ID don't match, need to call mspi_dev_config first.
 * @retval -Error transfer failed.
 */
static int emul_mspi_device_transceive(const struct emul *target,
				       const struct mspi_xfer_packet *packets,
				       uint32_t num_packet,
				       bool async,
				       uint32_t timeout)
{
	ARG_UNUSED(timeout);
	const struct flash_mspi_emul_device_config *cfg = target->dev->config;
	struct flash_mspi_emul_device_data *data = target->dev->data;
	struct emul_mspi_driver_api *api = (struct emul_mspi_driver_api *)data->bus->api;

	__ASSERT_NO_MSG(api);
	__ASSERT_NO_MSG(api->trigger_event);

	for (uint32_t count = 0; count < num_packet; ++count) {
		const struct mspi_xfer_packet *packet = &packets[count];

		if (packet->address > cfg->size ||
		    packet->address + packet->num_bytes > cfg->size) {
			return -ENOMEM;
		}

		if (packet->dir == MSPI_RX) {
			memcpy(packet->data_buf, data->mem + packet->address,
			       packet->num_bytes);
		} else if (packet->dir == MSPI_TX) {
			memcpy(data->mem + packet->address, packet->data_buf,
			       packet->num_bytes);
		}

		if (async) {
			if (packet->cb_mask == MSPI_BUS_XFER_COMPLETE_CB) {
				api->trigger_event(data->bus, MSPI_BUS_XFER_COMPLETE);
			}
		}
	}

	return 0;
}

static int flash_mspi_emul_erase(const struct device *flash, off_t offset, size_t size)
{
	const struct flash_mspi_emul_device_config *cfg = flash->config;
	struct flash_mspi_emul_device_data *data = flash->data;

	const size_t num_sectors = size / SPI_NOR_SECTOR_SIZE;
	const size_t num_blocks = size / SPI_NOR_BLOCK_SIZE;

	int i;

	acquire(flash);

	if (offset % SPI_NOR_SECTOR_SIZE) {
		LOG_ERR("Invalid offset");
		return -EINVAL;
	}

	if (size % SPI_NOR_SECTOR_SIZE) {
		LOG_ERR("Invalid size");
		return -EINVAL;
	}

	if ((offset == 0) && (size == cfg->size)) {

		memset(data->mem, cfg->flash_param.erase_value, size);

	} else if ((0 == (offset % SPI_NOR_BLOCK_SIZE)) && (0 == (size % SPI_NOR_BLOCK_SIZE))) {
		for (i = 0; i < num_blocks; i++) {
			memset(data->mem + offset, cfg->flash_param.erase_value,
			       SPI_NOR_BLOCK_SIZE);
			offset += SPI_NOR_BLOCK_SIZE;
		}
	} else {
		for (i = 0; i < num_sectors; i++) {
			memset(data->mem + offset, cfg->flash_param.erase_value,
			       SPI_NOR_SECTOR_SIZE);
			offset += SPI_NOR_SECTOR_SIZE;
		}
	}

	release(flash);

	return 0;
}

/**
 * API implementation of flash write.
 *
 * @param flash Pointer to MSPI flash device.
 * @param offset Flash device address.
 * @param wdata Pointer to the write data buffer.
 * @param len Number of bytes to write.
 *
 * @retval 0 if successful.
 * @retval -Error flash read fail.
 */
static int flash_mspi_emul_write(const struct device *flash, off_t offset,
				 const void *wdata, size_t len)
{
	const struct flash_mspi_emul_device_config *cfg = flash->config;
	struct flash_mspi_emul_device_data *data = flash->data;

	int ret;
	uint8_t *src = (uint8_t *)wdata;
	int i;

	acquire(flash);

	data->xfer.async               = false;
	data->xfer.xfer_mode           = MSPI_DMA;
	data->xfer.tx_dummy            = data->dev_cfg.tx_dummy;
	data->xfer.cmd_length          = data->dev_cfg.cmd_length;
	data->xfer.addr_length         = data->dev_cfg.addr_length;
	data->xfer.hold_ce             = false;
	data->xfer.priority            = 1;
	data->xfer.packets             = &data->packet;
	data->xfer.num_packet          = 1;
	data->xfer.timeout             = CONFIG_MSPI_COMPLETION_TIMEOUT_TOLERANCE;

	while (len) {
		/* If the offset isn't a multiple of the NOR page size, we first need
		 * to write the remaining part that fits, otherwise the write could
		 * be wrapped around within the same page
		 */
		i = MIN(SPI_NOR_PAGE_SIZE - (offset % SPI_NOR_PAGE_SIZE), len);

		data->packet.dir               = MSPI_TX;
		data->packet.cmd               = data->dev_cfg.write_cmd;
		data->packet.address           = offset;
		data->packet.data_buf          = src;
		data->packet.num_bytes         = i;

		LOG_DBG("Write %d bytes to 0x%08zx", i, (ssize_t)offset);

		ret = mspi_transceive(data->bus, &cfg->dev_id,
				      (const struct mspi_xfer *)&data->xfer);
		if (ret) {
			LOG_ERR("%u, MSPI write transaction failed with code: %d", __LINE__, ret);
			return -EIO;
		}

		/* emulate flash write busy wait */
		k_busy_wait(100);

		src += i;
		offset += i;
		len -= i;
	}

	release(flash);

	return ret;
}

/**
 * API implementation of flash read.
 *
 * @param flash Pointer to MSPI flash device.
 * @param offset Flash device address.
 * @param rdata Pointer to the read data buffer.
 * @param len Number of bytes to read.
 *
 * @retval 0 if successful.
 * @retval -Error flash read fail.
 */
static int flash_mspi_emul_read(const struct device *flash, off_t offset,
				void *rdata, size_t len)
{
	const struct flash_mspi_emul_device_config *cfg = flash->config;
	struct flash_mspi_emul_device_data *data = flash->data;

	int ret;

	acquire(flash);

	data->packet.dir               = MSPI_RX;
	data->packet.cmd               = data->dev_cfg.read_cmd;
	data->packet.address           = offset;
	data->packet.data_buf          = rdata;
	data->packet.num_bytes         = len;

	data->xfer.async               = false;
	data->xfer.xfer_mode           = MSPI_DMA;
	data->xfer.rx_dummy            = data->dev_cfg.rx_dummy;
	data->xfer.cmd_length          = data->dev_cfg.cmd_length;
	data->xfer.addr_length         = data->dev_cfg.addr_length;
	data->xfer.hold_ce             = false;
	data->xfer.priority            = 1;
	data->xfer.packets             = &data->packet;
	data->xfer.num_packet          = 1;
	data->xfer.timeout             = CONFIG_MSPI_COMPLETION_TIMEOUT_TOLERANCE;

	LOG_DBG("Read %d bytes from 0x%08zx", len, (ssize_t)offset);

	ret = mspi_transceive(data->bus, &cfg->dev_id, (const struct mspi_xfer *)&data->xfer);
	if (ret) {
		LOG_ERR("%u, MSPI read transaction failed with code: %d", __LINE__, ret);
		return -EIO;
	}

	release(flash);

	return ret;
}

/**
 * API implementation of flash get_parameters.
 *
 * @param flash Pointer to MSPI flash device.
 *
 * @retval @ref flash_parameters.
 */
static const struct flash_parameters *flash_mspi_emul_get_parameters(const struct device *flash)
{
	const struct flash_mspi_emul_device_config *cfg = flash->config;

	return &cfg->flash_param;
}

#if defined(CONFIG_FLASH_PAGE_LAYOUT)
/**
 * API implementation of flash pages_layout.
 *
 * @param flash Pointer to MSPI flash device.
 * @param layout @ref flash_pages_layout.
 * @param layout_size
 */
static void flash_mspi_emul_pages_layout(const struct device *flash,
					 const struct flash_pages_layout **layout,
					 size_t *layout_size)
{
	const struct flash_mspi_emul_device_config *cfg = flash->config;

	*layout = &cfg->page_layout;
	*layout_size = 1;
}
#endif /* CONFIG_FLASH_PAGE_LAYOUT */

static const struct flash_driver_api flash_mspi_emul_device_api = {
	.erase = flash_mspi_emul_erase,
	.write = flash_mspi_emul_write,
	.read = flash_mspi_emul_read,
	.get_parameters = flash_mspi_emul_get_parameters,
#if defined(CONFIG_FLASH_PAGE_LAYOUT)
	.page_layout = flash_mspi_emul_pages_layout,
#endif
};

static const struct emul_mspi_device_api emul_mspi_dev_api = {
	.transceive = emul_mspi_device_transceive,
};

/**
 * Set up a new MSPI device emulator
 *
 * @param emul The MSPI device emulator instance itself
 * @param bus The MSPI bus emulator instance
 * @return 0 If successful
 */
static int emul_mspi_device_init(const struct emul *emul_flash, const struct device *bus)
{
	const struct flash_mspi_emul_device_config *cfg = emul_flash->dev->config;
	struct flash_mspi_emul_device_data *data = emul_flash->dev->data;

	data->bus = bus;

	if (mspi_dev_config(data->bus, &cfg->dev_id, MSPI_DEVICE_CONFIG_ALL,
			    &cfg->tar_dev_cfg)) {
		LOG_ERR("%u, Failed to config mspi controller", __LINE__);
		return -EIO;
	}
	data->dev_cfg = cfg->tar_dev_cfg;

#if CONFIG_MSPI_XIP
	if (cfg->tar_xip_cfg.enable) {
		if (mspi_xip_config(data->bus, &cfg->dev_id, &cfg->tar_xip_cfg)) {
			LOG_ERR("%u, Failed to enable XIP.", __LINE__);
			return -EIO;
		}
		data->xip_cfg = cfg->tar_xip_cfg;
	}
#endif

#if CONFIG_MSPI_SCRAMBLE
	if (cfg->tar_scramble_cfg.enable) {
		if (mspi_scramble_config(data->bus, &cfg->dev_id, &cfg->tar_scramble_cfg)) {
			LOG_ERR("%u, Failed to enable scrambling.", __LINE__);
			return -EIO;
		}
		data->scramble_cfg = cfg->tar_scramble_cfg;
	}
#endif

#if CONFIG_MSPI_TIMING
	if (mspi_timing_config(data->bus, &cfg->dev_id,
			       MSPI_TIMING_PARAM_DUMMY, &data->timing_cfg)) {
		LOG_ERR("%u, Failed to configure timing.", __LINE__);
		return -EIO;
	}
#endif

	release(emul_flash->dev);

	return 0;
}

static int flash_mspi_emul_device_init_stub(const struct device *dev)
{
	ARG_UNUSED(dev);
	return 0;
}

#define FLASH_MSPI_EMUL_DEVICE(n)                                                                 \
	static uint8_t flash_mspi_emul_device_mem##n[DT_INST_PROP(n, size) / 8];                  \
	static const struct flash_mspi_emul_device_config flash_mspi_emul_device_config_##n = {   \
		.size                     = DT_INST_PROP(n, size) / 8,                            \
		.flash_param =                                                                    \
			{                                                                         \
				.write_block_size = 1,                                            \
				.erase_value      = 0xff,                                         \
			},                                                                        \
		.page_layout =                                                                    \
			{                                                                         \
				.pages_count      = DT_INST_PROP(n, size) / 8 / SPI_NOR_PAGE_SIZE,\
				.pages_size       = SPI_NOR_PAGE_SIZE,                            \
			},                                                                        \
		.dev_id                   = MSPI_DEVICE_ID_DT_INST(n),                            \
		.tar_dev_cfg              = MSPI_DEVICE_CONFIG_DT_INST(n),                        \
		.tar_xip_cfg              = MSPI_XIP_CONFIG_DT_INST(n),                           \
		.tar_scramble_cfg         = MSPI_SCRAMBLE_CONFIG_DT_INST(n),                      \
		.sw_multi_periph          = DT_PROP(DT_INST_BUS(n), software_multiperipheral)     \
	};                                                                                        \
	static struct flash_mspi_emul_device_data flash_mspi_emul_device_data_##n = {             \
		.lock = Z_SEM_INITIALIZER(flash_mspi_emul_device_data_##n.lock, 0, 1),            \
		.mem  = (uint8_t *)flash_mspi_emul_device_mem##n,                                 \
	};                                                                                        \
	DEVICE_DT_INST_DEFINE(n,                                                                  \
			      flash_mspi_emul_device_init_stub,                                   \
			      NULL,                                                               \
			      &flash_mspi_emul_device_data_##n,                                   \
			      &flash_mspi_emul_device_config_##n,                                 \
			      POST_KERNEL,                                                        \
			      CONFIG_FLASH_INIT_PRIORITY,                                         \
			      &flash_mspi_emul_device_api);

#define EMUL_TEST(n)                                                                              \
	EMUL_DT_INST_DEFINE(n,                                                                    \
			    emul_mspi_device_init,                                                \
			    NULL,                                                                 \
			    NULL,                                                                 \
			    &emul_mspi_dev_api,                                                   \
			    NULL);

DT_INST_FOREACH_STATUS_OKAY(EMUL_TEST);

DT_INST_FOREACH_STATUS_OKAY(FLASH_MSPI_EMUL_DEVICE);
