/*
 * Copyright (c) 2020 Lemonbeat GmbH
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT silabs_gecko_trng

 #include <zephyr/drivers/entropy.h>
 #include <string.h>
 #include "soc.h"
 #include "em_cmu.h"

#if defined(CONFIG_CRYPTO_ACC_GECKO_TRNG)

/*
 * Select the correct Crypto ACC FIFO memory base address.
 *
 * Problem: Gecko SDK doesn't provide macros that check if SL_TRUSTZONE is used or not for Crypto
 * ACC RNGOUT FIFO memory base address, like it does for register address definitions.
 *
 * Solution: Check which register base address is used for the Crypto ACC peripheral and select an
 * appropriate FIFO memory base address.
 */
#if (CRYPTOACC_BASE == CRYPTOACC_S_BASE)
#define S2_FIFO_BASE CRYPTOACC_RNGOUT_FIFO_S_MEM_BASE
#else
#define S2_FIFO_BASE CRYPTOACC_RNGOUT_FIFO_MEM_BASE
#endif

/**
 * Series 2 SoCs have different TRNG register definitions
 */
#if defined(_SILICON_LABS_32B_SERIES_2_CONFIG_2)	/* xG22 */
#define S2_FIFO_LEVEL	(CRYPTOACC_RNGCTRL->FIFOLEVEL)
#define S2_CTRL		(CRYPTOACC_RNGCTRL->RNGCTRL)
#define S2_CTRL_ENABLE	(CRYPTOACC_RNGCTRL_ENABLE)
#elif defined(_SILICON_LABS_32B_SERIES_2_CONFIG_7)	/* xG27 */
#define S2_FIFO_LEVEL	(CRYPTOACC->NDRNG_FIFOLEVEL)
#define S2_CTRL		(CRYPTOACC->NDRNG_CONTROL)
#define S2_CTRL_ENABLE	(CRYPTOACC_NDRNG_CONTROL_ENABLE)
#else	/* _SILICON_LABS_32B_SERIES_2_CONFIG_* */
#error "Building for unsupported Series 2 SoC"
#endif	/* _SILICON_LABS_32B_SERIES_2_CONFIG_* */
#endif	/* CONFIG_CRYPTO_ACC_GECKO_TRNG */

static int entropy_gecko_trng_init(const struct device *dev)
{
	/* Enable the TRNG0 clock. */
#ifndef CONFIG_CRYPTO_ACC_GECKO_TRNG
	CMU_ClockEnable(cmuClock_TRNG0, true);

	/* Enable TRNG0. */
	TRNG0->CONTROL = TRNG_CONTROL_ENABLE;
#else
	/* Enable the CRYPTO ACC clock. */
	CMU_ClockEnable(cmuClock_CRYPTOACC, true);

	/* Enable TRNG */
	S2_CTRL |= S2_CTRL_ENABLE;
#endif

	return 0;
}

static void entropy_gecko_trng_read(uint8_t *output, size_t len)
{
#ifndef CONFIG_CRYPTO_ACC_GECKO_TRNG
	uint32_t tmp;
	uint32_t *data = (uint32_t *) output;

	/* Read known good available data. */
	while (len >= 4) {
		*data++ = TRNG0->FIFO;
		len -= 4;
	}
	if (len > 0) {
		/* Handle the case where len is not a multiple of 4
		 * and FIFO data is available.
		 */
		tmp = TRNG0->FIFO;
		memcpy(data, (const uint8_t *) &tmp, len);
	}
#else
	memcpy(output, ((const uint8_t *) S2_FIFO_BASE), len);
#endif
}

static int entropy_gecko_trng_get_entropy(const struct device *dev,
					  uint8_t *buffer,
					  uint16_t length)
{
	size_t count = 0;
	size_t available;

	ARG_UNUSED(dev);

#ifdef CONFIG_CRYPTO_ACC_GECKO_TRNG
	entropy_gecko_trng_init(dev);
#endif

	while (length) {
#ifndef CONFIG_CRYPTO_ACC_GECKO_TRNG
		available = TRNG0->FIFOLEVEL * 4;
#else
		available = S2_FIFO_LEVEL * 4;
#endif
		count = SL_MIN(length, available);
		entropy_gecko_trng_read(buffer, count);
		buffer += count;
		length -= count;
	}

	return 0;
}

static int entropy_gecko_trng_get_entropy_isr(const struct device *dev,
					      uint8_t *buf,
					      uint16_t len, uint32_t flags)
{

	if ((flags & ENTROPY_BUSYWAIT) == 0U) {

		/* No busy wait; return whatever data is available. */
		size_t count;
#ifndef CONFIG_CRYPTO_ACC_GECKO_TRNG
		size_t available = TRNG0->FIFOLEVEL * 4;
#else
		CMU_ClockEnable(cmuClock_CRYPTOACC, true);
		size_t available = S2_FIFO_LEVEL * 4;
#endif

		if (available == 0) {
			return -ENODATA;
		}
		count = SL_MIN(len, available);
		entropy_gecko_trng_read(buf, count);
		return count;

	} else {
		/* Allowed to busy-wait */
		int ret = entropy_gecko_trng_get_entropy(dev, buf, len);

		if (ret == 0) {
			/* Data retrieved successfully. */
			return len;
		}
		return ret;
	}
}

static DEVICE_API(entropy, entropy_gecko_trng_api_funcs) = {
	.get_entropy = entropy_gecko_trng_get_entropy,
	.get_entropy_isr = entropy_gecko_trng_get_entropy_isr
};

DEVICE_DT_INST_DEFINE(0,
			entropy_gecko_trng_init, NULL,
			NULL, NULL,
			PRE_KERNEL_1, CONFIG_ENTROPY_INIT_PRIORITY,
			&entropy_gecko_trng_api_funcs);
