#!/usr/bin/env bash
# runInGno.sh — run a llvm816-built shell command under real GNO/ME in MAME.
#
# Boots GS/OS 6.0.4 + GNO/ME 2.0.6, logs in as root, runs the given
# program at the gsh prompt, and polls bank-2 memory for a signature.
#
# Usage:
#   runInGno.sh <program.omf> [--check addr=hexval ...] [--snapshots]
#
#   <program.omf>   a GS/OS-loadable OMF (NOT a flat link816 .bin).
#                   Build it through omfEmit — see demos/buildGno.sh.
#   --check A=V     after the program runs, assert mem[A] (16-bit) == V.
#   --snapshots     save PNGs of each boot/login/run stage to
#                   $GNO_SNAP_DIR (default /tmp/gnosnaps) for headless
#                   debugging.
#
# The program is placed on the disk as /bin/<name> (lowercased) and as
# /HELLO, so it can be invoked by bare name at the gsh prompt.
#
# Boot timeline (frame numbers tuned against MAME apple2gs; override
# via the GNO_*_FRAMES env vars if your MAME build differs):
#   ~3600   GS/OS Finder ready, GNO + System Disk icons visible
#    3800   select GNO volume, Open Apple+O to open it
#    4600   select KERN, Open Apple+O to launch the GNO kernel
#   ~16000  getty shows "login:" prompt
#   16200   type "root\n"  (root has empty password)
#   ~18000  gsh "% " prompt
#   18800   type "<name>\n" to run the program
#   19500+  poll bank-2 markers

set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

PROG="${1:-}"
shift || true
[ -n "$PROG" ] || { echo "usage: $0 <program.omf> [--check A=V ...] [--snapshots]" >&2; exit 2; }
[ -f "$PROG" ] || { echo "program not found: $PROG" >&2; exit 2; }

CADIUS="$ROOT/tools/cadius/cadius"
SYSDISK="$ROOT/tools/gsos/6.0.4 - System.Disk.po"
BASE="$ROOT/tools/gno/gnobase.po"

[ -x "$CADIUS" ]  || { echo "cadius missing" >&2; exit 1; }
[ -f "$SYSDISK" ] || { echo "GS/OS System Disk missing: $SYSDISK" >&2; exit 1; }
[ -f "$BASE" ]    || { echo "GNO base disk missing — run tools/gno/buildDisk.sh" >&2; exit 1; }

SNAPSHOTS=0
CHECK_LUA=""
ADDR_LIST=(); EXPECT_LIST=()
while [ $# -gt 0 ]; do
    case "$1" in
        --snapshots) SNAPSHOTS=1; shift;;
        --check)
            shift
            while [ $# -gt 0 ] && [[ "$1" != "--"* ]]; do
                a="${1%=*}"; v="${1#*=}"
                ADDR_LIST+=("$a"); EXPECT_LIST+=("$v")
                shift
            done;;
        *) echo "unknown arg: $1" >&2; exit 2;;
    esac
done

WORK=$(mktemp -d -t gno-run.XXXXXX)
trap 'rm -rf "$WORK"' EXIT
DATA="$WORK/data.po"
cp "$BASE" "$DATA"

NAME=$(basename "$PROG" | sed 's/\.[^.]*$//' | tr '[:upper:]' '[:lower:]' | cut -c1-15)
cp "$PROG" "$WORK/${NAME}#B50100"
cp "$PROG" "$WORK/HELLO#B50100"
"$CADIUS" ADDFILE "$DATA" /GNO.BOOT/bin "$WORK/${NAME}#B50100" >/dev/null
"$CADIUS" ADDFILE "$DATA" /GNO.BOOT     "$WORK/HELLO#B50100"   >/dev/null
# Optional second executable for concurrency tests: added to /bin so gsh
# can run e.g. GNO_RUNCMD="proga & progb".  Backward-compatible (unset -> no-op).
if [ -n "${GNO_PROG2:-}" ] && [ -f "${GNO_PROG2}" ]; then
    NAME2=$(basename "$GNO_PROG2" | sed 's/\.[^.]*$//' | tr '[:upper:]' '[:lower:]' | cut -c1-15)
    cp "$GNO_PROG2" "$WORK/${NAME2}#B50100"
    "$CADIUS" ADDFILE "$DATA" /GNO.BOOT/bin "$WORK/${NAME2}#B50100" >/dev/null
fi
# Optional: drop an extra TXT file in /home/root (the login cwd) for
# stdin-redirect tests.  Reference it relatively or as /home/root/<name>.
if [ -n "${GNO_ADDFILE:-}" ] && [ -f "${GNO_ADDFILE}" ]; then
    cp "${GNO_ADDFILE}" "$WORK/$(basename "${GNO_ADDFILE}")#040000"
    "$CADIUS" ADDFILE "$DATA" /GNO.BOOT/home/root "$WORK/$(basename "${GNO_ADDFILE}")#040000" >/dev/null
fi

SNAPDIR="${GNO_SNAP_DIR:-/tmp/gnosnaps}"
if [ "$SNAPSHOTS" = 1 ]; then
    mkdir -p "$SNAPDIR"; rm -rf "$SNAPDIR/apple2gs"/* 2>/dev/null || true
fi

# Frame tuning.
F_VOL=${GNO_VOL_FRAMES:-3800}
F_KERN=${GNO_KERN_FRAMES:-4600}
F_LOGIN=${GNO_LOGIN_FRAMES:-16200}
F_RUN=${GNO_RUN_FRAMES:-18800}
F_POLL=${GNO_POLL_FRAMES:-20000}
F_END=${GNO_END_FRAMES:-21000}
SECS=${MAME_SECS:-380}

# Optional: inject a line into the console after the program launches
# (for testing interactive stdin reads).  GNO_STDIN is the text; it is
# posted with a trailing newline at frame F_RUN + GNO_STDIN_DELAY.
STDIN_STEP=""
if [ -n "${GNO_STDIN:-}" ]; then
    STDIN_STEP="{$((F_RUN + ${GNO_STDIN_DELAY:-1600})), function() nat:post(\"${GNO_STDIN}\\n\") end},"
fi

# Build the bank-2 probe Lua for each --check.
for i in "${!ADDR_LIST[@]}"; do
    CHECK_LUA="$CHECK_LUA print(string.format('MAME-READ addr=0x%06x val=0x%04x', ${ADDR_LIST[$i]}, mem:read_u16(${ADDR_LIST[$i]})))"$'\n'
done

LUA="$WORK/gno.lua"
cat > "$LUA" <<EOF
local nat = manager.machine.natkeyboard
local frame = 0
local idx = 1
local function gf(p, n) local x = manager.machine.ioport.ports[p]; return x and x.fields[n] end
local key_cmd = gf(":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 snaps = $SNAPSHOTS
local function snap(t) if snaps == 1 then manager.machine.video:snapshot() end; print("MAME-STAGE "..t.." frame="..frame) end

local steps = {
    {$F_VOL,        function() nat:post("G") end},
    {$F_VOL + 200,  function() press(key_cmd) end},
    {$F_VOL + 206,  function() nat:post("o") end},
    {$F_VOL + 260,  function() release(key_cmd) end},
    {$F_VOL + 400,  function() snap("vol-open") end},
    {$F_KERN,       function() nat:post("K") end},
    {$F_KERN + 200, function() press(key_cmd) end},
    {$F_KERN + 206, function() nat:post("o") end},
    {$F_KERN + 260, function() release(key_cmd) end},
    {$F_LOGIN - 200,function() snap("login") end},
    {$F_LOGIN,      function() nat:post("root\n") end},
    {$F_RUN - 200,  function() snap("shell") end},
    {$F_RUN,        function() nat:post("${GNO_RUNCMD:-$NAME}\n") end},
    ${STDIN_STEP}
    {$F_POLL,       function() snap("running") end},
}
emu.register_frame_done(function()
    frame = frame + 1
    while idx <= #steps and frame >= steps[idx][1] do steps[idx][2](); idx = idx + 1 end
    if frame == $F_POLL then
        local mem = manager.machine.devices[":maincpu"].spaces["program"]
$CHECK_LUA
    end
    if frame > $F_END then manager.machine:exit() end
end)
EOF

OUT=$(SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy timeout "${MAME_WALL:-120}" mame apple2gs \
    -rompath "$ROOT/tools/mame/roms" \
    -flop3 "$SYSDISK" -flop4 "$DATA" \
    -snapshot_directory "$SNAPDIR" \
    -autoboot_script "$LUA" \
    -video none -sound none -nothrottle -seconds_to_run "$SECS" 2>&1)

echo "$OUT" | grep -E "^MAME-" || true

rc=0
for i in "${!ADDR_LIST[@]}"; do
    a="${ADDR_LIST[$i]}"; exp="${EXPECT_LIST[$i]}"
    # MAME's Lua `string.format("%06x", ...)` lowercases hex digits, but a
    # user-supplied --check arg like `0x02500A` keeps the uppercase A.
    # Normalize both sides for a literal compare.
    aLower=$(echo "$a" | tr 'A-F' 'a-f')
    got=$(echo "$OUT" | awk -v a="$aLower" 'index($0, "addr=" a) { print $NF }' | tail -1)
    got="${got#val=}"
    want="0x$(printf '%04x' "0x$exp")"
    if [ "$got" = "$want" ]; then
        echo "[llvm816] GNO check OK: $a = $got"
    else
        echo "[llvm816 FAIL] GNO check $a: expected $want got ${got:-(none)}"
        rc=1
    fi
done
exit $rc
