#!/usr/bin/env bash
#
# SPDX-License-Identifier: GPL-2.0
#
# Copyright (c) 2013-2026 Igor Pecovnik, igor@armbian.com
#
# This file is a part of the Armbian Build Framework
# https://github.com/armbian/build/

#!/usr/bin/env bash

function interactive_config_prepare_terminal() {
	if [[ -z $ROOT_FS_CREATE_ONLY ]]; then
		if [[ -t 0 ]]; then # "-t fd return True if file descriptor fd is open and refers to a terminal". 0 = stdin, 1 = stdout, 2 = stderr, 3+ custom
			# override stty size, if stdin is a terminal.
			[[ -n $COLUMNS ]] && stty cols $COLUMNS
			[[ -n $LINES ]] && stty rows $LINES
			declare -g TTY_X=$(($(stty size | awk '{print $2}') - 6)) # determine terminal width
			declare -g TTY_Y=$(($(stty size | awk '{print $1}') - 6)) # determine terminal height
		fi
	fi
	# We'll use this title on all menus
	declare current_year
	current_year=$(date +%Y)
	declare -g -r backtitle="Armbian Linux build framework, https://www.armbian.com | (c) 2013-${current_year} Igor Pecovnik "
	declare -A -g ARMBIAN_INTERACTIVE_CONFIGS=() # An associative array of all interactive configurations
}

# Set config variable and ARMBIAN_INTERACTIVE_CONFIGS in a consistent way
# $1: variable name
# $2: variable value
function set_interactive_config_value() {
	declare -g "${1}=${2}"
	ARMBIAN_INTERACTIVE_CONFIGS["${1}"]="${2}"
}

function interactive_finish() {
	unset TTY_X
	unset TTY_Y
	unset options
	unset DIALOG_EXIT_CODE
	unset WIP_STATE
	unset SHOW_WARNING
}

function interactive_config_ask_kernel() {
	interactive_config_ask_kernel_configure
}

function interactive_config_ask_kernel_configure() {
	[[ -n ${KERNEL_CONFIGURE} ]] && return 0
	options+=("no" "Do not change the kernel configuration")
	options+=("yes" "Show a kernel configuration menu before compilation")
	#options+=("prebuilt" "Use precompiled packages (maintained hardware only)") # @TODO armbian-next does not support this, I think.
	dialog_if_terminal_set_vars --title "Choose an option" --backtitle "$backtitle" --no-tags --menu "Select the kernel configuration" $TTY_Y $TTY_X $((TTY_Y - 8)) "${options[@]}"
	set_interactive_config_value KERNEL_CONFIGURE "${DIALOG_RESULT}"
	[[ ${DIALOG_EXIT_CODE} != 0 ]] && exit_with_error "You cancelled interactive during kernel configuration" "Build cancelled"
	unset options
}

# Required usage:
# declare -a arr_all_board_names=() arr_all_board_options=()                                                                                              # arrays
# declare -A dict_all_board_types=() dict_all_board_source_files=() dict_all_board_descriptions=()                                                        # dictionaries
# get_list_of_all_buildable_boards arr_all_board_names arr_all_board_options dict_all_board_types dict_all_board_source_files dict_all_board_descriptions # invoke
function get_list_of_all_buildable_boards() {
	display_alert "Generating list of all available boards" "might take a while" ""
	local -a board_types=("conf")
	[[ "${WIP_STATE}" != "supported" ]] && board_types+=("wip" "csc" "eos" "tvb")
	local -a board_file_paths=("${SRC}/config/boards" "${USERPATCHES_PATH}/config/boards")

	# local -n is a name reference, see https://www.linuxjournal.com/content/whats-new-bash-parameter-expansion
	# it works with arrays and associative arrays/dictionaries
	local -n ref_arr_all_board_names="${1}"
	[[ "${2}" != "" ]] && local -n ref_arr_all_board_options="${2}" # optional
	local -n ref_dict_all_board_types="${3}"
	local -n ref_dict_all_board_source_files="${4}"
	[[ "${5}" != "" ]] && local -n ref_dict_all_board_descriptions="${5}" # optional

	declare -i prepare_options=0
	if [[ "${2}" != "" || "${5}" != "" ]]; then # only if second or fifth reference specified, otherwise too costly
		prepare_options=1
	fi

	# Avoid a $(basename ... | cut ...) + $(head | cut ...) subshell pair per board file -
	# with ~400 boards this is ~800 fork/exec cycles per scan. Bash parameter expansion
	# plus a single `read` are equivalent and effectively free, which makes a noticeable
	# difference on every dialog redraw (CSC/WIP toggle, filter changes, etc.).
	local board_file_path board_type full_board_file board_name board_desc first_line
	for board_file_path in "${board_file_paths[@]}"; do
		[[ ! -d "${board_file_path}" ]] && continue
		for board_type in "${board_types[@]}"; do
			for full_board_file in "${board_file_path}"/*."${board_type}"; do
				[[ "${full_board_file}" == *"*"* ]] && continue # ignore non-matches, due to bash's (non-)globbing behaviour
				board_name="${full_board_file##*/}"             # strip directory (basename)
				board_name="${board_name%.*}"                   # strip extension (cut -d'.' -f1)
				ref_dict_all_board_types["${board_name}"]="${board_type}"
				ref_dict_all_board_source_files["${board_name}"]="${ref_dict_all_board_source_files["${board_name}"]} ${full_board_file}" # accumulate, will have extra space
				if [[ ${prepare_options} -gt 0 ]]; then
					IFS= read -r first_line < "${full_board_file}" || true
					board_desc="${first_line#*#}"  # everything after first '#' (cut -d'#' -f2 ...
					board_desc="${board_desc%%#*}" # ... up to next '#' if any)
					ref_dict_all_board_descriptions["${board_name}"]="${board_desc}"
				fi
			done
		done
	done

	if [[ ${prepare_options} -gt 0 ]]; then
		# get a sorted list of boards across all types
		declare ref_arr_all_board_names_unsorted=("${!ref_dict_all_board_types[@]}") # Expand the keys of one of the dicts, that's the list of boards (unsorted)
		declare -a ref_arr_all_board_names_sorted=()
		readarray -t ref_arr_all_board_names_sorted < <(printf '%s\n' "${ref_arr_all_board_names_unsorted[@]}" | sort -h)
		ref_arr_all_board_names=("${ref_arr_all_board_names_sorted[@]}")

		# prepare the options for the dialog menu; this is sorted the same order as the boards.
		for board_name in "${ref_arr_all_board_names[@]}"; do
			ref_arr_all_board_options+=("${board_name}" "\Z1(${ref_dict_all_board_types["${board_name}"]})\Zn ${ref_dict_all_board_descriptions["${board_name}"]}")
		done
	else
		ref_arr_all_board_names=("${!ref_dict_all_board_types[@]}") # Expand the keys of one of the dicts, that's the list of boards (unsorted)
	fi

	return 0
}

function interactive_config_ask_board_list() {
	# if BOARD is not set, display selection menu, otherwise return success
	[[ -n ${BOARD} ]] && return 0

	declare WIP_STATE=supported
	if [[ "${EXPERT}" == "yes" ]]; then
		display_alert "Expert mode!" "You can select all boards" "info"
		WIP_STATE=unsupported
	fi

	declare WIP_BUTTON='CSC/WIP/EOS/TVB'
	declare STATE_DESCRIPTION=' - boards with high level of software maturity'
	declare BOARD_FILTER=''
	declare FILTER_TAG='[set filter]'
	declare CLEAR_TAG='[clear filter]'
	declare temp_rc
	temp_rc=$(mktemp) # @TODO: this is a _very_ early call to mktemp - no TMPDIR set yet - it needs to be cleaned-up somehow

	# Cache the board scan across dialog redraws. The result only changes when WIP_STATE
	# changes (Show CSC/WIP/EOS/TVB button toggles the type set), so we re-scan only on
	# that branch below. Without the cache every redraw would re-glob ~400 board files.
	declare -a arr_all_board_names=() arr_all_board_options=()                                       # arrays
	declare -A dict_all_board_types=() dict_all_board_source_files=() dict_all_board_descriptions=() # dictionaries
	declare board_list_needs_refresh=yes
	while true; do
		if [[ "${board_list_needs_refresh}" == "yes" ]]; then
			arr_all_board_names=() arr_all_board_options=()
			dict_all_board_types=() dict_all_board_source_files=() dict_all_board_descriptions=()
			get_list_of_all_buildable_boards arr_all_board_names arr_all_board_options dict_all_board_types dict_all_board_source_files dict_all_board_descriptions # invoke
			board_list_needs_refresh=no
		fi

		# Apply substring filter (case-insensitive, matches name and description) and
		# prepend a sentinel entry that opens an --inputbox to set/change/clear the filter.
		# arr_all_board_options is a flat sequence of (tag, item) pairs as dialog expects.
		declare -a arr_menu_options=()
		if [[ -n "${BOARD_FILTER}" ]]; then
			declare lc_filter="${BOARD_FILTER,,}"
			declare i tag item hay
			declare -a arr_filter_hits=()
			for ((i = 0; i < ${#arr_all_board_options[@]}; i += 2)); do
				tag="${arr_all_board_options[i]}"
				item="${arr_all_board_options[i + 1]}"
				# Match against board name + raw description (from dict_all_board_descriptions),
				# not against arr_all_board_options' item which carries dialog --colors escapes
				# (\Z1...\Zn) and a leading "(type)" badge - those would cause false positives
				# for searches like "conf", "wip" or "z1".
				hay="${tag} ${dict_all_board_descriptions["${tag}"]}"
				hay="${hay,,}"
				if [[ "${hay}" == *"${lc_filter}"* ]]; then
					arr_filter_hits+=("${tag}" "${item}")
				fi
			done
			declare match_count=$((${#arr_filter_hits[@]} / 2))
			arr_menu_options=(
				"${FILTER_TAG}" "\Z1change filter (current: ${BOARD_FILTER}, ${match_count}/${#arr_all_board_names[@]} boards)\Zn"
				"${CLEAR_TAG}" "\Z1clear filter, show all ${#arr_all_board_names[@]} boards\Zn"
				"${arr_filter_hits[@]}"
			)
		else
			arr_menu_options=(
				"${FILTER_TAG}" "\Z1filter boards by substring of name or description\Zn"
				"${arr_all_board_options[@]}"
			)
		fi

		echo > "${temp_rc}"                    # zero out the rcfile to start
		if [[ $WIP_STATE != supported ]]; then # be if wip csc etc included. I personally disagree here.
			cat <<- 'EOF' > "${temp_rc}"
				dialog_color = (RED,WHITE,OFF)
				screen_color = (WHITE,RED,ON)
				tag_color = (RED,WHITE,ON)
				item_selected_color = (WHITE,RED,ON)
				tag_selected_color = (WHITE,RED,ON)
				tag_key_selected_color = (WHITE,RED,ON)
			EOF
		fi

		DIALOGRC=$temp_rc \
			dialog_if_terminal_set_vars --title "Choose a board" --backtitle "$backtitle" --scrollbar \
			--colors --extra-label "Show $WIP_BUTTON" --extra-button \
			--menu "Select the target board. Displaying:\n$STATE_DESCRIPTION" $TTY_Y $TTY_X $((TTY_Y - 8)) "${arr_menu_options[@]}"
		declare STATUS=${DIALOG_EXIT_CODE}
		declare RESULT="${DIALOG_RESULT}"

		if [[ $STATUS == 3 ]]; then
			if [[ $WIP_STATE == supported ]]; then
				[[ $SHOW_WARNING == yes ]] && show_developer_warning
				STATE_DESCRIPTION=' - \Z1(conf)\Zn - Boards with high level of software maturity
				\n - \Z1(CSC)\Zn  - Community Supported Configuration\n - \Z1(WIP)\Zn  - Work In Progress
				\n - \Z1(EOS)\Zn  - End Of Support\n - \Z1(TVB)\Zn  - TV boxes'
				WIP_STATE=unsupported
				WIP_BUTTON='matured'
				EXPERT=yes
			else
				STATE_DESCRIPTION=' - boards with high level of software maturity'
				WIP_STATE=supported
				WIP_BUTTON='CSC/WIP/EOS'
				EXPERT=no # @TODO: this overrides an "expert" mode that could be set on by the user. revert to original one?
			fi
			board_list_needs_refresh=yes # WIP_STATE changed - rescan to include/exclude wip/csc/eos/tvb
			continue
		elif [[ $STATUS == 0 ]]; then
			if [[ "${RESULT}" == "${FILTER_TAG}" ]]; then
				dialog_if_terminal_set_vars --title "Filter boards" --backtitle "$backtitle" \
					--inputbox "Case-insensitive substring of board name or description. Empty value clears." 8 "${TTY_X}" "${BOARD_FILTER}"
				[[ ${DIALOG_EXIT_CODE} == 0 ]] && BOARD_FILTER="${DIALOG_RESULT}"
				continue
			elif [[ "${RESULT}" == "${CLEAR_TAG}" ]]; then
				BOARD_FILTER=''
				continue
			fi
			set_interactive_config_value BOARD "${RESULT}"
			break
		else
			exit_with_error "You cancelled interactive config" "Build cancelled, board not chosen"
		fi
	done
}

function get_kernel_info_for_branch() {
	local search_branch="$1"
	local conf_file="${SRC}/config/sources/families/${BOARDFAMILY}.conf"

	# Recognises both `branch)` and `pat1 | pat2 | …)` case labels (#8957).
	awk -v branch="$search_branch" '
    BEGIN { found=0; major_minor=""; desc="" }
    /^[[:space:]]*[a-zA-Z0-9_*?-][a-zA-Z0-9_*? |-]*\)/ {
        label = $0
        sub(/\).*/, "", label)
        sub(/^[[:space:]]+/, "", label)
        sub(/[[:space:]]+$/, "", label)
        n = split(label, alts, /[[:space:]]*\|[[:space:]]*/)
        matched = 0
        for (i = 1; i <= n; i++) {
            if (alts[i] == branch) { matched = 1; break }
        }
        if (matched) {
            found = 1
            next
        } else if (found) {
            exit
        }
    }
    found && /declare[[:space:]]+-g[[:space:]]+KERNEL_MAJOR_MINOR=/ {
        if (match($0, /"([^"]+)"/, arr)) {
            major_minor=arr[1]
        }
    }
    found && /declare[[:space:]]+-g[[:space:]]+KERNEL_DESCRIPTION=/ {
        if (match($0, /"([^"]+)"/, arr)) {
            desc=arr[1]
        }
    }
    END {
        print major_minor "|" desc
    }
    ' "$conf_file"
}

function interactive_config_ask_branch() {
	if [[ -n $BRANCH ]]; then
		display_alert "Already set BRANCH, skipping interactive" "${BRANCH}" "debug"
		return 0
	fi

	declare -a options=()
	declare one_kernel_target

	for one_kernel_target in ${KERNEL_TARGET//,/ }; do

		local version=""
		local description=""

		local info
		info="$(get_kernel_info_for_branch "$one_kernel_target")"
		version="${info%%|*}"
		description="${info#*|}"

		# Fallback if description is empty
		if [[ -z "$description" ]]; then
			case "$one_kernel_target" in
				current)
					description="Recommended. Usually an LTS kernel"
					;;
				legacy)
					description="Old stable / Legacy kernel"
					;;
				edge)
					description="Bleeding edge / latest possible"
					;;
				cloud)
					description="Cloud optimized minimal LTS kernel"
					;;
				vendor*)
					description="Vendor BSP kernel"
					;;
				*)
					description="Experimental ${one_kernel_target} kernel / for Developers"
					;;
			esac
		fi

		# Append version if found
		if [[ -n "$version" ]]; then
			description="${description} (${version})"
		fi

		options+=("${one_kernel_target}" "${description}")
	done

	dialog_if_terminal_set_vars --title "Choose a kernel" --backtitle "$backtitle" --colors \
		--menu "Select the target kernel branch.\nSelected BOARD='${BOARD}'\nExact kernel versions depend on selected board and its family." \
		$TTY_Y $TTY_X $((TTY_Y - 8)) "${options[@]}"

	set_interactive_config_value BRANCH "${DIALOG_RESULT}"

	[[ -z ${BRANCH} ]] && exit_with_error "No kernel branch selected"
	return 0
}

function interactive_config_ask_release() {
	[[ -n ${RELEASE} ]] && return 0

	declare -a options=()
	distros_options
	dialog_if_terminal_set_vars --title "Choose a release package base" --backtitle "$backtitle" --menu "Select the target OS release package base; selected BRANCH='${BRANCH}'" $TTY_Y $TTY_X $((TTY_Y - 8)) "${options[@]}"
	set_interactive_config_value RELEASE "${DIALOG_RESULT}"
	[[ -z ${RELEASE} ]] && exit_with_error "No release selected"

	return 0 # shortcircuit above!
}

function interactive_config_ask_desktop_build() {
	# don't show desktop option if we choose minimal build
	[[ $HAS_VIDEO_OUTPUT == no || $BUILD_MINIMAL == yes ]] && BUILD_DESKTOP=no

	[[ -n ${BUILD_DESKTOP} ]] && return 0

	# read distribution support status which is written to the armbian-release file
	set_distribution_status
	options=()
	options+=("no" "Image with console interface (server)")
	options+=("yes" "Image with desktop environment")
	dialog_if_terminal_set_vars --title "Choose image type" --backtitle "$backtitle" --no-tags \
		--menu "Select the target image type" $TTY_Y $TTY_X $((TTY_Y - 8)) "${options[@]}"
	set_interactive_config_value BUILD_DESKTOP "${DIALOG_RESULT}"
	unset options
	[[ -z $BUILD_DESKTOP ]] && exit_with_error "No image type selected"
	if [[ ${BUILD_DESKTOP} == "yes" ]]; then
		set_interactive_config_value BUILD_MINIMAL no
		SELECTED_CONFIGURATION="desktop"
	fi
	return 0
}

function interactive_config_ask_standard_or_minimal() {
	[[ "${BUILDING_IMAGE}" != "yes" ]] && return 0
	[[ $BUILD_DESKTOP != no ]] && return 0
	[[ -n $BUILD_MINIMAL ]] && return 0
	options=()
	options+=("no" "Standard image with console interface")
	options+=("yes" "Minimal image with console interface")
	dialog_if_terminal_set_vars --title "Choose image type" --backtitle "$backtitle" --no-tags \
		--menu "Select the target image type" $TTY_Y $TTY_X $((TTY_Y - 8)) "${options[@]}"
	set_interactive_config_value BUILD_MINIMAL "${DIALOG_RESULT}"
	unset options
	[[ -z $BUILD_MINIMAL ]] && exit_with_error "No standard/minimal selected"
	if [[ $BUILD_MINIMAL == "yes" ]]; then
		SELECTED_CONFIGURATION="cli_minimal"
	else
		SELECTED_CONFIGURATION="cli_standard"
	fi
	return 0
}