#!/bin/bash
{ #prevents errors if script was modified while in use

function error {
  echo -e "\e[91m$1\e[39m" 1>&2
  exit 1
}

if [[ $(id -u) == 0 ]]; then
  error "Pi-Apps is not designed to be run as root! Please try again as a regular user."
fi

[ -z "$DIRECTORY" ] && DIRECTORY="$(readlink -f "$(dirname "$0")")"

#if being sourced, ensure DIRECTORY variable set
if [ -z "$DIRECTORY" ] || [ "$DIRECTORY" == "$HOME" ] || [ ! -d "$DIRECTORY" ] || [ ! -f "${DIRECTORY}/api" ] || [ ! -f "${DIRECTORY}/gui" ];then
  echo "DIRECTORY variable must be set to a valid pi-apps folder. Default folder: $HOME/pi-apps"
  return 1
fi

# runonce-entries is run in the updater, runonce requires that all api functions be available to subprocess (like is done in the gui script)
#for the will_reinstall() and list_intersect() functions
set -a #make all functions in the api available to subprocesses
source "${DIRECTORY}/api" || error "failed to source ${DIRECTORY}/api"

get_date() {
  #number of days since 1/1/1970
  echo $(($(date --utc +%s)/86400))
}

check_repo() { #download pi-apps repository to the update/pi-apps folder
  if [ "$speed" == fast ];then
    return 0
  fi
  echo -n "Checking for online changes... " 1>&2
  
  #if the updater script exists in update folder, then just git pull to save time
  if [ -s "${DIRECTORY}/update/pi-apps/updater" ];then
    cd "${DIRECTORY}/update/pi-apps"
    git pull -q 1>&2 || rm -rf "${DIRECTORY}/update"
  fi | grep -v "Already up to date." 1>&2
  
  git_url="$(cat "${DIRECTORY}/etc/git_url" || echo 'https://github.com/Botspot/pi-apps')"
  
  #if updater script still does not exist then do a git clone
  if [ ! -s "${DIRECTORY}/update/pi-apps/updater" ];then
    #keep trying until success
    while true;do
      rm -rf "${DIRECTORY}/update"
      mkdir -p "${DIRECTORY}/update" && cd "${DIRECTORY}/update" || error "failed to create and enter the update directory!"
      
      #clone the repository, and display an error message if it fails
      if git clone --depth=1 "$git_url" 1>&2 ;then
        echo "Done" 1>&2
        break #exit the loop
      else
        echo -e "\n${DIRECTORY}/updater: failed to download Pi-Apps repository!\nTrying again in 60 seconds." 1>&2
        sleep 60
      fi
    done
  else
    echo "Done" 1>&2
  fi
  cd #don't exit the function with the current directory being "${DIRECTORY}/update"
}

check_update_interval() { #return 0 if update-interval allows update-checking today, exit 1 otherwise
  local lastupdatecheck="$(cat "${DIRECTORY}/data/last-update-check" 2>/dev/null)"
  if [ -z $lastupdatecheck ];then
    warning "${DIRECTORY}/data/last-update-check does not exist!" 1>&2
    lastupdatecheck=0
  fi
  
  #write today's date to file. Format is "number of days since 1/1/1970"
  get_date > "${DIRECTORY}/data/last-update-check"
  
  local updateinterval="$(cat "${DIRECTORY}/data/settings/Check for updates")"
  
  #allowed values: Always, Daily, Weekly, Never
  if [ "$updateinterval" == 'Never' ];then
    return 1
  elif [ "$updateinterval" == 'Daily' ];then
    #if updates checked today, don't check
    if [ "$(get_date)" == "$lastupdatecheck" ];then
      return 1
    fi
  elif [ "$updateinterval" == 'Weekly' ];then
    #if updates checked less than 7 days ago, don't check
    if [ "$(get_date)" -le "$((lastupdatecheck + 7))" ];then
      return 1
    fi
  elif [ "$updateinterval" == 'Always' ];then
    return 0
  elif [ -z "$updateinterval" ];then
    warning "Something isn't right. Does '${DIRECTORY}/data/settings/Check for updates' exist?" 1>&2
  else
    echo -e "\e[91mUnrecognized update interval! \e[0m" 1>&2
  fi
  
  return 0
}

list_files() { #list all files on pi-apps with relative paths - both on main directory and update directory
  if [ ! -d "${DIRECTORY}/update" ];then
    error "${DIRECTORY}/update does not exist. Most likely there is no Internet connection."
  fi
  
  #list all files in update folder
  cd "${DIRECTORY}/update/pi-apps" || error "Failed to enter update directory!"
  local updatefiles="$(find . -type f | cut -c 3- | grep -v '^\.git/\|^apps/\|^data/')"

  #list all files in main folder
  cd "${DIRECTORY}"
  local localfiles="$(find . -type f | cut -c 3- | grep -v '^\.git/\|^apps/\|^update/\|^data/\|^logs/\|^xlunch/')"

  echo -e "${localfiles}\n${updatefiles}" | sort | uniq
  cd $HOME
}

get_updatable_files() { #sets the updatable_files variable
  local updatable_files
  if [ "$speed" == fast ] && [ -f "${DIRECTORY}/data/update-status/updatable-files" ];then
    #speed is set to 'fast' - don't hash anything but rely on past results
    updatable_files="$(cat "${DIRECTORY}/data/update-status/updatable-files")"
    
  else #speed was not set to 'fast', so compare each file to the one in the update folder
    #Use rsync for faster speed, if available
    if command -v rsync >/dev/null ;then
      updatable_files=$(rsync --exclude="/apps/" --exclude="/.git/" --exclude="/data/" -ric --dry-run --out-format="%n" "${DIRECTORY}/update/pi-apps/" "${DIRECTORY}/")
    fi
    if [ "$?" != "0" ] || ! command -v rsync >/dev/null ;then
      #get list of pi-apps files without absolute paths. Example line: 'etc/terminal-run'
      local file_list="$(list_files)" || exit 1

      updatable_files='' #the variable to be returned
      local IFS=$'\n'
      for file in $file_list ;do
        
        echo -en "Scanning files... $file\e[0K\r" 1>&2
        if [ ! -f "${DIRECTORY}/${file}" ];then
          #file is missing locally - add to updatable_files list
          updatable_files+=$'\n'"${file}"
          
        elif [ ! -f "${DIRECTORY}/update/pi-apps/${file}" ];then
          #file is missing in the update folder - local
          true #do not add to updatable_apps list
          
        elif ! files_match "${DIRECTORY}/update/pi-apps/${file}" "${DIRECTORY}/${file}" ;then
          #files don't match - add to updatable_files list
          updatable_files+=$'\n'"${file}"
        fi
      done
      #remove initial newline character
      updatable_files="${updatable_files:1}" 
    fi
  fi
  #If an updatable file is listed in update-exclusion, remove it from list and save notification text for later.
  for file in $(cat "${DIRECTORY}/data/update-exclusion" | grep "^[^#;]") ;do
    updatable_files="$(echo "$updatable_files" | grep -v "$file")"
    local exclusion_msg+="\n'$file' won't be updated - it's listed in data/update-exclusion."
  done
  
  echo -e "Scanning files... Done\e[0K" 1>&2
  echo "$updatable_files"
  
  #if any files were excluded by update-exclusion, list them now, after echoing "Done"
  [ ! -z "$exclusion_msg" ] && echo -e "$exclusion_msg\n" 1>&2
  
  return 0
}

get_updatable_apps() { #return a list of updatable apps
  local updatable_apps
  if [ "$speed" == fast ] && [ -f "${DIRECTORY}/data/update-status/updatable-apps" ];then
    #speed is set to 'fast' - don't hash anything but rely on past results
    updatable_apps="$(cat "${DIRECTORY}/data/update-status/updatable-apps")"
  else #compare all apps to the ones in the update folder

    #Use rsync for faster speed, if available
    if command -v rsync >/dev/null ;then
      # requires english locale to be properly enabled and for diff naming convention not to change
      #diff -rq "${DIRECTORY}/apps" "${DIRECTORY}/update/pi-apps/apps" | sed "\|^Only in ${DIRECTORY}/apps|d" | sed "s;Only in ${DIRECTORY}/update/pi-apps/apps: ;;g" | sed "s;Files ${DIRECTORY}/apps/;;g" | awk -F '/' '{print $1}' | uniq
      # same output, but with rsync which is much simpler. slightly slower
      updatable_apps=$(rsync -ric --dry-run --out-format="%n" "${DIRECTORY}/update/pi-apps/apps/" "${DIRECTORY}/apps/" | awk -F '/' '{print $1}' | sort | uniq; exit ${PIPESTATUS[0]})
    fi

    if [ "$?" != "0" ] || ! command -v rsync >/dev/null; then
      updatable_apps=''
      local IFS=$'\n'
      for app in $(list_apps online)
      do
        echo -en "Scanning apps... $app\e[0K\r" 1>&2

        if [ ! -d "${DIRECTORY}/apps/${app}" ];then
          #if app is missing locally, add to updatable list
          updatable_apps+=$'\n'"${app}"

        elif ! diff -r "${DIRECTORY}/apps/${app}" "${DIRECTORY}/update/pi-apps/apps/${app}" -q >/dev/null ;then
          #if app-folder contents don't match, add to updatable list
          updatable_apps+=$'\n'"${app}"
        fi
      done
      updatable_apps="${updatable_apps:1}" #remove initial newline character
    fi
  fi
  echo -e "Scanning apps... Done\e[0K" 1>&2
  echo "$updatable_apps"
}

list_updates_cli () {  #from https://unix.stackexchange.com/a/673436
    # little helpers for terminal print control and key input
    ESC=$( printf "\033")
    cursor_blink_on()   { printf "${ESC}[?25h"; }
    cursor_blink_off()  { printf "${ESC}[?25l"; }
    cursor_to()         { printf "${ESC}[$1;${2:-1}H"; }
    print_inactive()    { printf "$2   $1 "; }
    print_active()      { printf "$2  ${ESC}[7m $1 ${ESC}[27m"; }
    get_cursor_row()    { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }

    local return_value=$1 &>/dev/null
    local -n options=$2 &>/dev/null
    local -n defaults=$3 &>/dev/null

    local selected=()
    for ((i=0; i<${#options[@]}; i++)); do
      selected+=("true")
      printf "\n"
    done

    # determine current screen position for overwriting the options
    local lastrow=$(get_cursor_row)
    local startrow=$(($lastrow - ${#options[@]}))

    # ensure cursor and input echoing back on upon a ctrl+c during read -s
    trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
    cursor_blink_off

    key_input() {
        local key
        IFS= read -rsn1 key 2>/dev/null >&2
        if [[ $key = ""      ]]; then echo enter; fi;
        if [[ $key = $'\x20' ]]; then echo space; fi;
        if [[ $key = "k" ]]; then echo up; fi;
        if [[ $key = "j" ]]; then echo down; fi;
        if [[ $key = $'\x1b' ]]; then
            read -rsn2 key
            if [[ $key = [A || $key = k ]]; then echo up;    fi;
            if [[ $key = [B || $key = j ]]; then echo down;  fi;
        fi 
    }

    toggle_option() {
        local option=$1
        if [[ ${selected[option]} == true ]]; then
            selected[option]=false
        else
            selected[option]=true
        fi
    }

    print_options() {
        # print options by overwriting the last lines
        local idx=0
        for option in "${options[@]}"; do
            local prefix="[ ]"
            if [[ ${selected[idx]} == true ]]; then
              prefix="[\e[38;5;46m✔\e[0m]"
            fi

            cursor_to $(($startrow + $idx))
            if [ $idx -eq $1 ]; then
                print_active "$option" "$prefix"
            else
                print_inactive "$option" "$prefix"
            fi
            ((idx++))
        done
    }

    local active=0
    while true; do
        print_options $active

        # user key control
        case $(key_input) in
            space)  toggle_option $active;;
            enter)  print_options -1; break;;
            up)     ((active--));
                    if [ $active -lt 0 ]; then active=$((${#options[@]} - 1)); fi;;
            down)   ((active++));
                    if [ $active -ge ${#options[@]} ]; then active=0; fi;;
        esac
    done

    # cursor position back to normal
    cursor_to $lastrow
    printf "\n"
    cursor_blink_on

    eval $return_value='("${selected[@]}")'
}


list_updates_gui() { #input: updatable_apps and updatable_files variables, output: updatable_apps and updatable_files, with refreshable_apps being a subset of updatable_apps
  local IFS=$'\n'
  
  #If updatable_apps and updatable_files variables empty, set them to the results of the last update-check
  if [ -z "$updatable_apps" ] && [ -z "$updatable_files" ];then
    updatable_apps="$(cat "${DIRECTORY}/data/update-status/updatable-apps")"
    updatable_files="$(cat "${DIRECTORY}/data/update-status/updatable-files")"
  fi
  
  local LIST=''
  local app
  local compressed_update=''
  for app in $updatable_apps ;do #generate a yad list for every updatable app
    # get app tooltip
    read -r 2>/dev/null line <"${DIRECTORY}/update/pi-apps/apps/${app}/description" || line="Description unavailable"
    # if ${app} folder did not exist before and install-${arch}, install, or packages file is in the new update, then this is a "new app"
    # results in applications that only have support for a different architecture to show under the compressed update
    if [ ! -d "${DIRECTORY}/apps/${app}" ] && ( [ -f "${DIRECTORY}/update/pi-apps/apps/${app}/install-${arch}" ] || [ -f "${DIRECTORY}/update/pi-apps/apps/${app}/install" ] || [ -f "${DIRECTORY}/update/pi-apps/apps/${app}/packages" ] ); then
      LIST+="TRUE
${DIRECTORY}/update/pi-apps/apps/${app}/icon-24.png
$app (new app)
refresh-app:$app
$line"$'\n'
    # if install-${arch} is in the new update and it was not installable on this system before, then this is a "new app".
    elif [ -f "${DIRECTORY}/update/pi-apps/apps/${app}/install-${arch}" ] && [ ! -f "${DIRECTORY}/apps/${app}/install-${arch}" ] && [ ! -f "${DIRECTORY}/apps/${app}/install" ] && [ ! -f "${DIRECTORY}/apps/${app}/packages" ]; then
      LIST+="TRUE
${DIRECTORY}/update/pi-apps/apps/${app}/icon-24.png
$app (new app)
refresh-app:$app
$line"$'\n'
    # similar to the above. if install script is in the new update and it was not installable on this system before, then this is a "new app".
    elif [ -f "${DIRECTORY}/update/pi-apps/apps/${app}/install" ] && [ ! -f "${DIRECTORY}/apps/${app}/install-${arch}" ] && [ ! -f "${DIRECTORY}/apps/${app}/install" ] && [ ! -f "${DIRECTORY}/apps/${app}/packages" ]; then
      LIST+="TRUE
${DIRECTORY}/update/pi-apps/apps/${app}/icon-24.png
$app (new app)
refresh-app:$app
$line"$'\n'
    # if app will be reinstalled then include it on the list and notify the user that it will be reinstalled
    elif will_reinstall "$app"; then
      LIST+="TRUE
${DIRECTORY}/update/pi-apps/apps/${app}/icon-24.png
$app (<b>new update</b>)
reinstall-app:$app
$line"$'\n'
    # if app has been installed in the past then show it on the list. Useful to notify users that their app, which failed to install in the past, may now work.
    elif [ "$(app_status "${app}")" == 'corrupted' ];then
      LIST+="TRUE
${DIRECTORY}/update/pi-apps/apps/${app}/icon-24.png
$app (Try installing it again!)
refresh-app:$app
This app failed to install last time. Maybe it will work now! After updating, try installing this app again."$'\n'
    else # app is not new or getting reinstalled, add to compressed update list
      compressed_update+=$'\n'"refresh-app:${app}"
    fi
  done
  
  local file
  for file in $updatable_files ;do #generate a yad list for every updatable file
    
    #determine mimetype of updatable_apps file to display an informative icon in the list
    if [ "$(file -b --mime-type "${DIRECTORY}/${file}")" == 'text/x-shellscript' ];then
      #if updatable_apps file in question is a shell script, then display shellscript icon.
      mimeicon="${DIRECTORY}/icons/shellscript.png"
      mimetype='script'
    elif [[ "${DIRECTORY}/${file}" == *.png ]] || [[ "${DIRECTORY}/${file}" == *.svg ]];then
      mimeicon="${DIRECTORY}/icons/image.png"
      mimetype='image'
    else
      #otherwise display txt icon.
      mimeicon="${DIRECTORY}/icons/txt.png"
      mimetype='file'
    fi
    
    LIST+="TRUE
${mimeicon}
$file ($mimetype)
file:$file
"$'\n'
    
  done
  
  # add special entry to list for compressed app updates
  if [ ! -z "$compressed_update" ];then
    # display all apps to be updated if less than 11 in number (-lt 12 lines due to the trailing newline)
    if [[ $(wc -l <<< "$compressed_update") -lt 12 ]]; then
    LIST+="TRUE
${DIRECTORY}/icons/categories/All Apps.png
Inactive updates
compressed_apps
Refreshes the Pi-Apps details for these apps$(echo "${compressed_update}" | tr -d '\n' | sed 's/refresh-app:/, /g' | sed 's/^,/:/g')"$'\n'
    else
      LIST+="TRUE
${DIRECTORY}/icons/categories/All Apps.png
Inactive updates
compressed_apps
Refreshes the Pi-Apps details for apps that do not need to be reinstalled for this update. (Too many to list here)"$'\n'
    fi
  fi
  
  # remove trailing newline
  LIST="${LIST%$'\n'}"
  
  if [ -z "$LIST" ];then
    status_green "Nothing to update. Nothing to do!"
    exit 0
  fi
  
  #Display a list of everything updatable
  output="$(echo "$LIST" | yad --class Pi-Apps --name "Pi-Apps" --center --title='Pi-Apps' \
    --window-icon="${DIRECTORY}/icons/logo.png" --width=310 --height=300 \
    --list --checklist --separator='\n' --print-column=4 --no-headers \
    --text="Updates available:"$'\n'"Uncheck an item to skip updating it." \
    --column=:CHK --column=:IMG --column=Name --column=ID:HD --column=tip:HD --tooltip-column=5 \
    --button='Cancel'!"${DIRECTORY}/icons/exit.png"!"Close without updating anything":1 \
    --button='Update now'!"${DIRECTORY}/icons/download.png":0 \
    "${args[@]}")" || exit 1

  # add compressed_update apps to output
  if echo "$output" | grep -q '^compressed_apps' ;then
    output+=$'\n'"${compressed_update}"
  fi
  
  #remove empty newlines from output
  output="$(echo "$output" | grep .)"
  
  #regenerate list of updatable apps and files, based on what the user selected
  refreshable_apps="$(echo "$output" | grep '^refresh-app:' | sed 's/^refresh-app://g')"
  updatable_apps="$(echo "$output" | grep '^refresh-app:\|^reinstall-app:' | sed 's/^refresh-app://g ; s/^reinstall-app://g')"
  updatable_files="$(echo "$output" | grep '^file:' | sed 's/^file://g')"
  
}

update_app() { #first arg is app name
  local app="$1"
  [ -z "$app" ] && error "update_app(): no app specified!"  
  status "Updating \e[1m${app}\e[0m\e[96m..."
  echo
  
  #Set terminal title
  echo -ne "\e]0;Updating ${app}\a"

  # check if update app folder exists before doing anything
  # it can happen that executing the updater from the pi-apps GUI the update folder is missing
  # if the user has no internet or internet issues the update folder will be removed due to the failed git pull and then wait in the loop for a new git clone
  # the GUI updater executes check_repo with the "fast" option so it skips updating the pi-apps update folder and obtains any previously determined updatable apps and files
  # if we do not check for this then apps will be removed, the older version moved to trash, and then the new version will fail to copy over since it does not exist
  if [ -d "${DIRECTORY}/update/pi-apps/apps/${app}" ]; then
    local installback=no
    if will_reinstall "$app";then
      installback=yes
      status "$app's install script has been updated. Reinstalling $app..."
      #uninstall it
      "${DIRECTORY}/manage" uninstall "$app" update #report to the app uninstall script that this is an uninstall for the purpose of updating by passing "update"
      
      #fix edge case: if app is installed but uninstall script doesn't exist somehow, then pretend app was uninstalled so that the reinstall later will happen noninteractively
      if [ "$(app_status "$app")" == installed ];then
        echo 'uninstalled' > "${DIRECTORY}/data/status/${app}"
      fi
    fi

    no_status=true refresh_app "$app"
    
    failed=false
    if [ "$installback" == 'yes' ];then
      #install the app again
      "${DIRECTORY}/manage" install "$app" update #report to the app install script that this is an install for the purpose of updating by passing "update"
      if [ $? != 0 ]; then
        failed=true
      else
        # click update link only if app is already installed and the update succeeded
        shlink_link "$app" update &
      fi
    fi
  else
    failed=true
  fi
  
  if [ "$failed" == 'true' ]; then
    echo -e "\e[91mFailed to update ${app}.\e[0m"
    return 1
  else
    status_green "${app} was updated successfully."
    return 0
  fi
}

refresh_app() { #first arg is app name
  local app="$1"
  [ -z "$app" ] && error "refresh_app(): no app specified!"  
  [ "$no_status" != true ] && status -n "Refreshing \e[1m${app}\e[0m\e[96m... "
  #Set terminal title
  [ "$no_status" != true ] && echo -ne "\e]0;Refreshing ${app}\a"
  
  rm -rf "${DIRECTORY}/apps/${app}"
  
  #copy new version from update/ to apps/
  cp -rf "${DIRECTORY}/update/pi-apps/apps/${app}" "${DIRECTORY}/apps/${app}"
  if [ "$?" == "0" ]; then
    [ "$no_status" != true ] && status_green Done
    return 0
  else
    [ "$no_status" != true ] && warning "Failed to refresh app '$app'!"
    return 1
  fi
}

update_file() { #first arg is file name
  local file="$1"
  [ -z "$file" ] && error "update_file(): no file specified!"  
  mkdir -p "$(dirname "${DIRECTORY}/${file}")"
  
  #copy new version
  if cp -f "${DIRECTORY}/update/pi-apps/${file}" "${DIRECTORY}/${file}" ;then
    [ "$no_status" != true ] && status_green "${file} file was copied successfully."
    return 0
  else
    [ "$no_status" != true ] && echo -e "\e[91mFailed to copy ${DIRECTORY}/update/pi-apps/${file}! \e[0m"
    return 1
  fi
}

update_git() {
  #delete .git folder, then copy the new one
  rm -rf "${DIRECTORY}/.git" || sudo rm -rf "${DIRECTORY}/.git" || error "Failed to delete old ${DIRECTORY}/.git folder!"
  cp -a "${DIRECTORY}/update/pi-apps/.git" "${DIRECTORY}" || error "Failed to copy new .git folder!"
}

update_now_cli() { #input: updatable_files and updatable_apps variables
  local IFS=$'\n'
  for file in $updatable_files ;do
    update_file "$file"
  done
  
  for app in $updatable_apps ;do
    update_app "$app"
  done
  
  update_git
  
  [ "$no_status" != true ] && status_green "\nPi-Apps updates complete."
  true
}

update_now_gui_apps() { # deprecated function that is only here so old updater scripts can call it.
  # this function exists to avoid having to write commands in terminal-run formatting for update_now_gui
  local failed_apps=""
  local IFS=$'\n'
  for app in $updatable_apps ;do
    update_app "$app" || failed_apps+="$app"$'\n'
  done
  
  #Set terminal title
  echo -ne "\e]0;Updates complete\a"
  
  #load the app list now to reduce launch time
  refresh_app_list &
  
  action=update diagnose_apps "$failed_apps"
}

update_now_gui() { #input: updatable_files and updatable_apps variables
  local IFS=$'\n'
  local queue=''
  
  #contruct a queue to send to terminal_manage_multi.
  if [ ! -z "$updatable_files" ]; then
    queue+="$(echo "$updatable_files" | sed 's/^/update-file /g')"$'\n'
  fi
  
  if [ ! -z "$refreshable_apps" ];then
    queue+="$(echo "$refreshable_apps" | sed 's/^/refresh /g')"$'\n'
  fi
  
  if [ ! -z "$updatable_apps" ];then
    # for future reference, the for loop should be used here as opposed to using a function like list_subract
    # list_subtract requires input to be sorted which would be undesirable as it would change update order compared to what is shown to the user in the update GUI
    for app in $updatable_apps ;do
      # if app is not in refreshable_apps, do a regular update
      if ! echo "${refreshable_apps}" | grep -q "^$app"; then
        queue+="update $app"$'\n'
      fi
    done
  fi
  queue="${queue::-1}" #remove final newline character
  queue="$(tac <<<"$queue")" # reverse order because manage daemon reverses it again; affects alphabetization, not priority
  #echo "update_now_gui: queue is '$queue'"
  
  #update any files with terminal_manage. If terminal-run fails it will update the files in the background. This is a safety measure to update pi-apps scripts if terminal fails to launch.
  if [ "$no_update" != true ];then
    terminal_manage_multi "$queue" &
  else
    echo "$queue"
  fi
}

update_now_background() { #input: updatable_apps and updatable_files variables
  # only safe updates that can be done in the background are performed (app refreshes and file updates)
  local IFS=$'\n'
  
  #If updatable_apps and updatable_files variables empty, set them to the results of the last update-check
  if [ -z "$updatable_apps" ] && [ -z "$updatable_files" ];then
    updatable_apps="$(cat "${DIRECTORY}/data/update-status/updatable-apps")"
    updatable_files="$(cat "${DIRECTORY}/data/update-status/updatable-files")"
  fi
  
  local app
  local background_updatable_apps=''
  for app in $updatable_apps ;do #narrow down safe $updatable_apps entries
    # if ${app} folder did not exist before and install-${arch}, install, or packages file is in the new update, then this is a "new app"
    # results in applications that only have support for a different architecture to show under the compressed update
    if [ ! -d "${DIRECTORY}/apps/${app}" ] && ( [ -f "${DIRECTORY}/update/pi-apps/apps/${app}/install-${arch}" ] || [ -f "${DIRECTORY}/update/pi-apps/apps/${app}/install" ] || [ -f "${DIRECTORY}/update/pi-apps/apps/${app}/packages" ] ); then
      continue
    # if install-${arch} is in the new update and it was not installable on this system before, then this is a "new app".
    elif [ -f "${DIRECTORY}/update/pi-apps/apps/${app}/install-${arch}" ] && [ ! -f "${DIRECTORY}/apps/${app}/install-${arch}" ] && [ ! -f "${DIRECTORY}/apps/${app}/install" ] && [ ! -f "${DIRECTORY}/apps/${app}/packages" ]; then
      continue
    # similar to the above. if install script is in the new update and it was not installable on this system before, then this is a "new app".
    elif [ -f "${DIRECTORY}/update/pi-apps/apps/${app}/install" ] && [ ! -f "${DIRECTORY}/apps/${app}/install-${arch}" ] && [ ! -f "${DIRECTORY}/apps/${app}/install" ] && [ ! -f "${DIRECTORY}/apps/${app}/packages" ]; then
      continue
    # if app will be reinstalled then don't try to reinstall it in the background
    elif will_reinstall "$app"; then
      continue
    # if app failed to install last time, show this app refresh to the user.
    elif [ "$(app_status "${app}")" == 'corrupted' ];then
      continue
    else # app is not new or getting reinstalled, safe to refresh it now
      background_updatable_apps+="${app}"$'\n'
    fi
  done
  
  #remove empty newlines from background_updatable_apps
  background_updatable_apps="$(echo "$background_updatable_apps" | grep .)"
  
  if [ ! -z "$updatable_files" ] || [ ! -z "$background_updatable_apps" ];then
    no_status=true updatable_apps="$background_updatable_apps" update_now_cli
  fi
}

runmode="$1"
speed="$2"

if [ ! -z "$speed" ] && [ "$speed" != 'fast' ];then
  #error "Unknown value for speed: "\""$speed"\"". Allowed value: fast"
  true
  #speed is $2, which may be a yadflag if it's not 'fast'.
else
  shift #$2 is setting speed, so move everything over so $3 starts yad flags
fi
if [ "$runmode" == onboot ];then
  #older pi-apps installations used an autostarted script with the 'onboot' flag.
  runmode=autostarted
elif [ "$runmode" == source ];then
  #this script can be sourced to get functions: source "${DIRECTORY}/updater" source
  return 0
elif [ -z "$runmode" ];then
  #if no runmode was specified, default to gui.
  runmode=gui
fi

#get remaining arguments to pass them to yad
shift;args=("$@")
if [ ! -z "$(echo "${args[@]}")" ];then
  echo "Flags to be passed to yad: ${args[*]}"
fi

#runmode values: autostarted, get-status, set-status, gui, gui-yes, cli, cli-yes

mkdir -p "${DIRECTORY}/data/update-status"

status "\nUpdater mode: $runmode\n"
if [ "$runmode" == autostarted ];then #if update-interval allows, and one app installed, display notification on boot
  
  #check if update interval allows update-checks, otherwise exit
  check_update_interval
  if [ $? != 0 ];then
    status "Won't check for updates today, because the update interval is set to '$(cat "${DIRECTORY}/data/settings/Check for updates")' in Settings."
    exit 0
  fi
  
  #check that at least one app has been installed by the user
  if [ "$(ls "${DIRECTORY}/data/status" | wc -l)" == 0 ];then
    status "No apps have been installed yet, so exiting now."
    exit 0
  fi
  
  #wait until internet works
  iter=1
  while ! command wget --spider https://github.com &>/dev/null ;do
    echo -n "Pi-Apps updater script: no internet connection yet. "
    echo " Waiting 10 seconds..."
    sleep 10
    iter=$(( $iter + 1 ))
    if [[ "$iter" -gt 18 ]]; then
      exit 0
    fi
  done
  
  check_repo
  updatable_apps="$(get_updatable_apps)"
  updatable_files="$(get_updatable_files)"

  #Auto-refresh pi-apps files and apps that are not new and are not going to be reinstalled
  if [ ! -z "$updatable_files" ] || [ ! -z "$updatable_apps" ];then
    #run background updater
    update_now_background
    
    #check what is updatable again
    updatable_apps="$(get_updatable_apps)"
    updatable_files="$(get_updatable_files)"
  fi

  #write to updatable-apps file only if something was changed
  if [ "$updatable_apps" != "$(cat "${DIRECTORY}/data/update-status/updatable-apps" 2>/dev/null)" ];then
    echo "$updatable_apps" | grep . > "${DIRECTORY}/data/update-status/updatable-apps"
  fi
  
  #write to updatable-files file only if something was changed
  if [ "$updatable_files" != "$(cat "${DIRECTORY}/data/update-status/updatable-files" 2>/dev/null)" ];then
    echo "$updatable_files" | grep . > "${DIRECTORY}/data/update-status/updatable-files"
  fi
  
  if [ -z "$updatable_files" ] && [ -z "$updatable_apps" ];then
    status "Nothing is updatable."
    exit 0
  elif [ -z "$updatable_files" ] && [ -z "$(echo "$updatable_apps" | list_intersect "$(list_apps installed)")" ];then
    status "No installed apps are updatable."
    exit 0
  fi
  
  #get dimensions of primary screen
  screen_dimensions="$(xrandr --nograb --current | awk -F 'connected |\\+|\\('  '/ connected.*[0-9]+x[0-9]+\+/ && $2 {printf $2 ", "}' | sed -n -e 's/^.*primary //p' | tr 'x+' ' ' | tr ',+' ' ')"
  if [ -z "$screen_dimensions" ];then
    # if screen_dimensions is empty, this could be a single monitor wayland display which does not have the word "primary" in the output
    # workaround is to get the first output returned for the connected display
    screen_dimensions="$(xrandr --nograb --current | awk -F 'connected |\\+|\\('  '/ connected.*[0-9]+x[0-9]+\+/ && $2 {printf $2 ", "}' | tr 'x+' ' ' | tr ',+' ' ')"
  fi
  screen_width="$(awk '{print $1}' <<<"$screen_dimensions")"
  screen_height="$(awk '{print $2}' <<<"$screen_dimensions")"
  
  status "Displaying notification in lower-right of screen..."
  { #display notification in lower-right
    output="$(yad --class Pi-Apps --name "Pi-Apps" --form --text='Pi-Apps updates available.' --separator='\n' \
      --on-top --skip-taskbar --undecorated --close-on-unfocus \
      --geometry=260+$((screen_width-262))+$((screen_height-150)) \
      --image="${DIRECTORY}/icons/logo-64.png" \
      --field='Never show again':CHK FALSE \
      --button="Details!${DIRECTORY}/icons/info.png":0 --button="Skip!${DIRECTORY}/icons/exit.png":2)"
    button=$?
    
    #if Details not clicked, and checkbox clicked, launch a dialog to change the update interval
    if [ $button != 0 ];then
      if [ "$(echo "$output" | grep . )" == TRUE ];then
        #User checked the 'Never show again' box, so ask to change update interval
        curval="$(cat "${DIRECTORY}/data/settings/Check for updates")"
        [ -z "$curval" ] && curval="$(cat "${DIRECTORY}/etc/setting-params/Check for updates" | grep -v '#' | head -n1)"
        
        params="$(cat "${DIRECTORY}/etc/setting-params/Check for updates" | grep -v '#')"
        params="$(echo "$params" | grep -x "$curval" | tr '\n' '!')!$(echo "$params" | grep -vx "$curval" | tr '\n' '!')"
        params="$(echo -e "$params" | sed 's/!!/!/g' | sed 's/!$//g' | sed 's/^!//g')"
        
        echo "Params: '$params'"
        
        output="$(yad --class Pi-Apps --name "Pi-Apps" --center --title='Change Pi-Apps update interval' --width=440 \
          --form --separator='\n' --window-icon="${DIRECTORY}/icons/logo.png" \
          --text="You just requested for Pi-Apps to <i>never check for updates</i> on boot."$'\n'"Are you sure? If so, change the update interval to "\""<b>Never</b>"\"" below." \
          --field='Update interval: ':CB "$params" \
          --button=Cancel!"${DIRECTORY}/icons/exit.png":1 \
          --button=Save!"${DIRECTORY}/icons/check.png":0)"
        button=$?
        
        output="$(echo "$output" | grep .)"
        if [ $button == 0 ];then #save button clicked
          echo "$output" > "${DIRECTORY}/data/settings/Check for updates"
        fi
      fi
      #since Details was not clicked, exit now
      exit 0
    fi
  }
  
  list_updates_gui
  if [ -z "$updatable_files" ] && [ -z "$updatable_apps" ];then
    status "User did not allow anything to be updated."
    exit 0
  fi
  
  update_now_gui
  
elif [ "$runmode" == 'get-status' ];then #Check if anything was deemed updatable the last time updates were checked for.
  
  if [ -s "${DIRECTORY}/data/update-status/updatable-files" ] || [ -s "${DIRECTORY}/data/update-status/updatable-apps" ];then
    exit 0
  else
    exit 1
  fi
  
elif [ "$runmode" == 'set-status' ];then #check for updates and write updatable apps/files to "${DIRECTORY}/data/update-status"
  check_repo

  #runonce entries
  "${DIRECTORY}/etc/runonce-entries"

  updatable_apps="$(get_updatable_apps)"
  updatable_files="$(get_updatable_files)"
  
  #write to updatable-apps file only if something was changed
  if [ "$updatable_apps" != "$(cat "${DIRECTORY}/data/update-status/updatable-apps")" ];then
    echo "$updatable_apps" | grep . > "${DIRECTORY}/data/update-status/updatable-apps"
  fi
  
  #write to updatable-files file only if something was changed
  if [ "$updatable_files" != "$(cat "${DIRECTORY}/data/update-status/updatable-files")" ];then
    echo "$updatable_files" | grep . > "${DIRECTORY}/data/update-status/updatable-files"
  fi

  # this is the only part of any script that is called after running an update using the new files
  # since the new gui merge requires the gui script to be restarted, use this opportunity to kill the old GUI and start a new one
  # only done once with a runonce if current gui format version is not 2
  runonce <<"EOF"
    if [[ "$GUI_FORMAT_VERSION" != 2 ]]; then
      pkill gui && ( "${DIRECTORY}/gui" & )
      echo "stopped the GUI"
    else
      echo "skipped stopping the gui"
    fi
EOF
  
  "$0" get-status &>/dev/null
  exit $?
  
elif [ "$runmode" == gui ];then #dialog-list of updatable apps, with checkboxes and an Update button
  check_repo
  updatable_apps="$(get_updatable_apps)"
  updatable_files="$(get_updatable_files)"

  
  if [ -z "$updatable_files" ] && [ -z "$updatable_apps" ];then
    status "Nothing is updatable."
    exit 0
  fi
  
  list_updates_gui
  if [ -z "$updatable_files" ] && [ -z "$updatable_apps" ];then
    status "User did not allow anything to be updated."
    echo
    exit 0
  fi
  
  if [ "$use_terminal" == 0 ];then
    update_now_cli
    "${DIRECTORY}/updater" set-status 
  else
    update_now_gui
  fi
  
elif [ "$runmode" == gui-yes ];then #update now without asking for confirmation
  check_repo
  updatable_apps="$(get_updatable_apps)"
  updatable_files="$(get_updatable_files)"
  
  if [ -z "$updatable_files" ] && [ -z "$updatable_apps" ];then
    status "Nothing is updatable."
    exit 0
  fi
  
  update_now_gui
  

elif [ "$runmode" == cli ];then #return list of updatable apps, and ask the user permission to update
  check_repo
  updatable_apps="$(get_updatable_apps)"
  updatable_files="$(get_updatable_files)"
  echo
  
  #if no updatable files and updatebla apps
  if [ -z "$updatable_files" ] && [ -z "$updatable_apps" ];then
    status "Everything is up to date."
    exit 0
  fi
  
  #if no updatable apps
  if [ -z "$updatable_apps" ];then
    status "All apps are up to date."
  else
    IFS=$'\n'
    updatable_apps_options=($(echo -e "$updatable_apps"))
    status "App updates available.\nUncheck an app to skip updating it."
    list_updates_cli result updatable_apps_options 

    i=0
    IFS=$'\n'
    for app in $updatable_apps; do
      if [ "${result[$i]}" == "true" ]; then
        apps_to_update+="$app\n"
      fi
      i="$(($i+1))"
    done

    updatable_apps="$(echo -e "$apps_to_update")"
  fi

  echo

  #if no updatable files
  if [ -z "$updatable_files" ];then
    status "All files are up to date."
  else
    IFS=$'\n'
    updatable_files_options=($(echo -e "$updatable_files"))
    status "File updates available.\nUncheck a file to skip updating it."
    list_updates_cli result updatable_files_options 

    i=0
    IFS=$'\n'
    for file in $updatable_files; do
      if [ "${result[$i]}" == "true" ]; then
        files_to_update+="$file\n"
      fi
      i="$(($i+1))"
    done

    updatable_files="$(echo -e "$files_to_update")"
  fi
  
  echo
  
  if [ ! -z "$updatable_files" ] || [ ! -z "$updatable_apps" ];then
    printf "${ESC}[?25l"
    for i in {6..1}; do 
      echo -ne "Updating in: $i\r (Use Ctrl-C to abort) " && sleep 1
    done
    printf "${ESC}[?25h"
    echo -e "\n"

    update_now_cli
    "${DIRECTORY}/updater" set-status 
  else
    status "User did not allow anything to be updated."
    exit 0
  fi

  
elif [ "$runmode" == cli-yes ];then #update now without asking for confirmation
  check_repo
  updatable_apps="$(get_updatable_apps)"
  updatable_files="$(get_updatable_files)"
  echo 
  
  if [ -z "$updatable_files" ] && [ -z "$updatable_apps" ];then
    status "Nothing is updatable."
    exit 0
  fi
  
  if [ -z "$updatable_apps" ];then
    status "No apps can be updated."
    echo
  else
    status "These apps can be updated:"
    echo
    echo "$updatable_apps" | sed 's/^/  - /g'
    echo
  fi
  
  if [ -z "$updatable_files" ];then
    status "No files can be updated."
    echo
  else
    status "These files can be updated:"
    echo
    echo "$updatable_files" | sed 's/^/  - /g'
    echo
  fi
  
  echo
  update_now_cli
  
else
  error "updater: unknown run mode. Exiting now.\n"
fi

echo 
exit 0
} #prevents errors if script was modified while in use
