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

#include "NRF_HW_model_top.h"
#include "NRF_HWLowL.h"
#include "bs_tracing.h"
#include "bs_types.h"
#include "bs_utils.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>

/* Note: All timers are relative to hw_time and NOT to 'now' */
extern bs_time_t timer_nrf_main_timer;

/* The events priorities are as in this list from top to bottom
 * Priority being which timer executes first if several trigger at the same
 * instant
 */
static enum {
    NRF_HW_MAIN_TIMER = 0,
    NUMBER_OF_TIMERS,
    NONE
} next_timer_index = NONE;

static bs_time_t *Timer_list[NUMBER_OF_TIMERS] = {
    &timer_nrf_main_timer,
};
static bs_time_t next_timer_time = TIME_NEVER;

/*
 * Current absolute time of this device, as the device knows it.
 * It is never reset:
 */
bs_time_t now;
/* Current time the HW of this device things it is */
static bs_time_t hw_time;
/*
 * Offset between the current absolute time of the device and the HW time
 * That is, the absolute time when the HW_time got reset
 */
static bs_time_t hw_time_delta;

/* Last time we synchronized with the bsim PHY, in device abs time */
static bs_time_t last_bsim_phy_sync_time;

#define BSIM_DEFAULT_PHY_MAX_RESYNC_OFFSET 1000000
/* At least every second we will inform the simulator about our timing */
static bs_time_t max_resync_offset = BSIM_DEFAULT_PHY_MAX_RESYNC_OFFSET;

/**
 * Set the maximum amount of time the device will spend without talking
 * (synching) with the phy.
 * This does not change the functional behavior of the Zephyr code or of the
 * radio emulation, and it is only relevant if special test code running in the
 * device interacts behind the scenes with other devices test code.
 * Setting for example a value of 5ms will ensure that this device time
 * will never be more than 5ms away from the phy. Setting it in all devices
 * to 5ms would then ensure no device time is farther apart than 5ms from any
 * other.
 *
 * Note that setting low values has a performance penalty.
 */
void
tm_set_phy_max_resync_offset(bs_time_t offset_in_us)
{
    max_resync_offset = offset_in_us;
}

/**
 * Return the absolute current time (no HW model except the RADIO
 * should look into this)
 */
bs_time_t
tm_get_abs_time(void)
{
    return now;
}

/**
 * Return the current HW time
 */
bs_time_t
tm_get_hw_time(void)
{
    return hw_time;
}

bs_time_t
posix_get_hw_cycle(void)
{
    return tm_get_hw_time();
}

/**
 * Reset the HW time
 */
static void
tm_reset_hw_time(void)
{
    hw_time = 0;
    hw_time_delta = now;
    if (now != 0) {
        bs_trace_error_line("Reset not supposed to happen after "
                            "initialization\n");
    }
}

/**
 * Update the current hw_time value given the absolute time
 */
INLINE void
tm_update_HW_time(void)
{
    hw_time = now - hw_time_delta;
}

/*
 * Reset the HW time
 */
void
tm_reset_hw_times(void)
{
    tm_reset_hw_time();
}

/**
 * Advance the internal time values of this device until time
 */
void
tm_sleep_until_abs_time(bs_time_t time)
{
    if (time >= now) {
        /*
         * Ensure that at least we sync with the phy
         * every max_resync_offset
         */
        if (time > last_bsim_phy_sync_time + max_resync_offset) {
            hwll_sync_time_with_phy(time);
            last_bsim_phy_sync_time = time;
        }

        now = time;
    } else {
        /* LCOV_EXCL_START */
        bs_trace_warning_manual_time_line(now, "next_time_time "
                                               "corrupted (%"PRItime"<= %"PRItime", timer idx=%i)\n",
                                          time, now, next_timer_index);
        /* LCOV_EXCL_STOP */
    }
    tm_update_HW_time();
}

/**
 * Keep track of the last time we synchronized the time with the scheduler
 */
void
tm_update_last_phy_sync_time(bs_time_t abs_time)
{
    last_bsim_phy_sync_time = abs_time;
}

/**
 * Advance the internal time values of this device
 * until the HW time reaches hw_time
 */
void
tm_sleep_until_hw_time(bs_time_t hw_time)
{
    bs_time_t next_time = TIME_NEVER;

    if (hw_time != TIME_NEVER) {
        next_time = hw_time + hw_time_delta;
    }

    tm_sleep_until_abs_time(next_time);
}

/**
 * Look into all timers and update next_timer accordingly
 * To be called each time a "timed process" updates its timer
 */
void
tm_find_next_timer_to_trigger(void)
{
    next_timer_time = *Timer_list[0];
    next_timer_index = 0;

    for (uint i = 1; i < NUMBER_OF_TIMERS; i++) {
        if (next_timer_time > *Timer_list[i]) {
            next_timer_time = *Timer_list[i];
            next_timer_index = i;
        }
    }
}

bs_time_t
tm_get_next_timer_abstime(void)
{
    return next_timer_time + hw_time_delta;
}

bs_time_t
tm_hw_time_to_abs_time(bs_time_t hwtime)
{
    if (hwtime == TIME_NEVER) {
        return TIME_NEVER;
    }
    return hwtime + hw_time_delta;
}

bs_time_t
tm_abs_time_to_hw_time(bs_time_t abstime)
{
    if (abstime == TIME_NEVER) {
        return TIME_NEVER;
    }
    return abstime - hw_time_delta;
}

void
tm_tick_limited(bs_time_t max_time_diff)
{
    bs_time_t time_to_wait;

    if (max_time_diff != TIME_NEVER && now + max_time_diff < next_timer_time) {
        time_to_wait = now + max_time_diff;
    } else {
        time_to_wait = next_timer_time;
    }

    tm_sleep_until_hw_time(time_to_wait);
    switch (next_timer_index) {
        case NRF_HW_MAIN_TIMER:
            nrf_hw_some_timer_reached();
            break;
        default:
            bs_trace_error_time_line("next_timer_index "
                                     "corrupted\n");
            break;
    }
    tm_find_next_timer_to_trigger();
}

void
tm_tick(void)
{
    tm_tick_limited(TIME_NEVER);
}
