#!/usr/bin/bash
#
# smilebasic-launcher -- start SmileBASIC (or PasocomMini) under QEMU on this
# machine.  It launches the app if the Docker image is already installed; it does
# NOT download/install/remove the image (`docker load -i sbqemu.tar` to install,
# `docker rmi` to remove).  Steps:
#
#   1. checks the Docker image is present (else tells you how to load it, exits);
#   2. starts the container if it is not already running;
#   3. starts the host video/audio daemon (sb-daemon) if not running;
#   4. authorises local X clients and runs the chosen app inside the container.
#
# Usage:
#   smilebasic-launcher [-p] [-f] [-r WxH] [-h]
#     -p            PasocomMini mode (run PCM instead of SmileBASIC)
#     -f            fast: uncap the frame rate (no vsync, runs as fast as it can)
#     -r WxH        force render resolution, e.g. -r 640x480 (default 1280x720)
#     -h            show this help
#
# Env overrides:
#   SB_IMAGE            Docker image      (default: sbqemu:latest)
#   SB_CONTAINER        container name    (default: sbqemu)
#   DISPLAY             X display         (default: :0)
#   VIRGL_TEST_SERVER   virgl server the daemon should launch (default:
#                       autodetected -- see "GPU quirks" below)
#
set -uo pipefail

IMAGE="${SB_IMAGE:-sbqemu:latest}"
CONTAINER="${SB_CONTAINER:-sbqemu}"
DISPLAY="${DISPLAY:-:0}"
DAEMON_BIN="$(command -v sb-daemon || echo /usr/bin/sb-daemon)"
DAEMON_LOG="${TMPDIR:-/tmp}/sb-daemon.log"
VIRGL_SOCK="/tmp/.X11-unix/.virgl_test"
QUIRK_DIR="${SB_QUIRK_DIR:-/usr/lib/smilebasic}"

msg() { printf 'smilebasic: %s\n' "$*"; }
err() { printf 'smilebasic: %s\n' "$*" >&2; }

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

# ---------------------------------------------------------------------------
# 0. Options
# ---------------------------------------------------------------------------
APP=sb                 # sb | pcm
FAST=0
FORCE_W= ; FORCE_H=
while getopts ":pfr:h-:" opt; do
    case "$opt" in
        p) APP=pcm ;;
        f) FAST=1 ;;
        r) if [[ "$OPTARG" =~ ^([0-9]+)[xX]([0-9]+)$ ]]; then
               FORCE_W="${BASH_REMATCH[1]}"; FORCE_H="${BASH_REMATCH[2]}"
           else err "bad -r resolution '$OPTARG' (use WxH, e.g. 640x480)"; exit 2; fi ;;
        h) usage 0 ;;
        -) case "$OPTARG" in
               pasocom) APP=pcm ;; fast) FAST=1 ;; help) usage 0 ;;
               *) err "unknown option --$OPTARG"; usage 2 ;;
           esac ;;
        \?) err "unknown option -$OPTARG"; usage 2 ;;
        :) err "option -$OPTARG needs a value"; exit 2 ;;
    esac
done

case "$APP" in
    sb)  RUN_CMD=/root/run-sb.sh;  APP_NAME=SmileBASIC ;;
    pcm) RUN_CMD=/root/run-pcm.sh; APP_NAME=PasocomMini ;;
esac

# Per-app env passed into the container (resolution / fast).
RUN_ENV=()
[ -n "$FORCE_W" ] && RUN_ENV+=(-e "SBGL_WIDTH=$FORCE_W" -e "SBGL_HEIGHT=$FORCE_H")
[ "$FAST" = 1 ]   && RUN_ENV+=(-e "SBGL_FPS=0")

need() {
    command -v "$1" >/dev/null 2>&1 && return 0
    err "required command '$1' not found. Please install it (${2:-}).";
    exit 1
}
need docker "docker.io or docker-ce"

# ---------------------------------------------------------------------------
# GPU quirks: this virgl_test_server build + some vendor Mesas need help.  We ship
# two LD_PRELOAD wrappers:
#   * virgl_test_server_cx4     -- Zhaoxin cx4 iGPU (ZX C-nnnn): proven cx4 shim.
#   * virgl_test_server_generic -- everything else: virgl_gpu_compat.so, a
#     GPU-agnostic shim (GL-version retry, compat profile, GLSL #version bump,
#     extension softening, GL-error suppression), which makes e.g. the Moore
#     Threads MTT S80 render; all its fixups are no-ops where unneeded.
#
# Pick by the GPU the X server ACTUALLY renders on (glxinfo), NOT merely which
# render nodes exist -- a machine can have a cx4 iGPU present yet render on a
# different card, where the cx4 shim would GLXBadFBConfig and crash the video half.
# ---------------------------------------------------------------------------
if [ -z "${VIRGL_TEST_SERVER:-}" ]; then
    _renderer=""
    if command -v glxinfo >/dev/null 2>&1; then
        _renderer="$(DISPLAY="$DISPLAY" glxinfo -B 2>/dev/null | sed -n 's/.*OpenGL renderer string: //p' | head -1)"
    fi
    case "$_renderer" in
        *"ZX "*|*Zhaoxin*|*zhaoxin*)
            if [ -x "$QUIRK_DIR/virgl_test_server_cx4" ]; then
                export VIRGL_TEST_SERVER="$QUIRK_DIR/virgl_test_server_cx4"
                msg "Zhaoxin cx4 GPU ($_renderer) -- using cx4 virgl wrapper"
            fi ;;
        "")
            # glxinfo absent: fall back to render-node driver name
            if grep -qs '^DRIVER=cx4$' /sys/class/drm/renderD*/device/uevent 2>/dev/null \
               && [ -x "$QUIRK_DIR/virgl_test_server_cx4" ]; then
                export VIRGL_TEST_SERVER="$QUIRK_DIR/virgl_test_server_cx4"
                msg "cx4 render node present (glxinfo absent) -- using cx4 virgl wrapper"
            elif [ -x "$QUIRK_DIR/virgl_test_server_generic" ]; then
                export VIRGL_TEST_SERVER="$QUIRK_DIR/virgl_test_server_generic"
                msg "GPU unknown (glxinfo absent) -- using generic virgl compat wrapper"
            fi ;;
        *)
            if [ -x "$QUIRK_DIR/virgl_test_server_generic" ]; then
                export VIRGL_TEST_SERVER="$QUIRK_DIR/virgl_test_server_generic"
                msg "GPU: $_renderer -- using generic virgl compat wrapper"
            fi ;;
    esac
fi

# ---------------------------------------------------------------------------
# Server capability probe.  The vtest command line is NOT stable across
# virglrenderer releases, and distros pin wildly different ones (UOS Server 20:
# 1.1.0; Deepin 25: 0.8.2 from 2019).  0.8.2 has neither --multi-clients nor
# --socket-path, so sb-daemon's invocation makes it exit on a usage error: no
# socket, and the container silently falls back to llvmpipe.  We ship our own
# 1.1.0 in $QUIRK_DIR, so this should only fire if that has gone missing.
# ---------------------------------------------------------------------------
if [ -n "${VIRGL_TEST_SERVER:-}" ] && [ -x "${VIRGL_TEST_SERVER:-}" ]; then
    if ! "$VIRGL_TEST_SERVER" --help 2>&1 | grep -q -- '--multi-clients'; then
        err "warning: virgl server '$VIRGL_TEST_SERVER' is too old: no --multi-clients."
        err "         needs virglrenderer >= 1.0 -- video will be SOFTWARE (slow)."
        err "         the bundled server ($QUIRK_DIR/virgl_test_server) is missing;"
        err "         reinstall smilebasic-launcher to restore it."
    fi
fi

# ---------------------------------------------------------------------------
# 1. Is the image loaded?
# ---------------------------------------------------------------------------
if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
    err "Docker image '$IMAGE' is not installed."
    err ""
    err "  Download the SmileBASIC image tarball, then load it with:"
    err "      docker load -i sbqemu.tar"
    err ""
    err "  (This launcher does not download or install the image.)"
    exit 1
fi

# ---------------------------------------------------------------------------
# 2. Is the container running?  If not, start it.
# ---------------------------------------------------------------------------
running="$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || true)"
if [ "$running" != "true" ]; then
    # A stopped/leftover container of the same name would block `docker run`.
    if docker inspect "$CONTAINER" >/dev/null 2>&1; then
        msg "removing stale container '$CONTAINER'"
        docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
    fi
    msg "starting container '$CONTAINER'"
    xhost +local: >/dev/null 2>&1 || true
    if ! docker run -d --name "$CONTAINER" --privileged --network host \
            -e DISPLAY="$DISPLAY" -v /tmp/.X11-unix:/tmp/.X11-unix \
            -v "$HOME/.Xauthority:/root/.Xauthority:ro" -e XAUTHORITY=/root/.Xauthority \
            "$IMAGE" sleep infinity >/dev/null; then
        err "failed to start container '$CONTAINER'."
        exit 1
    fi
fi

# ---------------------------------------------------------------------------
# 3. Is the host daemon (video + audio server) running?  If not, start it.
#    A daemon that is running WITHOUT its video half (no virgl socket -- e.g.
#    started before the GPU quirk wrapper existed) is restarted so hardware
#    rendering actually engages.
# ---------------------------------------------------------------------------
# Match by comm, NOT by full cmdline (a cmdline pattern can self-match wrapper
# shells quoting this script).  Kill via `kill $(pgrep ...)`, NOT pkill: on some
# hosts (observed on UOS Server 20) /usr/bin/pkill is a broken copy of pgrep that
# lists pids without killing.
DAEMON_COMM="sb-daemon"
daemon_running() { pgrep -x "$DAEMON_COMM" >/dev/null 2>&1; }
daemon_kill()    { local p; p="$(pgrep -x "$DAEMON_COMM")" && kill ${1:-} $p 2>/dev/null; true; }

if daemon_running && [ ! -S "$VIRGL_SOCK" ]; then
    msg "daemon is running without video (no virgl socket) -- restarting it"
    daemon_kill
    for _ in $(seq 1 20); do daemon_running || break; sleep 0.1; done
    if daemon_running; then
        daemon_kill -9
        for _ in $(seq 1 20); do daemon_running || break; sleep 0.1; done
    fi
fi

if ! daemon_running; then
    if [ ! -x "$DAEMON_BIN" ]; then
        err "daemon '$DAEMON_BIN' not found or not executable."
        exit 1
    fi
    msg "starting host daemon (video + audio)"
    # Detach fully so it survives this launcher and any controlling terminal.
    # (VIRGL_TEST_SERVER, if set above, is inherited by the daemon here.)
    setsid "$DAEMON_BIN" >"$DAEMON_LOG" 2>&1 </dev/null &
    # Wait briefly for it to come up (virgl socket appears when video is ready).
    for _ in $(seq 1 30); do
        [ -S "$VIRGL_SOCK" ] && break
        daemon_running || break
        sleep 0.1
    done
    if ! daemon_running; then
        err "failed to start sb-daemon (see $DAEMON_LOG)."
        exit 1
    fi
    if [ ! -S "$VIRGL_SOCK" ]; then
        msg "note: no virgl socket after startup -- video may be software (see $DAEMON_LOG)"
    fi
fi

# ---------------------------------------------------------------------------
# 4. Launch the app inside the container.
# ---------------------------------------------------------------------------
xhost +local: >/dev/null 2>&1 || true

# Use a TTY only when we actually have one (so GUI/.desktop launches don't fail
# with "the input device is not a TTY").
# Allocate a pseudo-terminal (-t) only for SmileBASIC.  PasocomMini's pcm_execute
# does console job-control (ioctl(TIOCGPGRP) -> exits if it is not the terminal's
# foreground process group).  It is a windowed app here (X output + evdev input),
# so under `docker exec -it` it is a background child of the terminal and quits
# right after its splash.  Running it WITHOUT a TTY avoids that entirely; PCM
# needs no console (keyboard comes via evdev, not stdin).
EXEC_FLAGS=(-i)
if [ "$APP" != pcm ] && [ -t 0 ] && [ -t 1 ]; then
    EXEC_FLAGS=(-it)
fi

msg "launching $APP_NAME (Ctrl-C to stop)"
exec docker exec "${EXEC_FLAGS[@]}" "${RUN_ENV[@]}" "$CONTAINER" "$RUN_CMD"
