/*
 * Copyright (c) 2019, Piotr Mienkowski
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT silabs_gecko_rtcc

#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <soc.h>
#include <em_cmu.h>
#include <em_rtcc.h>
#include <zephyr/drivers/counter.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(counter_gecko, CONFIG_COUNTER_LOG_LEVEL);

#define RTCC_MAX_VALUE       (_RTCC_CNT_MASK)
#define RTCC_ALARM_NUM        2

struct counter_gecko_config {
	struct counter_config_info info;
	void (*irq_config)(void);
	uint32_t prescaler;
};

struct counter_gecko_alarm_data {
	counter_alarm_callback_t callback;
	void *user_data;
};

struct counter_gecko_data {
	struct counter_gecko_alarm_data alarm[RTCC_ALARM_NUM];
	counter_top_callback_t top_callback;
	void *top_user_data;
};

#ifdef CONFIG_SOC_GECKO_HAS_ERRATA_RTCC_E201
#define ERRATA_RTCC_E201_MESSAGE \
	"Errata RTCC_E201: In case RTCC prescaler != 1 the module does not " \
	"reset the counter value on CCV1 compare."
#endif

/* Map channel id to CC channel provided by the RTCC module */
static uint8_t chan_id2cc_idx(uint8_t chan_id)
{
	uint8_t cc_idx;

	switch (chan_id) {
	case 0:
		cc_idx = 2;
		break;
	default:
		cc_idx = 0;
		break;
	}
	return cc_idx;
}

static int counter_gecko_start(const struct device *dev)
{
	ARG_UNUSED(dev);

	RTCC_Enable(true);

	return 0;
}

static int counter_gecko_stop(const struct device *dev)
{
	ARG_UNUSED(dev);

	RTCC_Enable(false);

	return 0;
}

static int counter_gecko_get_value(const struct device *dev, uint32_t *ticks)
{
	ARG_UNUSED(dev);

	*ticks = RTCC_CounterGet();
	return 0;
}

static int counter_gecko_set_top_value(const struct device *dev,
				       const struct counter_top_cfg *cfg)
{
	struct counter_gecko_data *const dev_data = dev->data;
	uint32_t ticks;
	uint32_t flags;
	int err = 0;

#ifdef CONFIG_SOC_GECKO_HAS_ERRATA_RTCC_E201
	const struct counter_gecko_config *const dev_cfg = dev->config;

	if (dev_cfg->prescaler != 1) {
		LOG_ERR(ERRATA_RTCC_E201_MESSAGE);
		return -EINVAL;
	}
#endif

	/* Counter top value can only be changed when all alarms are disabled */
	for (int i = 0; i < RTCC_ALARM_NUM; i++) {
		if (dev_data->alarm[i].callback) {
			return -EBUSY;
		}
	}

	RTCC_IntClear(RTCC_IF_CC1);

	dev_data->top_callback = cfg->callback;
	dev_data->top_user_data = cfg->user_data;
	ticks = cfg->ticks;
	flags = cfg->flags;

	if (!(flags & COUNTER_TOP_CFG_DONT_RESET)) {
		RTCC_CounterSet(0);
	}

	RTCC_ChannelCCVSet(1, ticks);

	LOG_DBG("set top value: %u", ticks);

	if ((flags & COUNTER_TOP_CFG_DONT_RESET) &&
		RTCC_CounterGet() > ticks) {
		err = -ETIME;
		if (flags & COUNTER_TOP_CFG_RESET_WHEN_LATE) {
			RTCC_CounterSet(0);
		}
	}

	/* Enable the compare interrupt */
	RTCC_IntEnable(RTCC_IF_CC1);

	return err;
}

static uint32_t counter_gecko_get_top_value(const struct device *dev)
{
	ARG_UNUSED(dev);

	return RTCC_ChannelCCVGet(1);
}

static int counter_gecko_set_alarm(const struct device *dev, uint8_t chan_id,
				   const struct counter_alarm_cfg *alarm_cfg)
{
	uint32_t count = RTCC_CounterGet();
	struct counter_gecko_data *const dev_data = dev->data;
	uint32_t top_value = counter_gecko_get_top_value(dev);
	uint32_t ccv;

	if ((top_value != 0) && (alarm_cfg->ticks > top_value)) {
		return -EINVAL;
	}
	if (dev_data->alarm[chan_id].callback != NULL) {
		return -EBUSY;
	}

	if ((alarm_cfg->flags & COUNTER_ALARM_CFG_ABSOLUTE) != 0) {
		ccv = alarm_cfg->ticks;
	} else {
		if (top_value == 0) {
			ccv = count + alarm_cfg->ticks;
		} else {
			uint64_t ccv64 = count + alarm_cfg->ticks;

			ccv = (uint32_t)(ccv64 % top_value);
		}
	}

	uint8_t cc_idx = chan_id2cc_idx(chan_id);

	RTCC_IntClear(RTCC_IF_CC0 << cc_idx);

	dev_data->alarm[chan_id].callback = alarm_cfg->callback;
	dev_data->alarm[chan_id].user_data = alarm_cfg->user_data;

	RTCC_ChannelCCVSet(cc_idx, ccv);

	LOG_DBG("set alarm: channel %u, count %u", chan_id, ccv);

	/* Enable the compare interrupt */
	RTCC_IntEnable(RTCC_IF_CC0 << cc_idx);

	return 0;
}

static int counter_gecko_cancel_alarm(const struct device *dev,
				      uint8_t chan_id)
{
	struct counter_gecko_data *const dev_data = dev->data;

	uint8_t cc_idx = chan_id2cc_idx(chan_id);

	/* Disable the compare interrupt */
	RTCC_IntDisable(RTCC_IF_CC0 << cc_idx);
	RTCC_IntClear(RTCC_IF_CC0 << cc_idx);

	dev_data->alarm[chan_id].callback = NULL;
	dev_data->alarm[chan_id].user_data = NULL;

	RTCC_ChannelCCVSet(cc_idx, 0);

	LOG_DBG("cancel alarm: channel %u", chan_id);

	return 0;
}

static uint32_t counter_gecko_get_pending_int(const struct device *dev)
{
	ARG_UNUSED(dev);

	return 0;
}

static int counter_gecko_init(const struct device *dev)
{
	const struct counter_gecko_config *const dev_cfg = dev->config;

	RTCC_Init_TypeDef rtcc_config = {
		false,                /* Don't start counting */
		false,                /* Disable RTC during debug halt. */
		false,                /* Don't wrap prescaler on CCV0 */
		true,                 /* Counter wrap on CCV1 */
#if defined(_SILICON_LABS_32B_SERIES_2)
		(RTCC_CntPresc_TypeDef)(31UL - __CLZ(dev_cfg->prescaler)),
#else
		(RTCC_CntPresc_TypeDef)CMU_DivToLog2(dev_cfg->prescaler),
#endif
		rtccCntTickPresc,     /* Count according to prescaler value */
#if defined(_RTCC_CTRL_BUMODETSEN_MASK)
		false,                /* Don't store RTCC counter value in
				       * RTCC_CCV2 upon backup mode entry.
				       */
#endif
#if defined(_RTCC_CTRL_OSCFDETEN_MASK)
		false,                /* Don't enable LFXO fail detection */
#endif
#if defined (_RTCC_CTRL_CNTMODE_MASK)
		rtccCntModeNormal,    /* Use RTCC in normal mode */
#endif
#if defined (_RTCC_CTRL_LYEARCORRDIS_MASK)
		false                 /* No leap year correction. */
#endif
	};

	RTCC_CCChConf_TypeDef rtcc_channel_config = {
		rtccCapComChModeCompare,    /* Use compare mode */
		rtccCompMatchOutActionPulse,/* Don't care */
		rtccPRSCh0,                 /* PRS is not used */
		rtccInEdgeNone,             /* Capture input is not used */
		rtccCompBaseCnt,            /* Compare with base CNT register */
#if defined (_RTCC_CC_CTRL_COMPMASK_MASK)
		0,                          /* Compare mask */
#endif
#if defined (_RTCC_CC_CTRL_DAYCC_MASK)
		rtccDayCompareModeMonth,    /* Don't care */
#endif
	};

#if defined(cmuClock_CORELE)
	/* Ensure LE modules are clocked. */
	CMU_ClockEnable(cmuClock_CORELE, true);
#endif

#if defined(CMU_LFECLKEN0_RTCC)
	/* Enable LFECLK in CMU (will also enable oscillator if not enabled). */
	CMU_ClockSelectSet(cmuClock_LFE, cmuSelect_LFXO);
#elif defined(_SILICON_LABS_32B_SERIES_2)
	CMU_ClockSelectSet(cmuClock_RTCC, cmuSelect_LFXO);
#else
	/* Enable LFACLK in CMU (will also enable oscillator if not enabled). */
	CMU_ClockSelectSet(cmuClock_LFA, cmuSelect_LFXO);
#endif

	/* Enable RTCC module clock */
	CMU_ClockEnable(cmuClock_RTCC, true);

	/* Initialize RTCC */
	RTCC_Init(&rtcc_config);

	/* Set up compare channels */
	RTCC_ChannelInit(0, &rtcc_channel_config);
	RTCC_ChannelInit(1, &rtcc_channel_config);
	RTCC_ChannelInit(2, &rtcc_channel_config);

	/* Disable module's internal interrupt sources */
	RTCC_IntDisable(_RTCC_IF_MASK);
	RTCC_IntClear(_RTCC_IF_MASK);

	/* Clear the counter */
	RTCC->CNT = 0;

	/* Configure & enable module interrupts */
	dev_cfg->irq_config();

	LOG_INF("Device %s initialized", dev->name);

	return 0;
}

static const struct counter_driver_api counter_gecko_driver_api = {
	.start = counter_gecko_start,
	.stop = counter_gecko_stop,
	.get_value = counter_gecko_get_value,
	.set_alarm = counter_gecko_set_alarm,
	.cancel_alarm = counter_gecko_cancel_alarm,
	.set_top_value = counter_gecko_set_top_value,
	.get_pending_int = counter_gecko_get_pending_int,
	.get_top_value = counter_gecko_get_top_value,
};

/* RTCC0 */

ISR_DIRECT_DECLARE(counter_gecko_isr_0)
{
	const struct device *const dev = DEVICE_DT_INST_GET(0);
	struct counter_gecko_data *const dev_data = dev->data;
	counter_alarm_callback_t alarm_callback;
	uint32_t count = RTCC_CounterGet();
	uint32_t flags = RTCC_IntGetEnabled();

	RTCC_IntClear(flags);

	if (flags & RTCC_IF_CC1) {
		if (dev_data->top_callback) {
			dev_data->top_callback(dev, dev_data->top_user_data);
		}
	}
	for (int i = 0; i < RTCC_ALARM_NUM; i++) {
		uint8_t cc_idx = chan_id2cc_idx(i);

		if (flags & (RTCC_IF_CC0 << cc_idx)) {
			if (dev_data->alarm[i].callback) {
				alarm_callback = dev_data->alarm[i].callback;
				dev_data->alarm[i].callback = NULL;
				alarm_callback(dev, i, count,
					       dev_data->alarm[i].user_data);
			}
		}
	}

	ISR_DIRECT_PM();

	return 1;
}

BUILD_ASSERT((DT_INST_PROP(0, prescaler) > 0U) &&
	     (DT_INST_PROP(0, prescaler) <= 32768U));

static void counter_gecko_0_irq_config(void)
{
	IRQ_DIRECT_CONNECT(DT_INST_IRQN(0),
			   DT_INST_IRQ(0, priority),
			   counter_gecko_isr_0, 0);
	irq_enable(DT_INST_IRQN(0));
}

static const struct counter_gecko_config counter_gecko_0_config = {
	.info = {
		.max_top_value = RTCC_MAX_VALUE,
		.freq = DT_INST_PROP(0, clock_frequency) /
			DT_INST_PROP(0, prescaler),
		.flags = COUNTER_CONFIG_INFO_COUNT_UP,
		.channels = RTCC_ALARM_NUM,
	},
	.irq_config = counter_gecko_0_irq_config,
	.prescaler = DT_INST_PROP(0, prescaler),
};

static struct counter_gecko_data counter_gecko_0_data;

DEVICE_DT_INST_DEFINE(0, counter_gecko_init, NULL,
	&counter_gecko_0_data, &counter_gecko_0_config,
	PRE_KERNEL_1, CONFIG_COUNTER_INIT_PRIORITY,
	&counter_gecko_driver_api);
