#!/usr/bin/env bash
# Launch the IIgs sprite demo (or other example) under MAME's apple2gs
# driver instead of GSplus. We get:
#   - The MAME debugger window (always visible with -debug)
#   - debug.log file written to the working directory
#   - A Lua hook that, when the CPU halts (e.g., BRK), dumps registers
#     and the surrounding code bytes to /tmp/mame-iigs/crash.txt
#
# The boot path is the same as run-iigs.sh: gsos-system.po as flop3, our
# joey.2mg as flop4. Once Finder is up, navigate to JOEYLIB and double-
# click the example.
#
# Outputs of a run land in /tmp/mame-iigs/:
#   - debug.log         MAME's debugger console output (-debuglog)
#   - crash.txt         Lua-hook crash dump (if anything halts the CPU)

set -euo pipefail

if [[ $# -gt 1 ]]; then
    echo "usage: $0 [example-name]" >&2
    exit 2
fi

prog=${1:-pattern}
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)

bin_dir=$repo/build/iigs/bin
target=${prog^^}
if [[ ! -f "$bin_dir/$target" ]]; then
    echo "$bin_dir/$target not built. Run 'make iigs' first." >&2
    if compgen -G "$bin_dir/*" > /dev/null; then
        echo "available examples in $bin_dir:" >&2
        find "$bin_dir" -maxdepth 1 -type f -printf '%f\n' \
            | grep -vE '\.2mg$|\.txt$' >&2 || true
    fi
    exit 1
fi

sys_disk=$repo/toolchains/emulators/support/gsos-system.po
data_disk=$bin_dir/joey.2mg

for f in "$sys_disk" "$data_disk"; do
    if [[ ! -f $f ]]; then
        echo "missing: $f" >&2
        exit 1
    fi
done

work=$(mktemp -d -t joeylib-mame.XXXXXX)
out=/tmp/mame-iigs
mkdir -p "$out"

cp "$sys_disk"  "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"

# Lua script: drives Finder via natural keyboard + macadb key fields
# to launch the requested example, dumps register state on any halt
# (BRK, breakpoint, watchpoint, manual stop). Field names for keys
# come from MAME's apple2gs macadb input definitions:
#   :macadb:KEY0 -> "d  D"
#   :macadb:KEY1 -> "o  O"
#   :macadb:KEY2 -> "j  J", "p  P"
#   :macadb:KEY3 -> "Command / Open Apple"
# Letters not on KEY0..2 fall back to natkeyboard:post() (which
# handles modifier-less character entry only).
# Type the FULL program name to disambiguate (DRAW vs DATA which both
# start with D and live in the same JOEYLIB volume).
prog_select_str=${target}
cat > "$work/crash-hook.lua" <<LUA
-- Crash diagnostics + Finder driver for IIgs demos. Auto-resumes
-- the initial debug pause; at calibrated frame counts taps "J" to
-- select JOEYLIB, Cmd-O to open it, the program first letter to
-- select the binary, Cmd-O to launch it. On any halt (BRK trap,
-- watchpoint, breakpoint) dumps registers + bytes around PC to
-- crash.txt.
local cpu     = manager.machine.devices[":maincpu"]
local prog    = cpu.spaces["program"]
local outpath = "/tmp/mame-iigs/crash.txt"

local function dump(label)
    local f = io.open(outpath, "a")
    if f == nil then return end
    f:write(string.format("=== %s @ %s ===\n", label, os.date("%H:%M:%S")))
    for k, v in pairs(cpu.state) do
        f:write(string.format("  %s = %X\n", k, v.value))
    end
    local pc = cpu.state["CURPC"].value
    local lo = (pc - 16) & 0xFFFFFF
    f:write(string.format("  bytes %06X..%06X:", lo, lo + 32))
    for i = 0, 32 do
        local b = prog:read_u8(lo + i)
        f:write(string.format(" %02X", b))
    end
    f:write("\n")
    -- Probe addresses from joeyDraw.asm
    f:write(string.format("  probe 012050=%02X 011F80=%02X 023000=%02X\n",
                          prog:read_u8(0x012050),
                          prog:read_u8(0x011F80),
                          prog:read_u8(0x023000)))
    f:close()
end

local done_marker = "/tmp/mame-iigs/.done"
local started = false
local crashed = false
local boot_frames = 0

local function signal_done(reason)
    local f = io.open(done_marker, "w")
    if f ~= nil then
        f:write(reason)
        f:close()
    end
end

local nat = manager.machine.natkeyboard

local function get_field(port, field_name)
    local p = manager.machine.ioport.ports[port]
    if p == nil then return nil end
    return p.fields[field_name]
end

local key_cmd = get_field(":macadb:KEY3", "Command / Open Apple")

local function press(f)   if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end

local function log_step(label)
    local f = io.open("/tmp/mame-iigs/steps.txt", "a")
    if f ~= nil then
        f:write(string.format("frame=%d %s\n", boot_frames, label))
        f:close()
    end
end

local function snap(label)
    log_step("snap " .. label)
    manager.machine.video:snapshot()
end

-- Finder navigation: natkeyboard handles per-letter scancode/timing
-- correctly; the Cmd modifier is forced via the macadb field for the
-- duration of the post() call. Mirrors the proven approach in the
-- pre-existing /tmp/auto_run.lua.
local steps = {
    { 2700, function() snap("preboot") end },
    { 3000, function() snap("at_3000_finder") log_step("post J") nat:post("J") end },
    { 3120, function() snap("after_J") log_step("Cmd hold") press(key_cmd) end },
    { 3126, function() log_step("post o under Cmd") nat:post("o") end },
    { 3180, function() log_step("Cmd release") release(key_cmd) end },
    { 3300, function() snap("after_Cmd_O") end },
    { 3540, function() log_step("post name") nat:post("${prog_select_str}") end },
    { 3600, function() snap("after_first") end },
    { 3660, function() log_step("Cmd hold #2") press(key_cmd) end },
    { 3666, function() log_step("post o #2") nat:post("o") end },
    { 3720, function() log_step("Cmd release #2") release(key_cmd) end },
    { 3900, function() snap("after_launch") end },
    { 6000, function() snap("running") end },
    { 9000, function() snap("running_2") end },
    { 12000, function() snap("running_3") end },
    { 13500, function() snap("running_4") end },
    { 14500, function() snap("running_5") end },
    { 15500, function() snap("running_6") end },
    { 16500, function() snap("running_7") end },
}
local step_idx = 1

-- Probe-address poll. Watch 3 addresses and log every change, so we
-- can verify which (if any) is reachable by our long-mode STAs.
local last1, last2, last3, last4, last5, last6, last7 = -1, -1, -1, -1, -1, -1, -1
local function check_ckpt()
    local v1 = prog:read_u8(0x012050)
    local v2 = prog:read_u8(0x011F80)
    local v3 = prog:read_u8(0x023000)
    local v4 = prog:read_u8(0xE12050)   -- SHR pixel byte (mirror of $012050 once blitted)
    local v5 = prog:read_u8(0xE19D00)   -- SCB row 0 (0 = 320 mode)
    local v6 = prog:read_u8(0xE15000)   -- SHR pixel byte mid-screen (row ~38, col ~64)
    local v7 = prog:read_u8(0xE19E00)   -- palette[0][0] low byte (color 0 lo)
    if v1 ~= last1 or v2 ~= last2 or v3 ~= last3 or v4 ~= last4 or v5 ~= last5 or v6 ~= last6 or v7 ~= last7 then
        local f = io.open("/tmp/mame-iigs/ckpt-trace.txt", "a")
        if f ~= nil then
            f:write(string.format("frame=%d 012050=%02X 011F80=%02X 023000=%02X E12050=%02X E19D00=%02X E15000=%02X E19E00=%02X\n",
                                  boot_frames, v1, v2, v3, v4, v5, v6, v7))
            f:close()
        end
        last1, last2, last3, last4, last5, last6, last7 = v1, v2, v3, v4, v5, v6, v7
    end
end

emu.register_periodic(function()
    local dbg = manager.machine.debugger
    if dbg == nil then return end
    if dbg.execution_state == "stop" then
        if not started then
            started = true
            dbg.execution_state = "run"
            return
        end
        if not crashed then
            crashed = true
            dump("halt")
            signal_done("halt")
        end
    else
        boot_frames = boot_frames + 1
        check_ckpt()
        while step_idx <= #steps and boot_frames >= steps[step_idx][1] do
            steps[step_idx][2]()
            step_idx = step_idx + 1
        end
        if boot_frames > 18000 and not crashed then
            crashed = true
            dump("watchdog")
            signal_done("watchdog")
        end
    end
end)
LUA

# Wipe prior crash.txt so we don't confuse runs.
: > "$out/crash.txt"

cat <<EOF
MAME apple2gs (auto-launch ${prog^^}):
  Boot disk:  $work/boot.po (flop3)

Boots GS/OS, waits ~50s for Finder, then drives the keyboard via Lua
to select the JOEYLIB volume and double-launch ${prog^^}.
On crash the MAME debugger halts and Lua dumps state to:
  $out/crash.txt

After exit:
  $out/debug.log      MAME debugger console output

EOF

cleanup() {
    # Always rescue debug.log first -- it's written by MAME relative to
    # cwd ($work) and lost when the dir is removed.
    if [[ -f $work/debug.log ]]; then
        mv -f "$work/debug.log" "$out/debug.log"
    fi
    # Snapshots that the Lua hook captured for visual debugging.
    if [[ -d $work/snap ]]; then
        rm -rf "$out/snap"
        mv "$work/snap" "$out/snap"
    fi
    rm -rf "$work"
}
trap cleanup EXIT

# Visible by default. Set MAME_HEADLESS=1 to suppress the video window
# (CI / batch runs that only care about crash.txt / snapshots). The crash
# hook drives MAME's debugger, which needs a Qt platform; headless boxes
# have no X display, so force Qt's offscreen plugin and SDL's dummy
# video/audio drivers so MAME never tries to open a window.
video_arg="-window"
if [[ "${MAME_HEADLESS:-0}" = "1" ]]; then
    # Use the software renderer (not "-video none"): MAME's video:snapshot()
    # needs a real render target, so "-video none" silently drops snapshots.
    # "-video soft" renders to an offscreen surface that snapshot() can read.
    video_arg="-video soft"
    export QT_QPA_PLATFORM=offscreen
    export SDL_VIDEODRIVER=dummy
    export SDL_AUDIODRIVER=dummy
fi

# apple2gs ROM set. Default to the user's MAME roms; override via MAME_ROMPATH.
# (A bare `mame apple2gs` only finds ROMs if rompath is already configured;
# passing it explicitly keeps this script self-contained.)
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"

# Clear the done-marker the Lua hook uses to signal shutdown.
rm -f "$out/.done"

cd "$work"
mame apple2gs \
    -rompath "$rompath" \
    -flop3 "$work/boot.po" \
    -flop4 "$work/joey.2mg" \
    $video_arg -sound none \
    -nothrottle \
    -debug -debuglog \
    -autoboot_script "$work/crash-hook.lua" &
mame_pid=$!

# Poll for the done-marker. Kill MAME once Lua signals it. Cap total
# wall-clock at 6 min so we don't outlive the Lua-side watchdog (5 min).
deadline=$((SECONDS + 360))
while kill -0 "$mame_pid" 2>/dev/null; do
    if [[ -f $out/.done ]]; then
        kill "$mame_pid" 2>/dev/null
        break
    fi
    if (( SECONDS > deadline )); then
        kill "$mame_pid" 2>/dev/null
        break
    fi
    sleep 0.5
done
wait "$mame_pid" 2>/dev/null || true
