#!/usr/bin/env bash
#
# JoeyLib toolchain installer.
#
# Installs all cross-compilers, assemblers, and libraries needed to build
# JoeyLib for Apple IIgs, Amiga, Atari ST, and MS-DOS into the local
# toolchains/ folder. Nothing is installed system-wide. Free tools are
# fetched and built; tools that must be placed manually print clear
# placement instructions and the script verifies they were placed
# correctly on a subsequent run.
#
# Usage:
#   ./toolchains/install.sh                   install everything we can get
#   ./toolchains/install.sh --only iigs       install only one platform
#   ./toolchains/install.sh --force           wipe and reinstall
#   ./toolchains/install.sh --skip-emulators  skip emulators (cross tools only)
#   ./toolchains/install.sh --help            show this message
#
# Emulators: dosbox, fs-uae (Amiga), hatari (Atari ST), GSplus (Apple IIgs).
# On Linux with apt, the first three are installed via 'sudo apt install'.
# On macOS with Homebrew, they are installed via 'brew install'. GSplus is
# always built from source into toolchains/emulators/gsplus/.
#

set -euo pipefail

# Never prompt for git credentials. Missing / private GitHub repos
# otherwise ask for a username/password on HTTPS clones; we want those
# to fail fast so the installer reports clear instructions instead.
export GIT_TERMINAL_PROMPT=0
export GIT_ASKPASS=/bin/true

# ------------------------------------------------------------------------
# Paths and globals
# ------------------------------------------------------------------------

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
CACHE_DIR="${SCRIPT_DIR}/cache"
STATE_DIR="${SCRIPT_DIR}/.install_state"

ONLY_PLATFORM=""
FORCE_INSTALL=0
SKIP_EMULATORS=0

# Status tracking. Keys are component identifiers; values are "ok",
# "missing", or "failed".
declare -A STATUS
declare -A INSTRUCTIONS

# ------------------------------------------------------------------------
# Output helpers (ANSI color when stdout is a TTY)
# ------------------------------------------------------------------------

if [ -t 1 ]; then
    C_RESET=$'\033[0m'
    C_BOLD=$'\033[1m'
    C_RED=$'\033[31m'
    C_GREEN=$'\033[32m'
    C_YELLOW=$'\033[33m'
    C_CYAN=$'\033[36m'
else
    C_RESET=""
    C_BOLD=""
    C_RED=""
    C_GREEN=""
    C_YELLOW=""
    C_CYAN=""
fi

info()    { printf '%s[*]%s %s\n' "${C_CYAN}"   "${C_RESET}" "$*"; }
ok()      { printf '%s[ok]%s %s\n' "${C_GREEN}" "${C_RESET}" "$*"; }
warn()    { printf '%s[!!]%s %s\n' "${C_YELLOW}" "${C_RESET}" "$*"; }
err()     { printf '%s[xx]%s %s\n' "${C_RED}"   "${C_RESET}" "$*" 1>&2; }
header()  { printf '\n%s== %s ==%s\n' "${C_BOLD}" "$*" "${C_RESET}"; }

# ------------------------------------------------------------------------
# Utility functions
# ------------------------------------------------------------------------

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

require_tool() {
    local tool="$1"
    if ! command -v "${tool}" >/dev/null 2>&1; then
        err "Required host tool not found: ${tool}"
        err "Install it via your system package manager and re-run."
        exit 1
    fi
}

detect_host_os() {
    case "$(uname -s)" in
        Linux*)   HOST_OS="linux" ;;
        Darwin*)  HOST_OS="macos" ;;
        MINGW*|MSYS*|CYGWIN*)
                  HOST_OS="windows" ;;
        *)
            err "Unsupported host OS: $(uname -s)"
            exit 1
            ;;
    esac
    info "Host OS: ${HOST_OS}"
}

ensure_dirs() {
    mkdir -p "${CACHE_DIR}" "${STATE_DIR}"
}

mark_done() {
    touch "${STATE_DIR}/$1.done"
}

is_done() {
    [ -f "${STATE_DIR}/$1.done" ]
}

clear_done() {
    rm -f "${STATE_DIR}/$1.done"
}

download() {
    local url="$1"
    local dest="$2"
    if [ -f "${dest}" ]; then
        info "Cached: $(basename "${dest}")"
        return 0
    fi
    info "Downloading $(basename "${dest}")"
    if command -v curl >/dev/null 2>&1; then
        curl -fL --retry 3 -o "${dest}" "${url}"
    elif command -v wget >/dev/null 2>&1; then
        wget -O "${dest}" "${url}"
    else
        err "Neither curl nor wget is installed"
        return 1
    fi
}

# ------------------------------------------------------------------------
# Argument parsing
# ------------------------------------------------------------------------

while [ $# -gt 0 ]; do
    case "$1" in
        --only)
            ONLY_PLATFORM="$2"
            shift 2
            ;;
        --force)
            FORCE_INSTALL=1
            shift
            ;;
        --skip-emulators)
            SKIP_EMULATORS=1
            shift
            ;;
        --help|-h)
            usage
            ;;
        *)
            err "Unknown argument: $1"
            usage
            ;;
    esac
done

should_install() {
    local platform="$1"
    if [ -z "${ONLY_PLATFORM}" ]; then
        return 0
    fi
    [ "${ONLY_PLATFORM}" = "${platform}" ]
}

# ------------------------------------------------------------------------
# IIgs: llvm-mos w65816 toolchain (clang + runtime + MAME -- the primary
# build path, vendored under iigs/llvm-mos) + Merlin32 + NinjaTrackerPlus.
# ------------------------------------------------------------------------

install_audio_libxmp() {
    header "libxmp-lite (DOS / ST audio decoder)"
    local base="${SCRIPT_DIR}/audio"
    local target="${base}/libxmp-lite"
    local key="audio_libxmp"
    local version="4.7.0"

    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -rf "${target}"
        clear_done "${key}"
    fi
    if is_done "${key}" && [ -f "${target}/include/libxmp-lite/xmp.h" ]; then
        ok "libxmp-lite ${version} already installed"
        STATUS[${key}]="ok"
        return
    fi

    info "Downloading libxmp-lite ${version} from libxmp.org"
    local tarball="${CACHE_DIR}/libxmp-lite-${version}.tar.gz"
    local url="https://github.com/libxmp/libxmp/releases/download/libxmp-${version}/libxmp-lite-${version}.tar.gz"
    if ! download "${url}" "${tarball}"; then
        STATUS[${key}]="failed"
        INSTRUCTIONS[${key}]="Could not download ${url}"
        return
    fi

    mkdir -p "${base}"
    rm -rf "${target}"
    if (cd "${base}" && tar xzf "${tarball}") && [ -d "${base}/libxmp-lite-${version}" ]; then
        mv "${base}/libxmp-lite-${version}" "${target}"
        mark_done "${key}"
        ok "libxmp-lite installed at ${target}"
        STATUS[${key}]="ok"
    else
        STATUS[${key}]="failed"
        INSTRUCTIONS[${key}]="Extract of ${tarball} did not produce libxmp-lite-${version}/"
    fi
}


install_iigs() {
    header "IIgs toolchain"
    local base="${SCRIPT_DIR}/iigs"
    mkdir -p "${base}"

    # llvm-mos w65816 toolchain (clang + llvm-mc + link816 + the prebuilt
    # runtime, plus MAME + apple2gs ROMs). The primary IIgs build path:
    # clang-build.sh / make/iigs.mk resolve it via $LLVM816_ROOT, which
    # toolchains/env.sh defaults to ${base}/llvm-mos. It is a git clone of
    # the project repo, built by the repo's own setup.sh (clang/llc/llvm-mc
    # build + link816/omfEmit + runtime + MAME). Calypsi and GNO/ME are
    # optional reference targets we skip.
    local llvm_dir="${base}/llvm-mos"
    local llvm_repo="https://forge.duensing.digital/AI_Slop/65816-llvm-mos.git"
    local llvm_clang="${llvm_dir}/tools/llvm-mos-build/bin/clang"
    if is_done "iigs_llvm" && [ -x "${llvm_clang}" ]; then
        ok "llvm-mos w65816 toolchain already built"
        STATUS[iigs_llvm]="ok"
    else
        if [ ! -d "${llvm_dir}/.git" ]; then
            info "Cloning 65816-llvm-mos into ${llvm_dir}"
            git clone "${llvm_repo}" "${llvm_dir}" || \
                INSTRUCTIONS[iigs_llvm]="git clone ${llvm_repo} ${llvm_dir} failed"
        fi
        if [ -f "${llvm_dir}/setup.sh" ] && [ ! -x "${llvm_clang}" ]; then
            info "Building llvm-mos toolchain via setup.sh (~30-60 min: clang + runtime + MAME)"
            ( cd "${llvm_dir}" && bash setup.sh --skip-calypsi --skip-gno ) || \
                warn "llvm-mos setup.sh reported issues; see above"
        fi
        if [ -x "${llvm_clang}" ]; then
            mark_done "iigs_llvm"
            ok "llvm-mos w65816 toolchain built at ${llvm_dir}"
            STATUS[iigs_llvm]="ok"
        else
            STATUS[iigs_llvm]="missing"
            INSTRUCTIONS[iigs_llvm]=$(cat <<EOF
llvm-mos w65816 toolchain not found at ${llvm_dir}.
Clone and build it (the clang build takes ~30-60 min):
  git clone ${llvm_repo} ${llvm_dir}
  cd ${llvm_dir} && ./setup.sh --skip-calypsi --skip-gno
EOF
)
        fi
    fi

    # PHP CLI is required by tools/joeymod when converting .MOD files
    # to NinjaTrackerPlus's .NTP runtime format. Installed once here so
    # the IIgs audio asset pipeline works out of the box.
    if command -v php >/dev/null 2>&1; then
        ok "php CLI already on PATH"
        STATUS[iigs_php]="ok"
    else
        info "Installing php CLI for joeymod's MOD->NTP converter"
        local php_pkgs=""
        case "${HOST_OS}" in
            linux)  php_pkgs="php-cli" ;;
            macos)  php_pkgs="php"     ;;
        esac
        if [ -n "${php_pkgs}" ] && pkg_install ${php_pkgs}; then
            ok "php installed"
            STATUS[iigs_php]="ok"
        else
            STATUS[iigs_php]="missing"
            INSTRUCTIONS[iigs_php]=$(cat <<EOF
php CLI not installed. Required by tools/joeymod for the IIgs
.MOD -> .NTP conversion path.
  Debian/Ubuntu:  sudo apt install php-cli
  macOS Homebrew: brew install php
EOF
)
        fi
    fi

    # Merlin32 -- 65816 cross-assembler from Brutal Deluxe.
    # The canonical distribution is a zip on brutaldeluxe.fr that bundles
    # prebuilt binaries for Linux / macOS / Windows plus the C source.
    # We prefer the host's prebuilt binary; if not present, we build
    # from source with plain gcc (the source has no Makefile).
    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -rf "${base}/merlin32"
        clear_done "iigs_merlin32"
    fi

    if is_done "iigs_merlin32" && [ -x "${base}/merlin32/bin/merlin32" ]; then
        ok "Merlin32 already installed"
        STATUS[iigs_merlin32]="ok"
    else
        local merlin_installed=0
        local merlin_urls=(
            "https://brutaldeluxe.fr/products/crossdevtools/merlin/Merlin32_v1.1.zip"
            "https://brutaldeluxe.fr/products/crossdevtools/merlin/Merlin32.zip"
        )
        local merlin_zip="${CACHE_DIR}/merlin32.zip"
        local merlin_extract="${CACHE_DIR}/merlin32-extract"
        local prebuilt_subdir=""

        case "${HOST_OS}" in
            linux)  prebuilt_subdir="Linux" ;;
            macos)  prebuilt_subdir="MacOs" ;;
            *)      prebuilt_subdir=""       ;;
        esac

        for url in "${merlin_urls[@]}"; do
            if download "${url}" "${merlin_zip}" 2>/dev/null; then
                info "Extracting Merlin32"
                rm -rf "${merlin_extract}"
                mkdir -p "${merlin_extract}"
                if ! unzip -o "${merlin_zip}" -d "${merlin_extract}" >/dev/null 2>&1; then
                    continue
                fi
                mkdir -p "${base}/merlin32/bin"

                # 1. Try prebuilt binary for the host OS. Name may be
                #    capitalized ("Merlin32") or lowercase. No executable
                #    bit in the zip, so we chmod after copying.
                local mbin=""
                if [ -n "${prebuilt_subdir}" ]; then
                    mbin=$(find "${merlin_extract}" -type d -name "${prebuilt_subdir}" -print 2>/dev/null | head -n1)
                    if [ -n "${mbin}" ]; then
                        mbin=$(find "${mbin}" -maxdepth 1 -type f -iname "merlin32" 2>/dev/null | head -n1)
                    fi
                fi

                if [ -n "${mbin}" ] && [ -f "${mbin}" ]; then
                    cp "${mbin}" "${base}/merlin32/bin/merlin32"
                    chmod +x "${base}/merlin32/bin/merlin32"
                    mark_done "iigs_merlin32"
                    ok "Merlin32 installed (prebuilt ${prebuilt_subdir} binary)"
                    STATUS[iigs_merlin32]="ok"
                    merlin_installed=1
                    break
                fi

                # 2. No prebuilt for this host; build from Source/. The
                #    source has no Makefile; 'gcc -O2 *.c' produces the
                #    binary directly.
                local msrc
                msrc=$(find "${merlin_extract}" -type d -name "Source" 2>/dev/null | head -n1)
                if [ -n "${msrc}" ] && [ -f "${msrc}/Main.c" ]; then
                    info "Building Merlin32 from source"
                    (
                        cd "${msrc}"
                        gcc -O2 -o merlin32 *.c
                    ) && {
                        if [ -x "${msrc}/merlin32" ]; then
                            cp "${msrc}/merlin32" "${base}/merlin32/bin/merlin32"
                            mark_done "iigs_merlin32"
                            ok "Merlin32 built from source and installed"
                            STATUS[iigs_merlin32]="ok"
                            merlin_installed=1
                            break
                        fi
                    }
                fi
            fi
        done

        if [ "${merlin_installed}" -eq 0 ]; then
            STATUS[iigs_merlin32]="missing"
            INSTRUCTIONS[iigs_merlin32]=$(cat <<EOF
Merlin32 could not be installed automatically. Install manually:
  1. Visit https://brutaldeluxe.fr/products/crossdevtools/merlin/
  2. Download the latest Merlin32 zip.
  3. Either copy the prebuilt binary for your OS, or build from Source/
     with 'gcc -O2 -o merlin32 *.c'.
  4. Place the result at ${base}/merlin32/bin/merlin32 and chmod +x it.
  5. Re-run ./toolchains/install.sh to verify.
EOF
)
        fi
    fi

    # NinjaTrackerPlus -- Ninjaforce's IIgs MOD player. Distributed as
    # source (ZIP) plus binary tool disk images on ninjaforce.com.
    # Redistribution is granted by the copyright holder per the user's
    # standing permission. We download the source archive (extracted
    # into ${base}/ntp), the demo disk (kept in cache), and the tool
    # disk (kept in cache) so a fresh checkout can build the IIgs
    # audio HAL without manual intervention.
    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -rf "${base}/ntp"
        clear_done "iigs_ntp"
    fi
    if is_done "iigs_ntp" && [ -f "${base}/ntp/ninjatrackerplus.s" ]; then
        ok "NinjaTrackerPlus already installed"
        STATUS[iigs_ntp]="ok"
    else
        info "Downloading NinjaTrackerPlus from ninjaforce.com"
        local ntp_src_zip="${CACHE_DIR}/ntpsources.zip"
        local ntp_demo="${CACHE_DIR}/ntp.2mg"
        local ntp_tool="${CACHE_DIR}/ninjatrackerplus_tool222_v1.4.2mg"
        local ntp_failed=0
        download "https://www.ninjaforce.com/downloads/ntpsources.zip" "${ntp_src_zip}" || ntp_failed=1
        download "https://www.ninjaforce.com/downloads/ntp.2mg" "${ntp_demo}" || true
        download "https://www.ninjaforce.com/downloads/ninjatrackerplus_tool222_v1.4.2mg" "${ntp_tool}" || true
        if [ "${ntp_failed}" -eq 1 ]; then
            STATUS[iigs_ntp]="failed"
            INSTRUCTIONS[iigs_ntp]="Could not download ntpsources.zip from www.ninjaforce.com"
        else
            mkdir -p "${base}/ntp"
            if (cd "${base}/ntp" && unzip -oq "${ntp_src_zip}") && [ -f "${base}/ntp/ninjatrackerplus.s" ]; then
                # ntpsources.zip ships .s files with CRLF line
                # endings; Merlin32 rejects those as "Unknown line".
                # Normalize once here so the build doesn't have to.
                for f in "${base}/ntp"/*.s; do
                    sed -i 's/\r$//' "$f"
                done
                mark_done "iigs_ntp"
                ok "NinjaTrackerPlus installed at ${base}/ntp"
                STATUS[iigs_ntp]="ok"
            else
                STATUS[iigs_ntp]="failed"
                INSTRUCTIONS[iigs_ntp]="Extract of ${ntp_src_zip} did not produce ninjatrackerplus.s"
            fi
        fi
    fi
}

# ------------------------------------------------------------------------
# Amiga: Bebbo m68k-amigaos-gcc, vasm, NDK, PTPlayer
# ------------------------------------------------------------------------

install_amiga() {
    header "Amiga toolchain"
    local base="${SCRIPT_DIR}/amiga"
    mkdir -p "${base}"

    # Bebbo amiga-gcc -- build from source; long but reliable
    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -rf "${base}/gcc"
        clear_done "amiga_gcc"
    fi

    if is_done "amiga_gcc" && [ -x "${base}/gcc/bin/m68k-amigaos-gcc" ]; then
        ok "m68k-amigaos-gcc already installed"
        STATUS[amiga_gcc]="ok"
    else
        ensure_build_deps "m68k-amigaos-gcc" \
            "build-essential bison flex m4 texinfo libgmp-dev libmpfr-dev libmpc-dev autoconf automake lhasa" \
            "bison flex m4 texinfo gmp mpfr libmpc autoconf automake lhasa" || true
        info "Cloning AmigaPorts/m68k-amigaos-gcc (this build takes a while)"
        local src="${CACHE_DIR}/amiga-gcc-src"
        if [ ! -d "${src}" ]; then
            if ! git clone https://github.com/AmigaPorts/m68k-amigaos-gcc.git "${src}"; then
                STATUS[amiga_gcc]="failed"
                INSTRUCTIONS[amiga_gcc]=$(cat <<EOF
Clone https://github.com/AmigaPorts/m68k-amigaos-gcc manually and build:
  git clone https://github.com/AmigaPorts/m68k-amigaos-gcc.git
  cd m68k-amigaos-gcc
  make all PREFIX=${base}/gcc
Build deps (Debian/Ubuntu):
  sudo apt install build-essential bison flex texinfo libgmp-dev libmpfr-dev libmpc-dev
EOF
)
                return
            fi
        fi
        (
            cd "${src}"
            make all PREFIX="${base}/gcc"
        ) && {
            mark_done "amiga_gcc"
            ok "m68k-amigaos-gcc installed"
            STATUS[amiga_gcc]="ok"
        } || {
            STATUS[amiga_gcc]="failed"
            INSTRUCTIONS[amiga_gcc]=$(cat <<EOF
m68k-amigaos-gcc build failed. Install build dependencies:
  Debian/Ubuntu:  sudo apt install build-essential bison flex texinfo libgmp-dev libmpfr-dev libmpc-dev
  macOS Homebrew: brew install bison flex texinfo gmp mpfr libmpc
Then delete ${src} and re-run ./toolchains/install.sh.
Source: ${src}
EOF
)
        }
    fi

    # vasm -- assembler used by Amiga and ST
    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -rf "${base}/vasm"
        clear_done "vasm"
    fi

    if is_done "vasm" && [ -x "${base}/vasm/bin/vasmm68k_mot" ]; then
        ok "vasm already installed"
        STATUS[vasm]="ok"
    else
        info "Building vasm from source"
        local vasm_src="${CACHE_DIR}/vasm"
        if [ ! -d "${vasm_src}" ]; then
            download "http://sun.hasenbraten.de/vasm/release/vasm.tar.gz" \
                     "${CACHE_DIR}/vasm.tar.gz"
            mkdir -p "${vasm_src}"
            tar -xzf "${CACHE_DIR}/vasm.tar.gz" -C "${vasm_src}" --strip-components=1
        fi
        (
            cd "${vasm_src}"
            make CPU=m68k SYNTAX=mot
            mkdir -p "${base}/vasm/bin"
            cp vasmm68k_mot vobjdump "${base}/vasm/bin/" 2>/dev/null || true
        ) && {
            mark_done "vasm"
            ok "vasm installed"
            STATUS[vasm]="ok"
        } || {
            STATUS[vasm]="failed"
            INSTRUCTIONS[vasm]="vasm build failed; see ${vasm_src}"
        }
    fi

    # NDK -- AmigaOS includes (Bebbo's repo bundles these via 'make all')
    if [ -d "${base}/gcc/m68k-amigaos/sys-include" ] || [ -d "${base}/gcc/m68k-amigaos/include" ]; then
        ok "Amiga NDK headers present (bundled with amiga-gcc)"
        STATUS[amiga_ndk]="ok"
    else
        STATUS[amiga_ndk]="missing"
        INSTRUCTIONS[amiga_ndk]="Amiga NDK headers should be installed by Bebbo's amiga-gcc 'make all'. Re-run installer after fixing amiga-gcc."
    fi

    # PTPlayer -- MOD replayer by Frank Wille, distributed as an LHA
    # archive on Aminet. Extract with lhasa (installed as part of the
    # Amiga GCC build deps above).
    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -rf "${base}/ptplayer"
        clear_done "amiga_ptplayer"
    fi

    if is_done "amiga_ptplayer" && [ -f "${base}/ptplayer/ptplayer.asm" ]; then
        ok "PTPlayer already installed"
        STATUS[amiga_ptplayer]="ok"
    else
        info "Downloading PTPlayer from Aminet"
        local ptp_lha="${CACHE_DIR}/ptplayer.lha"
        if ! download "http://aminet.net/mus/play/ptplayer.lha" "${ptp_lha}"; then
            STATUS[amiga_ptplayer]="failed"
            INSTRUCTIONS[amiga_ptplayer]="Could not download http://aminet.net/mus/play/ptplayer.lha"
        else
            local extractor=""
            if command -v lhasa >/dev/null 2>&1; then
                extractor="lhasa"
            elif command -v lha >/dev/null 2>&1; then
                extractor="lha"
            fi

            if [ -z "${extractor}" ]; then
                STATUS[amiga_ptplayer]="missing"
                INSTRUCTIONS[amiga_ptplayer]=$(cat <<EOF
PTPlayer was downloaded but no LHA extractor is available.
Install one and re-run:
  Debian/Ubuntu:  sudo apt install lhasa
  macOS Homebrew: brew install lhasa
Archive at: ${ptp_lha}
Target:     ${base}/ptplayer/
EOF
)
            else
                mkdir -p "${base}/ptplayer"
                local extract_ok=0
                if [ "${extractor}" = "lhasa" ]; then
                    (cd "${base}/ptplayer" && lhasa xq "${ptp_lha}") && extract_ok=1
                else
                    (cd "${base}/ptplayer" && lha xq "${ptp_lha}") && extract_ok=1
                fi
                if [ "${extract_ok}" -eq 1 ] && [ -f "${base}/ptplayer/ptplayer.asm" ]; then
                    mark_done "amiga_ptplayer"
                    ok "PTPlayer installed"
                    STATUS[amiga_ptplayer]="ok"
                else
                    STATUS[amiga_ptplayer]="failed"
                    INSTRUCTIONS[amiga_ptplayer]="LHA extract of ${ptp_lha} failed or did not produce ptplayer.asm"
                fi
            fi
        fi
    fi
}

# ------------------------------------------------------------------------
# Atari ST: m68k-atari-mint-gcc, MiNTLib (vasm shared with Amiga)
# ------------------------------------------------------------------------

install_atarist() {
    header "Atari ST toolchain"
    local base="${SCRIPT_DIR}/atarist"
    mkdir -p "${base}"

    # m68k-atari-mint-gcc, binutils, and MiNTLib come as three separate
    # tarballs from tho-otto.de (Vincent Riviere's official mirror).
    # Each tarball is rooted at 'usr/'; stripping that single component
    # places everything into ${base}/gcc/ cleanly, and the relocated
    # tree works without -B/--sysroot flags.
    local mint_host=""
    case "${HOST_OS}" in
        linux)
            if [ "$(uname -m)" = "x86_64" ]; then
                mint_host="linux64"
            else
                mint_host="linux32"
            fi
            ;;
        macos)  mint_host="macos" ;;
    esac

    local mint_base_url="https://tho-otto.de/download/mint"
    local mint_tarballs=(
        "gcc-15.2.0-mint-20250810-bin-${mint_host}.tar.xz"
        "binutils-2.45-mint-20250812-bin-${mint_host}.tar.xz"
    )
    local mintlib_tarball="mintlib-0.60.1-mint-20240718-dev.tar.xz"

    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -rf "${base}/gcc"
        clear_done "atarist_gcc"
        clear_done "atarist_mintlib"
    fi

    # GCC + binutils
    if is_done "atarist_gcc" && [ -x "${base}/gcc/bin/m68k-atari-mint-gcc" ]; then
        ok "m68k-atari-mint-gcc already installed"
        STATUS[atarist_gcc]="ok"
    else
        if [ -z "${mint_host}" ]; then
            STATUS[atarist_gcc]="missing"
            INSTRUCTIONS[atarist_gcc]="No prebuilt m68k-atari-mint-gcc available for host '${HOST_OS}'. Install manually from https://tho-otto.de/download/mint/"
        else
            mkdir -p "${base}/gcc"
            local gcc_ok=1
            for tarball in "${mint_tarballs[@]}"; do
                local dest="${CACHE_DIR}/${tarball}"
                if ! download "${mint_base_url}/${tarball}" "${dest}"; then
                    gcc_ok=0
                    break
                fi
                if ! tar -xJf "${dest}" -C "${base}/gcc" --strip-components=1; then
                    gcc_ok=0
                    break
                fi
            done
            if [ "${gcc_ok}" -eq 1 ] && [ -x "${base}/gcc/bin/m68k-atari-mint-gcc" ]; then
                mark_done "atarist_gcc"
                ok "m68k-atari-mint-gcc installed (prebuilt ${mint_host})"
                STATUS[atarist_gcc]="ok"
            else
                STATUS[atarist_gcc]="failed"
                INSTRUCTIONS[atarist_gcc]="m68k-atari-mint-gcc tarball download or extract failed; see ${CACHE_DIR}"
            fi
        fi
    fi

    # MiNTLib -- headers and libc for cross-compile
    if is_done "atarist_mintlib" && [ -d "${base}/gcc/m68k-atari-mint/sys-root" ]; then
        ok "MiNTLib already installed"
        STATUS[atarist_mintlib]="ok"
    else
        if [ ! -x "${base}/gcc/bin/m68k-atari-mint-gcc" ]; then
            STATUS[atarist_mintlib]="missing"
            INSTRUCTIONS[atarist_mintlib]="Install m68k-atari-mint-gcc first; MiNTLib extracts into the same prefix."
        else
            local mlib_dest="${CACHE_DIR}/${mintlib_tarball}"
            if download "${mint_base_url}/${mintlib_tarball}" "${mlib_dest}" && \
               tar -xJf "${mlib_dest}" -C "${base}/gcc" --strip-components=1; then
                mark_done "atarist_mintlib"
                ok "MiNTLib installed"
                STATUS[atarist_mintlib]="ok"
            else
                STATUS[atarist_mintlib]="failed"
                INSTRUCTIONS[atarist_mintlib]="MiNTLib tarball download or extract failed; see ${mlib_dest}"
            fi
        fi
    fi

    # vasm -- symlink to the Amiga port's vasm
    if [ -L "${base}/vasm" ] || [ -d "${base}/vasm" ]; then
        ok "vasm symlink present for ST"
        STATUS[atarist_vasm]="ok"
    else
        if [ -d "${SCRIPT_DIR}/amiga/vasm" ]; then
            ln -s "../amiga/vasm" "${base}/vasm"
            ok "Linked atarist/vasm -> amiga/vasm"
            STATUS[atarist_vasm]="ok"
        else
            STATUS[atarist_vasm]="missing"
            INSTRUCTIONS[atarist_vasm]="vasm not yet installed for Amiga; install Amiga toolchain first."
        fi
    fi
}

# ------------------------------------------------------------------------
# DOS: DJGPP, NASM, CWSDPMI
# ------------------------------------------------------------------------

install_dos() {
    header "DOS toolchain"
    local base="${SCRIPT_DIR}/dos"
    mkdir -p "${base}"

    # DJGPP -- the canonical zip set
    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -rf "${base}/djgpp"
        clear_done "dos_djgpp"
    fi

    if is_done "dos_djgpp" && [ -x "${base}/djgpp/bin/i586-pc-msdosdjgpp-gcc" ]; then
        ok "DJGPP already installed"
        STATUS[dos_djgpp]="ok"
    else
        # Use prebuilt DJGPP binaries from andrewwutw/build-djgpp
        # releases. Much faster than source build and avoids a long list
        # of build-dep prerequisites.
        info "Downloading DJGPP prebuilt binaries"
        local djgpp_arch=""
        local djgpp_url=""
        case "${HOST_OS}" in
            linux)
                if [ "$(uname -m)" = "x86_64" ]; then
                    djgpp_arch="linux64"
                else
                    djgpp_arch="linux32"
                fi
                ;;
            macos)
                djgpp_arch="osx"
                ;;
        esac

        if [ -n "${djgpp_arch}" ]; then
            djgpp_url="https://github.com/andrewwutw/build-djgpp/releases/download/v3.4/djgpp-${djgpp_arch}-gcc1220.tar.bz2"
        fi

        local djgpp_tar="${CACHE_DIR}/djgpp.tar.bz2"
        if [ -n "${djgpp_url}" ] && download "${djgpp_url}" "${djgpp_tar}" 2>/dev/null; then
            mkdir -p "${base}/djgpp"
            # The tarball extracts to a 'djgpp' directory at the top level.
            # Strip one component so contents land directly in ${base}/djgpp.
            tar -xjf "${djgpp_tar}" -C "${base}/djgpp" --strip-components=1 && {
                if [ -x "${base}/djgpp/bin/i586-pc-msdosdjgpp-gcc" ]; then
                    mark_done "dos_djgpp"
                    ok "DJGPP installed (prebuilt ${djgpp_arch})"
                    STATUS[dos_djgpp]="ok"
                else
                    STATUS[dos_djgpp]="failed"
                    INSTRUCTIONS[dos_djgpp]="DJGPP tarball extracted but i586-pc-msdosdjgpp-gcc not found under ${base}/djgpp/bin/"
                fi
            } || {
                STATUS[dos_djgpp]="failed"
                INSTRUCTIONS[dos_djgpp]="Failed to extract DJGPP tarball ${djgpp_tar}"
            }
        else
            STATUS[dos_djgpp]="failed"
            INSTRUCTIONS[dos_djgpp]=$(cat <<EOF
No prebuilt DJGPP is available for host '${HOST_OS}'. Build from source:
  git clone https://github.com/andrewwutw/build-djgpp.git
  cd build-djgpp
  DJGPP_PREFIX=${base}/djgpp ./build-djgpp.sh 12.2.0
Build deps (Debian/Ubuntu):
  sudo apt install build-essential bison flex texinfo zlib1g-dev unzip curl
EOF
)
        fi
    fi

    # NASM
    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -rf "${base}/nasm"
        clear_done "dos_nasm"
    fi

    if is_done "dos_nasm" && [ -x "${base}/nasm/bin/nasm" ]; then
        ok "NASM already installed"
        STATUS[dos_nasm]="ok"
    else
        info "Building NASM"
        local nasm_ver="2.16.03"
        local nasm_url="https://www.nasm.us/pub/nasm/releasebuilds/${nasm_ver}/nasm-${nasm_ver}.tar.gz"
        download "${nasm_url}" "${CACHE_DIR}/nasm.tar.gz"
        local nasm_src="${CACHE_DIR}/nasm-src"
        rm -rf "${nasm_src}"
        mkdir -p "${nasm_src}"
        tar -xzf "${CACHE_DIR}/nasm.tar.gz" -C "${nasm_src}" --strip-components=1
        (
            cd "${nasm_src}"
            ./configure --prefix="${base}/nasm"
            make
            make install
        ) && {
            mark_done "dos_nasm"
            ok "NASM installed"
            STATUS[dos_nasm]="ok"
        } || {
            STATUS[dos_nasm]="failed"
            INSTRUCTIONS[dos_nasm]="NASM build failed; see ${nasm_src}"
        }
    fi

    # CWSDPMI
    if [ -f "${base}/cwsdpmi/bin/CWSDSTUB.EXE" ] && [ -f "${base}/cwsdpmi/bin/CWSDPMI.EXE" ]; then
        ok "CWSDPMI present"
        STATUS[dos_cwsdpmi]="ok"
    else
        info "Fetching CWSDPMI"
        download "https://sandmann.dotster.com/cwsdpmi/csdpmi7b.zip" \
                 "${CACHE_DIR}/cwsdpmi.zip" || {
            STATUS[dos_cwsdpmi]="failed"
            INSTRUCTIONS[dos_cwsdpmi]="Download CWSDPMI binaries from https://sandmann.dotster.com/cwsdpmi/ and unzip into ${base}/cwsdpmi/bin/"
            return
        }
        mkdir -p "${base}/cwsdpmi/bin"
        (cd "${base}/cwsdpmi/bin" && unzip -o "${CACHE_DIR}/cwsdpmi.zip" >/dev/null) && {
            ok "CWSDPMI installed"
            STATUS[dos_cwsdpmi]="ok"
        } || {
            STATUS[dos_cwsdpmi]="failed"
        }
    fi
}

# ------------------------------------------------------------------------
# Emulators: DOSBox, FS-UAE, Hatari, GSplus
#
# Package-manager-installable emulators (DOSBox, FS-UAE, Hatari) are
# installed via apt on Linux / brew on macOS and exist on the host PATH
# after install. GSplus has no package and is always built from source
# into toolchains/emulators/gsplus/.
# ------------------------------------------------------------------------

pkg_install() {
    # Install a list of package names on the current host. Returns 0 on
    # success, non-zero on any failure. Uses apt on Linux, brew on macOS.
    case "${HOST_OS}" in
        linux)
            if command -v apt-get >/dev/null 2>&1; then
                sudo apt-get install -y "$@"
                return $?
            fi
            ;;
        macos)
            if command -v brew >/dev/null 2>&1; then
                brew install "$@"
                return $?
            fi
            ;;
    esac
    return 1
}

# Install build dependencies. Args: label, apt-list, brew-list.
# The label identifies what we're about to build; the lists are the
# space-separated package names for each package manager. On failure,
# prints instructions but does not abort the installer (the build step
# will then fail with a clearer message).
ensure_build_deps() {
    local label="$1"
    local apt_pkgs="$2"
    local brew_pkgs="$3"
    local pkgs=""

    case "${HOST_OS}" in
        linux)  pkgs="${apt_pkgs}"  ;;
        macos)  pkgs="${brew_pkgs}" ;;
    esac

    if [ -z "${pkgs}" ]; then
        return 0
    fi

    info "Installing ${label} build dependencies"
    if ! pkg_install ${pkgs}; then
        warn "Failed to install ${label} dependencies via package manager."
        warn "You may need to install manually: ${pkgs}"
        return 1
    fi
    return 0
}

install_apt_emulator() {
    local key="$1"
    local cmd="$2"
    local aptpkg="$3"
    local brewpkg="$4"

    if command -v "${cmd}" >/dev/null 2>&1; then
        ok "${cmd} already on PATH"
        STATUS[${key}]="ok"
        return
    fi

    info "Installing ${cmd} via host package manager"
    local pkgs
    case "${HOST_OS}" in
        linux)  pkgs="${aptpkg}"  ;;
        macos)  pkgs="${brewpkg}" ;;
        *)      pkgs=""           ;;
    esac

    if [ -n "${pkgs}" ] && pkg_install ${pkgs}; then
        ok "${cmd} installed"
        STATUS[${key}]="ok"
    else
        STATUS[${key}]="missing"
        INSTRUCTIONS[${key}]=$(cat <<EOF
${cmd} was not installed. Install it manually:
  Debian/Ubuntu:  sudo apt install ${aptpkg}
  macOS Homebrew: brew install ${brewpkg}
  Windows:        install manually and ensure '${cmd}' is on PATH
Re-run ./toolchains/install.sh to verify.
EOF
)
    fi
}


install_gsplus() {
    local base="${SCRIPT_DIR}/emulators/gsplus"
    local bin="${base}/bin/gsplus"

    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -rf "${base}"
        clear_done "emu_gsplus"
    fi

    if command -v gsplus >/dev/null 2>&1; then
        ok "gsplus already on PATH"
        STATUS[emu_gsplus]="ok"
        return
    fi

    if is_done "emu_gsplus" && [ -x "${bin}" ]; then
        ok "GSplus already built at ${bin}"
        STATUS[emu_gsplus]="ok"
        return
    fi

    ensure_build_deps "GSplus" \
        "build-essential libx11-dev libxext-dev libpulse-dev libpcap-dev libfreetype6-dev" \
        "libpcap freetype pulseaudio" || true

    info "Building GSplus from source (Apple IIgs emulator)"
    local src="${CACHE_DIR}/gsplus-src"
    if [ ! -d "${src}" ]; then
        if ! git clone --depth 1 https://github.com/digarok/gsplus.git "${src}"; then
            STATUS[emu_gsplus]="failed"
            INSTRUCTIONS[emu_gsplus]="git clone https://github.com/digarok/gsplus.git failed"
            return
        fi
    fi

    # GSplus build layout: source lives at gsplus/src/ and expects one
    # of the vars_* config files to be copied into 'vars' before make.
    # On Linux we use the X11/PulseAudio build; on macOS we use the
    # Mac X11 build for command-line use (the Swift/clang .app build
    # needs Xcode and is skipped here).
    local gsplus_src_dir="${src}/gsplus/src"
    local gsplus_target=""
    local vars_file=""
    case "${HOST_OS}" in
        linux)
            vars_file="vars_x86linux"
            gsplus_target="gsplus-x"
            ;;
        macos)
            vars_file="vars_mac_x"
            gsplus_target="gsplus-x"
            ;;
    esac

    if [ -z "${vars_file}" ] || [ ! -f "${gsplus_src_dir}/${vars_file}" ]; then
        STATUS[emu_gsplus]="failed"
        INSTRUCTIONS[emu_gsplus]=$(cat <<EOF
GSplus: no build configuration available for host '${HOST_OS}'.
Available configurations in ${gsplus_src_dir}/:
  $(ls ${gsplus_src_dir}/vars_* 2>/dev/null | xargs -n1 basename 2>/dev/null | tr '\n' ' ')
Copy the appropriate vars_* file to 'vars' and run make manually.
EOF
)
        return
    fi

    (
        cd "${gsplus_src_dir}"
        cp "${vars_file}" vars
        make clean >/dev/null 2>&1 || true
        make "${gsplus_target}"
    ) && {
        # Built binary lands at gsplus/src/../gsplus per the Makefile
        # (it's moved up one level after linking).
        local gsbin="${src}/gsplus/gsplus"
        if [ ! -f "${gsbin}" ]; then
            # Fallback: find anywhere under the tree
            gsbin=$(find "${src}" -maxdepth 4 -name gsplus -type f 2>/dev/null | head -n1)
        fi
        if [ -n "${gsbin}" ] && [ -f "${gsbin}" ]; then
            mkdir -p "${base}/bin"
            cp "${gsbin}" "${base}/bin/gsplus"
            chmod +x "${base}/bin/gsplus"
            # Also copy the support files GSplus reads at runtime
            if [ -d "${src}/gsplus/lib" ]; then
                mkdir -p "${base}/lib"
                cp -r "${src}/gsplus/lib/." "${base}/lib/"
            fi
            mark_done "emu_gsplus"
            ok "GSplus built and installed"
            STATUS[emu_gsplus]="ok"
        else
            STATUS[emu_gsplus]="failed"
            INSTRUCTIONS[emu_gsplus]="GSplus build succeeded but binary not found; check ${src}"
        fi
    } || {
        STATUS[emu_gsplus]="failed"
        INSTRUCTIONS[emu_gsplus]=$(cat <<EOF
GSplus '${gsplus_target}' target build failed in ${gsplus_src_dir}.
Dependencies expected (Debian/Ubuntu):
  sudo apt install build-essential libx11-dev libxext-dev libpulse-dev libpcap-dev libfreetype6-dev
Then delete ${src} and re-run ./toolchains/install.sh.
EOF
)
    }
}

install_support_emutos() {
    local support_dir="$1"
    local dest="${support_dir}/emutos-512k.img"
    local key="emu_support_emutos"

    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -f "${dest}"
        clear_done "${key}"
    fi
    if is_done "${key}" && [ -f "${dest}" ]; then
        ok "EmuTOS already staged"
        STATUS[${key}]="ok"
        return 0
    fi

    local zip_path="${CACHE_DIR}/emutos-512k-1.4.zip"
    local extract_dir="${CACHE_DIR}/emutos-512k-1.4"
    local url="https://sourceforge.net/projects/emutos/files/emutos/1.4/emutos-512k-1.4.zip/download"

    download "${url}" "${zip_path}" || {
        STATUS[${key}]="failed"
        return 1
    }
    rm -rf "${extract_dir}"
    (cd "${CACHE_DIR}" && unzip -oq "${zip_path}") || {
        STATUS[${key}]="failed"
        return 1
    }
    if [ ! -f "${extract_dir}/etos512us.img" ]; then
        err "EmuTOS zip missing etos512us.img"
        STATUS[${key}]="failed"
        return 1
    fi
    cp "${extract_dir}/etos512us.img" "${dest}"
    mark_done "${key}"
    ok "EmuTOS staged at ${dest}"
    STATUS[${key}]="ok"
}


install_support_iigs_rom() {
    local support_dir="$1"
    local dest="${support_dir}/apple-iigs.rom"
    local key="emu_support_iigs_rom"

    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -f "${dest}"
        clear_done "${key}"
    fi
    if is_done "${key}" && [ -f "${dest}" ]; then
        ok "Apple IIgs ROM already staged"
        STATUS[${key}]="ok"
        return 0
    fi

    local zip_path="${CACHE_DIR}/iigs_rom03.zip"
    local url="https://mirrors.apple2.org.za/ftp.apple.asimov.net/emulators/rom_images/iigs_rom03.zip"

    download "${url}" "${zip_path}" || {
        STATUS[${key}]="failed"
        return 1
    }
    local tmp
    tmp=$(mktemp -d)
    (cd "${tmp}" && unzip -oq "${zip_path}") || {
        rm -rf "${tmp}"
        STATUS[${key}]="failed"
        return 1
    }
    local rom_src
    rom_src=$(find "${tmp}" -maxdepth 2 -type f \( -iname 'APPLE2GS.ROM*' -o -iname '*.rom' -o -iname '*.bin' \) | head -1)
    if [ -z "${rom_src}" ] || [ ! -f "${rom_src}" ]; then
        err "Apple IIgs ROM zip did not contain a ROM file"
        rm -rf "${tmp}"
        STATUS[${key}]="failed"
        return 1
    fi
    cp "${rom_src}" "${dest}"
    rm -rf "${tmp}"
    mark_done "${key}"
    ok "Apple IIgs ROM staged at ${dest}"
    STATUS[${key}]="ok"
}


install_support_gsos() {
    local support_dir="$1"
    local sys_dest="${support_dir}/gsos-system.po"
    local tools_dest="${support_dir}/gsos-tools1.po"
    local key="emu_support_gsos"

    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -f "${sys_dest}" "${tools_dest}"
        clear_done "${key}"
    fi
    if is_done "${key}" && [ -f "${sys_dest}" ]; then
        ok "GS/OS system disk already staged"
        STATUS[${key}]="ok"
        return 0
    fi

    local zip_path="${CACHE_DIR}/Apple_IIGS_System_6.0.4.zip"
    local extract_dir="${CACHE_DIR}/Apple_IIGS_System_6.0.4"
    local url="https://mirrors.apple2.org.za/ftp.apple.asimov.net/images/gs/os/gsos/Apple_IIGS_System_6.0.4.zip"

    download "${url}" "${zip_path}" || {
        STATUS[${key}]="failed"
        return 1
    }
    rm -rf "${extract_dir}"
    (cd "${CACHE_DIR}" && unzip -oq "${zip_path}") || {
        STATUS[${key}]="failed"
        return 1
    }
    local sys_src="${extract_dir}/PO Disk Images/System.Disk.po"
    local tools_src="${extract_dir}/PO Disk Images/SystemTools1.po"
    if [ ! -f "${sys_src}" ]; then
        err "GS/OS zip missing System.Disk.po"
        STATUS[${key}]="failed"
        return 1
    fi
    cp "${sys_src}"   "${sys_dest}"
    [ -f "${tools_src}" ] && cp "${tools_src}" "${tools_dest}"
    mark_done "${key}"
    ok "GS/OS system disk staged at ${sys_dest}"
    STATUS[${key}]="ok"
}


install_support_iigs_null_c600() {
    local support_dir="$1"
    local dest="${support_dir}/iigs-null-c600.rom"
    local key="emu_support_iigs_null_c600"

    if [ "${FORCE_INSTALL}" -eq 1 ]; then
        rm -f "${dest}"
        clear_done "${key}"
    fi
    if is_done "${key}" && [ -f "${dest}" ]; then
        ok "IIgs null slot-6 PROM already staged"
        STATUS[${key}]="ok"
        return 0
    fi

    # 256-byte null PROM for slot 6. Leading RTS (so any accidental
    # call returns cleanly), then zero-filled -- no Pascal 1.1 firmware
    # signature, so the IIgs boot ROM's slot scan skips slot 6 and the
    # two empty 5.25 drives never get probed. run-iigs.sh stages this
    # into each session's work dir as c600.rom; GSplus picks it up from
    # cwd and overrides its built-in Disk II PROM.
    { printf '\x60'; head -c 255 /dev/zero; } > "${dest}"
    mark_done "${key}"
    ok "IIgs null slot-6 PROM staged at ${dest}"
    STATUS[${key}]="ok"
}


install_emulator_support() {
    header "Emulator support files"
    local support_dir="${SCRIPT_DIR}/emulators/support"
    mkdir -p "${support_dir}"

    install_support_emutos           "${support_dir}"
    install_support_iigs_rom         "${support_dir}"
    install_support_iigs_null_c600   "${support_dir}"
    install_support_gsos             "${support_dir}"
}


install_emulators() {
    header "Emulators"

    install_apt_emulator emu_dosbox  dosbox  dosbox           dosbox-x
    install_apt_emulator emu_fsuae   fs-uae  fs-uae           fs-uae
    install_apt_emulator emu_hatari  hatari  hatari           hatari
    install_gsplus

    install_emulator_support
}

# ------------------------------------------------------------------------
# Final status report
# ------------------------------------------------------------------------

print_status_report() {
    header "Installation status"
    local any_missing=0
    local any_failed=0
    local key
    local s

    # First pass: compute overall status. Do this in the current shell
    # (not inside a pipeline) so variable assignments stick.
    for key in "${!STATUS[@]}"; do
        s="${STATUS[$key]}"
        case "$s" in
            missing) any_missing=1 ;;
            failed)  any_failed=1  ;;
        esac
    done

    # Second pass: render sorted output.
    {
        for key in "${!STATUS[@]}"; do
            s="${STATUS[$key]}"
            case "$s" in
                ok)
                    printf '  %s[ok]%s %s\n' "${C_GREEN}" "${C_RESET}" "$key"
                    ;;
                missing)
                    printf '  %s[--]%s %s (manual install required)\n' "${C_YELLOW}" "${C_RESET}" "$key"
                    ;;
                failed)
                    printf '  %s[xx]%s %s (install failed)\n' "${C_RED}" "${C_RESET}" "$key"
                    ;;
            esac
        done
    } | sort

    if [ "${any_missing}" -eq 1 ] || [ "${any_failed}" -eq 1 ]; then
        header "Action required"
        for key in "${!INSTRUCTIONS[@]}"; do
            if [ "${STATUS[$key]}" != "ok" ]; then
                printf '\n%s---- %s ----%s\n%s\n' "${C_BOLD}" "$key" "${C_RESET}" "${INSTRUCTIONS[$key]}"
            fi
        done
        echo ""
        warn "Some components are not yet installed. Re-run after addressing the items above."
        exit 1
    else
        echo ""
        ok "All requested toolchains are ready."
        info "Now: source ${SCRIPT_DIR}/env.sh"
    fi
}

# ------------------------------------------------------------------------
# Main
# ------------------------------------------------------------------------

main() {
    detect_host_os
    require_tool make
    require_tool tar
    require_tool unzip
    ensure_dirs

    if should_install iigs;    then install_iigs;    fi
    if should_install amiga;   then install_amiga;   fi
    if should_install atarist; then install_atarist; fi
    if should_install dos;     then install_dos;     fi

    # Audio third-party fetches that span multiple targets. libxmp-lite
    # is used by the DOS HAL today and will back the ST mixer's
    # Protracker decoder later, so it lives in toolchains/audio/ rather
    # than under any one platform's tree.
    if should_install dos || should_install atarist; then
        install_audio_libxmp
    fi

    if [ "${SKIP_EMULATORS}" -eq 0 ] && [ -z "${ONLY_PLATFORM}" ]; then
        install_emulators
    fi

    print_status_report
}

main "$@"
