#!/usr/bin/env python3
# testCost.py - what one pass of commNmiHandler actually costs, in cycles, measured.
#
#   python3 testCost.py <disk.d64> <rate> [tag] [captureSeconds]
#
# The handler's cost does not depend on the line rate - it is the same code either way - so it can be
# measured on a link that is healthy and busy and then compared with the character period at any
# rate.  19200 is the right place to measure it: heavy traffic in both directions, and (per
# highRateReport.md and testStack.py) no wedge.
#
# Five tracepoints, all inside the handler, so every entry->exit pair can be classified:
#   $E685  the first instruction of the handler          - entry, with SP and the cycle counter
#   $E699  sta aciaRxByte                                - this pass took a character off the chip
#   $E6C9  dec uartPendingCount                          - this pass counted a transmitted byte off
#   $E6D6  jsr aciaPutData                               - this pass handed the chip a new byte
#   $E6E9  rti  /  $E67F  jmp (nmiChainVector)           - the two exits
# The cycle stamp VICE prints is taken before the traced instruction executes, so an entry->exit span
# excludes the 7 cycles the CPU spends taking the NMI and the 6 the RTI costs; both are added back
# where the report quotes a whole interrupt.
import os
import sys
import time

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from viceHarness import ViceSession, aciaArgs, SCRATCH
from testTwoMachines import NullModemRelay, pickModemOpponent, answerModemPrompts
from testRealtime import (BAUD_INDEX, LOGS, NTSC_HZ, SHOTS, command, decodeControl, pauseAll,
                          resumeAll, snapshot, showState, waitForPhase)
from testHighRates import moduleResident
from testWedge import TRACE_LINE, captureTrace

ENTRY = 0xE685
RX_BYTE = 0xE699
TX_DEC = 0xE6C9
TX_PUT = 0xE6D6
EXITS = (0xE6E9, 0xE67F)
BITS_PER_CHAR = 10                      # 8N1


def classify(body, span, tag):
    events = [(int(pc, 16), int(sp, 16), int(cycle)) for pc, sp, cycle in TRACE_LINE.findall(body)]
    passes = []
    current = None
    for pc, sp, cycle in events:
        if pc == ENTRY:
            if current is not None:
                current["nested"] = True
            current = {"start": cycle, "sp": sp, "rx": False, "tx": False, "put": False,
                       "nested": False}
        elif current is None:
            continue
        elif pc == RX_BYTE:
            current["rx"] = True
        elif pc == TX_DEC:
            current["tx"] = True
        elif pc == TX_PUT:
            current["put"] = True
        elif pc in EXITS:
            current["cycles"] = cycle - current["start"]
            passes.append(current)
            current = None
    groups = {}
    for row in passes:
        if "cycles" not in row:
            continue
        name = ("receive+transmit" if row["rx"] and row["put"] else
                "receive only" if row["rx"] else
                "transmit only" if row["put"] else
                "transmit, ring ran dry" if row["tx"] else "neither half had work")
        groups.setdefault(name, []).append(row["cycles"])
    cycles = span[1] - span[0]
    print(f"\n  ---- one pass of commNmiHandler [{tag}] ----", flush=True)
    print(f"    window {cycles} cycles = {cycles / NTSC_HZ:.3f} emulated s, "
          f"{len(passes)} complete passes = {len(passes) / (cycles / NTSC_HZ):.0f}/s", flush=True)
    out = {}
    for name, values in sorted(groups.items(), key=lambda item: -len(item[1])):
        values.sort()
        whole = [value + 13 for value in values]     # + 7 to take the NMI, + 6 for the RTI
        print(f"    {name}: {len(values)} passes, trace-to-trace min {values[0]} median "
              f"{values[len(values) // 2]} max {values[-1]} cycles; whole interrupt "
              f"{whole[len(whole) // 2]} cycles", flush=True)
        out[name] = {"count": len(values), "min": values[0], "median": values[len(values) // 2],
                     "max": values[-1], "wholeInterrupt": whole[len(whole) // 2]}
    busy = sum(row["cycles"] for row in passes if "cycles" in row) + 13 * len(passes)
    print(f"    duty cycle in this window: {100.0 * busy / cycles:.1f}%", flush=True)
    print("\n    what that costs per character time, at each rate the table offers:", flush=True)
    print(f"    {'rate':>6} {'char period':>12} {'one pass':>9} {'two passes':>11} "
          f"{'verdict':>28}", flush=True)
    full = out.get("receive+transmit", {}).get("wholeInterrupt")
    one = out.get("receive only", {}).get("wholeInterrupt") or full
    two = (one or 0) + (out.get("transmit only", {}).get("wholeInterrupt") or one or 0)
    for rate in (300, 1200, 2400, 4800, 9600, 19200, 38400):
        period = NTSC_HZ / (rate / BITS_PER_CHAR)
        share = 100.0 * (two or 0) / period
        verdict = "cannot keep up" if share >= 100 else f"{share:.0f}% of the CPU"
        print(f"    {rate:>6} {period:>11.1f}c {one:>8}c {two:>10}c {verdict:>28}", flush=True)
    return out


def main():
    disk = os.path.abspath(sys.argv[1])
    rate = int(sys.argv[2])
    tag = sys.argv[3] if len(sys.argv) > 3 else "c"
    seconds = float(sys.argv[4]) if len(sys.argv) > 4 else 10.0
    os.makedirs(SHOTS, exist_ok=True)
    os.makedirs(LOGS, exist_ok=True)
    relay = NullModemRelay()
    args = aciaArgs(rsDevAddress=f"127.0.0.1:{relay.port}", baud=38400)
    a = ViceSession(disk, f"{SCRATCH}/cost.{tag}.a.vice.log", args, label="A", warp=False)
    b = ViceSession(disk, f"{SCRATCH}/cost.{tag}.b.vice.log", args, label="B", warp=False)
    sessions = [(a, "A"), (b, "B")]
    try:
        a.connect()
        b.connect()
        for session, _ in sessions:
            command(session, "warp on")
        a.bootPastLoader(waitSecs=900)
        b.bootPastLoader(waitSecs=900)
        pauseAll(sessions)
        for session, _ in sessions:
            command(session, "warp off")
        resumeAll(sessions)
        a.findWindow()
        b.findWindow()
        a.focus()
        b.focus()
        time.sleep(25)
        pickModemOpponent(a, "A", "a")
        pickModemOpponent(b, "B", "o")
        for session, sessionTag in sessions:
            if not moduleResident(session, sessionTag):
                raise SystemExit(f"{sessionTag}: the opponent module never loaded")
            pauseAll([(session, sessionTag)])
            command(session, f"> e055 {BAUD_INDEX[rate]:02x}")
            resumeAll([(session, sessionTag)])
        answerModemPrompts(a, "A", "a")
        answerModemPrompts(b, "B", "o")
        ok, phases = waitForPhase(sessions, 3, 180)
        print(f"both in the packet phase: {ok} ({phases})", flush=True)
        for state in [snapshot(session, sessionTag) for session, sessionTag in sessions]:
            print(f"  [{state['tag']}] control {decodeControl(state['aciaControl'])}", flush=True)
            showState(state)
        addresses = [ENTRY, RX_BYTE, TX_DEC, TX_PUT] + list(EXITS)
        results = {}
        for round in range(2):
            for session, sessionTag in sessions:
                body, span = captureTrace(session, sessionTag, addresses, seconds, 8 << 20,
                                          f"{LOGS}/cost.{tag}.{sessionTag}.{round}.trace.txt")
                results[f"{sessionTag}{round}"] = classify(body, span, f"{sessionTag} round {round}")
        print("\n================ SUMMARY ================", flush=True)
        for key, value in results.items():
            print(f"  {key}: {value}", flush=True)
    finally:
        a.close()
        b.close()
        time.sleep(1)
        relay.stop()
        for index in (0, 1):
            print(f"[relay] conn{index} total {len(relay.log[index])} bytes", flush=True)


if __name__ == "__main__":
    main()
