#!/usr/bin/env bash
set -euo pipefail

root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
config="$root/.config"
geometry_validator="$root/scripts/validate-xtide-geometry.py"

if ! command -v python3 >/dev/null 2>&1; then
  echo "menuconfig requires python3 for exact XTIDE profile validation" >&2
  exit 1
fi

if ! command -v whiptail >/dev/null 2>&1; then
  echo "menuconfig requires whiptail/ncurses on this host" >&2
  exit 1
fi

read_default()
{
  local name=$1
  local def=$2
  local val=

  if [ -f "$config" ]; then
    val=$(sed -n "s/^$name=//p" "$config" | tail -1)
    if [ -n "$val" ]; then
      printf '%s' "$val"
      return
    fi
  fi
  printf '%s' "$def"
}

write_config()
{
  # Snapshot the previous file so keys this menu does not manage (for
  # example hand-edited NET_* or 86BOX_* overrides) survive a save instead
  # of being silently dropped by the rewrite below.
  local old_config=""
  local line key
  if [ -f "$config" ]; then
    old_config=$(cat "$config")
  fi
  cat > "$config" <<EOF_CONFIG
ROOT_BLOCK_BACKEND=$root_backend
XTIDE_IO=$xtide_io
ROOT_DEVICE=$root_device
ROOT_FS_TYPE=$root_fs
ROOT_FLAGS=$root_flags
HD_GEOM_MODE=$hd_geom_mode
HD_IMAGE_MB=$hd_image_mb
BOOT_GEOM_SECT=$boot_geom_sect
BOOT_GEOM_HEAD=$boot_geom_head
BOOT_GEOM_CYL=$boot_geom_cyl
MACHINE_PROFILE=$machine_profile
86BOX_GEM_VIDEO=$gem_video
KERNEL_VDI=$kernel_vdi
VDI_VIDEO_DRIVER=$vdi_driver
GEM_RUNLEVEL=$gem_runlevel
USERLAND_MEMMODEL=$user_model
USERLAND_SERVER_MEMMODEL=small
ENABLE_NETWORK=$network
NET_DRIVER=$net_driver
NET_NE2K_IRQ=$net_ne2k_irq
ENABLE_LOADABLE_DRIVER_MODULES=$driver_modules
ENABLE_DRIVER_MODULE_COMPRESSION=$driver_module_compression
ENABLE_MFM_DRIVER=$mfm_driver
MFM_ST506_DMA=$mfm_st506_dma
MFM_DMA_MODE=$mfm_dma_mode
MFM_TRACE_SECTORS=$mfm_trace_sectors
ENABLE_KERNEL_COMPRESSION=$kernel_compression
KERNEL_TIGHT_SIZE_LIMIT=$kernel_tight_size_limit
ENABLE_SOUND=$sound
SOUND_DRIVER=$sound_driver
SB_PORT=$sb_port
SB_IRQ=$sb_irq
SB_DMA=$sb_dma
SB_BOUNCE=$sb_bounce
SOUND_OPTI_SB_MODE=$sound_opti_sb_mode
SOUND_OPTI_SB_PORT=$sound_opti_sb_port
SOUND_OPTI_SB_IRQ=$sound_opti_sb_irq
SOUND_OPTI_SB_DMA=$sound_opti_sb_dma
PROCESS_DEBUG=$process_debug
INIT_DEBUG=$init_debug
SERSYS_DEBUG=$sersys_debug
FSYS_DEBUG=$fsys_debug
MFM_DEBUG=$mfm_debug
DRIVER_LOG_VERBOSITY=$driver_log_verbosity
BOOT_USERLAND_DEBUG=$boot_userland_debug
KERNEL_DEBUG_TTY=$kernel_debug_tty
SERIAL_DEBUG=$serial_debug
SERVER_TRACE_COM2=$server_trace_com2
SERVER_TRACE_COM2_BASE=$server_trace_com2_base
SERVER_TRACE_COM2_DIVISOR=$server_trace_com2_divisor
SERVER_TRACE_DEBUG_PORT=$server_trace_debug_port
SERVER_TRACE_DEBUG_PORT_IO=$server_trace_debug_port_io
LEGACY_8250_TRACE=$legacy_8250_trace
LEGACY_8250_TRACE_BASE=$legacy_8250_trace_base
NET_NE2K_DEBUG_TX=$net_ne2k_debug_tx
NET_COM2_TRACE=$net_com2_trace
NET_PACKET_PATH=$net_packet_path
RAM_PROFILE=$ram_profile
KERNEL_BLOCK_CACHE_SECTORS=$kernel_block_cache_sectors
EOF_CONFIG
  if [ -n "$old_config" ]; then
    while IFS= read -r line; do
      case "$line" in
        ''|'#'*) continue ;;
      esac
      key=${line%%=*}
      case "$key" in
        *[!A-Z0-9_]*|'') continue ;;
      esac
      if ! grep -q "^$key=" "$config"; then
        printf '%s\n' "$line" >> "$config"
      fi
    done <<EOF_OLD
$old_config
EOF_OLD
  fi
}

choose_menu()
{
  local current=$1
  shift
  whiptail --title "Antler/86 menuconfig" --default-item "$current" \
    --menu "$1" 20 74 10 "${@:2}" \
    3>&1 1>&2 2>&3
}

edit_value()
{
  local title=$1
  local current=$2
  whiptail --title "Antler/86 menuconfig" --inputbox "$title" 9 68 "$current" \
    3>&1 1>&2 2>&3
}

sync_hd_geometry_from_mb()
{
  local mb=$hd_image_mb
  local geometry
  local total

  # Do the multiplication in the host validator instead of Bash arithmetic.
  # This lets a mistyped very large value fail cleanly rather than wrapping a
  # host shell integer before we can enforce the target's 16-bit limit.
  if ! geometry=$(python3 "$geometry_validator" mib "$mb" 2>&1); then
    hd_geometry_error=$geometry
    return 1
  fi
  read -r boot_geom_sect boot_geom_head boot_geom_cyl total <<<"$geometry"
  hd_geometry_error=
  return 0
}

validate_hd_configuration()
{
  local geometry
  local io_value
  local io_last

  # XTIDE owns sixteen consecutive ISA ports.  Normalize one C-style literal
  # before it is written to .config, and reject a base above fff0h so BASE+15
  # cannot wrap through the 8086 port boundary into an unrelated device.  A
  # zero base is the driver's uninitialized sentinel and is not hardware.
  if ! geometry=$(python3 "$geometry_validator" io "$xtide_io" 2>&1); then
    hd_geometry_error=$geometry
    return 1
  fi
  read -r xtide_io io_value io_last <<<"$geometry"

  # Validate the mode which will actually be written.  The top-level build
  # repeats this gate because students may also edit .config by hand.
  case "$hd_geom_mode" in
    xtelks)
      boot_geom_sect=17
      boot_geom_head=4
      boot_geom_cyl=612
      if ! geometry=$(python3 "$geometry_validator" chs 17 4 612 2>&1); then
        hd_geometry_error=$geometry
        return 1
      fi
      ;;
    mb)
      sync_hd_geometry_from_mb
      return
      ;;
    manual)
      if ! geometry=$(python3 "$geometry_validator" chs \
          "$boot_geom_sect" "$boot_geom_head" "$boot_geom_cyl" 2>&1); then
        hd_geometry_error=$geometry
        return 1
      fi
      ;;
    *)
      hd_geometry_error="Unsupported hard-disk geometry mode: $hd_geom_mode"
      return 1
      ;;
  esac

  hd_geometry_error=
  return 0
}

menu_root_backend()
{
  case "$1" in
    xtide)
      printf '%s' ide
      ;;
    ide|mfm|athd)
      printf '%s' "$1"
      ;;
    *)
      printf '%s' ide
      ;;
  esac
}

root_backend_label()
{
  case "$1" in
    ide|xtide)
      printf '%s' "IDE/XTIDE"
      ;;
    mfm)
      printf '%s' "MFM/ST506"
      ;;
    athd)
      printf '%s' "AT hard disk"
      ;;
    *)
      printf '%s' "$1"
      ;;
  esac
}

set_root_backend()
{
  local new_backend=$1

  root_backend=$(menu_root_backend "$new_backend")

  case "$root_device" in
    ''|/dev/boot|/dev/hda1|/dev/xtide0p1|/dev/mfm0p1)
      root_device=/dev/hda1
      ;;
  esac

  case "$root_backend" in
    mfm)
      root_device=/dev/mfm0p1
      hd_geom_mode=xtelks
      boot_geom_sect=17
      boot_geom_head=4
      boot_geom_cyl=612
      ;;
    *)
      root_device=/dev/hda1
      ;;
  esac
}

root_backend=$(menu_root_backend "$(read_default ROOT_BLOCK_BACKEND ide)")
xtide_io=$(read_default XTIDE_IO 0x0300)
root_device=$(read_default ROOT_DEVICE /dev/hda1)
root_fs=$(read_default ROOT_FS_TYPE ext2)
root_flags=$(read_default ROOT_FLAGS rw)
hd_geom_mode=$(read_default HD_GEOM_MODE xtelks)
hd_image_mb=$(read_default HD_IMAGE_MB 20)
boot_geom_sect=$(read_default BOOT_GEOM_SECT 17)
boot_geom_head=$(read_default BOOT_GEOM_HEAD 4)
boot_geom_cyl=$(read_default BOOT_GEOM_CYL 612)
machine_profile=$(read_default MACHINE_PROFILE pc1640)
gem_video=$(read_default 86BOX_GEM_VIDEO pega)
kernel_vdi=$(read_default KERNEL_VDI 0)
vdi_driver=$(read_default VDI_VIDEO_DRIVER pcvideo)
gem_runlevel=$(read_default GEM_RUNLEVEL 3)
user_model=$(read_default USERLAND_MEMMODEL small)
ram_profile=$(read_default RAM_PROFILE speed)
kernel_block_cache_sectors=$(read_default KERNEL_BLOCK_CACHE_SECTORS 16)
network=$(read_default ENABLE_NETWORK 0)
net_driver=$(read_default NET_DRIVER ne2k)
net_ne2k_irq=$(read_default NET_NE2K_IRQ 2)
driver_modules=$(read_default ENABLE_LOADABLE_DRIVER_MODULES 1)
driver_module_compression=$(read_default ENABLE_DRIVER_MODULE_COMPRESSION 0)
mfm_driver=$(read_default ENABLE_MFM_DRIVER 0)
mfm_st506_dma=$(read_default MFM_ST506_DMA 3)
mfm_dma_mode=$(read_default MFM_DMA_MODE 0)
mfm_trace_sectors=$(read_default MFM_TRACE_SECTORS 0)
kernel_compression=$(read_default ENABLE_KERNEL_COMPRESSION 0)
kernel_tight_size_limit=$(read_default KERNEL_TIGHT_SIZE_LIMIT 153600)
sound=$(read_default ENABLE_SOUND 0)
sound_driver=$(read_default SOUND_DRIVER sb)
sb_port=$(read_default SB_PORT 0x220)
sb_irq=$(read_default SB_IRQ 5)
sb_dma=$(read_default SB_DMA 1)
sb_bounce=$(read_default SB_BOUNCE 4096)
sound_opti_sb_mode=$(read_default SOUND_OPTI_SB_MODE off)
sound_opti_sb_port=$(read_default SOUND_OPTI_SB_PORT "")
sound_opti_sb_irq=$(read_default SOUND_OPTI_SB_IRQ "")
sound_opti_sb_dma=$(read_default SOUND_OPTI_SB_DMA "")

# Debug and trace defaults.
process_debug=$(read_default PROCESS_DEBUG 0)
init_debug=$(read_default INIT_DEBUG 0)
sersys_debug=$(read_default SERSYS_DEBUG 0)
fsys_debug=$(read_default FSYS_DEBUG 0)
mfm_debug=$(read_default MFM_DEBUG 0)
driver_log_verbosity=$(read_default DRIVER_LOG_VERBOSITY 0)
boot_userland_debug=$(read_default BOOT_USERLAND_DEBUG 0)
kernel_debug_tty=$(read_default KERNEL_DEBUG_TTY 0)
serial_debug=$(read_default SERIAL_DEBUG 0)
server_trace_com2=$(read_default SERVER_TRACE_COM2 0)
server_trace_com2_base=$(read_default SERVER_TRACE_COM2_BASE 0x02f8)
server_trace_com2_divisor=$(read_default SERVER_TRACE_COM2_DIVISOR 12)
server_trace_debug_port=$(read_default SERVER_TRACE_DEBUG_PORT 0)
server_trace_debug_port_io=$(read_default SERVER_TRACE_DEBUG_PORT_IO 0x00e9)
legacy_8250_trace=$(read_default LEGACY_8250_TRACE 0)
legacy_8250_trace_base=$(read_default LEGACY_8250_TRACE_BASE 0x02f8)
net_ne2k_debug_tx=$(read_default NET_NE2K_DEBUG_TX 0)
net_com2_trace=$(read_default NET_COM2_TRACE 0)
net_packet_path=$(read_default NET_PACKET_PATH 1)

hd_geometry_error=

if [ "$hd_geom_mode" = "mb" ]; then
  sync_hd_geometry_from_mb || true
fi

while :; do
  root_backend_text=$(root_backend_label "$root_backend")
  # Summarise debug/trace state for the main menu line.
  debug_active=0
  for _d in $process_debug $init_debug $sersys_debug $fsys_debug $mfm_debug \
            $boot_userland_debug $kernel_debug_tty $serial_debug; do
    [ "$_d" != 0 ] && debug_active=1 && break
  done
  trace_active=0
  for _t in $server_trace_com2 $server_trace_debug_port $legacy_8250_trace \
            $net_ne2k_debug_tx $net_com2_trace; do
    [ "$_t" != 0 ] && trace_active=1 && break
  done
  [ "$debug_active" = 1 ] && debug_tag="ON" || debug_tag="off"
  [ "$trace_active" = 1 ] && trace_tag="ON" || trace_tag="off"

  # Map the stored adapter name to its unique VDI driver for display.
  case "$gem_video" in
    pega) gem_video_display="ega (PEGA)" ;;
    *)    gem_video_display="$gem_video" ;;
  esac

  choice=$(whiptail --title "Antler/86 menuconfig" --menu \
    "Configure Antler/86 build options" 24 78 15 \
    root "Block mode: $root_backend_text root=$root_device $root_fs $root_flags" \
    disk "Hard disk: XTIDE=$xtide_io $hd_geom_mode ${boot_geom_cyl}c/${boot_geom_head}h/${boot_geom_sect}s MB=$hd_image_mb" \
    video "Machine: $machine_profile, display: $gem_video_display, VDI: $([ "$kernel_vdi" = 1 ] && echo "nucleus/$vdi_driver" || echo server)" \
    session "GEM boot session: init level $gem_runlevel" \
    memory "Userland model: user=$user_model servers=small" \
    ram "RAM profile: $ram_profile block-cache=${kernel_block_cache_sectors} sectors" \
    network "Networking: $network driver=$net_driver" \
    drivers "Driver modules: $driver_modules MFM=$mfm_driver DMA=$mfm_dma_mode ch=$mfm_st506_dma compression=$driver_module_compression" \
    compression "Kernel compression: $kernel_compression tight-limit=$kernel_tight_size_limit" \
    sound "Sound: $sound driver=$sound_driver SB=$sb_port,$sb_irq,$sb_dma OPTi=$sound_opti_sb_mode" \
    debug "Debugging: $debug_tag  (process/init/fsys/sersys/mfm/boot/serial)" \
    trace "Tracing: $trace_tag  (COM2/debug-port/8250/net)" \
    save "Save configuration" \
    exit "Exit without saving" \
    3>&1 1>&2 2>&3) || exit 1

  case "$choice" in
    root)
      new_backend=$(choose_menu "$root_backend" "Block mode / root block backend" \
        ide "IDE/XTIDE block mode, using the ATA driver server" \
        mfm "MFM/ST506 block mode, using the MFM driver server" \
        athd "AT hard disk block mode, using the ATA driver server")
      set_root_backend "$new_backend"
      root_device=$(edit_value "Root device" "$root_device")
      root_fs=$(choose_menu "$root_fs" "Root filesystem type" \
        ext2 "EXT2 root filesystem" \
        nfs "NFS root filesystem")
      root_flags=$(choose_menu "$root_flags" "Root mount flags" \
        ro "Read-only root" \
        rw "Read-write root")
      ;;
    disk)
      hd_geom_mode=$(choose_menu "$hd_geom_mode" "Hard disk geometry mode" \
        xtelks "Match XT-ELKS hd.img 17s/4h/612c" \
        mb "Auto CHS from image size in MiB" \
        manual "Manual sectors, heads, cylinders")
      case "$hd_geom_mode" in
        xtelks)
          boot_geom_sect=17
          boot_geom_head=4
          boot_geom_cyl=612
          ;;
        mb)
          hd_image_mb=$(edit_value "Hard disk image size in MiB" "$hd_image_mb")
          if sync_hd_geometry_from_mb; then
            whiptail --title "Antler/86 menuconfig" --msgbox \
              "Auto geometry: ${boot_geom_cyl} cylinders, ${boot_geom_head} heads/platters, ${boot_geom_sect} sectors/track" 8 70
          else
            whiptail --title "Antler/86 menuconfig" --msgbox \
              "Cannot use that image size.\n\n$hd_geometry_error" 10 72
          fi
          ;;
        manual)
          hd_image_mb=$(edit_value "Reference image size in MiB (kept for documentation)" "$hd_image_mb")
          boot_geom_sect=$(edit_value "Hard disk sectors per track" "$boot_geom_sect")
          boot_geom_head=$(edit_value "Hard disk heads/platters" "$boot_geom_head")
          boot_geom_cyl=$(edit_value "Hard disk cylinders" "$boot_geom_cyl")
          ;;
      esac
      xtide_io=$(edit_value \
        "XTIDE revision-1 I/O base (unsigned C literal, 1 through 0xfff0)" \
        "$xtide_io")
      ;;
    video)
      machine_profile=$(choose_menu "$machine_profile" "Machine profile" \
        pcxt "Generic IBM PC/XT compatible" \
        pc1640 "Amstrad PC1640 internal Paradise PEGA1A" \
        pc1640-dd "Amstrad PC1640 dual 5.25 DD" \
        tandy "Tandy PC compatible" \
        video7 "Video Seven EGA/VGA compatible" \
        qemu "QEMU ISA PC")
      # Each menu entry maps to exactly one VDI driver.  The PEGA adapter
      # is internally the EGA VDI driver (adapter ID 1) plus a PC1640-only
      # extended-latch mode-entry path.  Selecting "ega" on a PC1640
      # profile automatically stores the "pega" build tag; the user never
      # sees a separate "pega" entry that duplicates "ega".
      case "$machine_profile" in
        pc1640|pc1640-dd)
          whiptail --title "Antler/86 menuconfig" --msgbox \
            "PC1640 uses the internal Paradise PEGA1A (EGA VDI driver with extended-latch mode entry).\nDisplay adapter is set automatically." 9 74
          gem_video=pega
          ;;
        *)
          gem_video=$(choose_menu "${gem_video/pega/ega}" \
            "GEM display adapter (one per VDI driver)" \
            vga "VGA, 640x480 16-color planar VDI" \
            ega "EGA, 640x350 16-color planar VDI" \
            cga "CGA, 640x200 monochrome VDI" \
            hercules "Hercules, 720x348 monochrome VDI")
          ;;
      esac
      kernel_vdi=$(choose_menu "$kernel_vdi" \
        "VDI video system placement" \
        0 "Classic: VDI in the vdisys server (per-primitive IPC)" \
        1 "In-nucleus: VDI + driver in the kernel, direct far calls")
      if [ "$kernel_vdi" = 1 ]; then
        vdi_driver=$(choose_menu "$vdi_driver" \
          "In-nucleus VDI raster driver" \
          pcvideo "PC planar EGA/VGA/CGA video driver")
      fi
      ;;
    session)
      gem_runlevel=$(choose_menu "$gem_runlevel" "GEM boot session / init level" \
        3 "Normal: GEM Login, then Desktop and File Manager" \
        1 "Minimal: GEM Terminal with sash, no text console")
      ;;
    memory)
      user_model=$(choose_menu "$user_model" "User code model" \
        small "Small model" \
        tiny "Tiny model" \
        medium "Medium model" \
        compact "Compact model" \
        large "Large model" \
        huge "Huge model")
      ;;
    ram)
      ram_profile=$(choose_menu "$ram_profile" "Kernel RAM profile" \
        speed "Favor speed: 16-sector block cache" \
        small "Favor RAM: forces an 8-sector block cache")
      if [ "$ram_profile" = small ]; then
        kernel_block_cache_sectors=8
        whiptail --title "Antler/86 menuconfig" --msgbox \
          "RAM_PROFILE=small forces KERNEL_BLOCK_CACHE_SECTORS=8 in the build." 8 70
      else
        kernel_block_cache_sectors=$(edit_value \
          "Kernel block cache size in 512-byte sectors" \
          "$kernel_block_cache_sectors")
      fi
      ;;
    network)
      if whiptail --title "Networking" --yesno "Enable networking build?" 8 60; then
        network=1
      else
        network=0
      fi
      net_driver=$(choose_menu "$net_driver" "Network driver" \
        ne2k "NE2000 compatible" \
        wd80x3 "WD80x3/SMC" \
        ultra "SMC Ultra" \
        el3 "3Com EtherLink III" \
        83c790qf "83C790QF")
      net_ne2k_irq=$(edit_value "NE2000 IRQ" "$net_ne2k_irq")
      ;;
    drivers)
      if whiptail --title "Driver modules" --yesno "Package Antler /system/drivers records in the root image?" 8 72; then
        driver_modules=1
      else
        driver_modules=0
      fi
      if whiptail --title "MFM/ST506 driver" --yesno "Build MFM/ST506 support even when it is not the root block backend?" 8 72; then
        mfm_driver=1
      else
        mfm_driver=0
      fi
      if whiptail --title "MFM/ST506 driver" --yesno "Use 8237 DMA for MFM/ST506 sector transfers?" 8 72; then
        mfm_dma_mode=1
      else
        mfm_dma_mode=0
      fi
      mfm_st506_dma=$(edit_value "MFM/ST506 DMA channel" "$mfm_st506_dma")
      if whiptail --title "MFM/ST506 driver" --yesno "Trace every MFM sector transfer on the raw debug channel? This is very slow on XT hardware and emulators." 9 72; then
        mfm_trace_sectors=1
      else
        mfm_trace_sectors=0
      fi
      if whiptail --title "Driver modules" --yesno "Enable build-time driver module compression? Leave off until the real-mode loader decompressor is required." 9 72; then
        driver_module_compression=1
      else
        driver_module_compression=0
      fi
      ;;
    compression)
      if whiptail --title "Kernel compression" --yesno "Enable optional kernel compression build path? This is off by default for exact 8086 RAM accounting." 9 72; then
        kernel_compression=1
      else
        kernel_compression=0
      fi
      kernel_tight_size_limit=$(edit_value "Tight non-network kernel size target in bytes (0 disables)" "$kernel_tight_size_limit")
      ;;
    sound)
      if whiptail --title "Sound" --yesno "Enable sound service build?" 8 60; then
        sound=1
      else
        sound=0
      fi
      sound_driver=$(choose_menu "$sound_driver" "Sound PCM backend" \
        sb "Sound Blaster 2.0 compatible 8-bit DMA" \
        pcspeaker "IBM PC speaker 1-bit PCM output")
      sb_port=$(edit_value "Sound Blaster base port" "$sb_port")
      sb_irq=$(edit_value "Sound Blaster IRQ" "$sb_irq")
      sb_dma=$(edit_value "Sound Blaster DMA channel" "$sb_dma")
      sb_bounce=$(edit_value "Sound DMA bounce bytes" "$sb_bounce")
      if whiptail --title "Sound" --yesno "Force OPTi/MAD16 card into Sound Blaster mode?" 8 68; then
        sound_opti_sb_mode=on
      else
        sound_opti_sb_mode=off
      fi
      sound_opti_sb_port=$(edit_value "OPTi SB-mode port override (blank uses SB port)" "$sound_opti_sb_port")
      sound_opti_sb_irq=$(edit_value "OPTi SB-mode IRQ override (blank uses SB IRQ)" "$sound_opti_sb_irq")
      sound_opti_sb_dma=$(edit_value "OPTi SB-mode DMA override (blank uses SB DMA)" "$sound_opti_sb_dma")
      ;;
    debug)
      if whiptail --title "Process debugging" --yesno \
          "Enable PROCESS_DEBUG?  Traces process creation, exit, scheduling." 8 72; then
        process_debug=1
      else
        process_debug=0
      fi
      if whiptail --title "Init debugging" --yesno \
          "Enable INIT_DEBUG?  Traces init(8) startup and respawn logic." 8 72; then
        init_debug=1
      else
        init_debug=0
      fi
      if whiptail --title "Serial subsystem debugging" --yesno \
          "Enable SERSYS_DEBUG?  Traces the serial port service." 8 72; then
        sersys_debug=1
      else
        sersys_debug=0
      fi
      if whiptail --title "Filesystem debugging" --yesno \
          "Enable FSYS_DEBUG?  Traces the filesystem service (fsys)." 8 72; then
        fsys_debug=1
      else
        fsys_debug=0
      fi
      if whiptail --title "MFM debugging" --yesno \
          "Enable MFM_DEBUG?  Traces MFM/ST506 disk controller operations." 8 72; then
        mfm_debug=1
      else
        mfm_debug=0
      fi
      driver_log_verbosity=$(edit_value \
        "Driver log verbosity level (0=off, higher=more verbose)" \
        "$driver_log_verbosity")
      if whiptail --title "Boot userland debugging" --yesno \
          "Enable BOOT_USERLAND_DEBUG?  Overrides individual debug flags for early-boot diagnostics (init=1, sersys=1, process=off)." 9 76; then
        boot_userland_debug=1
      else
        boot_userland_debug=0
      fi
      if whiptail --title "Kernel debug TTY" --yesno \
          "Enable KERNEL_DEBUG_TTY?  Sends kernel debug output to the text console." 8 72; then
        kernel_debug_tty=1
      else
        kernel_debug_tty=0
      fi
      if whiptail --title "Serial driver debugging" --yesno \
          "Enable SERIAL_DEBUG?  Traces raw 8250/16550 UART register traffic." 8 72; then
        serial_debug=1
      else
        serial_debug=0
      fi
      ;;
    trace)
      if whiptail --title "COM2 server trace" --yesno \
          "Enable SERVER_TRACE_COM2?  Sends server trace output to the COM2 UART.\n\nWARNING: This conflicts with sersys owning COM2.  Use only for diagnostics." 10 76; then
        server_trace_com2=1
      else
        server_trace_com2=0
      fi
      server_trace_com2_base=$(edit_value \
        "COM2 trace UART base I/O port" "$server_trace_com2_base")
      server_trace_com2_divisor=$(edit_value \
        "COM2 trace UART baud-rate divisor (12=9600)" "$server_trace_com2_divisor")
      if whiptail --title "QEMU/Bochs debug port" --yesno \
          "Enable SERVER_TRACE_DEBUG_PORT?  Sends trace output to QEMU/Bochs I/O port 0xe9." 8 76; then
        server_trace_debug_port=1
      else
        server_trace_debug_port=0
      fi
      server_trace_debug_port_io=$(edit_value \
        "Debug port I/O address" "$server_trace_debug_port_io")
      if whiptail --title "Legacy 8250 trace" --yesno \
          "Enable LEGACY_8250_TRACE?  Polled-mode 8250 trace output used by fsys and other servers." 8 76; then
        legacy_8250_trace=1
      else
        legacy_8250_trace=0
      fi
      legacy_8250_trace_base=$(edit_value \
        "8250 trace UART base I/O port" "$legacy_8250_trace_base")
      if whiptail --title "NE2K TX trace" --yesno \
          "Enable NET_NE2K_DEBUG_TX?  Traces every NE2000 transmit frame." 8 72; then
        net_ne2k_debug_tx=1
      else
        net_ne2k_debug_tx=0
      fi
      if whiptail --title "Net COM2 trace" --yesno \
          "Enable NET_COM2_TRACE?  Mirrors network debug output to COM2 (follows SERVER_TRACE_COM2)." 8 76; then
        net_com2_trace=1
      else
        net_com2_trace=0
      fi
      if whiptail --title "Net packet path" --yesno \
          "Enable NET_PACKET_PATH?  Enables inline packet-path instrumentation in the network stack." 8 76; then
        net_packet_path=1
      else
        net_packet_path=0
      fi
      ;;
    save)
      if ! validate_hd_configuration; then
        whiptail --title "Antler/86 menuconfig" --msgbox \
          "Configuration not saved.\n\n$hd_geometry_error" 10 72
        continue
      fi
      write_config
      whiptail --title "Antler/86 menuconfig" --msgbox \
        "Wrote $config\n\nRun: make hd-image" 9 60
      exit 0
      ;;
    exit)
      exit 0
      ;;
  esac
done
