/*
 * Copyright (c) 2024 Silicon Laboratories Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <errno.h>
#include <stdint.h>
#include <stdbool.h>

#include <zephyr/init.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/sys_clock.h>
#include <zephyr/drivers/timer/system_timer.h>
#include <zephyr/logging/log.h>

#include <sl_sleeptimer.h>

LOG_MODULE_REGISTER(silabs_sleeptimer_timer);

/* Maximum time interval between timer interrupts (in hw_cycles) */
#define MAX_TIMEOUT_CYC (UINT32_MAX >> 1)
#define MIN_DELAY_CYC   (4U)

#define DT_RTC DT_COMPAT_GET_ANY_STATUS_OKAY(silabs_gecko_stimer)

/* Ensure interrupt names don't expand to register interface struct pointers */
#undef RTCC

/* With CONFIG_TIMER_READS_ITS_FREQUENCY_AT_RUNTIME, this global variable holds the clock frequency,
 * and must be written by the driver at init.
 */
extern int z_clock_hw_cycles_per_sec;

/* Global timer state */
struct sleeptimer_timer_data {
	uint32_t cyc_per_tick;      /* Number of hw_cycles per 1 kernel tick */
	uint32_t max_timeout_ticks; /* MAX_TIMEOUT_CYC expressed as ticks */
	atomic_t last_count;        /* Value of counter when the previous tick was announced */
	struct k_spinlock lock;     /* Spinlock to sync between ISR and updating the timeout */
	bool initialized;           /* Set to true when timer is initialized */
	sl_sleeptimer_timer_handle_t handle; /* Timer handle for system timer */
};
static struct sleeptimer_timer_data g_sleeptimer_timer_data = {0};

static void sleeptimer_cb(sl_sleeptimer_timer_handle_t *handle, void *data)
{
	ARG_UNUSED(handle);
	struct sleeptimer_timer_data *timer = data;

	uint32_t curr = sl_sleeptimer_get_tick_count();
	uint32_t prev = atomic_get(&timer->last_count);
	uint32_t pending = curr - prev;

	/* Number of unannounced ticks since the last announcement */
	uint32_t unannounced = pending / timer->cyc_per_tick;

	atomic_set(&timer->last_count, prev + unannounced * timer->cyc_per_tick);

	sys_clock_announce(unannounced);
}

static void sleeptimer_clock_set_timeout(int32_t ticks, struct sleeptimer_timer_data *timer)
{
	if (!IS_ENABLED(CONFIG_TICKLESS_KERNEL)) {
		return;
	}

	ticks = (ticks == K_TICKS_FOREVER) ? timer->max_timeout_ticks : ticks;
	ticks = CLAMP(ticks, 0, timer->max_timeout_ticks);

	k_spinlock_key_t key = k_spin_lock(&timer->lock);

	uint32_t curr = sl_sleeptimer_get_tick_count();
	uint32_t prev = atomic_get(&timer->last_count);
	uint32_t pending = curr - prev;
	uint32_t next = ticks * timer->cyc_per_tick;

	/* Next timeout is N ticks in the future, minus the current progress
	 * towards the timeout. If we are behind, set the timeout to the first
	 * possible upcoming tick.
	 */
	while (next < (pending + MIN_DELAY_CYC)) {
		next += timer->cyc_per_tick;
	}
	next -= pending;

	sl_sleeptimer_restart_timer(&timer->handle, next, sleeptimer_cb, timer, 0, 0);
	k_spin_unlock(&timer->lock, key);
}

static uint32_t sleeptimer_clock_elapsed(struct sleeptimer_timer_data *timer)
{
	if (!IS_ENABLED(CONFIG_TICKLESS_KERNEL) || !timer->initialized) {
		/* No unannounced ticks can have elapsed if not in tickless mode */
		return 0;
	} else {
		return (sl_sleeptimer_get_tick_count() - atomic_get(&timer->last_count)) /
		       timer->cyc_per_tick;
	}
}

void sys_clock_set_timeout(int32_t ticks, bool idle)
{
	ARG_UNUSED(idle);

	sleeptimer_clock_set_timeout(ticks, &g_sleeptimer_timer_data);
}

uint32_t sys_clock_elapsed(void)
{
	return sleeptimer_clock_elapsed(&g_sleeptimer_timer_data);
}

uint32_t sys_clock_cycle_get_32(void)
{
	return g_sleeptimer_timer_data.initialized ? sl_sleeptimer_get_tick_count() : 0;
}

static int sleeptimer_init(void)
{
	sl_status_t status = SL_STATUS_OK;
	struct sleeptimer_timer_data *timer = &g_sleeptimer_timer_data;

	IRQ_CONNECT(DT_IRQ(DT_RTC, irq), DT_IRQ(DT_RTC, priority),
		    CONCAT(DT_STRING_UPPER_TOKEN_BY_IDX(DT_RTC, interrupt_names, 0), _IRQHandler),
		    0, 0);

	sl_sleeptimer_init();

	z_clock_hw_cycles_per_sec = sl_sleeptimer_get_timer_frequency();

	BUILD_ASSERT(CONFIG_SYS_CLOCK_TICKS_PER_SEC > 0,
		     "Invalid CONFIG_SYS_CLOCK_TICKS_PER_SEC value");

	timer->cyc_per_tick = z_clock_hw_cycles_per_sec / CONFIG_SYS_CLOCK_TICKS_PER_SEC;

	__ASSERT(timer->cyc_per_tick >= MIN_DELAY_CYC,
		 "A tick of %u cycles is too short to be scheduled "
		 "(min is %u). Config: SYS_CLOCK_TICKS_PER_SEC is "
		 "%d and timer frequency is %u",
		 timer->cyc_per_tick, MIN_DELAY_CYC, CONFIG_SYS_CLOCK_TICKS_PER_SEC,
		 z_clock_hw_cycles_per_sec);

	timer->max_timeout_ticks = MAX_TIMEOUT_CYC / timer->cyc_per_tick;
	timer->initialized = true;

	atomic_set(&timer->last_count, sl_sleeptimer_get_tick_count());

	/* Start the timer and announce 1 kernel tick */
	if (IS_ENABLED(CONFIG_TICKLESS_KERNEL)) {
		status = sl_sleeptimer_start_timer(&timer->handle, timer->cyc_per_tick,
						   sleeptimer_cb, timer, 0, 0);
	} else {
		status = sl_sleeptimer_start_periodic_timer(&timer->handle, timer->cyc_per_tick,
							    sleeptimer_cb, timer, 0, 0);
	}
	if (status != SL_STATUS_OK) {
		return -ENODEV;
	}

	return 0;
}

SYS_INIT(sleeptimer_init, PRE_KERNEL_2, CONFIG_SYSTEM_CLOCK_INIT_PRIORITY);
