#!/usr/bin/env python3
"""Reduce a heatmovie session (samples.jsonl + events.jsonl) to cycle-averaged per-sensor movies.

For every run: align its cycles on heat_on, subtract each sensor's mean over the 2 s before, and
average the cycles in 0.25 s frames from -2 s to +19 s. Writes data.json for the page.
"""
import json
import sys

STEP, T0, T1, BASE = 0.25, -2.0, 19.0, 2.0
LABELS = {
    "core-S14": "One core (1 of 32) in shire S14, the centre",
    "shire-S0": "One shire, S0: the NW corner",
    "shire-S31": "One shire, S31: the SE corner",
    "shire-S14": "One shire, S14: the centre",
    "block-2x2-centre": "A 2×2 block in the centre (S13, S14, S21, S22)",
    "quadrant-NW": "The NW quadrant (9 shires)",
    "quadrant-SE": "The SE quadrant (8 shires)",
    "half-west": "The west half (18 shires)",
    "half-east": "The east half (14 shires)",
    "quadrant-NE": "The NE quadrant (6 shires)",
    "quadrant-SW": "The SW quadrant (9 shires)",
}
# Die frame (rows r0-r5, columns c1-c6) from the lab's placements.json; "M" = master/spare, "IO", "P" = PCIe.
GRID = [["S0", "S8", "S3", "M32", "IO", "P"],
        ["S24", "S16", "S4", "S12", "S20", "S28"],
        ["S9", "S1", "S13", "S21", "S29", "S5"],
        ["S25", "S17", "S14", "S22", "S30", "S6"],
        ["S2", "S10", "S18", "S26", "S15", "S7"],
        ["S11", "S19", "S27", "M33", "S23", "S31"]]


def main(d):
    S = []
    for line in open(d + "/samples.jsonl"):
        r = json.loads(line)
        if "c" in r and r.get("st", 0) == 0 and len(r["c"]) == 34:
            S.append(r)
    E = [json.loads(line) for line in open(d + "/events.jsonl")]
    times = [r["t"] for r in S]
    io_last = None
    for r in S:  # forward-fill the I/O sensor (read once a second) as sensor 34
        if r.get("io") is not None:
            io_last = r["io"]
        r["v"] = r["c"] + [io_last if io_last is not None else r["c"][0]]
    import bisect

    def window_mean(t_a, t_b):
        i, j = bisect.bisect_left(times, t_a), bisect.bisect_left(times, t_b)
        if j <= i:
            return None
        n = j - i
        return [sum(S[k]["v"][s] for k in range(i, j)) / n for s in range(35)]

    runs, order = {}, []
    for e in E:
        if e["ev"] == "heat_on":
            if e["run"] not in runs:
                runs[e["run"]] = {"name": e["run"], "shires": e["shires"], "per_shire": e["per_shire"],
                                  "mask": e["mask"], "cycles": []}
                order.append(e["run"])
            runs[e["run"]]["cycles"].append({"on": e["t"]})
        elif e["ev"] == "heat_off":
            runs[e["run"]]["cycles"][-1]["off"] = e["t"]
    nframes = int(round((T1 - T0) / STEP)) + 1
    rel = [round(T0 + i * STEP, 2) for i in range(nframes)]
    out_runs = []
    for name in order:
        R = runs[name]
        acc = [[0.0] * 35 for _ in range(nframes)]
        cnt = [0] * nframes
        base_acc, nb, changed = [0.0] * 35, 0, [0] * 35
        heat_s = []
        for c in R["cycles"]:
            base = window_mean(c["on"] - BASE, c["on"])
            if base is None:
                continue
            nb += 1
            for s in range(35):
                base_acc[s] += base[s]
            heat_s.append(c.get("off", c["on"]) - c["on"])
            lo_ix = bisect.bisect_left(times, c["on"])
            hi_ix = bisect.bisect_left(times, c["on"] + 10.0)
            for s in range(34):
                vals = {S[k]["c"][s] for k in range(max(lo_ix - 40, 0), hi_ix)}
                changed[s] += 1 if len(vals) > 1 else 0
            for f, tr in enumerate(rel):
                m = window_mean(c["on"] + tr - STEP / 2, c["on"] + tr + STEP / 2)
                if m is None:
                    continue
                cnt[f] += 1
                for s in range(35):
                    acc[f][s] += m[s] - base[s]
        frames = [[round(acc[f][s] / cnt[f], 3) if cnt[f] else 0.0 for s in range(35)] for f in range(nframes)]
        heated = [s for s in R["shires"]]
        others = [s for s in range(32) if s not in heated]
        hmean = [round(sum(fr[s] for s in heated) / len(heated), 3) for fr in frames]
        omean = [round(sum(fr[s] for s in others) / len(others), 3) for fr in frames]
        gmean = [round(sum(fr[:34]) / 34, 3) for fr in frames]
        contrast = [round(h - o, 3) for h, o in zip(hmean, omean)]
        ion = [i for i, t in enumerate(rel) if 0 <= t <= 10]
        fpk = max(ion, key=lambda i: contrast[i])
        pk_sensor = max(range(34), key=lambda s: frames[fpk][s])
        out_runs.append({
            "name": name, "label": LABELS.get(name, name), "shires": heated, "per_shire": R["per_shire"],
            "mask": R["mask"], "cycles": nb, "heat_s": round(sum(heat_s) / max(len(heat_s), 1), 2),
            "frames": frames, "heated_mean": hmean, "others_mean": omean, "global_mean": gmean,
            "contrast": contrast, "peak_contrast": contrast[fpk], "peak_t": rel[fpk],
            "peak_sensor": pk_sensor, "peak_sensor_heated": pk_sensor in heated,
            "peak_heated_dT": max(hmean[i] for i in ion), "peak_global_dT": max(gmean[i] for i in ion),
            "baseline": [round(base_acc[s] / max(nb, 1), 2) for s in range(35)],
            "sensors_changed": sum(1 for s in range(34) if changed[s] > 0),
        })
    dts = [b - a for a, b in zip(times, times[1:])]
    meta = {"samples": len(S), "median_dt_s": round(sorted(dts)[len(dts) // 2], 4), "t_start": times[0],
            "t_end": times[-1], "step_s": STEP, "t0": T0, "t1": T1, "base_s": BASE,
            "device_us_median": sorted(r["us"] for r in S)[len(S) // 2],
            "max_c": max(max(r["c"]) for r in S), "min_c": min(min(r["c"]) for r in S)}
    json.dump({"meta": meta, "grid": GRID, "times": rel, "runs": out_runs}, open(d + "/data.json", "w"))
    for r in out_runs:
        print("%-17s cyc %d heat %.1fs  heated peak %+.2f  global %+.2f  contrast %+.2f at %.2fs  peak sensor S%d %s  changed %d/34"
              % (r["name"], r["cycles"], r["heat_s"], r["peak_heated_dT"], r["peak_global_dT"], r["peak_contrast"],
                 r["peak_t"], r["peak_sensor"], "(heated)" if r["peak_sensor_heated"] else "", r["sensors_changed"]))
    print(json.dumps(meta))


if __name__ == "__main__":
    main(sys.argv[1])
