#!/usr/bin/env python3
# testAdopt.py - does the driver really use the line settings the user's hardware is already
# configured for, and does the 38400 hot key work again?
#
#   python3 testAdopt.py <disk.d64> [tag]
#
# Two questions, one boot, one machine (the far end is a TCP sink that never answers, which is enough
# for everything measured here - none of it needs a peer).
#
# 1. ADOPT MODE.  baudEntryTable's last entry, index 21, has no control byte of its own: aciaProbeAdopted
#    reads the 6551's control register before the page probe overwrites it and parks the result there,
#    corrected only where it cannot work.  So the test writes a control byte into $DE03 with the
#    monitor, exactly as a C64 Ultimate or a previously-configured cartridge would leave it, and then
#    makes the driver open the link and reads back what it chose.
#      * the first case is the real cold-start path: the byte goes in before the A/O prompt is
#        answered, i.e. before anything in the module has touched the chip;
#      * the rest re-run the same code by clearing aciaPageLatch $E5BF - which is the only thing that
#        stops aciaDetect probing twice - and pressing C=+U, so one boot covers the whole table of
#        cases.  Clearing that latch is a test manipulation and is called out as one.
#
# 2. THE 38400 HOT KEY.  C=+8 must put baudIndex $E055 = 18 and the control register = $1F again, and
#    C=+U must take the driver back out of the fixed rate and into adopt mode.
import os
import re
import subprocess
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 testModemSelect import SerialSink
from testTwoMachines import pickModemOpponent, answerModemPrompts
from testHighRates import moduleResident

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

CONTROL = ACIA_BASE + 3
BAUD_INDEX = 0xE055                     # the three-byte index into baudEntryTable
LIVE_CONTROL = 0xE056                   # bitPeriodLo: the control byte loadBaudParameters last wrote
ADOPT_SLOT = 0xE726                     # baudEntryTable ($E710) + 22, the adopt entry's control byte
PAGE_LATCH = 0xE5BF

# what the port is set to        what the driver must end up using, and why
CASES = [
    (0x18, 0x18, "2400 8N1 - usable as it stands, so it is copied through untouched"),
    (0x9F, 0x9F, "38400 with two stop bits - stop bits are free, so they survive"),
    (0x5A, 0x1A, "4800 but six data bits - the word length is corrected, the rate is not"),
    (0x3C, 0x1C, "9600 but seven data bits - same correction"),
    (0x00, 0x15, "a cold, unconfigured chip - $00 selects external clocks, so the 300-baud fallback"),
    (0x0C, 0x15, "9600 but bit 4 clear = external receive clock - unusable, so the fallback again"),
    (0x10, 0x15, "internal clock but no rate at all - unusable, so the fallback again"),
]


def pause(session):
    session.enterMonitor()


def resume(session):
    session.sock.sendall(b"x\n")
    session.recv(2)


def keyDown(session, keysym):
    subprocess.run(["xdotool", "keydown", keysym], env=session.env, check=False)


def keyUp(session, keysym):
    subprocess.run(["xdotool", "keyup", keysym], env=session.env, check=False)


def holdCombo(session, key, ms=900):
    keyDown(session, "Tab")             # the Commodore key under VICE's default symbolic keymap
    time.sleep(0.2)
    keyDown(session, key)
    time.sleep(ms / 1000.0)
    keyUp(session, key)
    time.sleep(0.1)
    keyUp(session, "Tab")
    time.sleep(0.4)


def pressUntil(session, key, addr, want, tries=6):
    # The hot-key layer ignores a repeat of the code it saw last ($E312 cmp lastHotkeyCode), so the
    # same combination pressed twice running does nothing the second time.  Every press here is
    # therefore checked against the byte it is supposed to move, and repeated until it lands.
    for attempt in range(tries):
        # Re-assert the input focus first: there is no window manager on the private display, and a
        # monitor stop between presses is enough to leave the keys going nowhere.
        session.focus()
        holdCombo(session, key)
        time.sleep(0.4)
        pause(session)
        got = readByte(session, addr)
        last = readByte(session, 0xE046)
        resume(session)
        if got == want:
            return attempt + 1
        print(f"[press] C=+{key} did not land (want ${want:02X}, got "
              f"{'None' if got is None else '$%02X' % got}, lastHotkeyCode=${last:02X}) - again",
              flush=True)
        time.sleep(0.6)
    return 0


def state(session):
    pause(session)
    out = {
        "control": readByte(session, CONTROL),
        "baudIndex": readByte(session, BAUD_INDEX),
        "liveControl": readByte(session, LIVE_CONTROL),
        "adoptSlot": readByte(session, ADOPT_SLOT),
        "pageLatch": readByte(session, PAGE_LATCH),
        "commandShadow": readByte(session, 0xE5BE),
        "nmiState": readByte(session, 0xE5BD),
    }
    resume(session)
    return out


def show(tag, s):
    print(f"[{tag}] $DE03={s['control']:02X} baudIndex={s['baudIndex']:02X} "
          f"live={s['liveControl']:02X} adoptSlot={s['adoptSlot']:02X} "
          f"latch={s['pageLatch']:02X} cmd={s['commandShadow']:02X} nmiState={s['nmiState']:02X}",
          flush=True)


def main():
    disk = os.path.abspath(sys.argv[1])
    tag = sys.argv[2] if len(sys.argv) > 2 else "adopt"
    os.makedirs(LOGS, exist_ok=True)
    os.makedirs(SHOTS, exist_ok=True)
    sink = SerialSink()
    session = ViceSession(disk, os.path.join(LOGS, f"vice.{tag}.log"),
                          aciaArgs(rsDevAddress=f"127.0.0.1:{sink.port}"), label=tag, warp=True)
    results = []
    try:
        session.connect()
        session.mon("warp on")          # -warp on the command line does not survive the autostart
        session.bootPastLoader()
        session.findWindow()
        session.focus()
        # Out of warp before any key is pressed: a joystick press held for 250 ms of wall clock is a
        # different number of emulated frames at warp speed than it is at C64 speed, and the options
        # menu scrolls one row per frame, so menu keys under warp land wherever they like.
        pause(session)
        session.mon("warp off")
        resume(session)
        pickModemOpponent(session, tag, "a")
        # Nothing below means anything until the opponent module is actually resident: before that,
        # $E055 and $E5BD are whatever the trainer or the loader left in those bytes.
        for attempt in range(30):
            time.sleep(2)
            if moduleResident(session, tag):
                break
        else:
            raise SystemExit(f"{tag}: the opponent module never loaded")

        # --- the module is resident and has not touched the chip yet ---
        pause(session)
        session.mon("warp off")
        before = {
            "baudIndex": readByte(session, BAUD_INDEX),
            "adoptSlot": readByte(session, ADOPT_SLOT),
            "latch": readByte(session, PAGE_LATCH),
            "control": readByte(session, CONTROL),
        }
        print(f"[{tag}] at the A/O prompt: baudIndex=${before['baudIndex']:02X} "
              f"adoptSlot=${before['adoptSlot']:02X} latch=${before['latch']:02X} "
              f"$DE03=${before['control']:02X}", flush=True)
        # set the port up the way a user's hardware would already be set up
        session.mon(f"> {CONTROL:04x} 18")
        resume(session)
        results.append(("disk default baudIndex", before["baudIndex"], 0x15))
        results.append(("adopt slot before the probe", before["adoptSlot"], 0x00))
        results.append(("page latch before the probe", before["latch"], 0x00))

        # --- the real cold-start path: open the link with $DE03 already set to 2400 ---
        opened = answerModemPrompts(session, tag, "a")
        cold = state(session)
        show(tag + "/cold", cold)
        results.append(("link opened", 1 if opened else 0, 1))
        results.append(("cold start adopted $18 into the slot", cold["adoptSlot"], 0x18))
        results.append(("cold start left the chip at $18", cold["control"], 0x18))
        results.append(("cold start used index 21", cold["baudIndex"], 0x15))
        session.shot(os.path.join(SHOTS, f"{tag}Cold.png"))

        # --- the table of cases, each one a fresh probe ---
        # Each case is two hot keys, and the pair is the point: C=+3 takes the driver out of adopt
        # mode and on to a fixed 300 baud (and, with the latch cleared, re-runs the probe, which is
        # what re-reads the preset), then C=+U puts it back on the port's own settings.  Pressing two
        # different combinations also keeps the hot-key layer's repeat filter out of the way.
        for preset, expect, why in CASES:
            pause(session)
            session.mon(f"> {CONTROL:04x} {preset:02x}")
            session.mon(f"> {PAGE_LATCH:04x} 00")       # test manipulation: let aciaDetect probe again
            resume(session)
            landed3 = pressUntil(session, "3", BAUD_INDEX, 0x00)
            afterFixed = state(session)
            landedU = pressUntil(session, "u", BAUD_INDEX, 0x15)
            s = state(session)
            print(f"[{tag}] preset ${preset:02X} -> slot=${s['adoptSlot']:02X} "
                  f"$DE03=${s['control']:02X}   ({why})", flush=True)
            results.append((f"preset ${preset:02X}: C=+3 left adopt mode", afterFixed["baudIndex"], 0x00))
            results.append((f"preset ${preset:02X}: C=+3 imposed 300 baud", afterFixed["control"], 0x15))
            results.append((f"preset ${preset:02X}: the probe adopted ${expect:02X}",
                            afterFixed["adoptSlot"], expect))
            results.append((f"preset ${preset:02X}: C=+U selected adopt", s["baudIndex"], 0x15))
            results.append((f"preset ${preset:02X}: C=+U programmed ${expect:02X}", s["control"], expect))

        # --- the 38400 hot key, and coming back out of it ---
        pressUntil(session, "8", BAUD_INDEX, 0x12)
        s = state(session)
        show(tag + "/C=+8", s)
        results.append(("C=+8 selects index 18", s["baudIndex"], 0x12))
        results.append(("C=+8 writes control $1F", s["control"], 0x1F))
        session.shot(os.path.join(SHOTS, f"{tag}Rate38400.png"))

        pause(session)
        session.mon(f"> {CONTROL:04x} 17")               # pretend the port is set to 1200
        session.mon(f"> {PAGE_LATCH:04x} 00")
        resume(session)
        pressUntil(session, "u", BAUD_INDEX, 0x15)
        s = state(session)
        show(tag + "/C=+U", s)
        results.append(("C=+U leaves the fixed rate", s["baudIndex"], 0x15))
        results.append(("C=+U adopts $17", s["control"], 0x17))
        session.shot(os.path.join(SHOTS, f"{tag}Adopt.png"))
    finally:
        try:
            session.shot(os.path.join(SHOTS, f"{tag}End.png"))
        except Exception:
            pass
        session.close()
        sink.stop()

    print("\n=== results ===", flush=True)
    bad = 0
    for name, got, want in results:
        ok = got == want
        bad += 0 if ok else 1
        gotText = "None" if got is None else f"${got:02X}"
        print(f"  {'PASS' if ok else 'FAIL'}  {name}: got {gotText}, want ${want:02X}", flush=True)
    print(f"\n{len(results) - bad} of {len(results)} checks passed", flush=True)
    return 1 if bad else 0


sys.exit(main())
