/*
 * Copyright (c) 2024 Nuvoton Technology Corporation.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT nuvoton_numaker_wwdt

#include <zephyr/kernel.h>
#include <zephyr/drivers/reset.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/clock_control/clock_control_numaker.h>
#include <zephyr/drivers/watchdog.h>
#include <zephyr/logging/log.h>
#include <soc.h>
#include <NuMicro.h>

LOG_MODULE_REGISTER(wwdt_numaker, CONFIG_WDT_LOG_LEVEL);

#define NUMAKER_PRESCALER_MAX 15U
#define NUMAKER_COUNTER_MAX   0x3eU
#define NUMAKER_COUNTER_MIN   0x01U

/* Device config */
struct wwdt_numaker_config {
	/* wdt base address */
	WWDT_T *wwdt_base;
	uint32_t clk_modidx;
	uint32_t clk_src;
	uint32_t clk_div;
	const struct device *clk_dev;
};

struct wwdt_numaker_data {
	wdt_callback_t cb;
	bool timeout_valid;
	/* watchdog timeout in milliseconds */
	uint32_t timeout;
	uint32_t prescaler;
	uint32_t counter;
};

static int m_wwdt_numaker_clk_get_rate(const struct wwdt_numaker_config *cfg, uint32_t *rate)
{

	if (cfg->clk_src == CLK_CLKSEL1_WWDTSEL_LIRC) {
		*rate = __LIRC / (cfg->clk_div + 1);
	} else {
		/* clock source is from HCLK, CLK_CLKSEL1_WWDTSEL_HCLK_DIV2048 */
		SystemCoreClockUpdate();
		*rate = CLK_GetHCLKFreq() / 2048 / (cfg->clk_div + 1);
	}

	return 0;
}


/* Convert watchdog clock to nearest ms (rounded up) */
static uint32_t m_wwdt_numaker_calc_ms(const struct device *dev, uint32_t pow2)
{
	const struct wwdt_numaker_config *cfg = dev->config;
	uint32_t clk_freq;
	uint32_t prescale_clks;
	uint32_t period_ms;

	m_wwdt_numaker_clk_get_rate(cfg, &clk_freq);
	prescale_clks = (1 << pow2) * 64;
	period_ms = DIV_ROUND_UP(prescale_clks * MSEC_PER_SEC, clk_freq);

	return period_ms;
}

static int m_wwdt_numaker_calc_window(const struct device *dev,
				      const struct wdt_window *win,
				      uint32_t *timeout,
				      uint32_t *prescaler,
				      uint32_t *counter)
{
	uint32_t pow2;
	uint32_t gap;

	/* Find nearest period value (rounded up) */
	for (pow2 = 0U; pow2 <= NUMAKER_PRESCALER_MAX; pow2++) {
		*timeout = m_wwdt_numaker_calc_ms(dev, pow2);

		if (*timeout >= win->max) {
			*prescaler = pow2 << WWDT_CTL_PSCSEL_Pos;
			if (win->min == 0U) {
				*counter = NUMAKER_COUNTER_MAX;
			} else {
				gap = DIV_ROUND_UP(win->min
						   * NUMAKER_COUNTER_MAX,
						   *timeout);
				*counter = NUMAKER_COUNTER_MAX - gap;
				if (*counter < NUMAKER_COUNTER_MIN) {
					*counter = NUMAKER_COUNTER_MIN;
				}
			}

			return 0;
		}
	}

	return -EINVAL;
}

static int wwdt_numaker_install_timeout(const struct device *dev,
					const struct wdt_timeout_cfg *cfg)
{
	struct wwdt_numaker_data *data = dev->data;
	const struct wwdt_numaker_config *config = dev->config;
	uint32_t timeout;
	uint32_t prescaler;
	uint32_t counter;

	LOG_DBG("");
	/* Validate watchdog already running */
	if (config->wwdt_base->CTL & WWDT_CTL_WWDTEN_Msk) {
		LOG_ERR("watchdog is busy");
		return -EBUSY;
	}

	if (cfg->window.max == 0U) {
		LOG_ERR("window.max should be non-zero");
		return -EINVAL;
	}

	if (m_wwdt_numaker_calc_window(dev, &cfg->window, &timeout, &prescaler, &counter) != 0) {
		LOG_ERR("window.max is out of range");
		return -EINVAL;
	}

	LOG_DBG("counter=%d", counter);
	data->timeout = timeout;
	data->prescaler = prescaler;
	data->counter = counter;
	data->cb = cfg->callback;
	data->timeout_valid = true;

	return 0;
}

static int wwdt_numaker_disable(const struct device *dev)
{
	struct wwdt_numaker_data *data = dev->data;
	const struct wwdt_numaker_config *cfg = dev->config;
	WWDT_T *wwdt_base = cfg->wwdt_base;

	LOG_DBG("");
	/* stop counting */
	wwdt_base->CTL &= ~WWDT_CTL_WWDTEN_Msk;

	/* disable interrupt enable bit */
	wwdt_base->CTL &= ~WWDT_CTL_INTEN_Msk;

	/* disable interrupt */
	irq_disable(DT_INST_IRQN(0));

	data->timeout_valid = false;

	return 0;
}

static int wwdt_numaker_setup(const struct device *dev, uint8_t options)
{
	struct wwdt_numaker_data *data = dev->data;
	const struct wwdt_numaker_config *cfg = dev->config;
	WWDT_T *wwdt_base = cfg->wwdt_base;
	uint32_t dbg_mask = 0U;

	LOG_DBG("");
	irq_disable(DT_INST_IRQN(0));

	/* Validate watchdog already running */
	if (wwdt_base->CTL & WWDT_CTL_WWDTEN_Msk) {
		LOG_ERR("watchdog is busy");
		return -EBUSY;
	}

	if (!data->timeout_valid) {
		LOG_ERR("No valid timeout installed");
		return -EINVAL;
	}

	if (options & WDT_OPT_PAUSE_IN_SLEEP) {
		LOG_ERR("WDT_OPT_PAUSE_IN_SLEEP is not supported");
		return -ENOTSUP;
	}

	if (options & WDT_OPT_PAUSE_HALTED_BY_DBG) {
		dbg_mask = WWDT_CTL_ICEDEBUG_Msk;
	}

	/* Clear  WWDT Reset & Compared Match Interrupt System Flag */
	wwdt_base->STATUS = WWDT_STATUS_WWDTRF_Msk |
			    WWDT_STATUS_WWDTIF_Msk;

	/* Open WWDT and start counting */
	wwdt_base->CTL = data->prescaler |
			 (data->counter << WWDT_CTL_CMPDAT_Pos) |
			 WWDT_CTL_INTEN_Msk |
			 WWDT_CTL_WWDTEN_Msk |
			 dbg_mask;

	irq_enable(DT_INST_IRQN(0));

	return 0;
}

static int wwdt_numaker_feed(const struct device *dev, int channel_id)
{
	const struct wwdt_numaker_config *cfg = dev->config;
	WWDT_T *wwdt_base = cfg->wwdt_base;

	LOG_DBG("CNT=%d, CTL=0x%x", wwdt_base->CNT, wwdt_base->CTL);
	ARG_UNUSED(channel_id);

	/* Reload WWDT Counter */
	wwdt_base->RLDCNT = WWDT_RELOAD_WORD;

	return 0;
}

static void wwdt_numaker_isr(const struct device *dev)
{
	struct wwdt_numaker_data *data = dev->data;
	const struct wwdt_numaker_config *cfg = dev->config;
	WWDT_T *wwdt_base = cfg->wwdt_base;

	LOG_DBG("CNT=%d", wwdt_base->CNT);
	if (wwdt_base->STATUS & WWDT_STATUS_WWDTIF_Msk) {
		/* Clear WWDT Compared Match Interrupt Flag */
		wwdt_base->STATUS = WWDT_STATUS_WWDTIF_Msk;

		if (data->cb != NULL) {
			data->cb(dev, 0);
		}
	}
}

static const struct wdt_driver_api wwdt_numaker_api = {
	.setup = wwdt_numaker_setup,
	.disable = wwdt_numaker_disable,
	.install_timeout = wwdt_numaker_install_timeout,
	.feed = wwdt_numaker_feed,
};

static int wwdt_numaker_init(const struct device *dev)
{
	const struct wwdt_numaker_config *cfg = dev->config;
	struct numaker_scc_subsys scc_subsys;
	int err;

	SYS_UnlockReg();

	irq_disable(DT_INST_IRQN(0));
	/* CLK controller */
	memset(&scc_subsys, 0x00, sizeof(scc_subsys));
	scc_subsys.subsys_id = NUMAKER_SCC_SUBSYS_ID_PCC;
	scc_subsys.pcc.clk_modidx = cfg->clk_modidx;
	scc_subsys.pcc.clk_src = cfg->clk_src;
	scc_subsys.pcc.clk_div = cfg->clk_div;

	/* Equivalent to CLK_EnableModuleClock() */
	err = clock_control_on(cfg->clk_dev, (clock_control_subsys_t)&scc_subsys);
	if (err != 0) {
		goto done;
	}

	/* Equivalent to CLK_SetModuleClock() */
	err = clock_control_configure(cfg->clk_dev, (clock_control_subsys_t)&scc_subsys, NULL);
	if (err != 0) {
		goto done;
	}

	/* Enable NVIC */
	IRQ_CONNECT(DT_INST_IRQN(0), DT_INST_IRQ(0, priority),
		    wwdt_numaker_isr, DEVICE_DT_INST_GET(0), 0);
	irq_enable(DT_INST_IRQN(0));

done:
	SYS_LockReg();
	return err;

}

/* Set config based on DTS */
static struct wwdt_numaker_config wwdt_numaker_cfg_inst = {
	.wwdt_base = (WWDT_T *)DT_INST_REG_ADDR(0),
	.clk_modidx = DT_INST_CLOCKS_CELL(0, clock_module_index),
	.clk_src = DT_INST_CLOCKS_CELL(0, clock_source),
	.clk_div = DT_INST_CLOCKS_CELL(0, clock_divider),
	.clk_dev = DEVICE_DT_GET(DT_PARENT(DT_INST_CLOCKS_CTLR(0))),
};

static struct wwdt_numaker_data wwdt_numaker_data_inst;

DEVICE_DT_INST_DEFINE(0, wwdt_numaker_init, NULL,
		      &wwdt_numaker_data_inst, &wwdt_numaker_cfg_inst,
		      POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEVICE,
		      &wwdt_numaker_api);
