/*
 * Copyright (c) 2017 Oticon A/S
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * This provides a model of:
 *  - A system tick
 *  - A real time clock
 *  - A one shot HW timer which can be used to awake the CPU at a given time
 *  - The clock source for all of this, and therefore for native_posix
 *
 * Please see doc/board.rst for more information, specially sections:
 *  About time in native_posix
 *  Peripherals:
 *	Clock source, system tick and timer
 *	Real time clock
 */

#include <stdint.h>
#include <time.h>
#include <stdbool.h>
#include <math.h>
#include "hw_models_top.h"
#include "irq_ctrl.h"
#include "board_soc.h"
#include "zephyr/types.h"
#include "posix_trace.h"
#include "misc/util.h"
#include "cmdline.h"
#include "soc.h"

#define DEBUG_NP_TIMER 0

#if DEBUG_NP_TIMER

/**
 * Helper function to convert a 64 bit time in microseconds into a string.
 * The format will always be: hh:mm:ss.ssssss\0
 *
 * Note: the caller has to allocate the destination buffer (at least 17 chars)
 */
#include <stdio.h>
static char *us_time_to_str(char *dest, u64_t time)
{
	if (time != NEVER) {
		unsigned int hour;
		unsigned int minute;
		unsigned int second;
		unsigned int us;

		hour   = (time / 3600 / 1000000) % 24;
		minute = (time / 60 / 1000000) % 60;
		second = (time / 1000000) % 60;
		us     = time % 1000000;

		sprintf(dest, "%02u:%02u:%02u.%06u", hour, minute, second, us);
	} else {
		sprintf(dest, " NEVER/UNKNOWN ");

	}
	return dest;
}
#endif

u64_t hw_timer_timer;

u64_t hw_timer_tick_timer;
u64_t hw_timer_awake_timer;

static u64_t tick_p; /* Period of the ticker */
static s64_t silent_ticks;

static bool real_time_mode =
#if (CONFIG_NATIVE_POSIX_SLOWDOWN_TO_REAL_TIME)
	true;
#else
	false;
#endif

static bool reset_rtc; /*"Reset" the RTC on boot*/

/*
 * When this executable started running, this value shall not be changed after
 * boot
 */
static u64_t boot_time;

/*
 * Ratio of the simulated clock to the real host time
 * For ex. a clock_ratio = 1+100e-6 means the simulated time is 100ppm faster
 * than real time
 */
static double clock_ratio = 1.0;

#if DEBUG_NP_TIMER
/*
 * Offset of the simulated time vs the real host time due to drift/clock ratio
 * until "last_radj_*time"
 *
 * A positive value means simulated time is ahead of the host time
 *
 * This variable is only kept for debugging purposes
 */
static s64_t last_drift_offset;
#endif

/*
 * Offsets of the RTC relative to the hardware models simu_time
 * "simu_time" == simulated time which starts at 0 on boot
 */
static s64_t rtc_offset;

/* Last host/real time when the ratio was adjusted */
static u64_t last_radj_rtime;
/* Last simulated time when the ratio was adjusted */
static u64_t last_radj_stime;

extern u64_t posix_get_hw_cycle(void);

void hwtimer_set_real_time_mode(bool new_rt)
{
	real_time_mode = new_rt;
}

static void hwtimer_update_timer(void)
{
	hw_timer_timer = MIN(hw_timer_tick_timer, hw_timer_awake_timer);
}

static inline void host_clock_gettime(struct timespec *tv)
{
#if defined(CLOCK_MONOTONIC_RAW)
	clock_gettime(CLOCK_MONOTONIC_RAW, tv);
#else
	clock_gettime(CLOCK_MONOTONIC, tv);
#endif
}

u64_t get_host_us_time(void)
{
	struct timespec tv;

	host_clock_gettime(&tv);
	return (u64_t)tv.tv_sec * 1e6 + tv.tv_nsec / 1000;
}

void hwtimer_init(void)
{
	silent_ticks = 0;
	hw_timer_tick_timer = NEVER;
	hw_timer_awake_timer = NEVER;
	hwtimer_update_timer();
	if (real_time_mode) {
		boot_time = get_host_us_time();
		last_radj_rtime = boot_time;
		last_radj_stime = 0U;
	}
	if (!reset_rtc) {
		struct timespec tv;
		u64_t realhosttime;

		clock_gettime(CLOCK_REALTIME, &tv);
		realhosttime = (u64_t)tv.tv_sec * 1e6 + tv.tv_nsec / 1000;

		rtc_offset += realhosttime;
	}
}

void hwtimer_cleanup(void)
{

}

/**
 * Enable the HW timer tick interrupts with a period <period> in micoseconds
 */
void hwtimer_enable(u64_t period)
{
	tick_p = period;
	hw_timer_tick_timer = hwm_get_time() + tick_p;
	hwtimer_update_timer();
	hwm_find_next_timer();
}

static void hwtimer_tick_timer_reached(void)
{
	if (real_time_mode) {
		u64_t expected_rt = (hw_timer_tick_timer - last_radj_stime)
				    / clock_ratio
				    + last_radj_rtime;
		u64_t real_time = get_host_us_time();

		s64_t diff = expected_rt - real_time;

#if DEBUG_NP_TIMER
		char es[30];
		char rs[30];

		us_time_to_str(es, expected_rt - boot_time);
		us_time_to_str(rs, real_time - boot_time);
		printf("tick @%5llims: diff = expected_rt - real_time = "
			"%5lli = %s - %s\n",
			hw_timer_tick_timer/1000, diff, es, rs);
#endif

		if (diff > 0) { /* we need to slow down */
			struct timespec requested_time;
			struct timespec remaining;

			requested_time.tv_sec  = diff / 1e6;
			requested_time.tv_nsec = (diff -
						 requested_time.tv_sec*1e6)*1e3;

			nanosleep(&requested_time, &remaining);
		}
	}

	hw_timer_tick_timer += tick_p;
	hwtimer_update_timer();

	if (silent_ticks > 0) {
		silent_ticks -= 1;
	} else {
		hw_irq_ctrl_set_irq(TIMER_TICK_IRQ);
	}
}

static void hwtimer_awake_timer_reached(void)
{
	hw_timer_awake_timer = NEVER;
	hwtimer_update_timer();
	hw_irq_ctrl_set_irq(PHONY_HARD_IRQ);
}

void hwtimer_timer_reached(void)
{
	u64_t Now = hw_timer_timer;

	if (hw_timer_awake_timer == Now) {
		hwtimer_awake_timer_reached();
	}

	if (hw_timer_tick_timer == Now) {
		hwtimer_tick_timer_reached();
	}
}

/**
 * The timer HW will awake the CPU (without an interrupt) at least when <time>
 * comes (it may awake it earlier)
 *
 * If there was a previous request for an earlier time, the old one will prevail
 *
 * This is meant for k_busy_wait() like functionality
 */
void hwtimer_wake_in_time(u64_t time)
{
	if (hw_timer_awake_timer > time) {
		hw_timer_awake_timer = time;
		hwtimer_update_timer();
	}
}

/**
 * The kernel wants to skip the next sys_ticks tick interrupts
 * If sys_ticks == 0, the next interrupt will be raised.
 */
void hwtimer_set_silent_ticks(s64_t sys_ticks)
{
	silent_ticks = sys_ticks;
}

s64_t hwtimer_get_pending_silent_ticks(void)
{
	return silent_ticks;
}


/**
 * During boot set the real time clock simulated time not start
 * from the real host time
 */
void hwtimer_reset_rtc(void)
{
	reset_rtc = true;
}

/**
 * Set a time offset (microseconds) of the RTC simulated time
 * Note: This should not be used after starting
 */
void hwtimer_set_rtc_offset(s64_t offset)
{
	rtc_offset = offset;
}

/**
 * Set the ratio of the simulated time to host (real) time.
 * Note: This should not be used after starting
 */
void hwtimer_set_rt_ratio(double ratio)
{
	clock_ratio = ratio;
}

/**
 * Increase or decrease the RTC simulated time by offset_delta
 */
void hwtimer_adjust_rtc_offset(s64_t offset_delta)
{
	rtc_offset += offset_delta;
}

/**
 * Adjust the ratio of the simulated time by a factor
 */
void hwtimer_adjust_rt_ratio(double ratio_correction)
{
	u64_t current_stime = hwm_get_time();
	s64_t s_diff = current_stime - last_radj_stime;
	/* Accumulated real time drift time since last adjustment: */

	last_radj_rtime += s_diff / clock_ratio;
	last_radj_stime = current_stime;

#if DEBUG_NP_TIMER
	char ct[30];
	s64_t r_drift = (long double)(clock_ratio-1.0)/(clock_ratio)*s_diff;

	last_drift_offset += r_drift;
	us_time_to_str(ct, current_stime);

	printf("%s(): @%s, s_diff= %llius after last adjust\n"
		" during which we drifted %.3fms\n"
		" total acc drift (last_drift_offset) = %.3fms\n"
		" last_radj_rtime = %.3fms (+%.3fms )\n"
		" Ratio adjusted to %f\n",
		__func__, ct, s_diff,
		r_drift/1000.0,
		last_drift_offset/1000.0,
		last_radj_rtime/1000.0,
		s_diff/clock_ratio/1000.0,
		clock_ratio*ratio_correction);
#endif

	clock_ratio *= ratio_correction;
}

/**
 * Return the current simulated RTC time in microseconds
 */
s64_t hwtimer_get_simu_rtc_time(void)
{
	return hwm_get_time() + rtc_offset;
}


/**
 * Return a version of the host time which would have drifted as if the host
 * real time clock had been running from the native_posix clock, and adjusted
 * both in rate and in offsets as the native_posix has been.
 *
 * Note that this time may be significantly ahead of the simulated time
 * (the time the Zephyr kernel thinks it is).
 * This will be the case in general if native_posix is not able to run at or
 * faster than real time.
 */
void hwtimer_get_pseudohost_rtc_time(u32_t *nsec, u64_t *sec)
{
	/*
	 * Note: long double has a 64bits mantissa in x86.
	 * Therefore to avoid loss of precision after 500 odd years into
	 * the epoc, we first calculate the offset from the last adjustment
	 * time split in us and ns. So we keep the full precision for 500 odd
	 * years after the last clock ratio adjustment (or native_posix boot,
	 * whichever is latest).
	 * Meaning, we will still start to loose precision after 500 off
	 * years of runtime without a clock ratio adjustment, but that really
	 * should not be much of a problem, given that the ns lower digits are
	 * pretty much noise anyhow.
	 * (So, all this is a huge overkill)
	 *
	 * The operation below in plain is just:
	 *   st = (rt - last_rt_adj_time)*ratio + last_dt_adj_time
	 * where st = simulated time
	 *       rt = real time
	 *       last_rt_adj_time = time (real) when the last ratio
	 *			    adjustment took place
	 *       last_st_adj_time = time (simulated) when the last ratio
	 *			    adjustment took place
	 *       ratio = ratio between simulated time and real time
	 */
	struct timespec tv;

	host_clock_gettime(&tv);

	u64_t rt_us = (u64_t)tv.tv_sec * 1000000ULL + tv.tv_nsec / 1000;
	u32_t rt_ns = tv.tv_nsec % 1000;

	long double drt_us = (long double)rt_us - last_radj_rtime;
	long double drt_ns = drt_us * 1000.0 + (long double)rt_ns;
	long double st = drt_ns * clock_ratio +
			 (long double)(last_radj_stime + rtc_offset) * 1000.0;

	*nsec = fmodl(st, 1e9);
	*sec = st / 1e9;
}

static struct {
	double stop_at;
	double rtc_offset;
	double rt_drift;
	double rt_ratio;
} args;

static void cmd_stop_at_found(char *argv, int offset)
{
	ARG_UNUSED(offset);
	if (args.stop_at < 0) {
		posix_print_error_and_exit("Error: stop-at must be positive "
					   "(%s)\n", argv);
	}
	hwm_set_end_of_time(args.stop_at*1e6);
}

static void cmd_realtime_found(char *argv, int offset)
{
	ARG_UNUSED(argv);
	ARG_UNUSED(offset);
	hwtimer_set_real_time_mode(true);
}

static void cmd_no_realtime_found(char *argv, int offset)
{
	ARG_UNUSED(argv);
	ARG_UNUSED(offset);
	hwtimer_set_real_time_mode(false);
}

static void cmd_rtcoffset_found(char *argv, int offset)
{
	ARG_UNUSED(argv);
	ARG_UNUSED(offset);
	hwtimer_set_rtc_offset(args.rtc_offset*1e6);
}

static void cmd_rt_drift_found(char *argv, int offset)
{
	ARG_UNUSED(argv);
	ARG_UNUSED(offset);
	if (!(args.rt_drift > -1)) {
		posix_print_error_and_exit("The drift needs to be > -1. "
					  "Please use --help for more info\n");
	}
	args.rt_ratio = args.rt_drift + 1;
	hwtimer_set_rt_ratio(args.rt_ratio);
}

static void cmd_rt_ratio_found(char *argv, int offset)
{
	ARG_UNUSED(argv);
	ARG_UNUSED(offset);
	if ((args.rt_ratio <= 0)) {
		posix_print_error_and_exit("The ratio needs to be > 0. "
					  "Please use --help for more info\n");
	}
	hwtimer_set_rt_ratio(args.rt_ratio);
}

static void cmd_rtcreset_found(char *argv, int offset)
{
	ARG_UNUSED(argv);
	ARG_UNUSED(offset);
	hwtimer_reset_rtc();
}

static void native_add_time_options(void)
{
	static struct args_struct_t timer_options[] = {
		/*
		 * Fields:
		 * manual, mandatory, switch,
		 * option_name, var_name ,type,
		 * destination, callback,
		 * description
		 */
		{false, false, true,
		"rt", "", 'b',
		NULL, cmd_realtime_found,
		"Slow down the execution to the host real time, "
		"or a ratio of it (see --rt-ratio below)"},

		{false, false, true,
		"no-rt", "", 'b',
		NULL, cmd_no_realtime_found,
		"Do NOT slow down the execution to real time, but advance "
		"Zephyr's time as fast as possible and decoupled from the host "
		"time"},

		{false, false, false,
		"rt-drift", "dratio", 'd',
		(void *)&args.rt_drift, cmd_rt_drift_found,
		"Drift of the simulated clock relative to the host real time. "
		"Normally this would be set to a value of a few ppm (e.g. 50e-6"
		") "
		"This option has no effect in non real time mode"
		},

		{false, false, false,
		"rt-ratio", "ratio", 'd',
		(void *)&args.rt_ratio, cmd_rt_ratio_found,
		"Relative speed of the simulated time vs real time. "
		"For ex. set to 2 to have simulated time pass at double the "
		"speed of real time. "
		"Note that both rt-drift & rt-ratio adjust the same clock "
		"speed, and therefore it does not make sense to use them "
		"simultaneously. "
		"This option has no effect in non real time mode"
		},

		{false, false, false,
		"rtc-offset", "time_offset", 'd',
		(void *)&args.rtc_offset, cmd_rtcoffset_found,
		"At boot offset the RTC clock by this amount of seconds"
		},

		{false, false, true,
		"rtc-reset", "", 'b',
		NULL, cmd_rtcreset_found,
		"Start the simulated real time clock at 0. Otherwise it starts "
		"matching the value provided by the host real time clock"},

		{false, false, false,
		 "stop_at", "time", 'd',
		(void *)&args.stop_at, cmd_stop_at_found,
		"In simulated seconds, when to stop automatically"},

		ARG_TABLE_ENDMARKER};

	native_add_command_line_opts(timer_options);
}

NATIVE_TASK(native_add_time_options, PRE_BOOT_1, 1);
