/*
 * Copyright (c) 2021 Henrik Brix Andersen <henrik@brixandersen.dk>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT neorv32_trng

#include <zephyr/device.h>
#include <zephyr/drivers/syscon.h>
#include <zephyr/drivers/entropy.h>
#include <zephyr/pm/device.h>
#include <zephyr/sys/sys_io.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(neorv32_trng, CONFIG_ENTROPY_LOG_LEVEL);

/* TRNG CTRL register bits */
#define NEORV32_TRNG_CTRL_DATA_MASK BIT_MASK(8)
#define NEORV32_TRNG_CTRL_EN        BIT(30)
#define NEORV32_TRNG_CTRL_VALID     BIT(31)

struct neorv32_trng_config {
	const struct device *syscon;
	mm_reg_t base;
};

static inline uint32_t neorv32_trng_read_ctrl(const struct device *dev)
{
	const struct neorv32_trng_config *config = dev->config;

	return sys_read32(config->base);
}

static inline void neorv32_trng_write_ctrl(const struct device *dev, uint32_t ctrl)
{
	const struct neorv32_trng_config *config = dev->config;

	sys_write32(ctrl, config->base);
}

static int neorv32_trng_get_entropy(const struct device *dev, uint8_t *buffer, uint16_t len)
{
	uint32_t ctrl;

	while (len > 0) {
		ctrl = neorv32_trng_read_ctrl(dev);

		if ((ctrl & NEORV32_TRNG_CTRL_VALID) != 0) {
			*buffer++ = ctrl & NEORV32_TRNG_CTRL_DATA_MASK;
			len--;
		}
	}

	return 0;
}

static int neorv32_trng_get_entropy_isr(const struct device *dev, uint8_t *buffer,
					uint16_t len, uint32_t flags)
{
	uint32_t ctrl;
	int err;

	if ((flags & ENTROPY_BUSYWAIT) == 0) {
		ctrl = neorv32_trng_read_ctrl(dev);
		if ((ctrl & NEORV32_TRNG_CTRL_VALID) != 0) {
			*buffer = ctrl & NEORV32_TRNG_CTRL_DATA_MASK;
			return 1;
		}

		/* No entropy available */
		return 0;
	}

	err = neorv32_trng_get_entropy(dev, buffer, len);
	if (err < 0) {
		return err;
	}

	return len;
}

static int neorv32_trng_init(const struct device *dev)
{
	const struct neorv32_trng_config *config = dev->config;
	uint32_t features;
	int err;

	if (!device_is_ready(config->syscon)) {
		LOG_ERR("syscon device not ready");
		return -EINVAL;
	}

	err = syscon_read_reg(config->syscon, NEORV32_SYSINFO_FEATURES, &features);
	if (err < 0) {
		LOG_ERR("failed to determine implemented features (err %d)", err);
		return err;
	}

	if ((features & NEORV32_SYSINFO_FEATURES_IO_TRNG) == 0) {
		LOG_ERR("neorv32 trng not supported");
		return -ENODEV;
	}

	neorv32_trng_write_ctrl(dev, NEORV32_TRNG_CTRL_EN);

	return 0;
}

#ifdef CONFIG_PM_DEVICE
static int neorv32_trng_pm_action(const struct device *dev, enum pm_device_action action)
{
	switch (action) {
	case PM_DEVICE_ACTION_SUSPEND:
		neorv32_trng_write_ctrl(dev, 0);
		break;
	case PM_DEVICE_ACTION_RESUME:
		neorv32_trng_write_ctrl(dev, NEORV32_TRNG_CTRL_EN);
		break;
	default:
		return -ENOTSUP;
	}

	return 0;
}
#endif /* CONFIG_PM_DEVICE */

static const struct entropy_driver_api neorv32_trng_driver_api = {
	.get_entropy = neorv32_trng_get_entropy,
	.get_entropy_isr = neorv32_trng_get_entropy_isr,
};

#define NEORV32_TRNG_INIT(n)						\
	static const struct neorv32_trng_config neorv32_trng_##n##_config = { \
		.syscon = DEVICE_DT_GET(DT_INST_PHANDLE(n, syscon)),	\
		.base = DT_INST_REG_ADDR(n),				\
	};								\
									\
	PM_DEVICE_DT_INST_DEFINE(n, neorv32_trng_pm_action);		\
									\
	DEVICE_DT_INST_DEFINE(n, &neorv32_trng_init,			\
			 PM_DEVICE_DT_INST_GET(n),			\
			 NULL,						\
			 &neorv32_trng_##n##_config,			\
			 PRE_KERNEL_1,					\
			 CONFIG_ENTROPY_INIT_PRIORITY,			\
			 &neorv32_trng_driver_api);

DT_INST_FOREACH_STATUS_OKAY(NEORV32_TRNG_INIT)
