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

#include <stddef.h>
#include <stdbool.h>
#include "bs_cmd_line.h"
#include "bs_dynargs.h"
#include "bs_utils.h"
#include "bs_types.h"
#include "bs_tracing.h"
#include "nsi_tasks.h"
#include "nsi_hws_models_if.h"
#include "NRF_HWLowL.h"
#include "xo_if.h"
#include "bsim_args_runner.h"

/* By default every second we will inform the Phy simulator about our timing */
#define BSIM_DEFAULT_PHY_MAX_RESYNC_OFFSET 1000000

static struct {
	double start_offset;
	bs_time_t max_resync_offset;
	bool delay_init;
	bool sync_preinit;
	bool sync_preboot;
} sync_args;

static bs_time_t phy_sync_ctrl_event_timer = TIME_NEVER;
static bs_time_t last_resync_time;

static void psc_program_next_event(void)
{
	if (sync_args.max_resync_offset != TIME_NEVER) {
		phy_sync_ctrl_event_timer = last_resync_time + sync_args.max_resync_offset;

		bs_time_t now = nsi_hws_get_time();

		if (phy_sync_ctrl_event_timer < now) {
			phy_sync_ctrl_event_timer = now;
		}
	} else {
		phy_sync_ctrl_event_timer = TIME_NEVER;
	}
	nsi_hws_find_next_event();
}

static void phy_sync_ctrl_event_reached(void)
{

	last_resync_time = nsi_hws_get_time();

	hwll_sync_time_with_phy(last_resync_time);
	psc_program_next_event();
}

NSI_HW_EVENT(phy_sync_ctrl_event_timer, phy_sync_ctrl_event_reached, 900);

static void phy_sync_ctrl_init(void)
{
	last_resync_time = nsi_hws_get_time();

	psc_program_next_event();
}

NSI_TASK(phy_sync_ctrl_init, HW_INIT, 100);

/**
 * Keep track of the last time we synchronized the time with the Phy
 */
void phy_sync_ctrl_set_last_phy_sync_time(bs_time_t time)
{
	last_resync_time = time;
	psc_program_next_event();
}

/**
 * Configure the maximum resynchronization offset
 * (How long, in simulation time, can we spend without talking with the Phy)
 * Note that this value may be configured as a command line argument too
 */
void phy_sync_ctrl_set_max_resync_offset(bs_time_t max_resync_offset)
{
	sync_args.max_resync_offset = max_resync_offset;

	psc_program_next_event();
}

/* For backwards compatibility with the old board code */
void tm_set_phy_max_resync_offset(bs_time_t offset_in_us)
{
	phy_sync_ctrl_set_max_resync_offset(offset_in_us);
}

static double tmp_start_of;

static void cmd_start_of_found(char *argv, int offset)
{
	if (tmp_start_of < 0) {
		bs_trace_error_line("start offset (%lf) cannot be smaller than 0\n", tmp_start_of);
	}
	sync_args.start_offset = tmp_start_of;
	xo_model_set_toffset(sync_args.start_offset);
}

static void cmd_no_delay_init_found(char *argv, int offset)
{
	sync_args.delay_init = false;
}

static void cmd_no_sync_preinit_found(char *argv, int offset)
{
	sync_args.sync_preinit = false;
}

static void cmd_no_sync_preboot_found(char *argv, int offset)
{
	sync_args.sync_preboot = false;
}

static double tmp_max_resync_offset;

static void cmd_max_resync_offset_found(char *argv, int offset)
{
	if (tmp_max_resync_offset < 500) {
		bs_trace_warning("You are attempting to set a very low phy resynchronization of %d."
				"Note this will have a performance impact\n",
				tmp_max_resync_offset);
	}
	sync_args.max_resync_offset = tmp_max_resync_offset;
}

static void phy_sync_ctrl_register_args(void)
{
	static bs_args_struct_t args_struct_toadd[] = {
		{
		.option = "start_offset",
		.name = "start_of",
		.type = 'f',
		.dest = (void *)&tmp_start_of,
		.call_when_found = cmd_start_of_found,
		.descript = "Offset in time (at the start of the simulation) of this device. "
			    "At time 0 of the device, the phy will be at <start_of>"
		},
		{
		.is_switch = true,
		.option = "sync_preinit",
		.type = 'b',
		.dest = (void *)&sync_args.sync_preinit,
		.descript = "Postpone HW initialization and CPU boot until the Phy has "
			    "reached time 0 (or start_offset) (by default not set)"
		},
		{
		.is_switch = true,
		.option = "no_sync_preinit",
		.type = 'b',
		.call_when_found = cmd_no_sync_preinit_found,
		.descript = "Clear sync_preinit. Note that by default sync_preinit is not set"
		},
		{
		.is_switch = true,
		.option = "sync_preboot",
		.type = 'b',
		.dest = (void *)&sync_args.sync_preboot,
		.descript = "Postpone CPU boot (but not HW initialization) until the "
			    "Phy has reached time 0 (or start_offset) (by default not set)"
			    "If sync_preinit is set, this option has no effect."
		},
		{
		.is_switch = true,
		.option = "no_sync_preboot",
		.type = 'b',
		.call_when_found = cmd_no_sync_preboot_found,
		.descript = "Clear sync_preboot. Note that by default sync_preboot is not set"
		},
		{
		.is_switch = true,
		.option = "delay_init",
		.type = 'b',
		.dest = (void *)&sync_args.delay_init,
		.descript = "If start_offset is used, postpone HW initialization and CPU boot "
			    "until start_offset is reached (by default not set)"
		},
		{
		.is_switch = true,
		.option = "no_delay_init",
		.type = 'b',
		.call_when_found = cmd_no_delay_init_found,
		.descript = "Clear delay_init. Note that by default delay_init is not set"
		},
		{
		.option = "mro",
		.name = "max_resync_offset",
		.type = 'd',
		.dest = (void *)&tmp_max_resync_offset,
		.call_when_found = cmd_max_resync_offset_found,
		.descript = "Set the max Phy synchronization offset, that is, how far the device "
			    "time can be from the Phy time before it resynchronizes with the Phy "
			    "again (by default 1e6, 1s). Note that this value may be changed "
			    "programmatically by tests"
		},
		ARG_TABLE_ENDMARKER
	};

	bs_add_extra_dynargs(args_struct_toadd);

	sync_args.max_resync_offset = BSIM_DEFAULT_PHY_MAX_RESYNC_OFFSET;
}

NSI_TASK(phy_sync_ctrl_register_args, PRE_BOOT_1, 10);

void phy_sync_ctrl_connect_to_2G4_phy(void)
{
	static bool ever_run;

	if (ever_run) {
		return;
	}
	ever_run = true;

	bs_trace_raw(9, "%s: Connecting to phy...\n", __func__);
	hwll_connect_to_phy(bsim_args_get_2G4_device_nbr(),
			    bsim_args_get_simid(),
			    bsim_args_get_2G4_phy_id());
	bs_trace_raw(9, "%s: Connected\n", __func__);
}

void phy_sync_ctrl_pre_boot2(void)
{
	static bool ever_run;

	if (ever_run) {
		return;
	}
	ever_run = true;

	if (((sync_args.start_offset > 0) && (sync_args.delay_init))
		|| sync_args.sync_preinit) {
		/* Delay the next steps until the simulation time has
		 * reached either time 0 or start_offset.
		 */
		hwll_wait_for_phy_simu_time(BS_MAX(sync_args.start_offset, 0));
		sync_args.sync_preboot = false; /* Already sync'ed */
	}
}

void phy_sync_ctrl_pre_boot3(void)
{
	static bool ever_run;

	if (ever_run) {
		return;
	}
	ever_run = true;

	/*
	 * If sync_preboot was set, we sync with the phy
	 * right before booting the CPU
	 */
	if (sync_args.sync_preboot) {
		hwll_wait_for_phy_simu_time(BS_MAX(sync_args.start_offset, 0));
	}
}
