#!/usr/bin/env python3
# testAdoptPair.py - two machines, true C64 speed: does the driver adopt the line settings the
# hardware was already configured for, and does the link still work afterwards?
#
#   python3 testAdoptPair.py <disk.d64> <presetControlHex> [presetCommandHex|-] [seconds] [tag]
#
# testAdopt.py answers the same question on one machine against a serial sink, which is enough to
# read a register but not enough to say the link works.  This script pre-programmes BOTH 6551s
# through the monitor at the one moment the value still means anything - after the opponent module is
# resident and before the "PRESS A OR O" prompt is answered, i.e. before anything in the module has
# touched the chip - and then opens a link between them and measures it.
#
#   * the control register is read on both machines before the preset is written, after it is
#     written, and again after the link is open, so "before and after" is a measurement rather than
#     an inference;
#   * the command register can be preset too (that is where a 6551 keeps parity), which is how the
#     question "is parity adopted as well?" gets an answer instead of an assumption;
#   * baudIndex $E055, the live control byte bitPeriodLo $E056 and the adopt entry's own byte
#     $E726 are read alongside, because those three say WHY the chip ends up holding what it holds.
#
# Nothing here writes any driver byte.  The only monitor writes are to the ACIA's own registers,
# which is what a user's hardware configuration would have left there.
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, answerModemPrompts
from testRealtime import (BITS_PER_BYTE, LOGS, NTSC_HZ, SHOTS, command, decodeControl,
                          measureWindowOnce, pauseAll, resumeAll, showState, snapshot, waitForPhase)
from testHighRates import moduleResident, perSecond, sampleRings

CONTROL = ACIA_BASE + 3
COMMAND = ACIA_BASE + 2
ADOPT_SLOT = 0xE726                     # baudEntryTable $E710 + 22: the adopt entry's control byte
PAGE_LATCH = 0xE5BF
LIVE_CONTROL = 0xE056                   # bitPeriodLo: whatever loadBaudParameters last wrote


def registers(session, tag, note):
    pauseAll([(session, tag)])
    row = {"control": readByte(session, CONTROL), "command": readByte(session, COMMAND),
           "baudIndex": readByte(session, 0xE055), "live": readByte(session, LIVE_CONTROL),
           "adoptSlot": readByte(session, ADOPT_SLOT), "latch": readByte(session, PAGE_LATCH),
           "shadow": readByte(session, 0xE5BE), "nmiState": readByte(session, 0xE5BD)}
    resumeAll([(session, tag)])
    text = " ".join(f"{name}=" + ("None" if value is None else f"${value:02X}")
                    for name, value in row.items())
    print(f"[{tag}] {note}: {text}", flush=True)
    if row["control"] is not None:
        print(f"[{tag}]   control {decodeControl(row['control'])}", flush=True)
    return row


def main():
    disk = os.path.abspath(sys.argv[1])
    preset = int(sys.argv[2], 16)
    presetCommand = None if len(sys.argv) < 4 or sys.argv[3] == "-" else int(sys.argv[3], 16)
    seconds = float(sys.argv[4]) if len(sys.argv) > 4 else 75.0
    tag = sys.argv[5] if len(sys.argv) > 5 else "ap"
    os.makedirs(SHOTS, exist_ok=True)
    os.makedirs(LOGS, exist_ok=True)
    relay = NullModemRelay()
    print(f"relay on 127.0.0.1:{relay.port}; both ports pre-set to control ${preset:02X}"
          + (f", command ${presetCommand:02X}" if presetCommand is not None else ""), flush=True)
    args = aciaArgs(rsDevAddress=f"127.0.0.1:{relay.port}", baud=38400)
    a = ViceSession(disk, f"{SCRATCH}/ap.{tag}.a.vice.log", args, label="A", warp=False)
    b = ViceSession(disk, f"{SCRATCH}/ap.{tag}.b.vice.log", args, label="B", warp=False)
    sessions = [(a, "A"), (b, "B")]
    rows = {}
    try:
        a.connect()
        b.connect()
        for session, sessionTag in sessions:
            command(session, "warp on")
        a.bootPastLoader(waitSecs=900)
        b.bootPastLoader(waitSecs=900)
        pauseAll(sessions)
        for session, sessionTag in sessions:
            command(session, "warp off")
            print(f"[{sessionTag}] {command(session, 'warp').strip()}", flush=True)
        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")

        # ---- the one moment the user's own setting still exists: the module is resident and has
        # ---- not touched the chip yet.
        for session, sessionTag in sessions:
            rows[(sessionTag, "atPrompt")] = registers(session, sessionTag,
                                                       "at the A/O prompt, before the preset")
            pauseAll([(session, sessionTag)])
            command(session, f"> {CONTROL:04x} {preset:02x}")
            if presetCommand is not None:
                command(session, f"> {COMMAND:04x} {presetCommand:02x}")
            resumeAll([(session, sessionTag)])
            rows[(sessionTag, "preset")] = registers(session, sessionTag,
                                                     "after the preset was written")
        a.shot(f"{SHOTS}/ap{tag}01preset.png")

        answerModemPrompts(a, "A", "a")
        answerModemPrompts(b, "B", "o")
        for session, sessionTag in sessions:
            rows[(sessionTag, "opened")] = registers(session, sessionTag,
                                                     "after the link was opened")
        ok, phases = waitForPhase(sessions, 3, 180)
        print(f"both in the packet phase: {ok} ({phases})", flush=True)
        for session, sessionTag in sessions:
            rows[(sessionTag, "phase3")] = registers(session, sessionTag, "in the packet phase")
        a.shot(f"{SHOTS}/ap{tag}02linked.png")
        b.shot(f"{SHOTS}/ap{tag}02linkedB.png")

        run = measureWindowOnce(relay, sessions, seconds, f"adopt ${preset:02X}")
        buckets = perSecond(run["samples"])
        zeros = {}
        for index, name in ((1, "conn0"), (2, "conn1")):
            values = [row[index] for row in buckets]
            zeros[name] = sum(1 for value in values if value < 0.5)
            print(f"  {name} per second: min {min(values):.1f}, max {max(values):.1f}, "
                  f"seconds carrying nothing: {zeros[name]} of {len(values)}", flush=True)
        open(f"{LOGS}/adoptPair.{tag}.samples.txt", "w").write(
            "\n".join(f"{t}\t{c0}\t{c1}" for t, c0, c1 in run["samples"]) + "\n")
        for session, sessionTag in sessions:
            rows[(sessionTag, "afterWindow")] = registers(session, sessionTag,
                                                          "at the end of the window")
        print("\n---- uartRxCount $E42D sampled repeatedly ----", flush=True)
        sampleRings(sessions, 12, 0.5, f"adoptPair.{tag}")
        for state in [snapshot(session, sessionTag) for session, sessionTag in sessions]:
            showState(state)
        a.shot(f"{SHOTS}/ap{tag}03end.png")
        b.shot(f"{SHOTS}/ap{tag}03endB.png")

        print("\n================ SUMMARY ================", flush=True)
        print(f"preset control ${preset:02X}"
              + (f", preset command ${presetCommand:02X}" if presetCommand is not None else ""),
              flush=True)
        for sessionTag in ("A", "B"):
            for when in ("atPrompt", "preset", "opened", "phase3", "afterWindow"):
                row = rows[(sessionTag, when)]
                text = "  ".join(f"{name}=" + ("None" if row[name] is None
                                                  else f"${row[name]:02X}")
                                 for name in ("control", "command", "baudIndex", "live",
                                              "adoptSlot", "latch"))
                print(f"  [{sessionTag}] {when:12s} {text}", flush=True)
        print(f"  window {run['wall']:.2f} s wall; speeds "
              f"{ {tag2: round(100.0 * value, 1) for tag2, value in run['speed'].items()} }",
              flush=True)
        for index in (0, 1):
            count = run["counts"][index]
            print(f"  conn{index} {count} bytes = {count / run['wall']:.2f} B/s; "
                  f"wire {run['wire'][index]}", flush=True)
        print(f"  seconds carrying nothing: {zeros}", flush=True)
        print(f"  both reached phase 3: {ok} ({phases})", 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()
