/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright (c) 2023 Nuvoton Technology Corporation.
 */

#define DT_DRV_COMPAT nuvoton_numaker_fmc

#include <string.h>
#include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/flash.h>
#include <zephyr/logging/log.h>
#include "flash_priv.h"
#include <NuMicro.h>

LOG_MODULE_REGISTER(flash_numaker, CONFIG_FLASH_LOG_LEVEL);

#define SOC_NV_FLASH_NODE DT_INST(0, soc_nv_flash)
#define SOC_NV_FLASH_WRITE_BLOCK_SIZE DT_PROP_OR(SOC_NV_FLASH_NODE, write_block_size, 0x04)

struct flash_numaker_data {
	FMC_T *fmc;
	struct k_sem write_lock;
	uint32_t flash_block_base;
};

static const struct flash_parameters flash_numaker_parameters = {
	.write_block_size = SOC_NV_FLASH_WRITE_BLOCK_SIZE,
	.erase_value = 0xff,
};

/* Validate offset and length */
static bool flash_numaker_is_range_valid(off_t offset, size_t len)
{
	uint32_t aprom_size = (FMC_APROM_END - FMC_APROM_BASE);

	/* check for min value */
	if ((offset < 0) || (len == 0)) {
		return false;
	}

	/* check for max value */
	if (offset >= aprom_size || len > aprom_size || (aprom_size - offset) < len) {
		return false;
	}

	return true;
}

/*
 * Erase a flash memory area.
 *
 * param dev       Device struct
 * param offset    The address's offset
 * param len       The size of the buffer
 * return 0       on success
 * return -EINVAL erroneous code
 */

static int flash_numaker_erase(const struct device *dev, off_t offset, size_t len)
{
	struct flash_numaker_data *dev_data = dev->data;
	uint32_t rc = 0;
	unsigned int key;
	int page_nums = (len / FMC_FLASH_PAGE_SIZE);
	uint32_t addr = dev_data->flash_block_base + offset;

	/* return SUCCESS for len == 0 (required by tests/drivers/flash) */
	if (!len) {
		return 0;
	}

	/* Validate range */
	if (!flash_numaker_is_range_valid(offset, len)) {
		return -EINVAL;
	}

	/* check alignment and erase only by pages */
	if (((addr % FMC_FLASH_PAGE_SIZE) != 0) || ((len % FMC_FLASH_PAGE_SIZE) != 0)) {
		return -EINVAL;
	}

	/* take semaphore */
	if (k_sem_take(&dev_data->write_lock, K_NO_WAIT)) {
		return -EACCES;
	}

	SYS_UnlockReg();
	key = irq_lock();
	while (page_nums) {
		if (((len >= FMC_BANK_SIZE)) && ((addr % FMC_BANK_SIZE) == 0)) {
			if (FMC_Erase_Bank(addr)) {
				LOG_ERR("Erase flash bank failed or erase time-out");
				rc = -EIO;
				goto done;
			}
			page_nums -= (FMC_BANK_SIZE / FMC_FLASH_PAGE_SIZE);
			addr += FMC_BANK_SIZE;
		} else {
			/* erase page */
			if (FMC_Erase(addr)) {
				LOG_ERR("Erase flash page failed or erase time-out");
				rc = -EIO;
				goto done;
			}
			page_nums--;
			addr += FMC_FLASH_PAGE_SIZE;
		}
	}

done:
	SYS_LockReg();
	irq_unlock(key);
	/* release semaphore */
	k_sem_give(&dev_data->write_lock);

	return rc;
}

/*
 * Read a flash memory area.
 *
 * param dev       Device struct
 * param offset    The address's offset
 * param data      The buffer to store or read the value
 * param length    The size of the buffer
 * return 0       on success,
 * return -EIO     erroneous code
 */
static int flash_numaker_read(const struct device *dev, off_t offset, void *data, size_t len)
{
	struct flash_numaker_data *dev_data = dev->data;
	uint32_t addr = dev_data->flash_block_base + offset;

	/* return SUCCESS for len == 0 (required by tests/drivers/flash) */
	if (!len) {
		return 0;
	}

	/* Validate range */
	if (!flash_numaker_is_range_valid(offset, len)) {
		return -EINVAL;
	}

	/* read flash */
	memcpy(data, (void *)addr, len);

	return 0;
}

static int32_t flash_numaker_block_write(uint32_t u32_addr, uint8_t *pu8_data, int block_size)
{
	int32_t retval;
	uint32_t *pu32_data = (uint32_t *)pu8_data;

	SYS_UnlockReg();
	if (block_size == 4) {
		retval = FMC_Write(u32_addr, *pu32_data);
	} else if (block_size == 8) {
		retval = FMC_Write8Bytes(u32_addr, *pu32_data, *(pu32_data + 1));
	} else {
		retval = -1;
	}
	SYS_LockReg();

	return retval;
}

static int flash_numaker_write(const struct device *dev, off_t offset, const void *data, size_t len)
{
	struct flash_numaker_data *dev_data = dev->data;
	uint32_t rc = 0;
	unsigned int key;
	uint32_t addr = dev_data->flash_block_base + offset;
	int block_size = flash_numaker_parameters.write_block_size;
	int blocks = (len / flash_numaker_parameters.write_block_size);
	uint8_t *pu8_data = (uint8_t *)data;

	/* return SUCCESS for len == 0 (required by tests/drivers/flash) */
	if (!len) {
		return 0;
	}

	/* Validate range */
	if (!flash_numaker_is_range_valid(offset, len)) {
		return -EINVAL;
	}

	/* Validate address alignment */
	if ((addr % flash_numaker_parameters.write_block_size) != 0) {
		return -EINVAL;
	}

	/* Validate write size be multiples of the write block size */
	if ((len % block_size) != 0) {
		return -EINVAL;
	}

	/* Validate offset be multiples of the write block size */
	if ((offset % block_size) != 0) {
		return -EINVAL;
	}

	if (k_sem_take(&dev_data->write_lock, K_FOREVER)) {
		return -EACCES;
	}

	key = irq_lock();

	while (blocks) {
		if (flash_numaker_block_write(addr, pu8_data, block_size)) {
			rc = -EIO;
			goto done;
		}
		pu8_data += block_size;
		addr += block_size;
		blocks--;
	}

done:
	irq_unlock(key);

	k_sem_give(&dev_data->write_lock);

	return rc;
}

#if defined(CONFIG_FLASH_PAGE_LAYOUT)
static const struct flash_pages_layout dev_layout = {
	.pages_count =
		DT_REG_SIZE(SOC_NV_FLASH_NODE) / DT_PROP(SOC_NV_FLASH_NODE, erase_block_size),
	.pages_size = DT_PROP(SOC_NV_FLASH_NODE, erase_block_size),
};

static void flash_numaker_pages_layout(const struct device *dev,
				       const struct flash_pages_layout **layout,
				       size_t *layout_size)
{
	*layout = &dev_layout;
	*layout_size = 1;
}
#endif /* CONFIG_FLASH_PAGE_LAYOUT */

static const struct flash_parameters *flash_numaker_get_parameters(const struct device *dev)
{
	ARG_UNUSED(dev);

	return &flash_numaker_parameters;
}

static struct flash_numaker_data flash_data;

static DEVICE_API(flash, flash_numaker_api) = {
	.erase = flash_numaker_erase,
	.write = flash_numaker_write,
	.read = flash_numaker_read,
	.get_parameters = flash_numaker_get_parameters,
#if defined(CONFIG_FLASH_PAGE_LAYOUT)
	.page_layout = flash_numaker_pages_layout,
#endif
};

static int flash_numaker_init(const struct device *dev)
{
	struct flash_numaker_data *dev_data = dev->data;

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

	/* Enable FMC ISP function */
	SYS_UnlockReg();
	FMC_Open();
	/* Enable APROM update. */
	FMC_ENABLE_AP_UPDATE();
	SYS_LockReg();
	dev_data->flash_block_base = (uint32_t)FMC_APROM_BASE;
	dev_data->fmc = (FMC_T *)DT_REG_ADDR(DT_NODELABEL(fmc));

	return 0;
}

DEVICE_DT_INST_DEFINE(0, flash_numaker_init, NULL, &flash_data, NULL, POST_KERNEL,
		      CONFIG_FLASH_INIT_PRIORITY, &flash_numaker_api);
