/*
 * Copyright (c) 2023 Antmicro <www.antmicro.com>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT ambiq_watchdog

#include <zephyr/kernel.h>
#include <zephyr/drivers/watchdog.h>

#include <errno.h>
#include <am_mcu_apollo.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(wdt_ambiq, CONFIG_WDT_LOG_LEVEL);

typedef void (*ambiq_wdt_cfg_func_t)(void);

struct wdt_ambiq_config {
	uint32_t base;
	uint32_t irq_num;
	uint8_t clk_freq;
	ambiq_wdt_cfg_func_t cfg_func;
};

struct wdt_ambiq_data {
	wdt_callback_t callback;
	uint32_t timeout;
	bool reset;
};

static void wdt_ambiq_isr(void *arg)
{
	const struct device *dev = (const struct device *)arg;
	struct wdt_ambiq_data *data = dev->data;

#if defined(CONFIG_SOC_SERIES_APOLLO3X)
	am_hal_wdt_int_clear();
#else
	uint32_t status;
	am_hal_wdt_interrupt_status_get(AM_HAL_WDT_MCU, &status, false);
	am_hal_wdt_interrupt_clear(AM_HAL_WDT_MCU, status);
#endif

	if (data->callback) {
		data->callback(dev, 0);
	}
}

static int wdt_ambiq_setup(const struct device *dev, uint8_t options)
{
	const struct wdt_ambiq_config *dev_cfg = dev->config;
	struct wdt_ambiq_data *data = dev->data;
	am_hal_wdt_config_t cfg;

#if defined(CONFIG_SOC_SERIES_APOLLO3X)
	uint32_t ui32ClockSource = AM_HAL_WDT_LFRC_CLK_DEFAULT;

	if (dev_cfg->clk_freq == 128) {
		ui32ClockSource = AM_HAL_WDT_LFRC_CLK_128HZ;
	} else if (dev_cfg->clk_freq == 16) {
		ui32ClockSource = AM_HAL_WDT_LFRC_CLK_16HZ;
	} else if (dev_cfg->clk_freq == 1) {
		ui32ClockSource = AM_HAL_WDT_LFRC_CLK_1HZ;
	}
	cfg.ui32Config = ui32ClockSource | _VAL2FLD(WDT_CFG_RESEN, data->reset) |
			 AM_HAL_WDT_ENABLE_INTERRUPT;
	cfg.ui16InterruptCount = data->timeout;
	cfg.ui16ResetCount = data->timeout;
	am_hal_clkgen_control(AM_HAL_CLKGEN_CONTROL_LFRC_START, 0);
	am_hal_wdt_init(&cfg);
	am_hal_wdt_int_enable();
	am_hal_wdt_start();
#else
	if (dev_cfg->clk_freq == 128) {
		cfg.eClockSource = AM_HAL_WDT_128HZ;
	} else if (dev_cfg->clk_freq == 16) {
		cfg.eClockSource = AM_HAL_WDT_16HZ;
	} else if (dev_cfg->clk_freq == 1) {
		cfg.eClockSource = AM_HAL_WDT_1HZ;
	}

	cfg.bInterruptEnable = true;
	cfg.ui32InterruptValue = data->timeout;
	cfg.bResetEnable = data->reset;
	cfg.ui32ResetValue = data->timeout;
	cfg.bAlertOnDSPReset = false;

	am_hal_wdt_config(AM_HAL_WDT_MCU, &cfg);
	am_hal_wdt_interrupt_enable(AM_HAL_WDT_MCU, AM_HAL_WDT_INTERRUPT_MCU);
	am_hal_wdt_start(AM_HAL_WDT_MCU, false);
#endif
	return 0;
}

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

#if defined(CONFIG_SOC_SERIES_APOLLO3X)
	am_hal_wdt_halt();
#else
	am_hal_wdt_stop(AM_HAL_WDT_MCU);
#endif
	return 0;
}

static int wdt_ambiq_install_timeout(const struct device *dev, const struct wdt_timeout_cfg *cfg)
{
	const struct wdt_ambiq_config *dev_cfg = dev->config;
	struct wdt_ambiq_data *data = dev->data;

	if (cfg->window.min != 0U || cfg->window.max == 0U) {
		return -EINVAL;
	}

	data->timeout = cfg->window.max / 1000 * dev_cfg->clk_freq;
	data->callback = cfg->callback;

	switch (cfg->flags) {
	case WDT_FLAG_RESET_CPU_CORE:
	case WDT_FLAG_RESET_SOC:
		data->reset = true;
		break;
	case WDT_FLAG_RESET_NONE:
		data->reset = false;
		break;
	default:
		LOG_ERR("Unsupported watchdog config flag");
		return -EINVAL;
	}

	return 0;
}

static int wdt_ambiq_feed(const struct device *dev, int channel_id)
{
	ARG_UNUSED(dev);
	ARG_UNUSED(channel_id);

#if defined(CONFIG_SOC_SERIES_APOLLO3X)
	am_hal_wdt_restart();
#else
	am_hal_wdt_restart(AM_HAL_WDT_MCU);
#endif
	LOG_DBG("Fed the watchdog");

	return 0;
}

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

	if (dev_cfg->clk_freq != 128 && dev_cfg->clk_freq != 16 && dev_cfg->clk_freq != 1) {
		return -ENOTSUP;
	}

	NVIC_ClearPendingIRQ(dev_cfg->irq_num);

	dev_cfg->cfg_func();

	irq_enable(dev_cfg->irq_num);

	return 0;
}

static const struct wdt_driver_api wdt_ambiq_driver_api = {
	.setup = wdt_ambiq_setup,
	.disable = wdt_ambiq_disable,
	.install_timeout = wdt_ambiq_install_timeout,
	.feed = wdt_ambiq_feed,
};

#define AMBIQ_WDT_INIT(n)                                                                          \
	static struct wdt_ambiq_data wdt_ambiq_data##n;                                            \
	static void ambiq_wdt_cfg_func_##n(void)                                                   \
	{                                                                                          \
                                                                                                   \
		IRQ_CONNECT(DT_INST_IRQN(n), DT_INST_IRQ(n, priority), wdt_ambiq_isr,              \
			    DEVICE_DT_INST_GET(n), 0);                                             \
	};                                                                                         \
	static const struct wdt_ambiq_config wdt_ambiq_config##n = {                               \
		.base = DT_INST_REG_ADDR(n),                                                       \
		.clk_freq = DT_INST_PROP(n, clock_frequency),                                      \
		.irq_num = DT_INST_IRQN(n),                                                        \
		.cfg_func = ambiq_wdt_cfg_func_##n};                                               \
                                                                                                   \
	DEVICE_DT_INST_DEFINE(n, wdt_ambiq_init, NULL, &wdt_ambiq_data##n, &wdt_ambiq_config##n,   \
			      PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEVICE,                    \
			      &wdt_ambiq_driver_api);

DT_INST_FOREACH_STATUS_OKAY(AMBIQ_WDT_INIT)
