#!/usr/bin/env bash
#
# joey-run: run a built JoeyLib program in the right emulator.
#
# Usage:
#   ./joey-run [options] PROGRAM [-- ARGS...]
#
# PROGRAM is any of:
#   a program file       build/dos/bin/DRAW.EXE, build/atarist/bin/DRAW.PRG,
#                        build/amiga/bin/Draw, build/x68000/bin/DRAW.X,
#                        build/iigs/bin/DRAW, build/dvx/bin/apps/joeylib/draw/draw.app
#   an app folder        a program in a folder of its own with its DATA:
#                        build/dvx/bin/apps/joeylib/audio
#   an IIgs disk image   build/iigs/bin/DRAW.2mg (make iigs-game-disk), or
#                        a data disk such as joey.2mg (boots GS/OS with it)
#   an example name      draw, audio, uber, ... together with --port
# The machine is recognised from the file itself. A program built outside
# JoeyLib (make/project.mk) runs the same way: name its file, folder or disk
# image in the project's build/, or give its name with --project and --port.
#
# Options:
#   --port PORT        iigs, amiga, atarist, dos, x68000 or dvx
#   --project DIR      look a program name up in the project at DIR (its
#                      build/PORT/bin) instead of JoeyLib's examples; DVX runs
#                      keep their DVX in DIR's build/dvx/run
#   --data DIR         copy DIR's files into the program's DATA folder for
#                      this run (for example the game an interpreter plays)
#   --headless         no window and no sound, for scripts and CI; stops
#                      after 60 seconds unless --seconds says otherwise
#   --seconds N        stop after N seconds
#   --snapshot FILE    save a PNG of the screen when the run stops
#                      (needs --seconds or --headless)
#   --machine rom01    IIgs: the ROM 01 machine instead of ROM 03
#   --ram SIZE         IIgs: memory size for MAME (default 4M)
#   --debug            IIgs and X68000: start MAME's debugger
#   --serial HOST:PORT connect the machine's serial port to the TCP server
#                      at HOST:PORT: a BBS, a program of your own, another
#                      run's --serial-listen, or `nc -l PORT`
#   --serial-listen PORT
#                      the serial port waits for a TCP connection on PORT
#                      (on every network interface) instead
#   --serial-baud N    IIgs and X68000: the rate MAME's end of the wire runs
#                      at, which must match the program's (default 9600,
#                      always 8N1); the other emulators pass bytes at any rate
#   -h, --help         this text
#
# Arguments after -- are passed to the program (DOS only: the other
# machines' autostart cannot pass arguments).
#
# The serial port is the machine's usual one: the IIgs modem port (the
# printer port and a Super Serial Card are not connected), the X68000's
# RS-232C port, the ST's Modem port, the Amiga's serial port (serial.device
# unit 0) and DOS's (and DVX's) COM1.
#
# A DVX program runs in DOSBox Staging on a copy of the DVX the installer
# unpacked from a DVX tree's dist/dvx.zip (install dvx), which boots
# straight into it: the program is DVX's desktop program, in its window on a
# 1024x768 desktop (640x480 with --headless). Game > Full Screen, or Alt
# with the keypad's Enter (DOSBox keeps Alt+Enter for its own full screen),
# puts it on the whole screen. Interactive runs keep that DVX in
# build/dvx/run (the project's with --project), so what the program saves
# (in DVX's CONFIG folder), and
# whether it was left on the whole screen, is still there next time. The link is raw TCP, without telnet negotiation.
# socat relays it where the emulator cannot make it itself: the ST, the
# Amiga with --serial, and the IIgs and X68000 with --serial-listen. When
# --serial's server is not there, joey-run stops, except on the IIgs and
# X68000, where MAME prints "Unable to load image" and runs with the port
# unplugged. Listening, what the program sends before the connection
# arrives waits for it, except on DOS, which drops it; the Amiga holds the
# whole machine until the connection arrives.
#
# The emulators and firmware come from ./install.sh
# (install emulator:PORT). Interactive runs use the program's own folder
# or disk image, so saves and logs persist; --headless and --data runs
# work on a private copy. Nothing is written to your home directory.

set -euo pipefail

repo="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/lib/joeyEnv.sh
source "$repo/scripts/lib/joeyEnv.sh"

port=""
projectRoot="$repo"
dataDir=""
headless=0
seconds=""
snapshot=""
machine="apple2gs"
ramSize="4M"
debug=0
program=""
progArgs=()
emuPid=""
xvfbPid=""
work=""
serialMode=""       # connect (--serial), listen (--serial-listen) or off
serialHost=""
serialPort=""
serialBaud=9600     # MAME's null modem's own default rate
serialArgs=()       # MAME's options for the link (mameSerial)
serialPty=""        # the pty socat hands Hatari and FS-UAE
relayPid=""
tcpPortMax=65535
# How long socat keeps trying to reach --serial's server before the run
# stops; an unreachable host otherwise hangs for the kernel's SYN timeout.
serialConnectSeconds=10
# DOSBox logs whether COM1 reached --serial's server as it starts.
dosSerialLogSeconds=5
pollSeconds=0.1


usage() {
    sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
}


fail() {
    echo "joey-run: $*" >&2
    exit 1
}


cleanup() {
    joeyStop "$emuPid"
    joeyStop "$relayPid"
    if [ -n "$xvfbPid" ]; then
        kill "$xvfbPid" 2>/dev/null || true
    fi
    if [ -n "$work" ]; then
        rm -rf "$work"
    fi
}


# ---- finding the program ----------------------------------------------------

# magicPort FILE -- print the machine FILE was built for, or nothing.
magicPort() {
    local file="$1"
    local magic
    case "$file" in
        *.2mg|*.2MG|*.po|*.PO|*.hdv|*.HDV)
            printf 'iigs\n'
            return 0
            ;;
    esac
    magic="$(head -c 16 "$file" | od -An -tx1 | tr -d ' \n')"
    case "$magic" in
        000003f3*) printf 'amiga\n' ;;
        601a*)     printf 'atarist\n' ;;
        4d5a*)     printf 'dos\n' ;;
        4855*)     printf 'x68000\n' ;;
        44584531*) printf 'dvx\n' ;;       # "DXE1": a DVX module (.app)
        *)
            # Apple IIgs OMF: segment header byte 14 (NUMLEN) is 4 and
            # byte 15 (VERSION) is 2.
            if [ "${magic:28:4}" = "0402" ]; then
                printf 'iigs\n'
            fi
            ;;
    esac
}


# folderProgram DIR -- the one program file inside an app folder.
folderProgram() {
    local file
    local found=""
    while IFS= read -r -d '' file; do
        if [ -n "$(magicPort "$file")" ]; then
            [ -z "$found" ] || fail "$1 holds more than one program; name the file"
            found="$file"
        fi
    done < <(find "$1" -maxdepth 1 -type f -print0)
    [ -n "$found" ] || fail "no program found in $1"
    printf '%s\n' "$found"
}


# ---- shared run plumbing ------------------------------------------------------

# stageFolder DEST -- copy the program's folder (its files and DATA tree)
# plus --data into DEST, for runs that must not touch the build output.
stageFolder() {
    local dest="$1"
    local progDir
    progDir="$(dirname "$program")"
    mkdir -p "$dest"
    find "$progDir" -maxdepth 1 -type f -exec cp -t "$dest" {} +
    if [ -d "$progDir/DATA" ]; then
        cp -r "$progDir/DATA" "$dest/"
    fi
    if [ -n "$dataDir" ]; then
        mkdir -p "$dest/DATA"
        cp -r "$dataDir/." "$dest/DATA/"
    fi
}


# startDisplay -- a virtual X display for --headless SDL emulators.
startDisplay() {
    local display
    display=":$((300 + RANDOM % 500))"
    Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp >/dev/null 2>&1 &
    xvfbPid=$!
    sleep 1
    export DISPLAY="$display"
    export SDL_AUDIODRIVER=dummy
}


# waitOrStop -- wait for the emulator, or stop it after --seconds (taking
# the --snapshot first).
waitOrStop() {
    local waited=0
    local win
    if [ -z "$seconds" ]; then
        wait "$emuPid" || true
        emuPid=""
        return 0
    fi
    while [ "$waited" -lt "$seconds" ] && kill -0 "$emuPid" 2>/dev/null; do
        sleep 1
        waited=$((waited + 1))
    done
    if [ -n "$snapshot" ]; then
        if [ "$headless" = "1" ]; then
            import -window root "$snapshot"
        elif win="$(joeyFindWindow "$DISPLAY" "$emuPid")"; then
            import -window "$win" "$snapshot"
        else
            echo "joey-run: no emulator window to snapshot" >&2
        fi
    fi
}


# mameStopScript FILE -- MAME Lua that snapshots (if asked) and exits after
# --seconds of emulated time.
mameStopScript() {
    cat > "$1" <<LUA
local done = false
emu.register_frame_done(function()
    if not done and manager.machine.time:as_double() >= $seconds then
        done = true
        if $([ -n "$snapshot" ] && echo true || echo false) then
            manager.machine.video:snapshot()
        end
        manager.machine:exit()
    end
end)
LUA
}


# runMame DRIVER ARGS... -- start MAME with the per-run directories, the
# headless/debug/stop options, then collect the snapshot.
runMame() {
    local driver="$1"
    shift
    local args=(-skip_gameinfo -cfg_directory "$work/cfg" -nvram_directory "$work/nvram"
                -snapshot_directory "$work/snap" -snapname "shot")
    if [ "$headless" = "1" ]; then
        args+=(-video none -sound none -nothrottle)
        export SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy QT_QPA_PLATFORM=offscreen
    else
        args+=(-window)
    fi
    if [ "$debug" = "1" ]; then
        args+=(-debug)
    fi
    if [ -n "$seconds" ]; then
        mameStopScript "$work/stop.lua"
        args+=(-autoboot_script "$work/stop.lua")
    fi
    (cd "$work" && exec "$MAME" "$driver" "$@" "${args[@]}") &
    emuPid=$!
    wait "$emuPid" || true
    emuPid=""
    if [ -n "$snapshot" ]; then
        if [ -f "$work/snap/shot.png" ]; then
            cp "$work/snap/shot.png" "$snapshot"
        else
            echo "joey-run: MAME saved no snapshot" >&2
        fi
    fi
}


# ---- the serial link ----------------------------------------------------------

# portFree PORT -- nothing on this machine listens on TCP PORT yet. Checked
# up front because an emulator that cannot take the port only says so in a
# log nobody reads, and runs with the serial port unplugged.
portFree() {
    python3 - "$1" <<'PY'
import socket
import sys

probe = socket.socket()
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    probe.bind(("", int(sys.argv[1])))
except OSError:
    sys.exit(1)
PY
}


# serialNote NAME -- tell the user where the serial port (NAME) is wired.
serialNote() {
    case "$serialMode" in
        connect) echo "joey-run: $1 connects to $serialHost:$serialPort." ;;
        listen)  echo "joey-run: $1 waits for a connection on TCP port $serialPort." ;;
    esac
}


# relaySerial NEAR PATH -- run socat between the emulator's end of the link
# (NEAR, a pty or unix socket that appears at PATH) and the network, and
# wait for PATH. socat opens its first address before its second, so the
# order matters. Connecting, the TCP connection goes first: an unreachable
# server stops the run before the machine boots. Listening, NEAR goes
# first: the emulator can open it at once, and what the program sends
# waits in the kernel until a client connects.
relaySerial() {
    local near="$1"
    local path="$2"
    local deadline
    command -v socat >/dev/null 2>&1 ||
        fail "the serial link on this machine goes through socat, which is not installed (package socat: apt install socat, dnf install socat)"
    if [ "$serialMode" = "connect" ]; then
        (cd "$work" && exec socat "tcp:$serialHost:$serialPort,connect-timeout=$serialConnectSeconds" "$near") 2>"$work/socat.log" &
    else
        (cd "$work" && exec socat "$near" "tcp-listen:$serialPort,reuseaddr") 2>"$work/socat.log" &
    fi
    relayPid=$!
    deadline=$((SECONDS + serialConnectSeconds + 1))
    while [ ! -e "$path" ]; do
        if ! joeyAlive "$relayPid" || [ "$SECONDS" -ge "$deadline" ]; then
            cat "$work/socat.log" >&2
            if [ "$serialMode" = "connect" ]; then
                fail "cannot connect to $serialHost:$serialPort"
            fi
            fail "socat cannot listen on TCP port $serialPort"
        fi
        sleep "$pollSeconds"
    done
}


# mameSerial DRIVER SLOT -- set serialArgs to the MAME options that plug a
# null modem into SLOT and wire it up. MAME's bitbanger connects out by
# itself; to listen, socat takes the TCP port and MAME connects to socat
# through a unix socket. The null modem clocks the wire itself, at
# --serial-baud, which cfg/DRIVER.cfg sets.
mameSerial() {
    local driver="$1"
    local slot="$2"
    local bitb="socket.$serialHost:$serialPort"
    [ -n "$serialMode" ] || return 0
    mkdir -p "$work/cfg"
    python3 "$repo/scripts/lib/mameNullModemCfg.py" "$driver" ":$slot:null_modem" "$serialBaud" \
        > "$work/cfg/$driver.cfg" || fail "--serial-baud $serialBaud is not a rate MAME's null modem has"
    if [ "$serialMode" = "listen" ]; then
        relaySerial "unix-listen:serial.sock" "$work/serial.sock"
        bitb="domain.serial.sock"
    fi
    serialArgs=(-"$slot" null_modem -bitb "$bitb")
}


# dosSerialCheck -- DOSBox tries --serial's server as it starts and, when
# that fails, only says so in its log and runs with COM1 unplugged; stop
# with the reason instead.
dosSerialCheck() {
    local deadline=$((SECONDS + dosSerialLogSeconds))
    local line
    [ "$serialMode" = "connect" ] || return 0
    while [ "$SECONDS" -lt "$deadline" ]; do
        line="$(grep -m 1 'SERIAL: Port 1 ' "$work/dosbox.log" 2>/dev/null)" || true
        case "$line" in
            *"connection failed"*) fail "DOSBox cannot connect COM1 to $serialHost:$serialPort" ;;
            ?*)                    return 0 ;;
        esac
        sleep "$pollSeconds"
    done
}


# ---- one function per machine -------------------------------------------------

runIigs() {
    local name
    local img
    local bytes
    local media=()
    joeyNeed MAME mame
    joeyNeed IIGS_ROMPATH iigsFirmware
    [ -z "$dataDir" ] || fail "--data is not supported on the IIgs; build a disk with the data on it (a game disk)"
    case "$program" in
        *.2mg|*.2MG|*.po|*.PO|*.hdv|*.HDV)
            joeyNeed CADIUS cadius
            img="$program"
            if [ "$headless" = "1" ]; then
                img="$work/$(basename "$program")"
                cp "$program" "$img"
            fi
            bytes="$(stat -c %s "$img")"
            if "$CADIUS" CATALOG "$img" 2>/dev/null | grep -qE '^  ProDOS +SYS'; then
                # A bootable game disk: it launches its program by itself.
                if [ "$bytes" -le 900000 ]; then
                    media=(-flop3 "$img")
                else
                    media=(-sl7 cffa2 -hard1 "$img")
                fi
            else
                # A data disk: boot GS/OS beside it and pick in the Finder.
                joeyNeed GSOS_SYSTEM_DISK iigsFirmware
                cp "$GSOS_SYSTEM_DISK" "$work/boot.po"
                if [ "$bytes" -le 900000 ]; then
                    media=(-flop3 "$work/boot.po" -flop4 "$img")
                else
                    media=(-flop3 "$work/boot.po" -sl7 cffa2 -hard1 "$img")
                fi
                echo "joey-run: $(basename "$img") is not bootable; GS/OS boots beside it -- open it in the Finder."
            fi
            ;;
        *)
            # A bare program: pack it onto a data disk and boot a GS/OS
            # whose startup program launches it (the developer path; needs
            # the IIgs toolchain).
            joeyNeed LLVM816_ROOT toolchain:iigs
            joeyNeed CADIUS cadius
            joeyNeed IIGS_CLANG_BUILD toolchain:iigs
            joeyNeed GSOS_SYSTEM_DISK iigsFirmware
            name="$(basename "$program")"
            BINDIR="$(dirname "$program")" JOEY_DISK_EXAMPLES="$name" \
                "$repo/scripts/make-iigs-disk.sh" "$work/data.2mg" >/dev/null
            "$repo/scripts/make-iigs-boot-disk.sh" "$work/boot.po" "$name" >/dev/null
            media=(-flop3 "$work/boot.po" -flop4 "$work/data.2mg")
            ;;
    esac
    mameSerial "$machine" modem
    echo "joey-run: booting the IIgs ($machine); the program comes up in about a minute. Close MAME to stop."
    serialNote "the modem port"
    runMame "$machine" -ramsize "$ramSize" -rompath "$IIGS_ROMPATH" "${media[@]}" "${serialArgs[@]}"
}


runAmiga() {
    local name
    local hd="$work/hd"
    local args=()
    joeyNeed FSUAE fsuae
    name="$(basename "$program")"
    # FS-UAE boots the folder as a hard drive and writes to it, so it is
    # always staged. s/startup-sequence runs the program.
    stageFolder "$hd"
    mkdir -p "$hd/s"
    printf ':%s\n' "$name" > "$hd/s/startup-sequence"
    # Kickstart 3.1 has no serial.device in ROM; AmigaOS loads it from
    # DEVS: (SYS:Devs), so a program that opens the serial port needs it on
    # this boot drive. Harmless for everything else.
    if [ -n "${AMIGA_SERIAL_DEVICE:-}" ] && [ -f "$AMIGA_SERIAL_DEVICE" ]; then
        mkdir -p "$hd/Devs"
        cp "$AMIGA_SERIAL_DEVICE" "$hd/Devs/serial.device"
    else
        echo "joey-run: no serial.device installed (./install.sh install amigaSerial); serial will not open on the Amiga" >&2
    fi
    args=(--amiga_model=A500 --fast_memory=2048
          --hard_drive_0="$hd" --hard_drive_0_label=JOEYLIB
          --automatic_input_grab=0 --middle_click_ungrab=1 --mouse_integration=1
          --floppy_drive_speed=800 --base_dir="$work/fs-uae")
    if [ -n "${KICKSTART_ROM:-}" ] && [ -f "$KICKSTART_ROM" ]; then
        args+=(--kickstart_file="$KICKSTART_ROM")
    else
        echo "joey-run: no Kickstart installed (./install.sh install kickstart); booting FS-UAE's AROS, which may not run every program" >&2
    fi
    if [ "$headless" = "1" ]; then
        startDisplay
        # A real gamepad crashes FS-UAE when there is no screen to grab.
        args+=(--joystick_port_0=none --joystick_port_1=none)
        export ALSOFT_DRIVERS=null
    fi
    case "$serialMode" in
        connect)
            # FS-UAE's tcp:// serial port only ever listens, so socat makes
            # the connection and hands FS-UAE a pty.
            relaySerial "pty,rawer,link=$serialPty" "$serialPty"
            args+=(--amiga_enable_serial_port=1 --serial_port="$serialPty")
            ;;
        listen)
            # /wait holds the machine until the client connects, so nothing
            # the program sends goes to nobody.
            args+=(--amiga_enable_serial_port=1 --serial_port="tcp://0.0.0.0:$serialPort/wait")
            ;;
    esac
    echo "joey-run: booting the Amiga; $name starts from the JOEYLIB hard drive. Close FS-UAE to stop."
    serialNote "the serial port"
    if [ "$serialMode" = "listen" ]; then
        echo "joey-run: the Amiga does not start until that connection arrives."
    fi
    "$FSUAE" "${args[@]}" >"$work/fs-uae.log" 2>&1 &
    emuPid=$!
    waitOrStop
    # Keep the program's log next to it, as a direct run would.
    find "$hd" -maxdepth 1 -iname 'joeylog.txt' -exec cp {} "$(dirname "$program")/" \; 2>/dev/null || true
}


runAtarist() {
    local dir
    local args=()
    joeyNeed HATARI hatari
    joeyNeed EMUTOS_IMG emutos
    dir="$(dirname "$program")"
    if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
        stageFolder "$work/hd"
        dir="$work/hd"
    fi
    args=(--tos "$EMUTOS_IMG" --harddrive "$dir" --gemdos-drive C --auto "C:\\$(basename "$program")")
    if [ "$headless" = "1" ]; then
        startDisplay
        args+=(--sound off)
    fi
    if [ -n "$serialMode" ]; then
        # Hatari's serial port can only be a file or a tty, so socat
        # stands between a pty and the network.
        relaySerial "pty,rawer,link=$serialPty" "$serialPty"
        args+=(--rs232-in "$serialPty" --rs232-out "$serialPty")
    fi
    echo "joey-run: booting the Atari ST (EmuTOS); $(basename "$program") starts from drive C. Close Hatari to stop."
    serialNote "the Modem port"
    "$HATARI" "${args[@]}" >"$work/hatari.log" 2>&1 &
    emuPid=$!
    waitOrStop
}


runDos() {
    local dir
    local command
    local args=()
    joeyNeed DOSBOX dosbox
    dir="$(dirname "$program")"
    if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
        stageFolder "$work/hd"
        dir="$work/hd"
    fi
    command="$(basename "$program")"
    if [ ${#progArgs[@]} -gt 0 ]; then
        command="$command ${progArgs[*]}"
    fi
    args=("${JOEY_DOSBOX_ISOLATE[@]}" --conf "$repo/scripts/dosbox-386sx16.conf"
          --set "mouse_capture=seamless" -c "MOUNT C \"$dir\"" -c "C:" -c "$command")
    # transparent:1 -- data bytes only: DOSBox's in-band escapes for the
    # control lines would reach a plain TCP peer as data.
    case "$serialMode" in
        connect) args+=(--set "serial1=nullmodem server:$serialHost port:$serialPort transparent:1") ;;
        listen)  args+=(--set "serial1=nullmodem port:$serialPort transparent:1") ;;
    esac
    if [ "$headless" = "1" ]; then
        startDisplay
    else
        # Leave the program's last screen up until a key is pressed.
        args+=(-c "pause")
    fi
    args+=(--exit)
    echo "joey-run: booting DOS (386SX-16); $command runs from drive C. Close DOSBox to stop."
    serialNote "COM1"
    XDG_CONFIG_HOME="$work/xdg" "$DOSBOX" "${args[@]}" >"$work/dosbox.log" 2>&1 &
    emuPid=$!
    dosSerialCheck
    waitOrStop
}


runDvx() {
    local dir
    local app
    joeyNeed DOSBOX dosbox
    joeyNeed DVX_ROOT dvx
    app="$(dirname "$program")"
    # Built as apps/<vendor>/<name>/: install it under the same vendor.
    if [ "$(basename "$(dirname "$(dirname "$app")")")" = apps ]; then
        DVX_VENDOR="$(basename "$(dirname "$app")")"
    fi
    if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
        dir="$work/dvx"
    else
        dir="$projectRoot/build/dvx/run"
        # DVX's own desktop size, where a program's window is the stage at 3x.
        DVX_VIDEO_W=1024
        DVX_VIDEO_H=768
    fi
    # shellcheck source=scripts/lib/dvxGate.sh
    source "$repo/scripts/lib/dvxGate.sh"
    if [ -n "$dataDir" ]; then
        stageFolder "$work/app/$(basename "$app")"
        app="$work/app/$(basename "$app")"
    fi
    dvxStage "$dir" "$app"
    dvxDosboxConf "$work/dosbox.conf" "$dir" sb16
    # transparent:1 -- data bytes only, as for DOS.
    case "$serialMode" in
        connect) printf '\n[serial]\nserial1 = nullmodem server:%s port:%s transparent:1\n' "$serialHost" "$serialPort" >> "$work/dosbox.conf" ;;
        listen)  printf '\n[serial]\nserial1 = nullmodem port:%s transparent:1\n' "$serialPort" >> "$work/dosbox.conf" ;;
    esac
    if [ "$headless" = "1" ]; then
        startDisplay
    fi
    echo "joey-run: booting DVX in DOSBox; $(basename "$program") is its desktop program. Close DOSBox to stop."
    serialNote "COM1"
    XDG_CONFIG_HOME="$work/xdg" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" --conf "$work/dosbox.conf" \
        --set "mouse_capture=seamless" > "$work/dosbox.log" 2>&1 &
    emuPid=$!
    dosSerialCheck
    waitOrStop
}


runX68000() {
    local name
    local stage="$work/files"
    joeyNeed MAME mame
    joeyNeed X68K_DIR x68kFirmware
    joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
    # shellcheck source=scripts/lib/x68kDisk.sh
    source "$repo/scripts/lib/x68kDisk.sh"
    name="$(basename "$program")"
    # Human68k boots from a trimmed copy of the system disk whose
    # AUTOEXEC.BAT runs the program from the data disk in drive B.
    x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$name" || fail "cannot build the boot disk"
    x68kDataDisk "$work/data.xdf" || fail "cannot build the data disk"
    x68kXdf add "$work/data.xdf" "$program" "$name" >/dev/null || fail "cannot copy $name onto the data disk"
    rm -rf "$stage"
    mkdir -p "$stage"
    if [ -d "$(dirname "$program")/DATA" ]; then
        cp -r "$(dirname "$program")/DATA" "$stage/"
    fi
    if [ -n "$dataDir" ]; then
        mkdir -p "$stage/DATA"
        cp -r "$dataDir/." "$stage/DATA/"
    fi
    if [ -d "$stage/DATA" ]; then
        x68kAddTree "$work/data.xdf" "$stage/DATA" DATA || fail "the DATA files do not fit on the 1232 KB data disk"
    fi
    mameSerial x68000 rs232c
    echo "joey-run: booting the X68000 (Human68k 3.02); $name runs from drive B. Close MAME to stop."
    serialNote "the RS-232C port"
    runMame x68000 -bios ipl10 -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" "${serialArgs[@]}"
}


# ---- main ---------------------------------------------------------------------

while [ $# -gt 0 ]; do
    case "$1" in
        --port)     port="${2:?--port needs iigs, amiga, atarist, dos, x68000 or dvx}"; shift ;;
        --project)
            [ -d "${2:-}" ] || fail "--project needs a project folder (one built on make/project.mk)"
            projectRoot="$(cd "$2" && pwd)"
            shift
            ;;
        --data)     dataDir="${2:?--data needs a folder}"; shift ;;
        --headless) headless=1 ;;
        --seconds)  seconds="${2:?--seconds needs a number}"; shift ;;
        --snapshot) snapshot="${2:?--snapshot needs a file name}"; shift ;;
        --machine)
            case "${2:-}" in
                rom01) machine="apple2gsr1" ;;
                rom03) machine="apple2gs" ;;
                *)     fail "--machine takes rom01 or rom03" ;;
            esac
            shift
            ;;
        --ram)      ramSize="${2:?--ram needs a size such as 4M}"; shift ;;
        --debug)    debug=1 ;;
        --serial)
            [ -z "$serialMode" ] || fail "one --serial or --serial-listen per run"
            [[ "${2:-}" =~ ^([^:]+):([0-9]+)$ ]] || fail "--serial takes HOST:PORT (a host name or IPv4 address)"
            serialMode=connect
            serialHost="${BASH_REMATCH[1]}"
            serialPort="${BASH_REMATCH[2]}"
            shift
            ;;
        --serial-listen)
            [ -z "$serialMode" ] || fail "one --serial or --serial-listen per run"
            [[ "${2:-}" =~ ^[0-9]+$ ]] || fail "--serial-listen takes a port number"
            serialMode=listen
            serialPort="$2"
            shift
            ;;
        --serial-baud) serialBaud="${2:?--serial-baud needs a rate such as 2400}"; shift ;;
        -h|--help)  usage; exit 0 ;;
        --)         shift; progArgs=("$@"); break ;;
        -*)         fail "unknown option $1 (see: joey-run --help)" ;;
        *)
            [ -z "$program" ] || fail "one program at a time (got '$program' and '$1')"
            program="$1"
            ;;
    esac
    shift
done

[ -n "$program" ] || { usage; exit 2; }
[[ "$seconds" =~ ^[0-9]*$ ]] || fail "--seconds takes a whole number"
if [ "$headless" = "1" ] && [ -z "$seconds" ]; then
    seconds=60
fi
if [ -n "$snapshot" ]; then
    [ -n "$seconds" ] || fail "--snapshot needs --seconds (or --headless)"
    snapshot="$(realpath -m "$snapshot")"
fi
if [ -n "$dataDir" ]; then
    [ -d "$dataDir" ] || fail "--data: no such folder $dataDir"
    dataDir="$(cd "$dataDir" && pwd)"
fi
if [ -n "$serialMode" ]; then
    [ "$serialPort" -ge 1 ] && [ "$serialPort" -le "$tcpPortMax" ] || fail "TCP port $serialPort is not 1 to $tcpPortMax"
fi
if [ "$serialMode" = "listen" ]; then
    portFree "$serialPort" || fail "TCP port $serialPort is already in use"
fi

# Resolve PROGRAM to a file and its machine.
if [ -d "$program" ]; then
    program="$(folderProgram "$program")"
elif [ ! -e "$program" ]; then
    [ -n "$port" ] || fail "$program is not a file; for a program name add --port"
    program="$(joeyExampleProgram "$program" "$port" "$projectRoot")" || exit 1
    [ -e "$program" ] || fail "$program is not built (make $port in $projectRoot)"
fi
program="$(cd "$(dirname "$program")" && pwd)/$(basename "$program")"
detected="$(magicPort "$program")"
[ -n "$detected" ] || fail "cannot tell which machine $program is for"
if [ -n "$port" ] && [ "$port" != "$detected" ]; then
    fail "$program is a $detected program, not $port"
fi
if [ ${#progArgs[@]} -gt 0 ] && [ "$detected" != "dos" ]; then
    fail "program arguments only work on DOS"
fi

work="$(mktemp -d -t joey-run.XXXXXX)"
serialPty="$work/serial.pty"
trap cleanup EXIT INT TERM

case "$detected" in
    iigs)    runIigs ;;
    amiga)   runAmiga ;;
    atarist) runAtarist ;;
    dos)     runDos ;;
    x68000)  runX68000 ;;
    dvx)     runDvx ;;
esac
if [ -n "$snapshot" ] && [ -f "$snapshot" ]; then
    echo "joey-run: snapshot saved to $snapshot"
fi
