"""
apply_glue_A.py - put the stimulus behaviour into the Candidate A skeletons vsiBuild generated.

    python apply_glue_A.py <workspace_A>\\Msp2DT_A

Copies msp2_stim.h into <twin>\\src and rewrites named "user custom code regions"
in each component's .cxx (C++ '//' markers; same discipline as the Python
delay-chain template). Everything outside the regions is vsiBuild's. Re-run after
any `generate -overwrite`, then rebuild with `mingw32-make compile build` in the
twin folder (no need to regenerate).

Exit status 1 if a component or region is missing, and it says which.
"""

import os
import re
import shutil
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
INC = ['#include "../msp2_stim.h"']


def detect_card(src):
    cands = [d for d in os.listdir(src) if d.startswith("MSP2_") and "_LB_" in d and "__" not in d]
    if len(cands) != 1:
        sys.exit("expected exactly one card component MSP2_*_LB_* under {}, found {}".format(src, cands))
    return cands[0]

# component -> {region -> lines}. Regions used: "Global Variables & Definitions"
# (top of the .cxx), "Before sending the packet" (per step, outputs go into
# mySignals here), "After sending the packet" (per step, inputs already unpacked
# by the previous step's callback; trace + step counter).
def glue_for(CARD):
  return {
      "MSP2_Platform_Time_Source": {
          "Global Variables & Definitions": INC + [
              "static long g_step = 0; static msp2::TimeSource g_ts;",
              'static msp2::Trace g_trace("MSP2_Platform_Time_Source", {"TIME_1PPS","TIME_IRIGB","REFCLK"});'],
          "Before sending the packet": [
              "g_ts.step(g_step);",
              "mySignals.ptsMsp2Time_TIME_1PPS = g_ts.pps; mySignals.ptsMsp2Time_TIME_IRIGB = g_ts.irigb; mySignals.ptsMsp2Time_REFCLK = g_ts.refclk;"],
          "After sending the packet": [
              "g_trace.row(g_step, convert.timeInNs(), {g_ts.pps, g_ts.irigb, g_ts.refclk}); g_step++;"],
      },
      CARD: {
          "Global Variables & Definitions": INC + [
              "static long g_step = 0; static msp2::CardTimeLock g_lock;",
              'static msp2::Trace g_trace("' + CARD + '", {"in_TIME_1PPS","in_TIME_IRIGB","in_REFCLK","pulses","locked","refclk_stuck"});'],
          "After sending the packet": [
              "g_lock.observe(mySignals.bpTime_TIME_1PPS, mySignals.bpTime_TIME_IRIGB, mySignals.bpTime_REFCLK);",
              "if (g_lock.locked && g_lock.pulses == 3 && mySignals.bpTime_TIME_1PPS) cout << \"\\n  [card] TIME LOCK acquired at step \" << g_step << \"\\n\";",
              "g_trace.row(g_step, convert.timeInNs(), {mySignals.bpTime_TIME_1PPS, mySignals.bpTime_TIME_IRIGB, mySignals.bpTime_REFCLK, g_lock.pulses, g_lock.locked ? 1 : 0, g_lock.refclk_stuck}); g_step++;"],
      },
      CARD + "__bpDisc": {
          "Global Variables & Definitions": INC + [
              "static long g_step = 0; static msp2::CardDiscretes g_cd;",
              'static msp2::Trace g_trace("' + CARD + '__bpDisc", {"BIT_FAIL","HEALTH_OK","CARD_PRESENT","SLOT_ID"});'],
          "Before sending the packet": [
              "g_cd.step(g_step);",
              "mySignals.bpDisc_BIT_FAIL = g_cd.bit_fail; mySignals.bpDisc_HEALTH_OK = g_cd.health_ok; mySignals.bpDisc_CARD_PRESENT = g_cd.card_present; mySignals.bpDisc_SLOT_ID = (int)g_cd.slot_id;"],
          "After sending the packet": [
              "g_trace.row(g_step, convert.timeInNs(), {g_cd.bit_fail, g_cd.health_ok, g_cd.card_present, g_cd.slot_id}); g_step++;"],
      },
      CARD + "__bpSafety": {
          "Global Variables & Definitions": INC + [
              "static long g_step = 0; static msp2::SafetyHeartbeat g_hb;",
              'static msp2::Trace g_trace("' + CARD + '__bpSafety", {"SAFETY_BUS","SAFETY_INHIBIT","SAFETY_MON"});'],
          "Before sending the packet": [
              "g_hb.step(g_step);",
              "mySignals.bpSafety_SAFETY_BUS = g_hb.bus; mySignals.bpSafety_SAFETY_INHIBIT = g_hb.inhibit; mySignals.bpSafety_SAFETY_MON = g_hb.mon;"],
          "After sending the packet": [
              "g_trace.row(g_step, convert.timeInNs(), {g_hb.bus, g_hb.inhibit, g_hb.mon}); g_step++;"],
      },
      "MSP2_Platform_Chassis_Controller": {
          "Global Variables & Definitions": INC + [
              "static long g_step = 0; static msp2::ChassisMonitor g_ch;",
              'static msp2::Trace g_trace("MSP2_Platform_Chassis_Controller", {"in_BIT_FAIL","in_HEALTH_OK","in_CARD_PRESENT","in_SLOT_ID","state","transitions","faults"});'],
          "After sending the packet": [
              "{ int before = g_ch.state; int st = g_ch.observe(mySignals.ccMsp2Disc_CARD_PRESENT, mySignals.ccMsp2Disc_HEALTH_OK, mySignals.ccMsp2Disc_BIT_FAIL);",
              "  if (st != before) cout << \"\\n  [chassis] card in slot \" << mySignals.ccMsp2Disc_SLOT_ID << \" -> \" << msp2::ChassisMonitor::name(g_ch.state) << \" at step \" << g_step << \"\\n\";",
              "  g_trace.row(g_step, convert.timeInNs(), {mySignals.ccMsp2Disc_BIT_FAIL, mySignals.ccMsp2Disc_HEALTH_OK, mySignals.ccMsp2Disc_CARD_PRESENT, mySignals.ccMsp2Disc_SLOT_ID, st, g_ch.transitions, g_ch.faults}); g_step++; }"],
      },
      "MSP2_Platform_Safety_Monitor": {
          "Global Variables & Definitions": INC + [
              "static long g_step = 0; static msp2::Watchdog g_wd;",
              'static msp2::Trace g_trace("MSP2_Platform_Safety_Monitor", {"in_SAFETY_BUS","in_SAFETY_INHIBIT","in_SAFETY_MON","silent","tripped","trips"});'],
          "After sending the packet": [
              "{ int before = g_wd.tripped; g_wd.observe(mySignals.psmMsp2Safety_SAFETY_MON);",
              "  if (g_wd.tripped != before) cout << \"\\n  [safety monitor] heartbeat \" << (g_wd.tripped ? \"LOST: watchdog TRIPPED\" : \"back: watchdog cleared\") << \" at step \" << g_step << \"\\n\";",
              "  g_trace.row(g_step, convert.timeInNs(), {mySignals.psmMsp2Safety_SAFETY_BUS, mySignals.psmMsp2Safety_SAFETY_INHIBIT, mySignals.psmMsp2Safety_SAFETY_MON, g_wd.silent, g_wd.tripped, g_wd.trips}); g_step++; }"],
      },
      "MSP2_Platform_Mission_Data_Recorder": {
          "Global Variables & Definitions": INC + [
              "static long g_step = 0;",
              'static msp2::Trace g_trace("MSP2_Platform_Mission_Data_Recorder", {"MDATA0","MDATA1","MDATA2","MDATA3"});'],
          "After sending the packet": [
              "// Ethernet bus: sockets only, no signal connection in this twin, so these stay at their unpacked value",
              "g_trace.row(g_step, convert.timeInNs(), {mySignals.mdrMsp2Data_MDATA0, mySignals.mdrMsp2Data_MDATA1, mySignals.mdrMsp2Data_MDATA2, mySignals.mdrMsp2Data_MDATA3}); g_step++;"],
      },
  }


START = re.compile(r"^(\s*)// Start of user custom code region\. Please apply edits only within these regions:\s+(.*?)\s*$")
END = re.compile(r"^\s*// End of user custom code region\.")


def patch(path, regions):
    with open(path, encoding="utf-8") as fh:
        lines = fh.read().split("\n")
    out, i, seen = [], 0, set()
    while i < len(lines):
        m = START.match(lines[i])
        if not m or m.group(2) not in regions or m.group(2) in seen:
            out.append(lines[i])
            i += 1
            continue
        indent, name = m.group(1), m.group(2)
        out.append(lines[i])
        i += 1
        while i < len(lines) and not END.match(lines[i]):
            i += 1
        if i >= len(lines):
            return "region {!r} has no end marker".format(name)
        out.append(indent + "// glue from msp2_glue/apply_glue.py")
        for g in regions[name]:
            out.append(indent + g)
        out.append(lines[i])
        seen.add(name)
        i += 1
    missing = set(regions) - seen
    if missing:
        return "regions not found: {}".format(sorted(missing))
    with open(path, "w", encoding="utf-8", newline="\n") as fh:
        fh.write("\n".join(out))
    return None


def main():
    if len(sys.argv) != 2:
        sys.exit(__doc__)
    twin = sys.argv[1]
    src = os.path.join(twin, "src")
    if not os.path.isdir(src):
        sys.exit("no src dir under {}".format(twin))
    shutil.copy(os.path.join(HERE, "msp2_stim.h"), os.path.join(src, "msp2_stim.h"))
    print("copied msp2_stim.h ->", src)
    card = detect_card(src)
    print("card component:", card)
    problems = []
    for comp, regions in glue_for(card).items():
        path = os.path.join(src, comp, comp + ".cxx")
        if not os.path.isfile(path):
            problems.append("{}: {} not found".format(comp, path))
            continue
        err = patch(path, regions)
        if err:
            problems.append("{}: {}".format(comp, err))
        else:
            print("patched", comp, "regions:", ", ".join(regions))
    if problems:
        print("PROBLEMS:")
        for p in problems:
            print("  " + p)
        sys.exit(1)


if __name__ == "__main__":
    main()
