#!/bin/bash
###############################################################################
# BRLTTY - A background process providing access to the console screen (when in
#          text mode) for a blind person using a refreshable braille display.
#
# Copyright (C) 1995-2025 by The BRLTTY Developers.
#
# BRLTTY comes with ABSOLUTELY NO WARRANTY.
#
# This is free software, placed under the terms of the
# GNU Lesser General Public License, as published by the Free Software
# Foundation; either version 2.1 of the License, or (at your option) any
# later version. Please see the file LICENSE-LGPL for details.
#
# Web Page: http://brltty.app/
#
# This software is maintained by Dave Mielke <dave@mielke.cc>.
###############################################################################

. "$(dirname "${BASH_SOURCE[0]}")/brltty-prologue.sh"

getElement() {
   eval setVariable "\${1}" "\"\${${2}[\"${3}\"]}\""
}

setElement() {
   eval setVariable "\${1}[\"${2}\"]" "\"\${3}\""
}

setElements() {
   local _array="${1}"
   shift 1
   eval "${_array}=(\"\${@}\")"
}

appendElements() {
   local _array="${1}"
   shift 1
   eval "${_array}+=(\"\${@}\")"
}

prependElements() {
   local _array="${1}"
   shift 1
   eval setElements "\${_array}" "\"\${@}\" \"\${${_array}[@]}\""
}

shiftElements() {
   local _array="${1}"
   local _count="${2}"

   eval set -- "\"\${${_array}[@]}\""
   shift "${_count}"
   setElements "${_array}" "${@}"
}

getElementCount() {
   eval setVariable "\${1}" "\${#${2}[*]}"
}

getElementNames() {
   eval "${1}=(\"\${!${2}[@]}\")"
}

forElements() {
   local _array="${1}"
   shift 1

   local _names=()
   getElementNames _names "${_array}"

   local name
   for name in "${_names[@]}"
   do
      local value
      getElement value "${_array}" "${name}"
      "${@}" "${name}" "${value}"
   done
}

getElements() {
   local _toArray="${1}"
   local _fromArray="${2}"
   forElements "${_fromArray}" setElement "${_toArray}"
}

listElement() {
   local array="${1}"
   local name="${2}"
   local value="${3}"
   echo "${array}[${name}]: ${value}"
}

listElements() {
   local _array="${1}"
   forElements "${_array}" listElement "${_array}" | sort
}

writeElement() {
   local name="${1}"
   local value="${2}"
   echo "${name} ${value}"
}

writeElements() {
   local _array="${1}"
   forElements "${_array}" writeElement | sort
}

readElements() {
   local _array="${1}"

   local name value
   while read name value
   do
      setElement "${_array}" "${name}" "${value}"
   done
}

toRelativePath() {
   local toPath="${1}"
   local variable="${2}"

   [ -n "${toPath}" ] || toPath="."
   resolveDirectory "${toPath}" toPath
   local fromPath="$(pwd)"

   [ "${fromPath%/}" = "${fromPath}" ] && fromPath+="/"
   [ "${toPath%/}" = "${toPath}" ] && toPath+="/"

   local fromLength="${#fromPath}"
   local toLength="${#toPath}"

   local limit=$(( (fromLength < toLength)? fromLength: toLength ))
   local index=0
   local start=0

   while (( index < limit ))
   do
      local fromChar="${fromPath:index:1}"
      local toChar="${toPath:index:1}"

      [ "${fromChar}" = "${toChar}" ] || break
      [ "${fromChar}" = "/" ] && start="$((index + 1))"
      let "index += 1"
   done

   fromPath="${fromPath:start}"
   toPath="${toPath:start}"

   while [ "${#fromPath}" -gt 0 ]
   do
      toPath="../${toPath}"
      fromPath="${fromPath#*/}"
   done

   [ -n "${toPath}" ] || toPath="."

   if [ -n "${variable}" ]
   then
      setVariable "${variable}" "${toPath}"
   else
      echo "${toPath}"
   fi
}

programConfigurationFilePrefixArray=()
makeProgramConfigurationFilePrefixArray() {
   [ "${#programConfigurationFilePrefixArray[*]}" -gt 0 ] || {
      programConfigurationFilePrefixArray+=("${programDirectory}/.")

      [ -n "${HOME}" ] && {
         programConfigurationFilePrefixArray+=("${HOME}/.config/${programName}/")
         programConfigurationFilePrefixArray+=("${HOME}/.")
      }

      programConfigurationFilePrefixArray+=("/etc/${programName}/")
      programConfigurationFilePrefixArray+=("/etc/xdg/${programName}/")
      programConfigurationFilePrefixArray+=("/etc/")
   }
}

findProgramConfigurationFile() {
   local fileVariable="${1}"
   shift 1

   [ "${#}" -gt 0 ] || set -- conf cfg
   local fileExtensions=("${@}")

   makeProgramConfigurationFilePrefixArray
   local prefix

   for prefix in "${programConfigurationFilePrefixArray[@]}"
   do
      local extension

      for extension in "${fileExtensions[@]}"
      do
         local _file="${prefix}${programName}.${extension}"
         [ -f "${_file}" ] || continue
         [ -r "${_file}" ] || continue

         setVariable "${fileVariable}" "${_file}"
         return 0
      done
   done

   return 1
}

programComponentDirectoryArray=()
makeProgramComponentDirectoryArray() {
   [ "${#programComponentDirectoryArray[*]}" -gt 0 ] || {
      local subdirectory="libexec"

      programComponentDirectoryArray+=("${PWD}")
      programComponentDirectoryArray+=("${PWD}/../${subdirectory}")

      programComponentDirectoryArray+=("${programDirectory}")
      programComponentDirectoryArray+=("${programDirectory}/../${subdirectory}")

      [ -n "${HOME}" ] && {
         programComponentDirectoryArray+=("${HOME}/.config/${programName}")
         programComponentDirectoryArray+=("${HOME}/${subdirectory}")
      }

      programComponentDirectoryArray+=("/usr/${subdirectory}")
      programComponentDirectoryArray+=("/${subdirectory}")

      programComponentDirectoryArray+=("/etc/${programName}")
      programComponentDirectoryArray+=("/etc/xdg/${programName}")
   }
}

findProgramComponent() {
   local fileVariable="${1}"
   local name="${2}"
   shift 2
   local fileExtensions=("${@}")

   if [ "${#fileExtensions[*]}" -eq 0 ]
   then
      fileExtensions=(bash sh)
   else
      name="${programName}.${name}"
   fi

   makeProgramComponentDirectoryArray
   local directory

   for directory in "${programComponentDirectoryArray[@]}"
   do
      local extension

      for extension in "${fileExtensions[@]}"
      do
         local _file="${directory}/${name}.${extension}"
         [ -f "${_file}" ] || continue
         [ -r "${_file}" ] || continue

         setVariable "${fileVariable}" "${_file}"
         return 0
      done
   done

   return 1
}

includeProgramComponent() {
   local name="${1}"

   local component
   findProgramComponent component "${name}" || {
      logWarning "program component not found: ${name}"
      return 1
   }

   . "${component}" || {
      logWarning "problem including program component: ${component}"
      return 2
   }

   logNote "program component included: ${component}"
   return 0
}

declare -A persistentProgramSettingsArray=()
persistentProgramSettingsChanged=false
readonly persistentProgramSettingsExtension="conf"

restorePersistentProgramSettins() {
   local settingsFile="${1}"
   local found=false

   if [ -n "${settingsFile}" ]
   then
      [ -e "${settingsFile}" ] && found=true
   elif findProgramConfigurationFile settingsFile "${persistentProgramSettingsExtension}"
   then
      found=true
   fi

   "${found}" && {
      logNote "restoring persistent program settings: ${settingsFile}"
      persistentProgramSettingsArray=()
      readElements persistentProgramSettingsArray <"${settingsFile}"
      persistentProgramSettingsChanged=false
   }
}

savePersistentProgramSettins() {
   local settingsFile="${1}"

   "${persistentProgramSettingsChanged}" && {
      [ -n "${settingsFile}" ] || {
         findProgramConfigurationFile settingsFile "${persistentProgramSettingsExtension}" && [ -f "${settingsFile}" -a -w "${settingsFile}" ] || {
            settingsFile=""
            local prefix

            for prefix in "${programConfigurationFilePrefixArray[@]}"
            do
               local directory="${prefix%/}"
               [ "${directory}" = "${prefix}" ] && continue

               if [ -e "${directory}" ]
               then
                  [ -d "${directory}" ] || continue
               else
                  mkdir --parents -- "${directory}" || continue
                  logNote "program configuration directory created: ${directory}"
               fi

               [ -w "${directory}" ] || continue
               settingsFile="${directory}/${programName}.${persistentProgramSettingsExtension}"
               break
            done

            [ -n "${settingsFile}" ] || {
               logWarning "no eligible program configuration directory"
               return 1
            }
         }
      }

      logNote "saving persistent program settings: ${settingsFile}"
      writeElements persistentProgramSettingsArray >"${settingsFile}"
      persistentProgramSettingsChanged=false
   }

   return 0
}

getPersistentProgramSetting() {
   local variable="${1}"
   local name="${2}"

   setVariable "${variable}" "${persistentProgramSettingsArray["${name}"]}"
}

changePersistentProgramSetting() {
   local name="${1}"
   local value="${2}"
   local -n setting="persistentProgramSettingsArray[\"${name}\"]"

   [ "${value}" = "${setting}" ] || {
      setting="${value}"
      persistentProgramSettingsChanged=true
   }
}

evaluateExpression() {
   local resultVariable="${1}"
   local expression="${2}"

   local _result
   _result="$(bc --quiet <<<"${expression}")" || return 1
   [ -n "${_result}" ] || return 1

   setVariable "${resultVariable}" "${_result}"
   return 0
}

if [ -n "${SRANDOM}" ]
then
   declare -n bashRandomNumberVariable="SRANDOM"
else
   declare -n bashRandomNumberVariable="RANDOM"
fi

getRandomInteger() {
   local resultVariable="${1}"
   local maximumValue="${2}"
   local minimumValue="${3:-1}"

   setVariable "${resultVariable}" $(( (bashRandomNumberVariable % (maximumValue - minimumValue + 1)) + minimumValue ))
}

convertUnit() {
   local resultVariable="${1}"
   local from="${2}"
   local to="${3}"
   local precision="${4}"

   local toValue
   toValue="$(units --terse --output-format "%.${precision:-0}f" "${from}" "${to}")" || return 1

   [ "${toValue}" = "${toValue%.*}" ] || {
      toValue="${toValue%%*(0)}"
      toValue="${toValue%.}"
   }

   setVariable "${resultVariable}" "${toValue}"
   return 0
}

convertSimpleUnit() {
   local resultVariable="${1}"
   local fromValue="${2}"
   local fromUnit="${3}"
   local toUnit="${4}"
   local precision="${5}"

   convertUnit "${resultVariable}" "${fromValue}${fromUnit}" "${toUnit}" "${precision}" || return 1
   return 0
}

formatSimpleUnit() {
   local variable="${1}"
   local fromUnit="${2}"
   local toUnit="${3}"
   local precision="${4}"

   local value="${!variable}"
   convertSimpleUnit value "${value}" "${fromUnit}" "${toUnit}" "${precision}" || return 1

   setVariable "${variable}" "${value}${toUnit}"
   return 0
}

convertComplexUnit() {
   local resultVariable="${1}"
   local fromValue="${2}"
   local fromUnit="${3}"
   local toUnit="${4}"
   local unitType="${5}"
   local precision="${6}"

   convertUnit "${resultVariable}" "${unitType}${fromUnit}(${fromValue})" "${unitType}${toUnit}" "${precision}" || return 1
   return 0
}

formatComplexUnit() {
   local variable="${1}"
   local fromUnit="${2}"
   local toUnit="${3}"
   local unitType="${4}"
   local precision="${5}"

   local value="${!variable}"
   convertComplexUnit value "${value}" "${fromUnit}" "${toUnit}" "${unitType}" "${precision}" || return 1

   setVariable "${variable}" "${value}${toUnit}"
   return 0
}

isAbbreviation() {
   local abbreviation="${1}"
   shift 1

   local length="${#abbreviation}"
   local word

   for word
   do
      [ "${length}" -le "${#word}" ] || continue
      [ "${abbreviation}" = "${word:0:length}" ] && return 0
   done

   return 1
}

verifyChoice() {
   local label="${1}"
   local valueVariable="${2}"
   shift 2

   local value
   getVariable "${valueVariable}" value

   local candidates=()
   local choice

   for choice
   do
      [ "${value}" = "${choice}" ] && return 0
      isAbbreviation "${value}" "${choice}" || continue
      candidates+=("${choice}")
   done

   local count="${#candidates[*]}"
   [ "${count}" -gt 0 ] || syntaxError "invalid ${label}: ${value}"

   [ "${count}" -eq 1 ] || {
      local message="ambiguous ${label}: ${value}"
      local delimiter=" ("

      for choice in "${candidates[@]}"
      do
         message+="${delimiter}${choice}"
         delimiter=", "
      done

      message+=")"
      syntaxError "${message}"
   }

   setVariable "${valueVariable}" "${candidates[0]}"
}

confirmAction() {
   local prompt="${1}"

   local noWord="no"
   local yesWord="yes"
   local response

   while read -p "${programName}: ${prompt} ([${noWord}] | ${yesWord})? " -r response
   do
      [ -n "${response}" ] || return 1
      response="${response,,*}"

      isAbbreviation "${response}" "${noWord}" && return 1 ||
      isAbbreviation "${response}" "${yesWord}" && return 0 ||
      programMessage "unrecognized response"
   done

   echo >&2 ""
   return 2
}

