#!/usr/bin/env python3
# testGuard.py - is the NMI re-entrancy guard actually being used, and how often?
#
#   python3 testGuard.py <disk.d64> <rate> [tag] [windowSeconds]
#
# The guard is only interesting if second NMIs really do arrive while a pass of commNmiHandler is
# running.  Two tracepoints answer that with the emulator's own counters:
#
#   trace exec e685    - every entry to the handler
#   trace exec e6f0    - nmiLeaveNote: the INC that a nested entry leaves behind instead of running
#                        the handler body
#
# Both are armed for a short window on a live 38400-baud link and the lines are counted, so the
# result is "N entries, M of them nested", measured rather than argued.  A trace does not stop the
# machine, but it does put a line on the monitor socket per hit, so the window is deliberately short.
import os
import re
import sys
import time

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from viceHarness import ViceSession, aciaArgs, readByte, ACIA_BASE, SCRATCH
from testTwoMachines import NullModemRelay, pickModemOpponent, answerModemPrompts
from testRealtime import (BAUD_INDEX, LOGS, NTSC_HZ, command, flush, pauseAll, resumeAll,
                          stopwatch, waitForPhase)
from testHighRates import moduleResident
from testStack import collect

ENTRY = 0xE685
NOTE = 0xE6F0                           # nmiLeaveNote - keep in step with the source


def main():
    disk = os.path.abspath(sys.argv[1])
    rate = int(sys.argv[2])
    tag = sys.argv[3] if len(sys.argv) > 3 else "g"
    window = float(sys.argv[4]) if len(sys.argv) > 4 else 5.0
    os.makedirs(LOGS, exist_ok=True)
    relay = NullModemRelay()
    a = ViceSession(disk, os.path.join(LOGS, f"vice.{tag}.A.log"),
                    aciaArgs(rsDevAddress=f"127.0.0.1:{relay.port}"), label="A", warp=False)
    b = ViceSession(disk, os.path.join(LOGS, f"vice.{tag}.B.log"),
                    aciaArgs(rsDevAddress=f"127.0.0.1:{relay.port}", dev=1), label="B", warp=False)
    sessions = [(a, "A"), (b, "B")]
    try:
        for session, sessionTag in sessions:
            session.connect()
            session.mon("warp on")
        for session, sessionTag in sessions:
            session.bootPastLoader()
            session.findWindow()
            session.focus()
        # Warp off the way testHighRates.py does it - with both emulators stopped, and read back,
        # because a monitor command sent to a RUNNING emulator is consumed by the break-in and the
        # machine can be left in warp.  The first version of this script did that, and its trace
        # covered 62 emulated seconds inside an eight-second read window.
        pauseAll(sessions)
        for session, sessionTag in sessions:
            command(session, "warp off")
            print(f"[{sessionTag}] {command(session, 'warp').strip()}", flush=True)
        resumeAll(sessions)
        for session, sessionTag in sessions:
            pickModemOpponent(session, sessionTag, "a")
        time.sleep(2)
        for session, sessionTag in sessions:
            if not moduleResident(session, sessionTag):
                raise SystemExit(f"{sessionTag}: the opponent module never loaded")
            pauseAll([(session, sessionTag)])
            if rate:
                command(session, f"> e055 {BAUD_INDEX[rate]:02x}")
            else:
                # rate 0 means "touch nothing": leave baudIndex at the disk value, which is adopt, and
                # let the driver take the line settings off the chip itself.  On this rig that chip
                # reads $00, so what is really being tested is the fallback and the default path
                # together - the way an ordinary boot with nothing configured comes up.
                print(f"[{sessionTag}] adopt mode: baudIndex left at the disk default", flush=True)
            command(session, "del")
            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 session, sessionTag in sessions:
            pauseAll([(session, sessionTag)])
            command(session, "del")
            command(session, f"trace exec {ENTRY:04x}")
            command(session, f"trace exec {NOTE:04x}")
            resumeAll([(session, sessionTag)])
        # The window is bracketed by both machines' own free-running cycle counters, so the run
        # says for itself whether it was at true C64 speed.
        pauseAll(sessions)
        startCycles = {sessionTag: stopwatch(session) for session, sessionTag in sessions}
        resumeAll(sessions)
        t0 = time.time()
        bodies, broke = collect(sessions, window, 16 << 20, tag)
        wall = time.time() - t0
        pauseAll(sessions)
        endCycles = {sessionTag: stopwatch(session) for session, sessionTag in sessions}
        resumeAll(sessions)
        for sessionTag in startCycles:
            span = endCycles[sessionTag] - startCycles[sessionTag]
            print(f"[{sessionTag}] window {wall:.2f} s wall, {span} emulated cycles = "
                  f"{span / NTSC_HZ:.2f} emulated s = {100.0 * span / NTSC_HZ / wall:.1f}% of real "
                  f"time", flush=True)
        for session, sessionTag in sessions:
            pauseAll([(session, sessionTag)])
            command(session, "del")
            state = readByte(session, 0xE5BD)
            phase = readByte(session, 0xE040)
            errs = readByte(session, 0xE047)
            resumeAll([(session, sessionTag)])
            body = bodies[sessionTag]
            entries = len(re.findall(r"C:\$?%04x" % ENTRY, body))
            notes = len(re.findall(r"C:\$?%04x" % NOTE, body))
            control = readByte(session, ACIA_BASE + 3)
            index = readByte(session, 0xE055)
            print(f"[{sessionTag}] baudIndex=${index:02X} control=${control:02X}", flush=True)
            print(f"[{sessionTag}] {entries} handler entries, {notes} of them nested "
                  f"({(100.0 * notes / entries) if entries else 0:.1f}%) in {window:.0f} s; "
                  f"nmiHandlerState=${state:02X} connectionPhase={phase} linkErrorCount={errs}",
                  flush=True)
    finally:
        for session, _ in sessions:
            session.close()
        relay.stop()
    return 0


sys.exit(main())
