/*
 * Copyright (c) 2023 Intel Corporation.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT snps_dw_timers

#include <zephyr/device.h>
#include <zephyr/drivers/counter.h>
#include <zephyr/spinlock.h>
#include <zephyr/logging/log.h>
#include <zephyr/drivers/reset.h>
#include <zephyr/drivers/clock_control.h>

LOG_MODULE_REGISTER(dw_timer, CONFIG_COUNTER_LOG_LEVEL);

static int counter_dw_timer_get_value(const struct device *timer_dev, uint32_t *ticks);

/* DW APB timer register offsets */
#define LOADCOUNT_OFST           0x0
#define CURRENTVAL_OFST          0x4
#define CONTROLREG_OFST          0x8
#define EOI_OFST                 0xc
#define INTSTAT_OFST             0x10

/* free running mode value */
#define FREE_RUNNING_MODE_VAL 0xFFFFFFFFUL

/* DW APB timer control flags */
#define TIMER_CONTROL_ENABLE_BIT    0
#define TIMER_MODE_BIT              1
#define TIMER_INTR_MASK_BIT         2

/* DW APB timer modes */
#define USER_DEFINED_MODE       1
#define FREE_RUNNING_MODE       0

#define DEV_CFG(_dev) ((const struct counter_dw_timer_config *)(_dev)->config)
#define DEV_DATA(_dev) ((struct counter_dw_timer_drv_data *const)(_dev)->data)

/* Device Configuration */
struct counter_dw_timer_config {
	struct counter_config_info info;

	DEVICE_MMIO_NAMED_ROM(timer_mmio);

	/* clock frequency of timer */
	uint32_t freq;
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(clocks)
	/* clock controller dev instance */
	const struct device *clk_dev;
	/* identifier for timer to get clock freq from clk manager */
	clock_control_subsys_t clkid;
#endif

#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(resets)
	/* reset controller device configuration*/
	const struct reset_dt_spec reset;
#endif

	/* interrupt config function ptr */
	void (*irq_config)(void);
};

/* Driver data */
struct counter_dw_timer_drv_data {
	/* mmio address mapping info */
	DEVICE_MMIO_NAMED_RAM(timer_mmio);
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(clocks)
	/* clock frequency of timer */
	uint32_t freq;
#endif
	/* spin lock to protect user data */
	struct k_spinlock lock;
	/* top callback function */
	counter_top_callback_t top_cb;
	/* alarm callback function */
	counter_alarm_callback_t alarm_cb;
	/* private user data */
	void *prv_data;
};

static void counter_dw_timer_irq_handler(const struct device *timer_dev)
{
	uint32_t ticks = 0;
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(timer_dev, timer_mmio);
	struct counter_dw_timer_drv_data *const data = DEV_DATA(timer_dev);
	k_spinlock_key_t key;
	counter_alarm_callback_t alarm_cb = data->alarm_cb;

	/* read EOI register to clear interrupt flag */
	sys_read32(reg_base + EOI_OFST);

	counter_dw_timer_get_value(timer_dev, &ticks);

	key = k_spin_lock(&data->lock);

	/* In case of alarm, mask interrupt and disable the callback. User
	 * can configure the alarm in same context within callback function.
	 */
	if (data->alarm_cb) {
		sys_set_bit(reg_base + CONTROLREG_OFST, TIMER_INTR_MASK_BIT);

		data->alarm_cb = NULL;
		alarm_cb(timer_dev, 0, ticks, data->prv_data);

	} else if (data->top_cb) {
		data->top_cb(timer_dev, data->prv_data);
	}

	k_spin_unlock(&data->lock, key);
}

static int counter_dw_timer_start(const struct device *dev)
{
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(dev, timer_mmio);

	/* disable timer before starting in free-running mode */
	sys_clear_bit(reg_base + CONTROLREG_OFST, TIMER_CONTROL_ENABLE_BIT);

	/* starting timer in free running mode */
	sys_clear_bit(reg_base + CONTROLREG_OFST, TIMER_MODE_BIT);
	sys_set_bit(reg_base + CONTROLREG_OFST, TIMER_INTR_MASK_BIT);
	sys_write32(FREE_RUNNING_MODE_VAL, reg_base + LOADCOUNT_OFST);

	/* enable timer */
	sys_set_bit(reg_base + CONTROLREG_OFST, TIMER_CONTROL_ENABLE_BIT);
	return 0;
}

int counter_dw_timer_disable(const struct device *dev)
{
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(dev, timer_mmio);

	/* stop timer */
	sys_clear_bit(reg_base + CONTROLREG_OFST, TIMER_CONTROL_ENABLE_BIT);
	return 0;
}

static uint32_t counter_dw_timer_get_top_value(const struct device *timer_dev)
{
	uint32_t top_val = 0;
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(timer_dev, timer_mmio);

	/* get the current top value from load count register */
	top_val = sys_read32(reg_base + LOADCOUNT_OFST);

	return top_val;
}

static int counter_dw_timer_get_value(const struct device *timer_dev, uint32_t *ticks)
{
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(timer_dev, timer_mmio);

	/* current value of the current value register */
	*ticks = sys_read32(reg_base + CURRENTVAL_OFST);

	return 0;
}

static int counter_dw_timer_set_top_value(const struct device *timer_dev,
					const struct counter_top_cfg *top_cfg)
{
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(timer_dev, timer_mmio);
	struct counter_dw_timer_drv_data *const data = DEV_DATA(timer_dev);
	k_spinlock_key_t key;

	if (top_cfg == NULL) {
		LOG_ERR("Invalid top value configuration");
		return -EINVAL;
	}

	/* top value cannot be updated without reset */
	if (top_cfg->flags & COUNTER_TOP_CFG_DONT_RESET) {
		LOG_ERR("Updating top value without reset is not supported");
		return -ENOTSUP;
	}

	key = k_spin_lock(&data->lock);

	/* top value cannot be updated if the alarm is active */
	if (data->alarm_cb) {
		k_spin_unlock(&data->lock, key);
		LOG_ERR("Top value cannot be updated, alarm is active!");
		return -EBUSY;
	}

	if (!top_cfg->callback) {
		/* mask an interrupt if callback is not passed */
		sys_set_bit(reg_base + CONTROLREG_OFST, TIMER_INTR_MASK_BIT);
	} else {
		/* unmask interrupt if callback is passed */
		sys_clear_bit(reg_base + CONTROLREG_OFST, TIMER_INTR_MASK_BIT);
	}

	data->top_cb = top_cfg->callback;
	data->prv_data = top_cfg->user_data;

	/* top value can be loaded only when timer is stopped and re-enabled */
	sys_clear_bit(reg_base + CONTROLREG_OFST, TIMER_CONTROL_ENABLE_BIT);

	/* configuring timer in user-defined mode */
	sys_set_bit(reg_base + CONTROLREG_OFST, TIMER_MODE_BIT);

	/* set new top value */
	sys_write32(top_cfg->ticks, reg_base + LOADCOUNT_OFST);
	sys_set_bit(reg_base + CONTROLREG_OFST, TIMER_CONTROL_ENABLE_BIT);

	k_spin_unlock(&data->lock, key);

	return 0;
}

static int counter_dw_timer_set_alarm(const struct device *timer_dev, uint8_t chan_id,
				 const struct counter_alarm_cfg *alarm_cfg)
{
	ARG_UNUSED(chan_id);
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(timer_dev, timer_mmio);
	struct counter_dw_timer_drv_data *const data = DEV_DATA(timer_dev);
	k_spinlock_key_t key;

	if (alarm_cfg == NULL) {
		LOG_ERR("Invalid alarm configuration");
		return -EINVAL;
	}

	/* Alarm callback is mandatory */
	if (!alarm_cfg->callback) {
		LOG_ERR("Alarm callback function cannot be null");
		return -EINVAL;
	}

	/* absolute alarm is not supported as interrupts are triggered
	 * only when the counter reaches 0(downcounter)
	 */
	if (alarm_cfg->flags & COUNTER_ALARM_CFG_ABSOLUTE) {
		LOG_ERR("Absolute alarm is not supported");
		return -ENOTSUP;
	}

	key = k_spin_lock(&data->lock);

	/* check if alarm is already active */
	if (data->alarm_cb != NULL) {
		LOG_ERR("Alarm is already active\n");
		k_spin_unlock(&data->lock, key);
		return -EBUSY;
	}

	data->alarm_cb = alarm_cfg->callback;
	data->prv_data = alarm_cfg->user_data;

	sys_clear_bit(reg_base + CONTROLREG_OFST, TIMER_CONTROL_ENABLE_BIT);

	/* start timer in user-defined mode */
	sys_set_bit(reg_base + CONTROLREG_OFST, TIMER_MODE_BIT);
	sys_clear_bit(reg_base + CONTROLREG_OFST, TIMER_INTR_MASK_BIT);

	sys_write32(alarm_cfg->ticks, reg_base + LOADCOUNT_OFST);
	sys_set_bit(reg_base + CONTROLREG_OFST, TIMER_CONTROL_ENABLE_BIT);

	k_spin_unlock(&data->lock, key);

	return 0;
}

static int counter_dw_timer_cancel_alarm(const struct device *timer_dev, uint8_t chan_id)
{
	ARG_UNUSED(chan_id);
	uintptr_t reg_base = DEVICE_MMIO_NAMED_GET(timer_dev, timer_mmio);
	struct counter_dw_timer_drv_data *const data = DEV_DATA(timer_dev);
	k_spinlock_key_t key;

	key = k_spin_lock(&data->lock);

	sys_write32(0, reg_base + CONTROLREG_OFST);

	data->alarm_cb = NULL;
	data->prv_data = NULL;

	k_spin_unlock(&data->lock, key);

	return 0;
}

uint32_t counter_dw_timer_get_freq(const struct device *timer_dev)
{
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(clocks)
	struct counter_dw_timer_drv_data *const data = DEV_DATA(timer_dev);

	return data->freq;
#else
	const struct counter_dw_timer_config *config = DEV_CFG(timer_dev);

	return config->freq;
#endif
}

static const struct counter_driver_api dw_timer_driver_api = {
	.start = counter_dw_timer_start,
	.stop = counter_dw_timer_disable,
	.get_value = counter_dw_timer_get_value,
	.set_top_value = counter_dw_timer_set_top_value,
	.get_top_value = counter_dw_timer_get_top_value,
	.set_alarm = counter_dw_timer_set_alarm,
	.cancel_alarm = counter_dw_timer_cancel_alarm,
	.get_freq = counter_dw_timer_get_freq,
};

static int counter_dw_timer_init(const struct device *timer_dev)
{
	DEVICE_MMIO_NAMED_MAP(timer_dev, timer_mmio, K_MEM_CACHE_NONE);
	const struct counter_dw_timer_config *timer_config = DEV_CFG(timer_dev);
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(clocks) || DT_ANY_INST_HAS_PROP_STATUS_OKAY(resets)
	int ret;
#endif

	/*
	 * get clock rate from clock_frequency property if valid,
	 * otherwise, get clock rate from clock manager
	 */
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(clocks)
	struct counter_dw_timer_drv_data *const data = DEV_DATA(timer_dev);

	if (!device_is_ready(timer_config->clk_dev)) {
		LOG_ERR("clock controller device not ready");
		return -ENODEV;
	}
	ret = clock_control_get_rate(timer_config->clk_dev,
					timer_config->clkid, &data->freq);
	if (ret != 0) {
		LOG_ERR("Unable to get clock rate: err:%d", ret);
		return ret;
	}
#endif

	/* Reset timer only if reset controller driver is supported */
#if DT_ANY_INST_HAS_PROP_STATUS_OKAY(resets)
	if (timer_config->reset.dev != NULL) {
		if (!device_is_ready(timer_config->reset.dev)) {
			LOG_ERR("Reset controller device not ready");
			return -ENODEV;
		}

		ret = reset_line_toggle(timer_config->reset.dev, timer_config->reset.id);
		if (ret != 0) {
			LOG_ERR("Timer reset failed");
			return ret;
		}
	}
#endif

	timer_config->irq_config();

	return 0;
}

#define DW_SNPS_TIMER_CLOCK_RATE_INIT(inst)							\
	COND_CODE_1(DT_INST_NODE_HAS_PROP(inst, clock_frequency),				\
		(										\
			.freq = DT_INST_PROP(inst, clock_frequency),				\
		),										\
		(										\
			.freq = 0,								\
			.clk_dev = DEVICE_DT_GET(DT_INST_CLOCKS_CTLR(inst)),			\
			.clkid = (clock_control_subsys_t)DT_INST_CLOCKS_CELL(inst, clkid),	\
		)										\
	)

#define DW_SNPS_TIMER_SNPS_RESET_SPEC_INIT(inst)				\
	.reset = RESET_DT_SPEC_INST_GET(inst),					\

#define CREATE_DW_TIMER_DEV(inst)						\
	static void counter_dw_timer_irq_config_##inst(void); \
	static struct counter_dw_timer_drv_data timer_data_##inst;		\
	static const struct counter_dw_timer_config timer_config_##inst = {	\
		DEVICE_MMIO_NAMED_ROM_INIT(timer_mmio, DT_DRV_INST(inst)),	\
		DW_SNPS_TIMER_CLOCK_RATE_INIT(inst)				\
		.info = {							\
					.max_top_value = UINT32_MAX,		\
					.channels = 1,				\
		},								\
		IF_ENABLED(DT_INST_NODE_HAS_PROP(inst, resets),			\
			(DW_SNPS_TIMER_SNPS_RESET_SPEC_INIT(inst)))		\
		.irq_config = counter_dw_timer_irq_config_##inst,		\
	};									\
	DEVICE_DT_INST_DEFINE(inst,						\
			counter_dw_timer_init,					\
			NULL, &timer_data_##inst,				\
			&timer_config_##inst, POST_KERNEL,			\
			CONFIG_COUNTER_INIT_PRIORITY,				\
			&dw_timer_driver_api);					\
	static void counter_dw_timer_irq_config_##inst(void)			\
	{									\
		IRQ_CONNECT(DT_INST_IRQN(inst),					\
				DT_INST_IRQ(inst, priority),			\
				counter_dw_timer_irq_handler,			\
				DEVICE_DT_INST_GET(inst), 0);			\
		irq_enable(DT_INST_IRQN(inst));					\
	}

DT_INST_FOREACH_STATUS_OKAY(CREATE_DW_TIMER_DEV);
