// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of either the GNU Lesser General Public License Version 3
// or the Perl Artistic License Version 2.0.
// SPDX-FileCopyrightText: 2021 Wilson Snyder and Marlon James
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//*************************************************************************

// Setup multiple one-time callbacks with different time delays.
// Ensure they are not called before the delay has elapsed.

#include "vpi_user.h"

#include <cstdlib>

// These require the above. Comment prevents clang-format moving them
#include "TestSimulator.h"
#include "TestVpi.h"

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>

typedef struct {
    PLI_UINT32 count;
    PLI_UINT32* exp_times;
    PLI_UINT32 number_of_exp_times;
} cb_stats;

static cb_stats CallbackStats[cbAtEndOfSimTime + 1];

int errors = 0;

static vpiHandle ValueHandle, ToggleHandle, ClockHandle;

#ifdef TEST_VERBOSE
bool verbose = true;
#else
bool verbose = false;
#endif

#define STRINGIFY_CB_CASE(_cb) \
    case _cb: return #_cb

static const char* cb_reason_to_string(int cb_name) {
    switch (cb_name) {
        STRINGIFY_CB_CASE(cbAtStartOfSimTime);
        STRINGIFY_CB_CASE(cbReadWriteSynch);
        STRINGIFY_CB_CASE(cbReadOnlySynch);
        STRINGIFY_CB_CASE(cbNextSimTime);
        STRINGIFY_CB_CASE(cbStartOfSimulation);
        STRINGIFY_CB_CASE(cbEndOfSimulation);
        STRINGIFY_CB_CASE(cbAtEndOfSimTime);
    default: return "Unsupported callback";
    }
}

#undef STRINGIFY_CB_CASE

bool cb_time_is_delay(int cb_name) {
    // For some callbacks, time is interpreted as a delay from current time
    // instead of an absolute time
    if (cb_name == cbReadOnlySynch || cb_name == cbReadWriteSynch) return true;
    return false;
}

vpiHandle test_vpi_register_cb(p_cb_data cb_datap) {
    if (verbose)
        vpi_printf(const_cast<char*>("- test_vpi_register_cb(%s @%d)\n"),
                   strFromVpiCallbackReason(cb_datap->reason),
                   (cb_datap->time ? cb_datap->time->low : 0));
    return vpi_register_cb(cb_datap);
}

// forward declaration
static PLI_INT32 TheCallback(s_cb_data* data);

static PLI_INT32 AtEndOfSimTimeCallback(s_cb_data* data) {
    s_vpi_time t;

    CHECK_RESULT(data->reason, cbAtEndOfSimTime);
    cb_stats* stats = &CallbackStats[data->reason];

    t.type = vpiSimTime;
    vpi_get_time(0, &t);

    if (verbose)
        vpi_printf(const_cast<char*>("- [@%d] AtEndOfSimTime Callback (count=%d)\n"), t.low,
                   stats->count);

    CHECK_RESULT(stats->count < stats->number_of_exp_times, 1);
    CHECK_RESULT(t.low, stats->exp_times[stats->count]);
    stats->count += 1;

    s_cb_data cb_data{};
    s_vpi_time time = {vpiSimTime, 0, 417, 0};  // non-zero time to check that it's ignored
    cb_data.time = &time;
    cb_data.reason = cbNextSimTime;
    cb_data.cb_rtn = TheCallback;
    vpiHandle Handle = test_vpi_register_cb(&cb_data);
    CHECK_RESULT_NZ(Handle);

    return 0;
}

static PLI_INT32 TheCallback(s_cb_data* data) {
    s_vpi_time t;

    cb_stats* stats = &CallbackStats[data->reason];

    t.type = vpiSimTime;
    vpi_get_time(0, &t);

    if (verbose) {
        vpi_printf(const_cast<char*>("- [@%d] %s Callback (count=%d)\n"), t.low,
                   cb_reason_to_string(data->reason), stats->count);
    }

    CHECK_RESULT(stats->count < stats->number_of_exp_times, 1);
    CHECK_RESULT(t.low, stats->exp_times[stats->count]);
    stats->count += 1;

    if (stats->count >= stats->number_of_exp_times) return 0;

    s_cb_data cb_data{};
    PLI_UINT32 next_time;

    if (data->reason == cbNextSimTime) {
        // if a cbNextSimTime calback is scheduled from
        // another cbNextSimTime callback, it will
        // be called in the same timestep, so we need
        // to delay registering
        next_time = t.low;
        cb_data.reason = cbAtEndOfSimTime;
        cb_data.cb_rtn = AtEndOfSimTimeCallback;
    } else {
        next_time = stats->exp_times[stats->count];
        if (cb_time_is_delay(data->reason)) next_time -= t.low;
        cb_data.reason = data->reason;
        cb_data.cb_rtn = TheCallback;
    }

    if (verbose) {
        vpi_printf(const_cast<char*>("- [@%d] Registering %s Callback with time = %d\n"), t.low,
                   cb_reason_to_string(cb_data.reason), next_time);
    }

    s_vpi_time time = {vpiSimTime, 0, next_time, 0};
    cb_data.time = &time;
    vpiHandle Handle = test_vpi_register_cb(&cb_data);
    CHECK_RESULT_NZ(Handle);

    return 0;
}

static PLI_INT32 StartOfSimulationCallback(s_cb_data* data) {
    s_cb_data cb_data{};
    s_vpi_time timerec = {vpiSimTime, 0, 0, 0};

    s_vpi_time t;
    t.type = vpiSimTime;
    vpi_get_time(0, &t);

    if (verbose) vpi_printf(const_cast<char*>("- [@%d] cbStartOfSimulation Callback\n"), t.low);

    CHECK_RESULT(t.low, CallbackStats[data->reason].exp_times[0]);
    CallbackStats[data->reason].count += 1;

    cb_data.time = &timerec;
    cb_data.value = 0;
    cb_data.user_data = 0;
    cb_data.obj = 0;
    cb_data.cb_rtn = TheCallback;

    CallbackStats[cbAtStartOfSimTime].exp_times = new PLI_UINT32[3]{5, 15, 20};
    CallbackStats[cbAtStartOfSimTime].number_of_exp_times = 3;
    timerec.low = 5;
    cb_data.reason = cbAtStartOfSimTime;
    vpiHandle ASOSHandle = test_vpi_register_cb(&cb_data);
    CHECK_RESULT_NZ(ASOSHandle);

    CallbackStats[cbReadWriteSynch].exp_times = new PLI_UINT32[3]{6, 16, 21};
    CallbackStats[cbReadWriteSynch].number_of_exp_times = 3;
    timerec.low = 6;
    cb_data.reason = cbReadWriteSynch;
    vpiHandle RWHandle = test_vpi_register_cb(&cb_data);
    CHECK_RESULT_NZ(RWHandle);

    CallbackStats[cbReadOnlySynch].exp_times = new PLI_UINT32[3]{7, 17, 22};
    CallbackStats[cbReadOnlySynch].number_of_exp_times = 3;
    timerec.low = 7;
    cb_data.reason = cbReadOnlySynch;
    vpiHandle ROHandle = test_vpi_register_cb(&cb_data);
    CHECK_RESULT_NZ(ROHandle);

    CallbackStats[cbNextSimTime].exp_times
        = new PLI_UINT32[10]{5, 6, 7, 10, 15, 16, 17, 20, 21, 22};
    CallbackStats[cbNextSimTime].number_of_exp_times = 10;
    timerec.low = 8;
    cb_data.reason = cbNextSimTime;
    vpiHandle NSTHandle = test_vpi_register_cb(&cb_data);
    CHECK_RESULT_NZ(NSTHandle);

    CallbackStats[cbAtEndOfSimTime].exp_times = new PLI_UINT32[9]{5, 6, 7, 10, 15, 16, 17, 20, 21};
    CallbackStats[cbAtEndOfSimTime].number_of_exp_times = 9;

    return (0);
}

static int EndOfSimulationCallback(p_cb_data cb_data) {
    s_vpi_time t;
    t.type = vpiSimTime;
    vpi_get_time(0, &t);

    (void)cb_data;  // Unused

    if (verbose) vpi_printf(const_cast<char*>("- [@%d] cbEndOfSimulation Callback\n"), t.low);

    CHECK_RESULT(t.low, CallbackStats[cbEndOfSimulation].exp_times[0]);
    CallbackStats[cbEndOfSimulation].count += 1;

    CHECK_RESULT(CallbackStats[cbStartOfSimulation].count, 1);
    CHECK_RESULT(CallbackStats[cbAtStartOfSimTime].count, 3);
    CHECK_RESULT(CallbackStats[cbNextSimTime].count, 10);
    CHECK_RESULT(CallbackStats[cbReadWriteSynch].count, 3);
    CHECK_RESULT(CallbackStats[cbReadOnlySynch].count, 3);
    CHECK_RESULT(CallbackStats[cbAtEndOfSimTime].count, 9);
    CHECK_RESULT(CallbackStats[cbEndOfSimulation].count, 1);

    if (!errors) printf("*-* All Finished *-*\n");

    return 0;
}

// cver entry
static void VPIRegister(void) {
    if (verbose) vpi_printf(const_cast<char*>("- VPIRegister callback\n"));

    // Clear stats
    for (int cb = 1; cb <= cbAtEndOfSimTime; cb++) CallbackStats[cb].count = 0;
    CallbackStats[cbStartOfSimulation].exp_times = new PLI_UINT32(0);
    CallbackStats[cbEndOfSimulation].exp_times = new PLI_UINT32(100);
    s_cb_data cb_data{};
    s_vpi_time timerec = {vpiSuppressTime, 0, 0, 0};

    cb_data.time = &timerec;
    cb_data.value = 0;
    cb_data.user_data = 0;
    cb_data.obj = 0;
    cb_data.reason = cbStartOfSimulation;
    cb_data.cb_rtn = StartOfSimulationCallback;
    test_vpi_register_cb(&cb_data);

    cb_data.reason = cbEndOfSimulation;
    cb_data.cb_rtn = EndOfSimulationCallback;
    test_vpi_register_cb(&cb_data);
}

// simulator entry
void (*vlog_startup_routines[])(void) = {VPIRegister, 0};
