/*
 * Copyright (c) 2024 STMicroelectronics
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT st_stm32wb0_flash_controller

#include <zephyr/init.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/flash.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/math_extras.h>

/* <soc.h> also brings "stm32wb0x_hal_flash.h"
 * and "system_stm32wb0x.h", which provide macros
 * used by the driver, such as FLASH_PAGE_SIZE or
 * _MEMORY_FLASH_SIZE_ respectively.
 */
#include <soc.h>
#include <stm32_ll_bus.h>
#include <stm32_ll_rcc.h>
#include <stm32_ll_system.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(flash_stm32wb0x, CONFIG_FLASH_LOG_LEVEL);

/**
 * Driver private definitions & assertions
 */
#define SYSTEM_FLASH_SIZE	_MEMORY_FLASH_SIZE_
#define PAGES_IN_FLASH		(SYSTEM_FLASH_SIZE / FLASH_PAGE_SIZE)

#define WRITE_BLOCK_SIZE \
	DT_PROP(DT_INST(0, soc_nv_flash), write_block_size)

/* Size of flash words, in bytes (equal to write block size) */
#define WORD_SIZE	WRITE_BLOCK_SIZE

#define ERASE_BLOCK_SIZE \
	DT_PROP(DT_INST(0, soc_nv_flash), erase_block_size)

/**
 * Driver private structures
 */
struct flash_wb0x_data {
	/** Used to serialize write/erase operations */
	struct k_sem write_lock;

	/** Flash size, in bytes */
	size_t flash_size;
};

/**
 * Driver private utility functions
 */
static inline uint32_t read_mem_u32(const uint32_t *ptr)
{
	/**
	 * Fetch word using sys_get_le32, which performs byte-sized
	 * reads instead of word-sized. This is important as ptr may
	 * be unaligned. We also want to use le32 because the data is
	 * stored in little-endian inside the flash.
	 */
	return sys_get_le32((const uint8_t *)ptr);
}

static inline size_t get_flash_size_in_bytes(void)
{
	/* FLASH.SIZE contains the highest flash address supported
	 * on this MCU, which is also the number of words in flash
	 * minus one.
	 */
	const uint32_t words_in_flash =
		READ_BIT(FLASH->SIZE, FLASH_FLASH_SIZE_FLASH_SIZE) + 1;

	return words_in_flash * WORD_SIZE;
}

/**
 * @brief Returns the associated error to IRQ flags.
 *
 * @returns a negative error value
 */
static int error_from_irq_flags(uint32_t flags)
{
	/**
	 * Only two errors are expected:
	 *  - illegal command
	 *  - command error
	 */
	if (flags & FLASH_FLAG_ILLCMD) {
		return -EINVAL;
	}

	if (flags & FLASH_FLAG_CMDERR) {
		return -EIO;
	}

	/*
	 * Unexpected error flag -> "out of domain"
	 * In practice, this should never be reached.
	 */
	return -EDOM;
}

static bool is_valid_flash_range(const struct device *dev,
				off_t offset, uint32_t len)
{
	const struct flash_wb0x_data *data = dev->data;
	uint32_t offset_plus_len;

		/* (offset + len) must not overflow */
	return !u32_add_overflow(offset, len, &offset_plus_len)
		/* offset must be a valid offset in flash */
		&& IN_RANGE(offset, 0, data->flash_size - 1)
		/* (offset + len) must be in [0; flash size]
		 * because it is equal to the last accessed
		 * byte in flash plus one (an access of `len`
		 * bytes starting at `offset` touches bytes
		 * `offset` to `offset + len` EXCLUDED)
		 */
		&& IN_RANGE(offset_plus_len, 0, data->flash_size);
}

static bool is_writeable_flash_range(const struct device *dev,
				off_t offset, uint32_t len)
{
	if ((offset % WRITE_BLOCK_SIZE) != 0
		|| (len % WRITE_BLOCK_SIZE) != 0) {
		return false;
	}

	return is_valid_flash_range(dev, offset, len);
}

static bool is_erasable_flash_range(const struct device *dev,
				off_t offset, uint32_t len)
{
	if ((offset % ERASE_BLOCK_SIZE) != 0
		|| (len % ERASE_BLOCK_SIZE) != 0) {
		return false;
	}

	return is_valid_flash_range(dev, offset, len);
}

/**
 * Driver private functions
 */

static uint32_t poll_flash_controller(void)
{
	uint32_t flags;

	/* Poll until an interrupt flag is raised */
	do {
		flags = FLASH->IRQRAW;
	} while (flags == 0);

	/* Acknowledge the flag(s) we have seen */
	FLASH->IRQRAW = flags;

	return flags;
}

static int execute_flash_command(uint8_t cmd)
{
	uint32_t irq_flags;

	/* Clear all pending interrupt bits */
	FLASH->IRQRAW = FLASH->IRQRAW;

	/* Start command */
	FLASH->COMMAND = cmd;

	/* Wait for CMDSTART */
	irq_flags = poll_flash_controller();

	/* If command didn't start, an error occurred */
	if (!(irq_flags & FLASH_IT_CMDSTART)) {
		return error_from_irq_flags(irq_flags);
	}

	/**
	 * Both CMDSTART and CMDDONE may be set if the command was
	 * executed fast enough. In this case, we're already done.
	 * Otherwise, we need to poll again until CMDDONE/error occurs.
	 */
	if (!(irq_flags & FLASH_IT_CMDDONE)) {
		irq_flags = poll_flash_controller();
	}

	if (!(irq_flags & FLASH_IT_CMDDONE)) {
		return error_from_irq_flags(irq_flags);
	} else {
		return 0;
	}
}

int erase_page_range(uint32_t start_page, uint32_t page_count)
{
	int res = 0;

	__ASSERT_NO_MSG(start_page < PAGES_IN_FLASH);
	__ASSERT_NO_MSG((start_page + page_count - 1) < PAGES_IN_FLASH);

	for (uint32_t i = start_page;
		i < (start_page + page_count);
		i++) {
		/* ADDRESS[16:9] = XADR[10:3] (address of page to erase)
		 * ADDRESS[8:0]  = 0 (row & word address, must be 0)
		 */
		FLASH->ADDRESS = (i << 9);

		res = execute_flash_command(FLASH_CMD_ERASE_PAGES);
		if (res < 0) {
			break;
		}
	}

	return res;
}

int write_word_range(const void *buf, uint32_t start_word, uint32_t num_words)
{
	/* Special value to load in DATAx registers to skip
	 * writing corresponding word with BURSTWRITE command.
	 */
	const uint32_t BURST_IGNORE_VALUE = 0xFFFFFFFF;
	const size_t WORDS_IN_BURST = 4;
	uint32_t dst_addr = start_word;
	uint32_t remaining = num_words;
	/**
	 * Note that @p buf may not be aligned to 32-bit boundary.
	 * However, declaring src_ptr as uint32_t* makes the address
	 * increment by 4 every time we do src_ptr++, which makes it
	 * behave like the other counters in this function.
	 */
	const uint32_t *src_ptr = buf;
	int res = 0;

	/**
	 * Write to flash is performed as a 3 step process:
	 *  - write single words using WRITE commands until the write
	 *    write address is aligned to flash quadword boundary
	 *
	 *  - after write address is aligned to quadword, we can use
	 *    the BURSTWRITE commands to write 4 words at a time
	 *
	 *  - once less than 4 words remain to write, a last BURSTWRITE
	 *    is used with unneeded DATAx registers filled with 0xFFFFFFFF
	 *    (this makes BURSTWRITE ignore write to these addresses)
	 */

	/* (1) Align to quadword boundary with WRITE commands */
	while (remaining > 0 && (dst_addr % WORDS_IN_BURST) != 0) {
		FLASH->ADDRESS = dst_addr;
		FLASH->DATA0 = read_mem_u32(src_ptr);

		res = execute_flash_command(FLASH_CMD_WRITE);
		if (res < 0) {
			return res;
		}

		src_ptr++;
		dst_addr++;
		remaining--;
	}

	/* (2) Write bursts of quadwords */
	while (remaining >= WORDS_IN_BURST) {
		__ASSERT_NO_MSG((dst_addr % WORDS_IN_BURST) == 0);

		FLASH->ADDRESS = dst_addr;
		FLASH->DATA0 = read_mem_u32(src_ptr + 0);
		FLASH->DATA1 = read_mem_u32(src_ptr + 1);
		FLASH->DATA2 = read_mem_u32(src_ptr + 2);
		FLASH->DATA3 = read_mem_u32(src_ptr + 3);

		res = execute_flash_command(FLASH_CMD_BURSTWRITE);
		if (res < 0) {
			return res;
		}

		src_ptr += WORDS_IN_BURST;
		dst_addr += WORDS_IN_BURST;
		remaining -= WORDS_IN_BURST;
	}

	/* (3) Write trailing (between 1 and 3 words) */
	if (remaining > 0) {
		__ASSERT_NO_MSG(remaining < WORDS_IN_BURST);
		__ASSERT_NO_MSG((dst_addr % WORDS_IN_BURST) == 0);

		FLASH->ADDRESS = dst_addr;
		FLASH->DATA0 = read_mem_u32(src_ptr + 0);

		FLASH->DATA1 = (remaining >= 2)
			? read_mem_u32(src_ptr + 1)
			: BURST_IGNORE_VALUE;

		FLASH->DATA2 = (remaining == 3)
			? read_mem_u32(src_ptr + 2)
			: BURST_IGNORE_VALUE;

		FLASH->DATA3 = BURST_IGNORE_VALUE;

		remaining = 0;

		res = execute_flash_command(FLASH_CMD_BURSTWRITE);
	}

	return res;
}

/**
 * Driver subsystem API implementation
 */
int flash_wb0x_read(const struct device *dev, off_t offset,
			void *buffer, size_t len)
{
	if (!len) {
		return 0;
	}

	if (!is_valid_flash_range(dev, offset, len)) {
		return -EINVAL;
	}

	const uint8_t *flash_base = ((void *)DT_REG_ADDR(DT_INST(0, st_stm32_nv_flash)));

	memcpy(buffer, flash_base + (uint32_t)offset, len);

	return 0;
}

int flash_wb0x_write(const struct device *dev, off_t offset,
			const void *buffer, size_t len)
{
	struct flash_wb0x_data *data = dev->data;
	int res;

	if (!len) {
		return 0;
	}

	if (!is_writeable_flash_range(dev, offset, len)) {
		return -EINVAL;
	}

	/* Acquire driver lock */
	res = k_sem_take(&data->write_lock, K_NO_WAIT);
	if (res < 0) {
		return res;
	}

	const uint32_t start_word = (uint32_t)offset / WORD_SIZE;
	const uint32_t num_words = len / WORD_SIZE;

	res = write_word_range(buffer, start_word, num_words);

	/* Release driver lock */
	k_sem_give(&data->write_lock);

	return res;
}

int flash_wb0x_erase(const struct device *dev, off_t offset, size_t size)
{
	struct flash_wb0x_data *data = dev->data;
	int res;

	if (!size) {
		return 0;
	}

	if (!is_erasable_flash_range(dev, offset, size)) {
		return -EINVAL;
	}

	/* Acquire driver lock */
	res = k_sem_take(&data->write_lock, K_NO_WAIT);
	if (res < 0) {
		return res;
	}

	const uint32_t start_page = (uint32_t)offset / ERASE_BLOCK_SIZE;
	const uint32_t page_count = size / ERASE_BLOCK_SIZE;

	res = erase_page_range(start_page, page_count);

	/* Release driver lock */
	k_sem_give(&data->write_lock);

	return res;
}

const struct flash_parameters *flash_wb0x_get_parameters(
					const struct device *dev)
{
	static const struct flash_parameters fp = {
		.write_block_size = WRITE_BLOCK_SIZE,
		.erase_value = 0xff,
	};

	return &fp;
}

#if defined(CONFIG_FLASH_PAGE_LAYOUT)
void flash_wb0x_pages_layout(const struct device *dev,
				const struct flash_pages_layout **layout,
				size_t *layout_size)
{
	/**
	 * STM32WB0 flash: single bank, 2KiB pages
	 * (the number of pages depends on MCU)
	 */
	static const struct flash_pages_layout fpl[] = {{
		.pages_count = PAGES_IN_FLASH,
		.pages_size = FLASH_PAGE_SIZE
	}};

	*layout = fpl;
	*layout_size = ARRAY_SIZE(fpl);
}
#endif /* CONFIG_FLASH_PAGE_LAYOUT */

static DEVICE_API(flash, flash_wb0x_api) = {
	.erase = flash_wb0x_erase,
	.write = flash_wb0x_write,
	.read = flash_wb0x_read,
	.get_parameters = flash_wb0x_get_parameters,
#ifdef CONFIG_FLASH_PAGE_LAYOUT
	.page_layout = flash_wb0x_pages_layout,
#endif
	/* extended operations not supported */
};

int stm32wb0x_flash_init(const struct device *dev)
{
	struct flash_wb0x_data *data = dev->data;

	k_sem_init(&data->write_lock, 1, 1);

	data->flash_size = get_flash_size_in_bytes();

	return 0;
}

/**
 * Driver device instantiation
 */
static struct flash_wb0x_data wb0x_flash_drv_data;

DEVICE_DT_INST_DEFINE(0, stm32wb0x_flash_init, NULL,
		    &wb0x_flash_drv_data, NULL, POST_KERNEL,
		    CONFIG_FLASH_INIT_PRIORITY, &flash_wb0x_api);
