#!/usr/bin/env python3
# testStrapPair.py - two machines whose SwiftLinks are both strapped to $DF00, cross connected through
# the socket null modem, with a tripwire on the register mirrors for the whole session.
#
#   python3 testStrapPair.py <disk.d64> [seconds]
#
# testRealtime.py answers "does the link still come up and carry ARQ frames at $DF00" at true C64
# speed.  This script answers the other half of the strap question: over a long session that includes
# the module load, the Hayes dialogue, the byte sync, the packet phase and the setup-overlay disk
# loads that happen while the link is live, does anything in the game reach into the cartridge page
# other than the driver's own six accessors?
#
# A watchpoint on $DE04-$DEFF and $DF04-$DFFF is the test.  A real SwiftLink decodes only A0 and A1
# inside its page, so those ranges are all mirrors of the same four registers; VICE maps only the
# first four bytes, which makes a stop anywhere in them harmless in the emulator and fatal on the
# hardware.  The stock driver's 'sta $DF59,y' - whose dummy read lands on $DF58, the data register -
# is exactly the instruction this is looking for.
import os
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
from testRealtime import analyseWire
from testStrap import describe, readAccessors, showAccessors

SHOTS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "shots")
LOGS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testLogs")


def armMirrorWatch(session, tag):
    session.enterMonitor()
    for kind in ("store", "load"):
        session.mon(f"watch {kind} $de04 $deff")
        session.mon(f"watch {kind} $df04 $dfff")
    session.sock.sendall(b"x\n")
    session.recv(2)
    print(f"[{tag}] mirror tripwire armed on $DE04-$DEFF and $DF04-$DFFF", flush=True)


def answerPrompts(session, tag, answerKey, tries=24):
    # testTwoMachines.answerModemPrompts calls the link open as soon as $E5BE reads non-zero, and
    # $E5BE is ordinary RAM until the module has been loaded over it - on one run it held $AC from
    # whatever was there before and the script stopped pressing keys at a machine that was still on
    # the options menu.  Two independent conditions instead: the module's own jump table has to be
    # resident at $E000, and the ACIA's control register has to have been programmed.
    for attempt in range(tries):
        session.hold(answerKey, 250)
        session.hold("space", 250)
        time.sleep(1.5)
        session.enterMonitor()
        jump = (readByte(session, 0xE000), readByte(session, 0xE001))
        control = readByte(session, ACIA_BASE + 3)
        session.sock.sendall(b"x\n")
        session.recv(2)
        if jump == (0x4C, 0x11) and control:
            print(f"[{tag}] link open after {attempt + 1} attempt(s): module resident at $E000, "
                  f"control register ${control:02X}", flush=True)
            return True
    print(f"[{tag}] link never opened (jump table {jump}, control {control})", flush=True)
    return False


def collect(sessions, seconds):
    # Drain both monitor sockets, resuming any emulator a watchpoint stopped.  With nothing tripping
    # the watchpoints this reads nothing and costs the emulators nothing.
    text = {tag: "" for _, tag in sessions}
    deadline = time.time() + seconds
    while time.time() < deadline:
        for session, tag in sessions:
            chunk = session.recv(0.5)
            if chunk.strip():
                text[tag] += chunk
                session.sock.sendall(b"x\n")
    return text


def state(session, tag):
    session.enterMonitor()
    values = {"connectionPhase": readByte(session, 0xE040), "isLinkActive": readByte(session, 0xE03B),
              "linkErrorCount": readByte(session, 0xE047), "baudIndex": readByte(session, 0xE055),
              "txCharActive": readByte(session, 0xE5BB),
              "aciaCommandShadow": readByte(session, 0xE5BE),
              "control": readByte(session, ACIA_BASE + 3), "command": readByte(session, ACIA_BASE + 2)}
    session.sock.sendall(b"x\n")
    session.recv(2)
    print(f"[{tag}] " + " ".join(f"{k}={'??' if v is None else '$%02X' % v}"
                                 for k, v in values.items()), flush=True)
    return values


def main():
    disk = os.path.abspath(sys.argv[1])
    seconds = float(sys.argv[2]) if len(sys.argv) > 2 else 120.0
    os.makedirs(SHOTS, exist_ok=True)
    os.makedirs(LOGS, exist_ok=True)
    relay = NullModemRelay()
    print(f"relay on 127.0.0.1:{relay.port}", flush=True)
    print(f"acia base for this run: ${ACIA_BASE:04X}", flush=True)
    args = aciaArgs(rsDevAddress=f"127.0.0.1:{relay.port}", baud=2400)
    a = ViceSession(disk, f"{SCRATCH}/pair.a.vice.log", args, label="A")
    b = ViceSession(disk, f"{SCRATCH}/pair.b.vice.log", args, label="B")
    sessions = [(a, "A"), (b, "B")]
    try:
        a.connect()
        b.connect()
        a.bootPastLoader()
        b.bootPastLoader()
        a.findWindow()
        b.findWindow()
        a.focus()
        b.focus()
        time.sleep(8)
        for session, tag in sessions:
            armMirrorWatch(session, tag)
        a.shot(f"{SHOTS}/pairDf0001aMenu.png")
        b.shot(f"{SHOTS}/pairDf0001bMenu.png")

        pickModemOpponent(a, "A", "a")
        pickModemOpponent(b, "B", "o")
        answerPrompts(a, "A", "a")
        answerPrompts(b, "B", "o")
        for session, tag in sessions:
            state(session, tag)
        a.shot(f"{SHOTS}/pairDf0002aLinkOpen.png")
        b.shot(f"{SHOTS}/pairDf0002bLinkOpen.png")

        marks = [len(relay.log[0]), len(relay.log[1])]
        text = collect(sessions, seconds)
        for tag in text:
            events = describe(text[tag])
            print(f"[{tag}] mirror-range stops during {seconds:.0f} s of play: {len(events)}",
                  flush=True)
            for event in events[:20]:
                print(f"  [{tag}] MIRROR {event}", flush=True)
            if text[tag].strip():
                open(f"{LOGS}/pairDf00.{tag}.mirror.txt", "w").write(text[tag])
        for i in (0, 1):
            chunk = bytes(relay.log[i][marks[i]:])
            print(f"  conn{i} carried {len(chunk)} bytes during the window; wire "
                  f"{analyseWire(chunk)}", flush=True)
        for session, tag in sessions:
            state(session, tag)
            session.enterMonitor()
            showAccessors(readAccessors(session), f"machine {tag}, end of run")
            session.mon("m e540 e56f")
            session.sock.sendall(b"x\n")
            session.recv(2)
        a.shot(f"{SHOTS}/pairDf0003aLater.png")
        b.shot(f"{SHOTS}/pairDf0003bLater.png")
    finally:
        a.close()
        b.close()
        time.sleep(1)
        relay.stop()
        for i in (0, 1):
            print(f"[relay] conn{i} total {len(relay.log[i])} bytes, "
                  f"wire {analyseWire(bytes(relay.log[i]))}", flush=True)
            print(f"[relay] conn{i} tail {bytes(relay.log[i][-60:])!r}", flush=True)


if __name__ == "__main__":
    main()
