#!/bin/bash
# This checkout's emulator: which one it is, whether there is room for it,
# and getting it up and down.
#
# The AVD is named after the enclosing git checkout and nobody has to say so
# (see `project_avd`). That is the whole point: several agent sessions work on
# this machine at once, and an emulator named after the checkout is one that
# cannot be somebody else's to install onto or take the foreground from.
#
# `up` refuses rather than tries when memory is short, which is not caution
# for its own sake. On 2026-08-30 an emulator started with 2.8 GB available
# invoked the OOM killer, and what it took was not the emulator that had just
# started: it walked the user slice and killed pipewire, dbus-broker and
# another session's running emulator first. The cost of one too many lands on
# somebody else's work, minutes later, looking like an unrelated crash.
set -euo pipefail

. "$(dirname "$(readlink -f "$0")")/../lib/project-avd.sh"

# What one costs, measured: 3.8 GB resident for a headless x86_64 AVD, plus
# what it grows into while an app builds and installs.
EMU_EXPECTED_MB=${EMU_EXPECTED_MB:-4200}
# What must be left afterwards. A machine at exactly zero swaps itself to a
# halt rather than failing cleanly.
EMU_HEADROOM_MB=${EMU_HEADROOM_MB:-1200}
# Two are comfortable on 23 GB; three is what invoked the OOM killer.
EMU_MAX_RUNNING=${EMU_MAX_RUNNING:-2}

DEVICE_PROFILE=${DEVICE_PROFILE:-pixel_10}
SYSTEM_IMAGE=${SYSTEM_IMAGE:-system-images;android-36;google_apis;x86_64}

sdk=$(android_sdk) || {
    echo "emu: no Android SDK found; set ANDROID_HOME" >&2
    exit 127
}
adb=$(android_adb)
avd=$(project_avd)
export ANDROID_AVD_HOME="${ANDROID_AVD_HOME:-$HOME/.android/avd}"

usage() {
    cat >&2 <<'USAGE'
usage: emu <command>

  name     the AVD this directory means
  serial   its adb serial, if it is running (exit 1 if not)
  list     every emulator attached, with its AVD and what it is costing
  up       start it, refusing if the machine has no room
  down     stop it

Environment: AVD_NAME overrides the name, EMU_FORCE=1 overrides the memory
refusal, DEVICE_PROFILE and SYSTEM_IMAGE decide what `up` creates.
USAGE
    exit 2
}

# Every emulator's serial, AVD and resident size -- the last being the number
# the refusal below is about, so it is worth seeing before asking for another.
cmd_list() {
    found=false
    while IFS=$'\t' read -r serial name; do
        found=true
        port=${serial#emulator-}
        rss=$(ps -eo rss,args | awk -v p="-port $port" '$0 ~ p && $0 ~ /qemu-system/ {print int($1/1024); exit}')
        printf '  %-16s %-24s %s\n' "$serial" "$name" "${rss:+$rss MB}"
    done < <(running_avds "$adb")
    [ "$found" = true ] || echo "  (none attached)"
}

# Whether starting one now is likely to cost somebody else their work.
#
# Reported in full rather than as a bare refusal: the caller cannot see this
# machine, and "not enough memory" without the numbers or a way forward is a
# message that gets overridden blind.
check_room() {
    available=$(mem_available_mb)
    needed=$((EMU_EXPECTED_MB + EMU_HEADROOM_MB))
    running=$(running_avds "$adb" | wc -l)
    problem=""
    if [ "$running" -ge "$EMU_MAX_RUNNING" ]; then
        problem="$running emulator(s) are already running, and $EMU_MAX_RUNNING is as many as this machine takes"
    elif [ "$available" -lt "$needed" ]; then
        problem="$available MB available, and one of these needs about $EMU_EXPECTED_MB MB plus $EMU_HEADROOM_MB MB of headroom"
    fi
    [ -z "$problem" ] && return 0

    if [ "${EMU_FORCE:-}" = 1 ]; then
        echo "emu: $problem -- starting anyway because EMU_FORCE=1" >&2
        return 0
    fi
    {
        echo "emu: not starting '$avd' -- $problem."
        echo
        echo "  Emulators attached:"
        cmd_list
        echo
        echo "  Biggest processes:"
        ps -eo rss,comm --sort=-rss | awk 'NR>1 && NR<=6 {printf "    %-24s %6.0f MB\n", $2, $1/1024}'
        echo
        echo "  What usually frees enough:"
        echo "    gradle --stop            (an idle Gradle daemon holds 1-2 GB between builds)"
        echo "    emu down                 (in the checkout that owns an emulator you are done with)"
        echo "    ask the session that owns one -- they are named after their checkout"
        echo "    EMU_FORCE=1 emu up       (if you have decided the numbers above are wrong)"
    } >&2
    exit 1
}

cmd_up() {
    if serial=$(avd_serial "$adb" "$avd"); then
        echo "emu: '$avd' is already running ($serial)" >&2
        echo "$serial"
        return 0
    fi
    check_room

    if [ ! -f "$ANDROID_AVD_HOME/$avd.ini" ]; then
        echo "emu: creating AVD '$avd' ($DEVICE_PROFILE, $SYSTEM_IMAGE)" >&2
        "$sdk/cmdline-tools/latest/bin/sdkmanager" "emulator" "$SYSTEM_IMAGE" >/dev/null ||
            echo "emu: sdkmanager could not confirm the system image; trying anyway" >&2
        echo no | "$sdk/cmdline-tools/latest/bin/avdmanager" create avd \
            -n "$avd" -k "$SYSTEM_IMAGE" --device "$DEVICE_PROFILE" --sdcard 512M >&2
    fi

    # The host keyboard, so anything with a text field can be typed into
    # rather than tapped out on the on-screen one.
    config="$ANDROID_AVD_HOME/$avd.avd/config.ini"
    if [ -f "$config" ] && ! grep -qx 'hw.keyboard=yes' "$config"; then
        grep -v '^hw\.keyboard=' "$config" >"$config.tmp"
        echo "hw.keyboard=yes" >>"$config.tmp"
        mv "$config.tmp" "$config"
    fi

    # A stray process for this AVD that never registered with adb. The
    # bracketed first character keeps the pattern from matching the shell
    # running this script, which would kill it mid-flight.
    pkill -f "[e]mulator.*-avd $avd" >/dev/null 2>&1 || true

    log="/tmp/$avd-emulator.log"
    : >"$log"
    if [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then
        echo "emu: starting '$avd' with GPU acceleration" >&2
        "$sdk/emulator/emulator" -avd "$avd" -gpu host -no-audio >"$log" 2>&1 &
    else
        echo "emu: no display -- starting '$avd' headless" >&2
        "$sdk/emulator/emulator" -avd "$avd" -gpu swiftshader_indirect -no-audio -no-window \
            >"$log" 2>&1 &
    fi
    pid=$!

    for _ in $(seq 150); do
        if ! kill -0 "$pid" 2>/dev/null; then
            echo "emu: the emulator exited before it finished booting:" >&2
            tail -20 "$log" >&2
            exit 1
        fi
        if serial=$(avd_serial "$adb" "$avd"); then
            booted=$("$adb" -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')
            if [ "$booted" = 1 ]; then
                echo "emu: '$avd' booted ($serial)" >&2
                echo "$serial"
                return 0
            fi
        fi
        sleep 2
    done
    echo "emu: '$avd' did not finish booting in five minutes; log at $log" >&2
    exit 1
}

cmd_down() {
    if ! serial=$(avd_serial "$adb" "$avd"); then
        echo "emu: '$avd' is not running" >&2
        return 0
    fi
    "$adb" -s "$serial" emu kill >/dev/null
    rm -f "${XDG_RUNTIME_DIR:-/tmp}/emulator-tools/$avd.serial"
    echo "emu: stopped '$avd' ($serial)" >&2
}

case "${1:-}" in
    name) echo "$avd" ;;
    serial) avd_serial "$adb" "$avd" || { echo "emu: '$avd' is not running" >&2; exit 1; } ;;
    list) cmd_list ;;
    up) cmd_up ;;
    down) cmd_down ;;
    *) usage ;;
esac
