/*
 * Copyright (c) 2019 Laczen
 * Copyright (c) 2018 Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT zephyr_sim_eeprom

#ifdef CONFIG_ARCH_POSIX
#include "eeprom_simulator_native.h"
#include "cmdline.h"
#include "soc.h"
#endif /* CONFIG_ARCH_POSIX */

#include <zephyr/device.h>
#include <zephyr/drivers/eeprom.h>

#include <zephyr/init.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>
#include <zephyr/stats/stats.h>
#include <string.h>
#include <errno.h>

#define LOG_LEVEL CONFIG_EEPROM_LOG_LEVEL
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(eeprom_simulator);

struct eeprom_sim_config {
	size_t size;
	bool readonly;
};

#define EEPROM(addr) (mock_eeprom + (addr))

#if defined(CONFIG_MULTITHREADING)
/* semaphore for locking flash resources (tickers) */
static struct k_sem sem_lock;
#define SYNC_INIT() k_sem_init(&sem_lock, 1, 1)
#define SYNC_LOCK() k_sem_take(&sem_lock, K_FOREVER)
#define SYNC_UNLOCK() k_sem_give(&sem_lock)
#else
#define SYNC_INIT()
#define SYNC_LOCK()
#define SYNC_UNLOCK()
#endif

/* simulator statistics */
STATS_SECT_START(eeprom_sim_stats)
STATS_SECT_ENTRY32(bytes_read)		/* total bytes read */
STATS_SECT_ENTRY32(bytes_written)	/* total bytes written */
STATS_SECT_ENTRY32(eeprom_read_calls)	/* calls to eeprom_read() */
STATS_SECT_ENTRY32(eeprom_read_time_us) /* time spent in eeprom_read() */
STATS_SECT_ENTRY32(eeprom_write_calls)  /* calls to eeprom_write() */
STATS_SECT_ENTRY32(eeprom_write_time_us)/* time spent in eeprom_write() */
STATS_SECT_END;

STATS_SECT_DECL(eeprom_sim_stats) eeprom_sim_stats;
STATS_NAME_START(eeprom_sim_stats)
STATS_NAME(eeprom_sim_stats, bytes_read)
STATS_NAME(eeprom_sim_stats, bytes_written)
STATS_NAME(eeprom_sim_stats, eeprom_read_calls)
STATS_NAME(eeprom_sim_stats, eeprom_read_time_us)
STATS_NAME(eeprom_sim_stats, eeprom_write_calls)
STATS_NAME(eeprom_sim_stats, eeprom_write_time_us)
STATS_NAME_END(eeprom_sim_stats);

/* simulator dynamic thresholds */
STATS_SECT_START(eeprom_sim_thresholds)
STATS_SECT_ENTRY32(max_write_calls)
STATS_SECT_ENTRY32(max_len)
STATS_SECT_END;

STATS_SECT_DECL(eeprom_sim_thresholds) eeprom_sim_thresholds;
STATS_NAME_START(eeprom_sim_thresholds)
STATS_NAME(eeprom_sim_thresholds, max_write_calls)
STATS_NAME(eeprom_sim_thresholds, max_len)
STATS_NAME_END(eeprom_sim_thresholds);

#ifdef CONFIG_ARCH_POSIX
static char *mock_eeprom;
static int eeprom_fd = -1;
static const char *eeprom_file_path;
#define DEFAULT_EEPROM_FILE_PATH "eeprom.bin"
static bool eeprom_erase_at_start;
static bool eeprom_rm_at_exit;
static bool eeprom_in_ram;
#else
static uint8_t mock_eeprom[DT_INST_PROP(0, size)];
#endif /* CONFIG_ARCH_POSIX */

static int eeprom_range_is_valid(const struct device *dev, off_t offset,
				 size_t len)
{
	const struct eeprom_sim_config *config = dev->config;

	if ((offset + len) <= config->size) {
		return 1;
	}

	return 0;
}

static int eeprom_sim_read(const struct device *dev, off_t offset, void *data,
			   size_t len)
{
	if (!len) {
		return 0;
	}

	if (!eeprom_range_is_valid(dev, offset, len)) {
		LOG_WRN("attempt to read past device boundary");
		return -EINVAL;
	}

	SYNC_LOCK();

	STATS_INC(eeprom_sim_stats, eeprom_read_calls);
	memcpy(data, EEPROM(offset), len);
	STATS_INCN(eeprom_sim_stats, bytes_read, len);

	SYNC_UNLOCK();

#ifdef CONFIG_EEPROM_SIMULATOR_SIMULATE_TIMING
	k_busy_wait(CONFIG_EEPROM_SIMULATOR_MIN_READ_TIME_US);
	STATS_INCN(eeprom_sim_stats, eeprom_read_time_us,
		   CONFIG_EEPROM_SIMULATOR_MIN_READ_TIME_US);
#endif

	return 0;
}

static int eeprom_sim_write(const struct device *dev, off_t offset,
			    const void *data,
			    size_t len)
{
	const struct eeprom_sim_config *config = dev->config;

	if (config->readonly) {
		LOG_WRN("attempt to write to read-only device");
		return -EACCES;
	}

	if (!len) {
		return 0;
	}

	if (!eeprom_range_is_valid(dev, offset, len)) {
		LOG_WRN("attempt to write past device boundary");
		return -EINVAL;
	}

	SYNC_LOCK();

	STATS_INC(eeprom_sim_stats, eeprom_write_calls);

	bool data_part_ignored = false;

	if (eeprom_sim_thresholds.max_write_calls != 0) {
		if (eeprom_sim_stats.eeprom_write_calls >
			eeprom_sim_thresholds.max_write_calls) {
			goto end;
		} else if (eeprom_sim_stats.eeprom_write_calls ==
				eeprom_sim_thresholds.max_write_calls) {
			if (eeprom_sim_thresholds.max_len == 0) {
				goto end;
			}

			data_part_ignored = true;
		}
	}

	if ((data_part_ignored) && (len > eeprom_sim_thresholds.max_len)) {
		len = eeprom_sim_thresholds.max_len;
	}

	memcpy(EEPROM(offset), data, len);

	STATS_INCN(eeprom_sim_stats, bytes_written, len);

#ifdef CONFIG_EEPROM_SIMULATOR_SIMULATE_TIMING
	/* wait before returning */
	k_busy_wait(CONFIG_EEPROM_SIMULATOR_MIN_WRITE_TIME_US);
	STATS_INCN(eeprom_sim_stats, eeprom_write_time_us,
		   CONFIG_EEPROM_SIMULATOR_MIN_WRITE_TIME_US);
#endif

end:
	SYNC_UNLOCK();
	return 0;
}

static size_t eeprom_sim_size(const struct device *dev)
{
	const struct eeprom_sim_config *config = dev->config;

	return config->size;
}

static DEVICE_API(eeprom, eeprom_sim_api) = {
	.read = eeprom_sim_read,
	.write = eeprom_sim_write,
	.size = eeprom_sim_size,
};

static const struct eeprom_sim_config eeprom_sim_config_0 = {
	.size = DT_INST_PROP(0, size),
	.readonly = DT_INST_PROP(0, read_only),
};

#ifdef CONFIG_ARCH_POSIX

static int eeprom_mock_init(const struct device *dev)
{
	int rc;

	ARG_UNUSED(dev);

	if (eeprom_in_ram == false && eeprom_file_path == NULL) {
		eeprom_file_path = DEFAULT_EEPROM_FILE_PATH;
	}

	rc = eeprom_mock_init_native(eeprom_in_ram, &mock_eeprom, DT_INST_PROP(0, size), &eeprom_fd,
				     eeprom_file_path, 0xFF, eeprom_erase_at_start);

	if (rc < 0) {
		return -EIO;
	} else {
		return 0;
	}
}

#else

static int eeprom_mock_init(const struct device *dev)
{
	memset(mock_eeprom, 0xFF, ARRAY_SIZE(mock_eeprom));
	return 0;
}

#endif /* CONFIG_ARCH_POSIX */

static int eeprom_sim_init(const struct device *dev)
{
	SYNC_INIT();
	STATS_INIT_AND_REG(eeprom_sim_stats, STATS_SIZE_32, "eeprom_sim_stats");
	STATS_INIT_AND_REG(eeprom_sim_thresholds, STATS_SIZE_32,
			   "eeprom_sim_thresholds");

	return eeprom_mock_init(dev);
}

DEVICE_DT_INST_DEFINE(0, &eeprom_sim_init, NULL, NULL, &eeprom_sim_config_0, POST_KERNEL,
		      CONFIG_EEPROM_INIT_PRIORITY, &eeprom_sim_api);

#ifdef CONFIG_ARCH_POSIX

static void eeprom_native_cleanup(void)
{
	eeprom_mock_cleanup_native(eeprom_in_ram, eeprom_fd, mock_eeprom, DT_INST_PROP(0, size),
				   eeprom_file_path, eeprom_rm_at_exit);
}

static void eeprom_native_options(void)
{
	static struct args_struct_t eeprom_options[] = {
		{.option = "eeprom",
		 .name = "path",
		 .type = 's',
		 .dest = (void *)&eeprom_file_path,
		 .descript = "Path to binary file to be used as EEPROM, by default "
			     "\"" DEFAULT_EEPROM_FILE_PATH "\""},
		{.is_switch = true,
		 .option = "eeprom_erase",
		 .type = 'b',
		 .dest = (void *)&eeprom_erase_at_start,
		 .descript = "Erase the EEPROM content at startup"},
		{.is_switch = true,
		 .option = "eeprom_rm",
		 .type = 'b',
		 .dest = (void *)&eeprom_rm_at_exit,
		 .descript = "Remove the EEPROM file when terminating the execution"},
		{.is_switch = true,
		 .option = "eeprom_in_ram",
		 .type = 'b',
		 .dest = (void *)&eeprom_in_ram,
		 .descript = "Instead of a file, keep the file content just in RAM. If this is "
			     "set, eeprom, eeprom_erase & eeprom_rm are ignored, and the EEPROM "
			     "content is always erased at startup"},
		ARG_TABLE_ENDMARKER
	};

	native_add_command_line_opts(eeprom_options);
}

NATIVE_TASK(eeprom_native_options, PRE_BOOT_1, 1);
NATIVE_TASK(eeprom_native_cleanup, ON_EXIT, 1);

#endif /* CONFIG_ARCH_POSIX */
