// msp2_stim.h - stimulus and monitor behaviour for the MSP-2 Candidate A twin.
//
// Header-only, no VSI dependency, so the same code is proven host-side by
// test_msp2_stim.cxx and then runs inside the generated VSI clients, where
// apply_glue_A.py injects one or two lines per user region that call into it.
//
// Scenario (steps; the twin runs simulationStep = 100 us, totalSimTime = 10 ms,
// i.e. 100 steps; every number is overridable by an environment variable
// MSP2_<NAME> so a demo can be re-shaped without a rebuild):
//   PPS_PERIOD   10  time source pulses TIME_1PPS every 10 steps ("1 Hz" scaled to the run)
//   IRIGB_START  43200  IRIG-B time of day at step 0 (seconds; noon)
//   INSERT_AT     3  card reports CARD_PRESENT from this step (before it the chassis sees ABSENT)
//   BOOT_STEPS   20  card asserts HEALTH_OK from this step
//   FAULT_AT     60  card raises BIT_FAIL for FAULT_LEN steps (built-in-test failure)
//   FAULT_LEN     5
//   STALL_AT     70  card's safety heartbeat SAFETY_MON stops for STALL_LEN steps
//   STALL_LEN    15
//   WD_TIMEOUT    5  safety monitor trips on the 5th consecutive step without a heartbeat change
//                    (stall at 70 -> trip at step 74, cleared at 85 when the heartbeat moves again)
//   SLOT_ID       3  backplane slot identifier the card reports
//   FRAME_PERIOD  5  card sends one SAFETY_BUS frame every 5 steps
//
// What is stimulus and what is reaction: TimeSource, CardDiscretes and
// SafetyHeartbeat are SCRIPTED from the step count (the card's satellites are
// separate VSI processes and share no state, so the card's timeline is the step
// count). CardTimeLock, ChassisMonitor and Watchdog REACT to what arrives on
// their inputs; that is the part a sim proves.
#ifndef MSP2_STIM_H
#define MSP2_STIM_H

#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>

namespace msp2 {

inline long param(const char *name, long dflt) {
    std::string key = std::string("MSP2_") + name;
    const char *v = std::getenv(key.c_str());
    return (v && *v) ? std::atol(v) : dflt;
}

// One CSV per component in the client's working directory: step,time_ns,<columns...>
class Trace {
  public:
    Trace(const std::string &component, const std::vector<std::string> &columns)
        : path_(component + "_trace.csv"), columns_(columns), header_(false) {}
    void row(long step, long long time_ns, const std::vector<long> &values) {
        FILE *fh = std::fopen(path_.c_str(), header_ ? "a" : "w");
        if (!fh) return;
        if (!header_) {
            std::fprintf(fh, "step,time_ns");
            for (size_t i = 0; i < columns_.size(); ++i) std::fprintf(fh, ",%s", columns_[i].c_str());
            std::fprintf(fh, "\n");
            header_ = true;
        }
        std::fprintf(fh, "%ld,%lld", step, time_ns);
        for (size_t i = 0; i < values.size(); ++i) std::fprintf(fh, ",%ld", values[i]);
        std::fprintf(fh, "\n");
        std::fclose(fh);
    }
  private:
    std::string path_;
    std::vector<std::string> columns_;
    bool header_;
};

// ---- scripted stimulus ----------------------------------------------------

struct TimeSource {              // MSP2 Platform Time Source -> card bpTime
    long pps_period, irigb_start;
    int pps, irigb, refclk;
    TimeSource() : pps_period(param("PPS_PERIOD", 10)), irigb_start(param("IRIGB_START", 43200)),
                   pps(0), irigb(0), refclk(0) {}
    void step(long n) {
        pps = (pps_period > 0 && n % pps_period == 0) ? 1 : 0;
        irigb = (int)(irigb_start + (pps_period > 0 ? n / pps_period : 0));
        refclk = (int)(n & 1);                       // free-running reference clock, toggles every step
    }
};

struct CardDiscretes {           // card bpDisc -> chassis controller
    long insert_at, boot_steps, fault_at, fault_len, slot_id;
    int bit_fail, health_ok, card_present;
    CardDiscretes() : insert_at(param("INSERT_AT", 3)), boot_steps(param("BOOT_STEPS", 20)),
                      fault_at(param("FAULT_AT", 60)), fault_len(param("FAULT_LEN", 5)),
                      slot_id(param("SLOT_ID", 3)), bit_fail(0), health_ok(0), card_present(0) {}
    void step(long n) {
        card_present = (n >= insert_at) ? 1 : 0;
        bit_fail = (card_present && n >= fault_at && n < fault_at + fault_len) ? 1 : 0;
        health_ok = (card_present && n >= boot_steps && !bit_fail) ? 1 : 0;
    }
};

struct SafetyHeartbeat {         // card bpSafety -> safety monitor
    long stall_at, stall_len, frame_period;
    int mon, bus, inhibit;
    SafetyHeartbeat() : stall_at(param("STALL_AT", 70)), stall_len(param("STALL_LEN", 15)),
                        frame_period(param("FRAME_PERIOD", 5)), mon(0), bus(0), inhibit(0) {}
    void step(long n) {
        bool stalled = (n >= stall_at && n < stall_at + stall_len);
        if (!stalled) mon += 1;                       // alive counter: a stall is "no change"
        if (frame_period > 0 && n % frame_period == 0) bus += 1;   // one safety frame
        inhibit = 0;   // the real signal is an inhibit INTO the card; the generator guessed the
                       // card as source for this bus, so the card sends a constant 0 and says so
    }
};

// ---- reactions --------------------------------------------------------------

struct CardTimeLock {            // card bpTime input: counts 1PPS edges, checks REFCLK moves
    int last_pps, last_refclk, pulses, refclk_stuck;
    bool locked;
    CardTimeLock() : last_pps(0), last_refclk(-1), pulses(0), refclk_stuck(0), locked(false) {}
    void observe(int pps, int irigb, int refclk) {
        (void)irigb;
        if (pps == 1 && last_pps == 0) pulses += 1;
        if (last_refclk >= 0 && refclk == last_refclk) refclk_stuck += 1;
        last_pps = pps;
        last_refclk = refclk;
        locked = pulses >= 3;
    }
};

struct ChassisMonitor {          // chassis controller: card state from the discretes
    enum State { ABSENT = 0, BOOTING = 1, HEALTHY = 2, FAULT = 3 };
    State state;
    int transitions, faults;
    ChassisMonitor() : state(ABSENT), transitions(0), faults(0) {}
    static const char *name(State s) {
        switch (s) { case ABSENT: return "ABSENT"; case BOOTING: return "BOOTING";
                     case HEALTHY: return "HEALTHY"; default: return "FAULT"; }
    }
    State observe(int present, int health, int bit_fail) {
        State next = !present ? ABSENT : bit_fail ? FAULT : health ? HEALTHY : BOOTING;
        if (next != state) {
            transitions += 1;
            if (next == FAULT) faults += 1;
            state = next;
        }
        return state;
    }
};

struct Watchdog {                // safety monitor: heartbeat must change within WD_TIMEOUT steps
    long timeout;
    int last_mon, silent, tripped, trips;
    bool seen;
    Watchdog() : timeout(param("WD_TIMEOUT", 5)), last_mon(0), silent(0), tripped(0), trips(0), seen(false) {}
    void observe(int mon) {
        if (!seen) { seen = true; last_mon = mon; silent = 0; return; }
        if (mon != last_mon) { silent = 0; last_mon = mon; if (tripped) tripped = 0; }
        else { silent += 1; if (!tripped && silent >= timeout) { tripped = 1; trips += 1; } }
    }
};

}  // namespace msp2
#endif
