"""
vsi_delay.py - a variable-length delay line for Innexis VSI Python components.

One instance delays a signal by N simulation steps. N is set per instance from,
in order: an environment variable VSI_DELAY_<NAME>, a delay_config.json next to
this file, or the default. N can also change while running, either by calling
set_steps() or through a schedule in the config ({"at_step": new_steps}).

Instances chain: source -> VariableDelay(a) -> VariableDelay(b) -> sink delays
by a + b steps exactly, which test_delay_chain.py asserts. This module has no
VSI dependency, so that test runs with a plain interpreter. The only VSI-facing
line is in the generated component's "Before sending the packet" region:

    self.mySignals.tick_out = self.delay.step(self.mySignals.tick_in)

Transport latency added by the VSI fabric between components is NOT modelled
here; measure it once a twin runs and state it separately.
"""

import collections
import json
import os


class VariableDelay:
    def __init__(self, steps=1, initial=0, name="delay", schedule=None):
        if steps < 0:
            raise ValueError("steps must be >= 0")
        self.name = name
        self.initial = initial
        self.steps = 0
        self.buf = collections.deque()
        self.step_count = 0
        self.schedule = {int(k): int(v) for k, v in (schedule or {}).items()}
        self.set_steps(steps)

    # ---- behaviour ---------------------------------------------------------
    def step(self, value):
        """Push this step's input, return the value from `steps` steps ago."""
        if self.step_count in self.schedule:
            self.set_steps(self.schedule[self.step_count])
        self.step_count += 1
        if self.steps == 0:
            return value
        self.buf.append(value)
        return self.buf.popleft()

    def set_steps(self, steps):
        """Change the delay at run time, keeping the most recent samples."""
        if steps < 0:
            raise ValueError("steps must be >= 0")
        buf = collections.deque(self.buf)
        if steps == 0:
            buf.clear()
        elif len(buf) > steps:
            for _ in range(len(buf) - steps):
                buf.popleft()          # drop the oldest, shortening the line
        else:
            while len(buf) < steps:
                buf.appendleft(self.initial)   # lengthen with the initial value
        self.buf = buf
        self.steps = steps

    def __repr__(self):
        return "VariableDelay({}, steps={}, count={})".format(
            self.name, self.steps, self.step_count)

    # ---- configuration -----------------------------------------------------
    @classmethod
    def from_config(cls, name, default=1, initial=0, config_path=None):
        """Env var VSI_DELAY_<NAME> wins, then delay_config.json, then default."""
        env_key = "VSI_DELAY_" + name.upper()
        steps, schedule = default, None
        path = config_path or os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                           "delay_config.json")
        if os.path.isfile(path):
            with open(path, encoding="utf-8") as fh:
                cfg = json.load(fh).get(name)
            if isinstance(cfg, dict):
                steps = int(cfg.get("steps", default))
                schedule = cfg.get("schedule")
            elif cfg is not None:
                steps = int(cfg)
        if os.environ.get(env_key):
            steps = int(os.environ[env_key])
        inst = cls(steps=steps, initial=initial, name=name, schedule=schedule)
        print("  {}: delay {} step(s){}".format(
            name, steps, " with schedule " + str(inst.schedule) if inst.schedule else ""))
        return inst


def run_chain(delays, n_steps, source=lambda k: k + 1):
    """Drive source -> delays... -> sink for n_steps. Returns (sent, received)."""
    sent, received = [], []
    for k in range(n_steps):
        value = source(k)
        sent.append(value)
        for d in delays:
            value = d.step(value)
        received.append(value)
    return sent, received
