/*
 * Copyright (c) 2019 Brett Witherspoon
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT ti_cc13xx_cc26xx_trng

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/entropy.h>
#include <zephyr/irq.h>
#include <zephyr/pm/policy.h>
#include <zephyr/pm/device.h>

#include <zephyr/sys/ring_buffer.h>
#include <zephyr/sys/sys_io.h>

#include <driverlib/prcm.h>
#include <driverlib/trng.h>

#include <ti/drivers/Power.h>
#include <ti/drivers/power/PowerCC26X2.h>

#define CPU_FREQ DT_PROP(DT_PATH(cpus, cpu_0), clock_frequency)

#define US_PER_SAMPLE (1000000ULL * \
	CONFIG_ENTROPY_CC13XX_CC26XX_SAMPLES_PER_CYCLE / CPU_FREQ + 1ULL)

struct entropy_cc13xx_cc26xx_data {
	struct k_sem lock;
	struct k_sem sync;
	struct ring_buf pool;
	uint8_t data[CONFIG_ENTROPY_CC13XX_CC26XX_POOL_SIZE];
#ifdef CONFIG_PM
	Power_NotifyObj post_notify;
	bool constrained;
#endif
};

static void start_trng(struct entropy_cc13xx_cc26xx_data *data)
{
	/* Initialization as described in TRM section 18.6.1.2 */
	TRNGReset();
	while (sys_read32(TRNG_BASE + TRNG_O_SWRESET)) {
		continue;
	}

	/* Set samples per cycle */
	TRNGConfigure(0, CONFIG_ENTROPY_CC13XX_CC26XX_SAMPLES_PER_CYCLE,
		0);
	/* De-tune FROs */
	sys_write32(TRNG_FRODETUNE_FRO_MASK_M, TRNG_BASE +
		TRNG_O_FRODETUNE);
	/* Enable FROs */
	sys_write32(TRNG_FROEN_FRO_MASK_M, TRNG_BASE + TRNG_O_FROEN);
	/* Set shutdown and alarm thresholds */
	sys_write32((CONFIG_ENTROPY_CC13XX_CC26XX_SHUTDOWN_THRESHOLD
		<< 16) | CONFIG_ENTROPY_CC13XX_CC26XX_ALARM_THRESHOLD,
		TRNG_BASE + TRNG_O_ALARMCNT);

	TRNGEnable();
	TRNGIntEnable(TRNG_NUMBER_READY | TRNG_FRO_SHUTDOWN);
}

#ifdef CONFIG_PM_DEVICE
static void stop_trng(struct entropy_cc13xx_cc26xx_data *data)
{
	TRNGDisable();

	TRNGIntClear(TRNG_NUMBER_READY | TRNG_FRO_SHUTDOWN);
	TRNGIntDisable(TRNG_NUMBER_READY | TRNG_FRO_SHUTDOWN);
}
#endif

static void handle_shutdown_ovf(void)
{
	uint32_t off;

	/* Clear shutdown */
	TRNGIntClear(TRNG_FRO_SHUTDOWN);
	/* Disabled FROs */
	off = sys_read32(TRNG_BASE + TRNG_O_ALARMSTOP);
	/* Clear alarms */
	sys_write32(0, TRNG_BASE + TRNG_O_ALARMMASK);
	sys_write32(0, TRNG_BASE + TRNG_O_ALARMSTOP);
	/* De-tune FROs */
	sys_write32(off, TRNG_BASE + TRNG_O_FRODETUNE);
	/* Re-enable FROs */
	sys_write32(off, TRNG_BASE + TRNG_O_FROEN);
}

static int entropy_cc13xx_cc26xx_get_entropy(const struct device *dev,
					     uint8_t *buf,
					     uint16_t len)
{
	struct entropy_cc13xx_cc26xx_data *data = dev->data;
	uint32_t cnt;

#ifdef CONFIG_PM
	unsigned int key = irq_lock();

	if (!data->constrained) {
		pm_policy_state_lock_get(PM_STATE_STANDBY, PM_ALL_SUBSTATES);
		data->constrained = true;
	}
	irq_unlock(key);
#endif

	TRNGIntEnable(TRNG_NUMBER_READY);

	while (len) {
		k_sem_take(&data->lock, K_FOREVER);
		cnt = ring_buf_get(&data->pool, buf, len);
		k_sem_give(&data->lock);

		if (cnt) {
			buf += cnt;
			len -= cnt;
		} else {
			k_sem_take(&data->sync, K_FOREVER);
		}
	}

	return 0;
}

static void entropy_cc13xx_cc26xx_isr(const struct device *dev)
{
	struct entropy_cc13xx_cc26xx_data *data = dev->data;
	uint32_t src = 0;
	uint32_t cnt;
	uint32_t num[2];

	/* Interrupt service routine as described in TRM section 18.6.1.3.2 */
	src = TRNGStatusGet();

	if (src & TRNG_NUMBER_READY) {
		/* This function acknowledges the ready status */
		num[1] = TRNGNumberGet(TRNG_HI_WORD);
		num[0] = TRNGNumberGet(TRNG_LOW_WORD);

		cnt = ring_buf_put(&data->pool, (uint8_t *)num, sizeof(num));

		/* When pool is full disable interrupt and stop reading numbers */
		if (cnt != sizeof(num)) {
#ifdef CONFIG_PM
			if (data->constrained) {
				pm_policy_state_lock_put(
					PM_STATE_STANDBY,
					PM_ALL_SUBSTATES);
				data->constrained = false;
			}
#endif
			TRNGIntDisable(TRNG_NUMBER_READY);
		}

		k_sem_give(&data->sync);
	}

	/* Change the shutdown FROs' oscillating frequency in an attempt to
	 * prevent further locking on to the sampling clock frequency.
	 */
	if (src & TRNG_FRO_SHUTDOWN) {
		handle_shutdown_ovf();
	}
}

static int entropy_cc13xx_cc26xx_get_entropy_isr(const struct device *dev,
						 uint8_t *buf, uint16_t len,
						 uint32_t flags)
{
	struct entropy_cc13xx_cc26xx_data *data = dev->data;
	uint16_t cnt;
	uint16_t read = len;
	uint32_t src;
	uint32_t num[2];
	unsigned int key;

	key = irq_lock();
	cnt = ring_buf_get(&data->pool, buf, len);
	irq_unlock(key);

	if ((cnt == len) || ((flags & ENTROPY_BUSYWAIT) == 0U)) {
		read = cnt;
	} else {
		buf += cnt;
		len -= cnt;

		/* Allowed to busy-wait. We should use a polling approach */
		while (len) {
			key = irq_lock();

			src = TRNGStatusGet();
			if (src & TRNG_NUMBER_READY) {
				/*
				 * This function acknowledges the ready
				 * status
				 */
				num[1] = TRNGNumberGet(TRNG_HI_WORD);
				num[0] = TRNGNumberGet(TRNG_LOW_WORD);

				ring_buf_put(&data->pool, (uint8_t *)num,
					sizeof(num));
			}

			/*
			 * If interrupts were enabled during busy wait, this
			 * would allow us to pick up anything that has been put
			 * in by the ISR as well.
			 */
			cnt = ring_buf_get(&data->pool, buf, len);

			if (src & TRNG_FRO_SHUTDOWN) {
				handle_shutdown_ovf();
			}

			irq_unlock(key);

			if (cnt) {
				buf += cnt;
				len -= cnt;
			} else {
				k_busy_wait(US_PER_SAMPLE);
			}
		}

	}

	return read;
}

#ifdef CONFIG_PM
/*
 *  ======== post_notify_fxn ========
 *  Called by Power module when waking up the CPU from Standby. The TRNG needs
 *  to be reconfigured afterwards, unless Zephyr's device PM turned it off, in
 *  which case it'd be responsible for turning it back on and reconfiguring it.
 */
static int post_notify_fxn(unsigned int eventType, uintptr_t eventArg,
	uintptr_t clientArg)
{
	const struct device *dev = (const struct device *)clientArg;
	int ret = Power_NOTIFYDONE;
	int16_t res_id;

	/* Reconfigure the hardware if returning from sleep */
	if (eventType == PowerCC26XX_AWAKE_STANDBY) {
		res_id = PowerCC26XX_PERIPH_TRNG;

		if (Power_getDependencyCount(res_id) != 0) {
			/* Reconfigure and enable TRNG only if powered */
			start_trng(dev->data);
		}
	}

	return (ret);
}
#endif

#ifdef CONFIG_PM_DEVICE
static int entropy_cc13xx_cc26xx_pm_action(const struct device *dev,
					   enum pm_device_action action)
{
	struct entropy_cc13xx_cc26xx_data *data = dev->data;

	switch (action) {
	case PM_DEVICE_ACTION_RESUME:
		Power_setDependency(PowerCC26XX_PERIPH_TRNG);
		start_trng(data);
		break;
	case PM_DEVICE_ACTION_SUSPEND:
		stop_trng(data);
		Power_releaseDependency(PowerCC26XX_PERIPH_TRNG);
		break;
	default:
		return -ENOTSUP;
	}

	return 0;
}
#endif /* CONFIG_PM_DEVICE */

static int entropy_cc13xx_cc26xx_init(const struct device *dev)
{
	struct entropy_cc13xx_cc26xx_data *data = dev->data;

	/* Initialize driver data */
	ring_buf_init(&data->pool, sizeof(data->data), data->data);

#if defined(CONFIG_PM)
	Power_setDependency(PowerCC26XX_PERIPH_TRNG);
	/* Stay out of standby until buffer is filled with entropy */
	pm_policy_state_lock_get(PM_STATE_STANDBY, PM_ALL_SUBSTATES);
	data->constrained = true;
	/* Register notification function */
	Power_registerNotify(&data->post_notify,
		PowerCC26XX_AWAKE_STANDBY,
		post_notify_fxn, (uintptr_t)dev);
#else
	/* Power TRNG domain */
	PRCMPowerDomainOn(PRCM_DOMAIN_PERIPH);

	/* Enable TRNG peripheral clocks */
	PRCMPeripheralRunEnable(PRCM_PERIPH_TRNG);
	/* Enabled the TRNG while in sleep mode to keep the entropy pool full. After
	 * the pool is full the TRNG will enter idle mode when random numbers are no
	 * longer being read. */
	PRCMPeripheralSleepEnable(PRCM_PERIPH_TRNG);
	PRCMPeripheralDeepSleepEnable(PRCM_PERIPH_TRNG);


	/* Load PRCM settings */
	PRCMLoadSet();
	while (!PRCMLoadGet()) {
		continue;
	}

	/* Peripherals should not be accessed until power domain is on. */
	while (PRCMPowerDomainsAllOn(PRCM_DOMAIN_PERIPH) !=
	       PRCM_DOMAIN_POWER_ON) {
		continue;
	}
#endif

	start_trng(data);

	IRQ_CONNECT(DT_INST_IRQN(0),
		    DT_INST_IRQ(0, priority),
		    entropy_cc13xx_cc26xx_isr,
		    DEVICE_DT_INST_GET(0), 0);
	irq_enable(DT_INST_IRQN(0));

	return 0;
}

static const struct entropy_driver_api entropy_cc13xx_cc26xx_driver_api = {
	.get_entropy = entropy_cc13xx_cc26xx_get_entropy,
	.get_entropy_isr = entropy_cc13xx_cc26xx_get_entropy_isr,
};

static struct entropy_cc13xx_cc26xx_data entropy_cc13xx_cc26xx_data = {
	.lock = Z_SEM_INITIALIZER(entropy_cc13xx_cc26xx_data.lock, 1, 1),
	.sync = Z_SEM_INITIALIZER(entropy_cc13xx_cc26xx_data.sync, 0, 1),
};

PM_DEVICE_DT_INST_DEFINE(0, entropy_cc13xx_cc26xx_pm_action);

DEVICE_DT_INST_DEFINE(0,
		entropy_cc13xx_cc26xx_init,
		PM_DEVICE_DT_INST_GET(0),
		&entropy_cc13xx_cc26xx_data, NULL,
		PRE_KERNEL_1, CONFIG_ENTROPY_INIT_PRIORITY,
		&entropy_cc13xx_cc26xx_driver_api);
