"""
generic_stim.py - the Python twin of generic_stim.h, for vsiBuild Python components.

Same roles, same formula, same CSV trace as the C++ header, so one checker
(check_traces.py) verifies a mixed C++/Python twin. apply_glue_generic.py copies this
file into <twin dir>/src and writes the calls into each Python component's user
regions. Keep produce() byte-identical in behaviour to generic_stim.h; the checker's
self-test compares the C++ formula with check_traces.py's copy, and this one is the
same expression.
"""

import os


def param(name, dflt):
    v = os.environ.get("TWIN_" + name, "")
    try:
        return int(v) if v else dflt
    except ValueError:
        return dflt


def period():
    p = param("PERIOD", 1)
    return p if p > 0 else 1


def produce(step, k, p=None):
    """the ONE produced value: an 8-bit counter distinct per signal (k), advancing every p steps"""
    if p is None:
        p = period()
    if p <= 0:
        p = 1
    return (k * 16 + step // p) & 0xFF


class Consumer(object):
    def __init__(self):
        self.last = 0
        self.changes = 0
        self.silent = 0
        self.seen = False

    def observe(self, v):
        if not self.seen:
            self.seen = True
            self.last = v
            return
        if v != self.last:
            self.changes += 1
            self.silent = 0
            self.last = v
        else:
            self.silent += 1


class Trace(object):
    """one CSV per component, <component>_trace.csv in the working directory, a row per step"""

    def __init__(self, component, columns):
        # The generated Makefiles run a C++ client from vsi.sim/_logs but a Python
        # client from the twin root (`python src/<c>/<c>.py`, measured 2026-09-08), so
        # write next to the C++ traces when that directory is visible from here.
        d = os.getcwd()
        if os.path.basename(d) != "_logs" and os.path.isdir(os.path.join(d, "vsi.sim", "_logs")):
            d = os.path.join(d, "vsi.sim", "_logs")
        self.path = os.path.join(d, component + "_trace.csv")
        self.columns = list(columns)
        self.header = False

    def row(self, step, time_ns, values):
        with open(self.path, "a" if self.header else "w") as fh:
            if not self.header:
                fh.write("step,time_ns" + "".join("," + c for c in self.columns) + "\n")
                self.header = True
            fh.write("{},{}".format(step, time_ns) + "".join("," + str(int(v)) for v in values) + "\n")


class Glue(object):
    """per-component state held in ONE module-level object, so the generated methods
    can advance the step without a `global` statement (a `global` after a read of the
    same name in the same method is a SyntaxError in Python)"""

    def __init__(self, component, columns, n_consumers):
        self.step = 0
        self.trace = Trace(component, columns)
        self.cons = [Consumer() for _ in range(n_consumers)]


class MutedSend(object):
    """wraps a gateway module attribute so that ONE method name becomes a no-op and
    every other attribute passes through; used to silence the unconditional CAN frame
    a consumer's generated stub would otherwise transmit each step"""

    def __init__(self, real, muted):
        self._real = real
        self._muted = muted

    def __getattr__(self, name):
        if name == self._muted:
            return lambda *a, **k: None
        return getattr(self._real, name)
