/*
 * Copyright (c) 2019 Derek Hageman <hageman@inthat.cloud>
 * Copyright (c) 2024 Gerson Fernando Budke <nandojve@gmail.com>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT atmel_sam0_tc32

#include <zephyr/drivers/counter.h>
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/device.h>
#include <zephyr/irq.h>
#include <soc.h>

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

/* clang-format off */

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

struct counter_sam0_tc32_data {
	counter_top_callback_t top_cb;
	void *top_user_data;

	struct counter_sam0_tc32_ch_data ch;
};

struct counter_sam0_tc32_config {
	struct counter_config_info info;
	TcCount32 *regs;
	const struct pinctrl_dev_config *pcfg;
	volatile uint32_t *mclk;
	uint32_t mclk_mask;
	uint32_t gclk_gen;
	uint16_t gclk_id;
	uint16_t prescaler;
	void (*irq_config_func)(const struct device *dev);
};

static void wait_synchronization(TcCount32 *regs)
{
#if defined(TC_SYNCBUSY_MASK)
	/* SYNCBUSY is a register */
	while ((regs->SYNCBUSY.reg & TC_SYNCBUSY_MASK) != 0) {
	}
#elif defined(TC_STATUS_SYNCBUSY)
	/* SYNCBUSY is a bit */
	while ((regs->STATUS.reg & TC_STATUS_SYNCBUSY) != 0) {
	}
#else
#error Unsupported device
#endif
}

static void read_synchronize_count(TcCount32 *regs)
{
#if defined(TC_READREQ_RREQ)
	regs->READREQ.reg = TC_READREQ_RREQ |
			    TC_READREQ_ADDR(TC_COUNT32_COUNT_OFFSET);
	wait_synchronization(regs);
#elif defined(TC_CTRLBSET_CMD_READSYNC)
	regs->CTRLBSET.reg = TC_CTRLBSET_CMD_READSYNC;
	wait_synchronization(regs);
#else
	ARG_UNUSED(regs);
#endif
}

static int counter_sam0_tc32_start(const struct device *dev)
{
	const struct counter_sam0_tc32_config *const cfg = dev->config;
	TcCount32 *tc = cfg->regs;

	/*
	 * This will also reset the current counter value if it's
	 * already running.
	 */
	tc->CTRLBSET.reg = TC_CTRLBSET_CMD_RETRIGGER;
	wait_synchronization(tc);
	return 0;
}

static int counter_sam0_tc32_stop(const struct device *dev)
{
	const struct counter_sam0_tc32_config *const cfg = dev->config;
	TcCount32 *tc = cfg->regs;

	/*
	 * The older (pre SAML1x) manuals claim the counter retains its
	 * value on stop, but this doesn't actually seem to happen.
	 * The SAML1x manual says it resets, which is what the SAMD21
	 * counter actually appears to do.
	 */
	tc->CTRLBSET.reg = TC_CTRLBSET_CMD_STOP;
	wait_synchronization(tc);
	return 0;
}

static uint32_t counter_sam0_tc32_read(const struct device *dev)
{
	const struct counter_sam0_tc32_config *const cfg = dev->config;
	TcCount32 *tc = cfg->regs;

	read_synchronize_count(tc);
	return tc->COUNT.reg;
}

static int counter_sam0_tc32_get_value(const struct device *dev,
				       uint32_t *ticks)
{
	*ticks = counter_sam0_tc32_read(dev);
	return 0;
}

static void counter_sam0_tc32_relative_alarm(const struct device *dev,
					     uint32_t ticks)
{
	struct counter_sam0_tc32_data *data = dev->data;
	const struct counter_sam0_tc32_config *const cfg = dev->config;
	TcCount32 *tc = cfg->regs;
	uint32_t before;
	uint32_t target;
	uint32_t after;
	uint32_t max;

	read_synchronize_count(tc);
	before = tc->COUNT.reg;

	target = before + ticks;
	max = tc->CC[0].reg;
	if (target > max) {
		target -= max;
	}

	tc->CC[1].reg = target;
	wait_synchronization(tc);
	tc->INTFLAG.reg = TC_INTFLAG_MC1;

	read_synchronize_count(tc);
	after = tc->COUNT.reg;

	/* Pending now, so no further checking required */
	if (tc->INTFLAG.bit.MC1) {
		goto out_future;
	}

	/*
	 * Check if we missed the interrupt and call the handler
	 * immediately if we did.
	 */
	if (after < target) {
		goto out_future;
	}

	/* Check wrapped */
	if (target < before && after >= before) {
		goto out_future;
	}

	counter_alarm_callback_t cb = data->ch.callback;

	tc->INTENCLR.reg = TC_INTENCLR_MC1;
	tc->INTFLAG.reg = TC_INTFLAG_MC1;
	data->ch.callback = NULL;

	cb(dev, 0, target, data->ch.user_data);

	return;

out_future:
	tc->INTENSET.reg = TC_INTFLAG_MC1;
}

static int counter_sam0_tc32_set_alarm(const struct device *dev,
				       uint8_t chan_id,
				       const struct counter_alarm_cfg *alarm_cfg)
{
	struct counter_sam0_tc32_data *data = dev->data;
	const struct counter_sam0_tc32_config *const cfg = dev->config;
	TcCount32 *tc = cfg->regs;

	ARG_UNUSED(chan_id);

	if (alarm_cfg->ticks > tc->CC[0].reg) {
		return -EINVAL;
	}

	unsigned int key = irq_lock();

	if (data->ch.callback) {
		irq_unlock(key);
		return -EBUSY;
	}

	data->ch.callback = alarm_cfg->callback;
	data->ch.user_data = alarm_cfg->user_data;

	if ((alarm_cfg->flags & COUNTER_ALARM_CFG_ABSOLUTE) != 0) {
		tc->CC[1].reg = alarm_cfg->ticks;
		wait_synchronization(tc);
		tc->INTFLAG.reg = TC_INTFLAG_MC1;
		tc->INTENSET.reg = TC_INTFLAG_MC1;
	} else {
		counter_sam0_tc32_relative_alarm(dev, alarm_cfg->ticks);
	}

	irq_unlock(key);

	return 0;
}

static int counter_sam0_tc32_cancel_alarm(const struct device *dev,
					  uint8_t chan_id)
{
	struct counter_sam0_tc32_data *data = dev->data;
	const struct counter_sam0_tc32_config *const cfg = dev->config;
	TcCount32 *tc = cfg->regs;

	unsigned int key = irq_lock();

	ARG_UNUSED(chan_id);

	data->ch.callback = NULL;
	tc->INTENCLR.reg = TC_INTENCLR_MC1;
	tc->INTFLAG.reg = TC_INTFLAG_MC1;

	irq_unlock(key);
	return 0;
}

static int counter_sam0_tc32_set_top_value(const struct device *dev,
					   const struct counter_top_cfg *top_cfg)
{
	struct counter_sam0_tc32_data *data = dev->data;
	const struct counter_sam0_tc32_config *const cfg = dev->config;
	TcCount32 *tc = cfg->regs;
	int err = 0;
	unsigned int key = irq_lock();

	if (data->ch.callback) {
		irq_unlock(key);
		return -EBUSY;
	}

	if (top_cfg->callback) {
		data->top_cb = top_cfg->callback;
		data->top_user_data = top_cfg->user_data;
		tc->INTENSET.reg = TC_INTFLAG_MC0;
	} else {
		tc->INTENCLR.reg = TC_INTFLAG_MC0;
	}

	tc->CC[0].reg = top_cfg->ticks;

	if (top_cfg->flags & COUNTER_TOP_CFG_DONT_RESET) {
		/*
		 * Top trigger is on equality of the rising edge only, so
		 * manually reset it if the counter has missed the new top.
		 */
		if (counter_sam0_tc32_read(dev) >= top_cfg->ticks) {
			err = -ETIME;
			if (top_cfg->flags & COUNTER_TOP_CFG_RESET_WHEN_LATE) {
				tc->CTRLBSET.reg = TC_CTRLBSET_CMD_RETRIGGER;
			}
		}
	} else {
		tc->CTRLBSET.reg = TC_CTRLBSET_CMD_RETRIGGER;
	}

	wait_synchronization(tc);

	tc->INTFLAG.reg = TC_INTFLAG_MC0;
	irq_unlock(key);
	return err;
}

static uint32_t counter_sam0_tc32_get_pending_int(const struct device *dev)
{
	const struct counter_sam0_tc32_config *const cfg = dev->config;
	TcCount32 *tc = cfg->regs;

	return tc->INTFLAG.reg & (TC_INTFLAG_MC0 | TC_INTFLAG_MC1);
}

static uint32_t counter_sam0_tc32_get_top_value(const struct device *dev)
{
	const struct counter_sam0_tc32_config *const cfg = dev->config;
	TcCount32 *tc = cfg->regs;

	/*
	 * Unsync read is safe here because we're not using
	 * capture mode, so things are only set from the CPU
	 * end.
	 */
	return tc->CC[0].reg;
}

static void counter_sam0_tc32_isr(const struct device *dev)
{
	struct counter_sam0_tc32_data *data = dev->data;
	const struct counter_sam0_tc32_config *const cfg = dev->config;
	TcCount32 *tc = cfg->regs;
	uint8_t status = tc->INTFLAG.reg;

	/* Acknowledge all interrupts */
	tc->INTFLAG.reg = status;

	if (status & TC_INTFLAG_MC1) {
		if (data->ch.callback) {
			counter_alarm_callback_t cb = data->ch.callback;

			tc->INTENCLR.reg = TC_INTENCLR_MC1;
			data->ch.callback = NULL;

			cb(dev, 0, tc->CC[1].reg, data->ch.user_data);
		}
	}

	if (status & TC_INTFLAG_MC0) {
		if (data->top_cb) {
			data->top_cb(dev, data->top_user_data);
		}
	}
}

static int counter_sam0_tc32_initialize(const struct device *dev)
{
	const struct counter_sam0_tc32_config *const cfg = dev->config;
	TcCount32 *tc = cfg->regs;
	int retval;

	*cfg->mclk |= cfg->mclk_mask;

#ifdef MCLK
	GCLK->PCHCTRL[cfg->gclk_id].reg = GCLK_PCHCTRL_CHEN
					| GCLK_PCHCTRL_GEN(cfg->gclk_gen);
#else
	GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN
			  | GCLK_CLKCTRL_GEN(cfg->gclk_gen)
			  | GCLK_CLKCTRL_ID(cfg->gclk_id);
#endif

	/*
	 * In 32 bit mode, NFRQ mode always uses MAX as the counter top, so
	 * use MFRQ mode which uses CC0 as the top at the expense of only
	 * having CC1 available for alarms.
	 */
	tc->CTRLA.reg = TC_CTRLA_MODE_COUNT32 |
#ifdef TC_CTRLA_WAVEGEN_MFRQ
			TC_CTRLA_WAVEGEN_MFRQ |
#endif
			cfg->prescaler;
	wait_synchronization(tc);

#ifdef TC_WAVE_WAVEGEN_MFRQ
	tc->WAVE.reg = TC_WAVE_WAVEGEN_MFRQ;
#endif

	/* Disable all interrupts */
	tc->INTENCLR.reg = TC_INTENCLR_MASK;

	retval = pinctrl_apply_state(cfg->pcfg, PINCTRL_STATE_DEFAULT);
	if (retval < 0) {
		return retval;
	}

	/* Set the initial top as the maximum */
	tc->CC[0].reg = UINT32_MAX;

	cfg->irq_config_func(dev);

	tc->CTRLA.bit.ENABLE = 1;
	wait_synchronization(tc);

	/* Stop the counter initially */
	tc->CTRLBSET.reg = TC_CTRLBSET_CMD_STOP;
	wait_synchronization(tc);

	return 0;
}

static DEVICE_API(counter, counter_sam0_tc32_driver_api) = {
	.start = counter_sam0_tc32_start,
	.stop = counter_sam0_tc32_stop,
	.get_value = counter_sam0_tc32_get_value,
	.set_alarm = counter_sam0_tc32_set_alarm,
	.cancel_alarm = counter_sam0_tc32_cancel_alarm,
	.set_top_value = counter_sam0_tc32_set_top_value,
	.get_pending_int = counter_sam0_tc32_get_pending_int,
	.get_top_value = counter_sam0_tc32_get_top_value,
};


#define ASSIGNED_CLOCKS_CELL_BY_NAME						\
	ATMEL_SAM0_DT_INST_ASSIGNED_CLOCKS_CELL_BY_NAME

#define SAM0_TC32_PRESCALER(n)							\
	COND_CODE_1(DT_INST_NODE_HAS_PROP(n, prescaler),			\
		    (DT_INST_PROP(n, prescaler)), (1))

#define COUNTER_SAM0_TC32_DEVICE(n)						\
	PINCTRL_DT_INST_DEFINE(n);						\
	static void counter_sam0_tc32_config_##n(const struct device *dev);	\
	static const struct counter_sam0_tc32_config				\
										\
	counter_sam0_tc32_dev_config_##n = {					\
		.info = {							\
			.max_top_value = UINT32_MAX,				\
			.freq = SOC_ATMEL_SAM0_GCLK0_FREQ_HZ /			\
				SAM0_TC32_PRESCALER(n),				\
			.flags = COUNTER_CONFIG_INFO_COUNT_UP,			\
			.channels = 1						\
		},								\
		.regs = (TcCount32 *)DT_INST_REG_ADDR(n),			\
		.gclk_gen = ASSIGNED_CLOCKS_CELL_BY_NAME(n, gclk, gen),		\
		.gclk_id = DT_INST_CLOCKS_CELL_BY_NAME(n, gclk, id),		\
		.mclk = ATMEL_SAM0_DT_INST_MCLK_PM_REG_ADDR_OFFSET(n),		\
		.mclk_mask = ATMEL_SAM0_DT_INST_MCLK_PM_PERIPH_MASK(n, bit),	\
		.prescaler = UTIL_CAT(TC_CTRLA_PRESCALER_DIV,			\
				      SAM0_TC32_PRESCALER(n)),			\
		.irq_config_func = &counter_sam0_tc32_config_##n,		\
		.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(n),			\
	};									\
										\
	static struct counter_sam0_tc32_data counter_sam0_tc32_dev_data_##n;	\
										\
	DEVICE_DT_INST_DEFINE(n,						\
			    &counter_sam0_tc32_initialize,			\
			    NULL,						\
			    &counter_sam0_tc32_dev_data_##n,			\
			    &counter_sam0_tc32_dev_config_##n,			\
			    PRE_KERNEL_1,					\
			    CONFIG_COUNTER_INIT_PRIORITY,			\
			    &counter_sam0_tc32_driver_api);			\
										\
	static void counter_sam0_tc32_config_##n(const struct device *dev)	\
	{									\
		IRQ_CONNECT(DT_INST_IRQN(n),					\
			    DT_INST_IRQ(n, priority),				\
			    counter_sam0_tc32_isr,				\
			    DEVICE_DT_INST_GET(n), 0);				\
		irq_enable(DT_INST_IRQN(n));					\
	}

DT_INST_FOREACH_STATUS_OKAY(COUNTER_SAM0_TC32_DEVICE)

/* clang-format on */
