#!/bin/sh
# MiSTercade Setup Installer
# Generated by scripts/populate_consolidated_branches.py.

REPO_RAW_BASE="https://raw.githubusercontent.com/misteraddons/MiSTercade/main"
SD_ROOT="${MISTERCADE_SD_ROOT:-/media/fat}"
DOWNLOADER_INI="$SD_ROOT/downloader.ini"
MISTER_INI="$SD_ROOT/MiSTer.ini"
CONFIG_INPUTS_DIR="$SD_ROOT/config/inputs"
GAMECONTROLLERDB_FILE="$SD_ROOT/gamecontrollerdb/gamecontrollerdb_user.txt"

options_data() {
    cat <<'OPTIONS_EOF'
1|MiSTercade V1|Map files|Standard / OSD/menu combo|v1|maps|maps-v1-osd|mistercade_v1/mappings/downloader_maps-v1-osd.ini|downloader_maps-v1-osd.ini
2|MiSTercade V1|Map files|Freeplay / OSD/menu combo|v1|maps|maps-v1-freeplay-osd|mistercade_v1/mappings/downloader_maps-v1-freeplay-osd.ini|downloader_maps-v1-freeplay-osd.ini
3|MiSTercade V1|Map files|Standard / No-menu combo|v1|maps|maps-v1-nomenu|mistercade_v1/mappings/downloader_maps-v1-nomenu.ini|downloader_maps-v1-nomenu.ini
4|MiSTercade V1|Map files|Freeplay / No-menu combo|v1|maps|maps-v1-freeplay-nomenu|mistercade_v1/mappings/downloader_maps-v1-freeplay-nomenu.ini|downloader_maps-v1-freeplay-nomenu.ini
5|MiSTercade V1 2025|Map files|Standard / OSD/menu combo|v1-2025|maps|maps-v1-2025-osd|mistercade_v1_2025/mappings/downloader_maps-v1-2025-osd.ini|downloader_maps-v1-2025-osd.ini
6|MiSTercade V1 2025|Map files|Freeplay / OSD/menu combo|v1-2025|maps|maps-v1-2025-freeplay-osd|mistercade_v1_2025/mappings/downloader_maps-v1-2025-freeplay-osd.ini|downloader_maps-v1-2025-freeplay-osd.ini
7|MiSTercade V1 2025|Map files|Standard / No-menu combo|v1-2025|maps|maps-v1-2025-nomenu|mistercade_v1_2025/mappings/downloader_maps-v1-2025-nomenu.ini|downloader_maps-v1-2025-nomenu.ini
8|MiSTercade V1 2025|Map files|Freeplay / No-menu combo|v1-2025|maps|maps-v1-2025-freeplay-nomenu|mistercade_v1_2025/mappings/downloader_maps-v1-2025-freeplay-nomenu.ini|downloader_maps-v1-2025-freeplay-nomenu.ini
9|MiSTercade V2|Map files|Standard / OSD/menu combo|v2|maps|maps-v2-osd|mistercade_v2/mappings/downloader_maps-v2-osd.ini|downloader_maps-v2-osd.ini
10|MiSTercade V2|Map files|Freeplay / OSD/menu combo|v2|maps|maps-v2-freeplay-osd|mistercade_v2/mappings/downloader_maps-v2-freeplay-osd.ini|downloader_maps-v2-freeplay-osd.ini
11|MiSTercade V2|Map files|Standard / No-menu combo|v2|maps|maps-v2-nomenu|mistercade_v2/mappings/downloader_maps-v2-nomenu.ini|downloader_maps-v2-nomenu.ini
12|MiSTercade V2|Map files|Freeplay / No-menu combo|v2|maps|maps-v2-freeplay-nomenu|mistercade_v2/mappings/downloader_maps-v2-freeplay-nomenu.ini|downloader_maps-v2-freeplay-nomenu.ini
13|MiSTercade V1 2025|GamecontrollerDB|Standard / OSD/menu combo|v1-2025|gamecontrollerdb|gamecontrollerdb-v1-2025-osd|mistercade_v1_2025/gamecontrollerdb/downloader_gamecontrollerdb-v1-2025-osd.ini|downloader_gamecontrollerdb-v1-2025-osd.ini
14|MiSTercade V1 2025|GamecontrollerDB|Freeplay / OSD/menu combo|v1-2025|gamecontrollerdb|gamecontrollerdb-v1-2025-freeplay-osd|mistercade_v1_2025/gamecontrollerdb/downloader_gamecontrollerdb-v1-2025-freeplay-osd.ini|downloader_gamecontrollerdb-v1-2025-freeplay-osd.ini
15|MiSTercade V1 2025|GamecontrollerDB|Standard / No-menu combo|v1-2025|gamecontrollerdb|gamecontrollerdb-v1-2025-nomenu|mistercade_v1_2025/gamecontrollerdb/downloader_gamecontrollerdb-v1-2025-nomenu.ini|downloader_gamecontrollerdb-v1-2025-nomenu.ini
16|MiSTercade V1 2025|GamecontrollerDB|Freeplay / No-menu combo|v1-2025|gamecontrollerdb|gamecontrollerdb-v1-2025-freeplay-nomenu|mistercade_v1_2025/gamecontrollerdb/downloader_gamecontrollerdb-v1-2025-freeplay-nomenu.ini|downloader_gamecontrollerdb-v1-2025-freeplay-nomenu.ini
17|MiSTercade V2|GamecontrollerDB|Standard / OSD/menu combo|v2|gamecontrollerdb|gamecontrollerdb-v2-osd|mistercade_v2/gamecontrollerdb/downloader_gamecontrollerdb-v2-osd.ini|downloader_gamecontrollerdb-v2-osd.ini
18|MiSTercade V2|GamecontrollerDB|Freeplay / OSD/menu combo|v2|gamecontrollerdb|gamecontrollerdb-v2-freeplay-osd|mistercade_v2/gamecontrollerdb/downloader_gamecontrollerdb-v2-freeplay-osd.ini|downloader_gamecontrollerdb-v2-freeplay-osd.ini
19|MiSTercade V2|GamecontrollerDB|Standard / No-menu combo|v2|gamecontrollerdb|gamecontrollerdb-v2-nomenu|mistercade_v2/gamecontrollerdb/downloader_gamecontrollerdb-v2-nomenu.ini|downloader_gamecontrollerdb-v2-nomenu.ini
20|MiSTercade V2|GamecontrollerDB|Freeplay / No-menu combo|v2|gamecontrollerdb|gamecontrollerdb-v2-freeplay-nomenu|mistercade_v2/gamecontrollerdb/downloader_gamecontrollerdb-v2-freeplay-nomenu.ini|downloader_gamecontrollerdb-v2-freeplay-nomenu.ini
OPTIONS_EOF
}

conflicting_files() {
    case "$1" in
        v1)
            printf '%s\n' 'downloader_maps-v1-osd.ini'
            printf '%s\n' 'downloader_maps-v1-freeplay-osd.ini'
            printf '%s\n' 'downloader_maps-v1-nomenu.ini'
            printf '%s\n' 'downloader_maps-v1-freeplay-nomenu.ini'
            ;;
        v1-2025)
            printf '%s\n' 'downloader_maps-v1-2025-osd.ini'
            printf '%s\n' 'downloader_maps-v1-2025-freeplay-osd.ini'
            printf '%s\n' 'downloader_maps-v1-2025-nomenu.ini'
            printf '%s\n' 'downloader_maps-v1-2025-freeplay-nomenu.ini'
            printf '%s\n' 'downloader_gamecontrollerdb-v1-2025-osd.ini'
            printf '%s\n' 'downloader_gamecontrollerdb-v1-2025-freeplay-osd.ini'
            printf '%s\n' 'downloader_gamecontrollerdb-v1-2025-nomenu.ini'
            printf '%s\n' 'downloader_gamecontrollerdb-v1-2025-freeplay-nomenu.ini'
            ;;
        v2)
            printf '%s\n' 'downloader_maps-v2-osd.ini'
            printf '%s\n' 'downloader_maps-v2-freeplay-osd.ini'
            printf '%s\n' 'downloader_maps-v2-nomenu.ini'
            printf '%s\n' 'downloader_maps-v2-freeplay-nomenu.ini'
            printf '%s\n' 'downloader_gamecontrollerdb-v2-osd.ini'
            printf '%s\n' 'downloader_gamecontrollerdb-v2-freeplay-osd.ini'
            printf '%s\n' 'downloader_gamecontrollerdb-v2-nomenu.ini'
            printf '%s\n' 'downloader_gamecontrollerdb-v2-freeplay-nomenu.ini'
            ;;
        *) return 0 ;;
    esac
}

map_vid_pid_for_hardware() {
    case "$1" in
        v1)
            printf '%s\n' '16D0_10BE'
            ;;
        v1-2025)
            printf '%s\n' '16D0_10BE'
            ;;
        v2)
            printf '%s\n' '16D0_1358'
            ;;
        *) return 0 ;;
    esac
}

stamp() {
    date +%Y%m%d-%H%M%S 2>/dev/null || date
}

say() {
    printf '%s\n' "$*"
}

ask_yes_no() {
    prompt="$1"
    default="$2"
    while :; do
        printf '%s' "$prompt"
        IFS= read -r answer || answer=""
        case "$answer" in
            y|Y|yes|YES) return 0 ;;
            n|N|no|NO) return 1 ;;
            "")
                if [ "$default" = "yes" ]; then
                    return 0
                fi
                return 1
                ;;
            *) say "Please answer y or n." ;;
        esac
    done
}

count_mistercade_sections() {
    if [ ! -f "$DOWNLOADER_INI" ]; then
        printf '0\n'
        return
    fi
    awk '
        BEGIN { in_section = 0; has_match = 0; count = 0 }
        /^\[/ {
            if (in_section && has_match) count++
            in_section = 1
            has_match = ($0 ~ /[Mm][Ii][Ss][Tt][Ee][Rr][Cc][Aa][Dd][Ee]/)
            next
        }
        in_section && $0 ~ /[Mm][Ii][Ss][Tt][Ee][Rr][Cc][Aa][Dd][Ee]/ { has_match = 1 }
        END {
            if (in_section && has_match) count++
            print count
        }
    ' "$DOWNLOADER_INI"
}

has_mistercade_dropins() {
    for file in "$SD_ROOT"/downloader_*.ini; do
        [ -f "$file" ] || continue
        if grep -qi 'mistercade' "$file" 2>/dev/null; then
            return 0
        fi
    done
    return 1
}

list_mistercade_dropins() {
    found=0
    for file in "$SD_ROOT"/downloader_*.ini; do
        [ -f "$file" ] || continue
        if grep -qi 'mistercade' "$file" 2>/dev/null; then
            found=1
            db_id=$(awk -F'[][]' '/^\[/ { print $2; exit }' "$file" 2>/dev/null)
            if [ -n "$db_id" ]; then
                say "  - ${file##*/} ($db_id)"
            else
                say "  - ${file##*/}"
            fi
        fi
    done
    if [ "$found" -eq 0 ]; then
        say "  none"
    fi
}

ini_value() {
    key="$1"
    if [ ! -f "$MISTER_INI" ]; then
        printf 'missing\n'
        return
    fi
    awk -v key="$key" '
        BEGIN { found = 0 }
        {
            line = $0
            sub(/^[ \t]*/, "", line)
            if (line ~ "^" key "[ \t]*=") {
                sub(/^[^=]*=[ \t]*/, "", line)
                sub(/[ \t;#].*$/, "", line)
                print line
                found = 1
                exit
            }
        }
        END {
            if (!found) print "(unset)"
        }
    ' "$MISTER_INI"
}

status_report() {
    say ""
    say "MiSTercade Setup Installer"
    say "SD root: $SD_ROOT"
    say ""

    section_count=$(count_mistercade_sections)
    if [ "$section_count" -gt 0 ] 2>/dev/null; then
        say "Legacy/current MiSTercade entries in downloader.ini: $section_count"
    else
        say "Legacy/current MiSTercade entries in downloader.ini: none"
    fi

    if [ -f "$MISTER_INI" ]; then
        say "MiSTer.ini: present (direct_video=$(ini_value direct_video), composite_sync=$(ini_value composite_sync), vga_mode=$(ini_value vga_mode))"
    else
        say "MiSTer.ini: not found"
    fi

    say "MiSTercade downloader drop-in files:"
    list_mistercade_dropins

    if [ -f "$GAMECONTROLLERDB_FILE" ]; then
        say "Installed gamecontrollerdb_user.txt: present at $GAMECONTROLLERDB_FILE"
    else
        say "Installed gamecontrollerdb_user.txt: not found"
    fi
}

clean_downloader_ini() {
    if [ ! -f "$DOWNLOADER_INI" ]; then
        say "No downloader.ini found at $DOWNLOADER_INI."
        return 0
    fi

    section_count=$(count_mistercade_sections)
    if [ "$section_count" -eq 0 ] 2>/dev/null; then
        say "No MiSTercade sections found in downloader.ini."
        return 0
    fi

    backup="$DOWNLOADER_INI.mistercade_backup_$(stamp)"
    tmp="$DOWNLOADER_INI.tmp.$$"
    cp "$DOWNLOADER_INI" "$backup" || return 1
    awk '
        function flush() {
            if (!drop) printf "%s", block
            block = ""
            drop = 0
        }
        /^\[/ {
            if (block != "") flush()
        }
        {
            block = block $0 "\n"
            if ($0 ~ /[Mm][Ii][Ss][Tt][Ee][Rr][Cc][Aa][Dd][Ee]/) drop = 1
        }
        END {
            if (block != "") flush()
        }
    ' "$DOWNLOADER_INI" > "$tmp" && mv "$tmp" "$DOWNLOADER_INI"
    result=$?
    if [ "$result" -ne 0 ]; then
        rm -f "$tmp"
        say "Could not update downloader.ini. Backup is at $backup."
        return "$result"
    fi
    say "Removed MiSTercade sections from downloader.ini."
    say "Backup: $backup"
}

remove_all_mistercade_dropins() {
    if ! has_mistercade_dropins; then
        say "No MiSTercade downloader drop-in files found."
        return 0
    fi
    if ! ask_yes_no "Remove all MiSTercade downloader_*.ini files from $SD_ROOT? [y/N] " "no"; then
        return 0
    fi
    for file in "$SD_ROOT"/downloader_*.ini; do
        [ -f "$file" ] || continue
        if grep -qi 'mistercade' "$file" 2>/dev/null; then
            rm -f "$file" && say "Removed ${file##*/}"
        fi
    done
}

remove_conflicting_dropins() {
    conflict_hardware_key="$1"
    for conflict_filename in $(conflicting_files "$conflict_hardware_key"); do
        conflict_file="$SD_ROOT/$conflict_filename"
        if [ -f "$conflict_file" ]; then
            rm -f "$conflict_file" && say "Removed conflicting $conflict_filename"
        fi
    done
}

backup_and_remove_gamecontrollerdb() {
    if [ ! -f "$GAMECONTROLLERDB_FILE" ]; then
        return 0
    fi
    backup="$GAMECONTROLLERDB_FILE.mistercade_backup_$(stamp)"
    cp "$GAMECONTROLLERDB_FILE" "$backup" || return 1
    rm -f "$GAMECONTROLLERDB_FILE" || return 1
    say "Backed up and removed gamecontrollerdb_user.txt."
    say "Backup: $backup"
}

find_existing_map_files() {
    map_hardware_key="$1"
    map_vid_pid=$(map_vid_pid_for_hardware "$map_hardware_key")
    if [ -z "$map_vid_pid" ] || [ ! -d "$CONFIG_INPUTS_DIR" ]; then
        return 0
    fi
    find "$CONFIG_INPUTS_DIR" -maxdepth 1 -type f \( \
        -iname "*_input_${map_vid_pid}_v3.map" -o \
        -iname "input_${map_vid_pid}_v3.map" \
    \) 2>/dev/null
}

backup_existing_map_files() {
    backup_hardware_key="$1"
    backup_hardware_label="$2"
    backup_vid_pid=$(map_vid_pid_for_hardware "$backup_hardware_key")
    if [ -z "$backup_vid_pid" ]; then
        return 0
    fi
    if [ ! -d "$CONFIG_INPUTS_DIR" ]; then
        say "No config/inputs folder found at $CONFIG_INPUTS_DIR."
        return 0
    fi

    tmp_list="${TMPDIR:-/tmp}/mistercade_map_backup_$$.txt"
    tmp_names="${TMPDIR:-/tmp}/mistercade_map_backup_names_$$.txt"
    find_existing_map_files "$backup_hardware_key" > "$tmp_list"
    map_count=$(wc -l < "$tmp_list" | tr -d ' ')
    if [ "$map_count" -eq 0 ] 2>/dev/null; then
        rm -f "$tmp_list" "$tmp_names"
        say "No existing $backup_hardware_label map files were found in $CONFIG_INPUTS_DIR."
        return 0
    fi

    say ""
    say "$map_count existing $backup_hardware_label map file(s) were found in $CONFIG_INPUTS_DIR."
    say "VID/PID: $backup_vid_pid"
    say "The backup does not delete the existing map files."
    if ! ask_yes_no "Create a timestamped backup archive before continuing? [Y/n] " "yes"; then
        rm -f "$tmp_list" "$tmp_names"
        return 0
    fi

    backup_dir="$CONFIG_INPUTS_DIR/mistercade_map_backups"
    mkdir -p "$backup_dir" || {
        rm -f "$tmp_list" "$tmp_names"
        say "Could not create backup folder: $backup_dir"
        return 1
    }

    sed 's|.*/||' "$tmp_list" > "$tmp_names"
    backup_base="mistercade_${backup_hardware_key}_${backup_vid_pid}_maps_$(stamp)"
    if command -v zip >/dev/null 2>&1; then
        backup_archive="$backup_dir/$backup_base.zip"
        (cd "$CONFIG_INPUTS_DIR" && zip -q "$backup_archive" -@ < "$tmp_names")
        result=$?
    elif command -v tar >/dev/null 2>&1; then
        backup_archive="$backup_dir/$backup_base.tar.gz"
        (cd "$CONFIG_INPUTS_DIR" && tar -czf "$backup_archive" -T "$tmp_names" >/dev/null 2>&1)
        result=$?
    else
        rm -f "$tmp_list" "$tmp_names"
        say "No zip or tar command was found, so map files were not backed up."
        return 1
    fi

    rm -f "$tmp_list" "$tmp_names"
    if [ "$result" -ne 0 ]; then
        rm -f "$backup_archive"
        say "Could not create map backup archive."
        return "$result"
    fi
    say "Created map backup: $backup_archive"
}

ensure_mister_ini() {
    if [ -f "$MISTER_INI" ]; then
        return 0
    fi
    if ! ask_yes_no "MiSTer.ini was not found. Create $MISTER_INI? [Y/n] " "yes"; then
        return 1
    fi
    printf '[MiSTer]\n' > "$MISTER_INI" || return 1
    say "Created $MISTER_INI"
}

backup_mister_ini() {
    if [ ! -f "$MISTER_INI" ]; then
        return 0
    fi
    backup="$MISTER_INI.mistercade_backup_$(stamp)"
    cp "$MISTER_INI" "$backup" || return 1
    say "Backup: $backup"
}

set_ini_value() {
    key="$1"
    value="$2"
    tmp="$MISTER_INI.tmp.$$"
    awk -v key="$key" -v value="$value" '
        BEGIN { done = 0 }
        {
            probe = $0
            sub(/^[ \t;]*/, "", probe)
            if (!done && probe ~ "^" key "[ \t]*=") {
                print key "=" value
                done = 1
                next
            }
            print
        }
        END {
            if (!done) print key "=" value
        }
    ' "$MISTER_INI" > "$tmp" && mv "$tmp" "$MISTER_INI"
}

apply_ini_setting() {
    key="$1"
    value="$2"
    set_ini_value "$key" "$value" || return 1
    say "  $key=$value"
}

configure_osd_rotation() {
    say ""
    say "OSD rotation:"
    say "  0. Horizontal monitor"
    say "  1. Clockwise rotated monitor"
    say "  2. Counter-clockwise rotated monitor"
    say ""
    printf 'Rotation number, or Enter to keep current setting: '
    IFS= read -r rotate_choice || rotate_choice=""
    case "$rotate_choice" in
        "") return 0 ;;
        0|1|2) apply_ini_setting osd_rotate "$rotate_choice" ;;
        *) say "Unknown rotation: $rotate_choice" ;;
    esac
}

configure_mister_ini() {
    choose_hardware || return 0

    say ""
    say "This will update $MISTER_INI for $chosen_hardware_label."
    say "Shared MiSTercade V1/V2 video settings: direct_video=2, composite_sync=1, vga_mode=rgb."
    say "A timestamped backup is created before changes are written."
    if ! ask_yes_no "Continue? [Y/n] " "yes"; then
        return 0
    fi

    ensure_mister_ini || return 1
    backup_mister_ini || return 1

    say "Applying common MiSTercade video/audio settings:"
    apply_ini_setting direct_video 2 || return 1
    apply_ini_setting composite_sync 1 || return 1
    apply_ini_setting vga_mode rgb || return 1
    apply_ini_setting hdmi_audio_96k 0 || return 1

    if ask_yes_no "Apply recommended controller assignment for $chosen_hardware_label? [Y/n] " "yes"; then
        say "Applying controller assignment:"
        case "$chosen_hardware_key" in
            v1|v1-2025)
                apply_ini_setting player_1_controller "16D0:10BE.0001" || return 1
                ;;
            v2)
                apply_ini_setting player_1_controller "usb-1.3/input0" || return 1
                apply_ini_setting player_2_controller "usb-1.4/input0" || return 1
                ;;
        esac
    fi

    if ask_yes_no "Disable resolution and controller info popups? [Y/n] " "yes"; then
        say "Applying quiet-launch settings:"
        apply_ini_setting video_info 0 || return 1
        apply_ini_setting controller_info 0 || return 1
    fi

    if ask_yes_no "Set OSD rotation now? [y/N] " "no"; then
        configure_osd_rotation || return 1
    fi

    say ""
    say "MiSTer.ini configuration complete. Restart MiSTer for these settings to take effect."
}

download_file() {
    url="$1"
    target="$2"
    if command -v curl >/dev/null 2>&1; then
        curl -LfsS "$url" -o "$target"
        return $?
    fi
    if command -v wget >/dev/null 2>&1; then
        wget -q -O "$target" "$url"
        return $?
    fi
    say "curl or wget is required to download the installer choice."
    return 1
}

hardware_options() {
    options_data | awk -F'|' '
        !seen[$5]++ {
            count++
            print count "|" $5 "|" $2
        }
    '
}

kind_options() {
    wanted_hardware_key="$1"
    options_data | awk -F'|' -v wanted="$wanted_hardware_key" '
        $5 == wanted && !seen[$6]++ {
            count++
            print count "|" $6 "|" $3
        }
    '
}

hardware_line_for_key() {
    wanted_hardware_key="$1"
    hardware_options | awk -F'|' -v wanted="$wanted_hardware_key" '$2 == wanted { print; exit }'
}

select_hardware_line() {
    selected_hardware_line="$1"
    old_ifs="$IFS"
    IFS='|'
    set -- $selected_hardware_line
    IFS="$old_ifs"
    chosen_hardware_key="$2"
    chosen_hardware_label="$3"
}

detect_hardware() {
    detected_hardware_key=""
    detected_hardware_vid_pid=""
    detected_hardware_note=""

    if ! command -v lsusb >/dev/null 2>&1; then
        return 1
    fi

    if lsusb 2>/dev/null | grep -qi '16d0:1358'; then
        detected_hardware_key="v2"
        detected_hardware_vid_pid="16D0:1358"
    elif lsusb 2>/dev/null | grep -qi '16d0:10be'; then
        detected_hardware_key="v1-2025"
        detected_hardware_vid_pid="16D0:10BE"
        detected_hardware_note="The 16D0:10BE USB ID is shared by legacy V1 and V1 2025 firmware; using MiSTercade V1 2025 by default."
    else
        return 1
    fi

    detected_hardware_line=$(hardware_line_for_key "$detected_hardware_key")
    [ -n "$detected_hardware_line" ] || return 1
    select_hardware_line "$detected_hardware_line"
    return 0
}

print_hardware_options() {
    say ""
    say "Choose your MiSTercade version:"
    hardware_options | while IFS='|' read -r option_number option_key option_label; do
        [ -n "$option_number" ] || continue
        say "  $option_number. $option_label"
    done
}

print_kind_options() {
    wanted_hardware_key="$1"
    say ""
    say "Choose what to install:"
    kind_options "$wanted_hardware_key" | while IFS='|' read -r option_number option_kind option_label; do
        [ -n "$option_number" ] || continue
        if [ "$option_kind" = "gamecontrollerdb" ]; then
            say "  $option_number. $option_label (experimental; no .map pause/core credits feature)"
        else
            say "  $option_number. $option_label"
        fi
    done
}

choose_hardware() {
    if detect_hardware; then
        say ""
        say "Detected: $chosen_hardware_label ($detected_hardware_vid_pid)"
        if [ -n "$detected_hardware_note" ]; then
            say "$detected_hardware_note"
        fi
        if ask_yes_no "Use detected MiSTercade version? [Y/n] " "yes"; then
            return 0
        fi
    elif command -v lsusb >/dev/null 2>&1; then
        say ""
        say "No supported MiSTercade USB VID/PID was detected with lsusb."
    else
        say ""
        say "lsusb was not found, so auto-detection is unavailable."
    fi

    print_hardware_options
    say ""
    printf 'MiSTercade version number, or Enter to cancel: '
    IFS= read -r hardware_choice || hardware_choice=""
    [ -n "$hardware_choice" ] || return 1

    hardware_line=$(hardware_options | awk -F'|' -v wanted="$hardware_choice" '$1 == wanted { print; exit }')
    if [ -z "$hardware_line" ]; then
        say "Unknown MiSTercade version: $hardware_choice"
        return 1
    fi

    select_hardware_line "$hardware_line"
    return 0
}

choose_kind() {
    wanted_hardware_key="$1"
    print_kind_options "$wanted_hardware_key"
    say ""
    printf 'Install type number, or Enter to cancel: '
    IFS= read -r kind_choice || kind_choice=""
    [ -n "$kind_choice" ] || return 1

    kind_line=$(kind_options "$wanted_hardware_key" | awk -F'|' -v wanted="$kind_choice" '$1 == wanted { print; exit }')
    if [ -z "$kind_line" ]; then
        say "Unknown install type: $kind_choice"
        return 1
    fi

    old_ifs="$IFS"
    IFS='|'
    set -- $kind_line
    IFS="$old_ifs"
    chosen_kind="$2"
    chosen_kind_label="$3"
    return 0
}

resolve_selected_option() {
    wanted_hardware_key="$1"
    wanted_kind="$2"
    wanted_freeplay="$3"
    wanted_menu="$4"
    options_data | awk -F'|'         -v hardware_key="$wanted_hardware_key"         -v kind="$wanted_kind"         -v freeplay="$wanted_freeplay"         -v menu="$wanted_menu" '
        $5 == hardware_key && $6 == kind {
            is_freeplay = ($7 ~ /freeplay/)
            is_nomenu = ($7 ~ /nomenu/)
            want_freeplay = (freeplay == "yes")
            want_nomenu = (menu == "nomenu")
            if (is_freeplay == want_freeplay && is_nomenu == want_nomenu) {
                print
                exit
            }
        }
    '
}

install_selected_line() {
    line="$1"

    old_ifs="$IFS"
    IFS='|'
    set -- $line
    IFS="$old_ifs"
    selected_number="$1"
    selected_hardware="$2"
    selected_install_type="$3"
    selected_label="$4"
    selected_hardware_key="$5"
    selected_kind="$6"
    selected_branch="$7"
    selected_rel_path="$8"
    selected_filename="$9"

    say ""
    say "Selected: $selected_hardware - $selected_install_type - $selected_label"
    if [ "$selected_kind" = "gamecontrollerdb" ]; then
        say "Note: gamecontrollerdb_user.txt does not include the MiSTercade .map pause/core credits feature."
    fi

    section_count=$(count_mistercade_sections)
    if [ "$section_count" -gt 0 ] 2>/dev/null; then
        say ""
        say "MiSTercade entries were found inside $DOWNLOADER_INI."
        if ask_yes_no "Back up downloader.ini and remove those sections? [Y/n] " "yes"; then
            clean_downloader_ini || return 1
        fi
    fi

    remove_conflicting_dropins "$selected_hardware_key"
    backup_existing_map_files "$selected_hardware_key" "$selected_hardware" || return 1

    if [ "$selected_kind" = "maps" ] && [ -f "$GAMECONTROLLERDB_FILE" ]; then
        say ""
        say "A gamecontrollerdb_user.txt file is installed. Map-file choices do not need it."
        if ask_yes_no "Back it up and remove it now? [Y/n] " "yes"; then
            backup_and_remove_gamecontrollerdb || return 1
        fi
    fi

    url="$REPO_RAW_BASE/$selected_rel_path"
    target="$SD_ROOT/$selected_filename"
    say ""
    say "Downloading $selected_filename..."
    download_file "$url" "$target" || return 1
    say "Installed $target"
    say ""
    say "Run your MiSTer updater script to download the selected MiSTercade mappings."
}

install_choice() {
    choose_hardware || return 0
    choose_kind "$chosen_hardware_key" || return 0

    chosen_freeplay="no"
    if ask_yes_no "Enable freeplay? [y/N] " "no"; then
        chosen_freeplay="yes"
    fi

    chosen_menu="osd"
    if [ "$chosen_kind" = "maps" ]; then
        if ! ask_yes_no "Include the MiSTer menu/OSD button combo? [Y/n] " "yes"; then
            chosen_menu="nomenu"
        fi
    else
        say "The menu/OSD combo option only applies to .map files."
    fi

    selected_line=$(resolve_selected_option "$chosen_hardware_key" "$chosen_kind" "$chosen_freeplay" "$chosen_menu")
    if [ -z "$selected_line" ]; then
        say "No matching downloader file was found for that choice."
        return 1
    fi

    install_selected_line "$selected_line"
}

remove_gamecontrollerdb_menu() {
    if [ ! -f "$GAMECONTROLLERDB_FILE" ]; then
        say "No gamecontrollerdb_user.txt found at $GAMECONTROLLERDB_FILE."
        return 0
    fi
    if ask_yes_no "Back up and remove $GAMECONTROLLERDB_FILE? [y/N] " "no"; then
        backup_and_remove_gamecontrollerdb
    fi
}

main_menu() {
    while :; do
        status_report
        say ""
        say "Menu"
        say "  1. Install or change a mapping choice"
        say "  2. Remove MiSTercade downloader drop-in files"
        say "  3. Remove MiSTercade sections from downloader.ini"
        say "  4. Back up and remove installed gamecontrollerdb_user.txt"
        say "  5. Configure MiSTer.ini for MiSTercade"
        say "  q. Quit"
        say ""
        printf 'Selection: '
        if ! IFS= read -r menu_choice; then
            exit 0
        fi
        case "$menu_choice" in
            1) install_choice ;;
            2) remove_all_mistercade_dropins ;;
            3) clean_downloader_ini ;;
            4) remove_gamecontrollerdb_menu ;;
            5) configure_mister_ini ;;
            q|Q) exit 0 ;;
            *) say "Unknown selection: $menu_choice" ;;
        esac
        say ""
        printf 'Press Enter to continue...'
        IFS= read -r unused || unused=""
    done
}

if [ ! -d "$SD_ROOT" ]; then
    say "SD root not found: $SD_ROOT"
    say "Set MISTERCADE_SD_ROOT to override it, or run this script on MiSTer."
    exit 1
fi

main_menu
