#!/usr/bin/env bash
# Ninja Gaiden II launcher: game files, writable settings/logs/saves, then
# the game. Extra arguments go to the game (ReXGlue options).
set -euo pipefail

here=$(cd "$(dirname "$0")" && pwd)
config_dir=${XDG_CONFIG_HOME:-$HOME/.config}/ng2-recomp
data_dir=${XDG_DATA_HOME:-$HOME/.local/share}/ng2-recomp
# Portable mode: with portable.txt next to the AppImage, everything (settings,
# saves, the log, a copied game) goes in a userdata folder next to it. The
# first start offers it when that folder can be written.
beside=${APPIMAGE:+$(dirname "$APPIMAGE")}

# Dialogs: kdialog (KDE), else qarma or zenity (other desktops, such as GNOME,
# Cinnamon or Xfce), else questions in the terminal the AppImage was started
# from. ui_* return non-zero when the player cancels.
ui= markup=()
if command -v kdialog >/dev/null; then ui=kdialog
elif command -v qarma >/dev/null; then ui=qarma
elif command -v zenity >/dev/null; then ui=zenity markup=(--no-markup)
elif [ -t 0 ] && [ -t 1 ]; then ui=terminal
fi
ui_message() {  # <title> <text>
  case $ui in
    kdialog) kdialog --title "$1" --msgbox "$2" ;;
    qarma|zenity) "$ui" --info "${markup[@]}" --title "$1" --text "$2" --width 480 2>/dev/null ;;
    terminal) printf '\n%s\n%s\n' "$1" "$2" ;;
    *) return 1 ;;
  esac
}
ui_error() {  # <text>: shown, also on the terminal
  echo "$1" >&2
  case $ui in
    kdialog) kdialog --title "Ninja Gaiden II" --error "$1" || true ;;
    qarma|zenity) "$ui" --error "${markup[@]}" --title "Ninja Gaiden II" --text "$1" --width 480 \
                    2>/dev/null || true ;;
  esac
  return 0
}
ui_yesno() {  # <title> <yes label> <no label> <text>: 0 for yes
  case $ui in
    kdialog) kdialog --title "$1" --yes-label "$2" --no-label "$3" --yesno "$4" 2>/dev/null ;;
    qarma|zenity) "$ui" --question "${markup[@]}" --title "$1" --ok-label "$2" --cancel-label "$3" --text "$4" \
                    --width 480 2>/dev/null ;;
    terminal)
      local answer
      printf '\n%s\n%s\n' "$1" "$4"
      read -r -p "[1] $2  [2] $3: " answer || return 1
      case $answer in 1|y|Y|yes|Yes) return 0 ;; *) return 1 ;; esac ;;
    *) return 1 ;;
  esac
}
ui_path() {  # a typed or dropped path (quoted by the terminal, ~ for home): as a path
  local path=$1
  path=${path%"${path##*[![:space:]]}"}  # trailing spaces
  case $path in
    \'*\') path=${path:1:${#path}-2} ;;
    \"*\") path=${path:1:${#path}-2} ;;
  esac
  case $path in "~"|"~/"*) path="$HOME${path:1}" ;; esac
  [ -n "$path" ] && printf '%s\n' "$path"
}
ui_file() {  # <title> <kdialog filter>: the chosen file's path
  case $ui in
    kdialog) kdialog --title "$1" --getopenfilename "$HOME" "$2" ;;
    qarma|zenity) "$ui" --file-selection --title "$1" --filename "$HOME/" 2>/dev/null ;;
    terminal)
      local path
      read -r -e -p "$1, path: " path || return 1
      ui_path "$path" ;;
    *) return 1 ;;
  esac
}
ui_folder() {  # <title>: the chosen folder's path
  case $ui in
    kdialog) kdialog --title "$1" --getexistingdirectory "$HOME" ;;
    qarma|zenity) "$ui" --file-selection --directory --title "$1" --filename "$HOME/" 2>/dev/null ;;
    terminal)
      local path
      read -r -e -p "$1, folder: " path || return 1
      ui_path "$path" ;;
    *) return 1 ;;
  esac
}
if [ -n "$beside" ] && [ ! -f "$beside/portable.txt" ] && [ ! -f "$config_dir/game_dir" ] &&
   [ ! -f "$config_dir/portable_asked" ] &&
   [ -z "${NG2_GAME_DIR:-}" ] && [ -w "$beside" ] && [ -n "$ui" ]; then
  if ui_yesno "Ninja Gaiden II" "Portable" "Normal" \
      "Keep the settings, saves and the installed game in a userdata folder next to the AppImage ($beside), instead of in your home folder?"; then
    : > "$beside/portable.txt"
  else
    mkdir -p "$config_dir" && : > "$config_dir/portable_asked"  # (Normal: not asked again)
  fi
fi
if [ -n "$beside" ] && [ -f "$beside/portable.txt" ]; then
  config_dir=$beside/userdata
  data_dir=$beside/userdata
fi
mkdir -p "$config_dir" "$data_dir/user" 2>/dev/null || {
  msg="Can't write $data_dir. In portable mode the folder next to the AppImage must be writable (or delete portable.txt there)."
  ui_error "$msg"
  exit 1
}

error() {
  ui_error "$1"
  exit 1
}

# A disc image or package into a folder, with kdialog's progress bar when
# qdbus is there to drive it; 130 if the player cancelled.
extract() {
  local qdbus ref pid percent
  qdbus=$(command -v qdbus6 || command -v qdbus || true)
  if [ "$ui" != kdialog ] || [ -z "$qdbus" ] || ! ref=$(kdialog --title "Ninja Gaiden II" \
      --progressbar "Copying the game from the disc..." 100 2>/dev/null); then
    [ "$ui" = terminal ] && echo "Copying the game from the disc..."
    python3 -I "$here/usr/bin/xdvdfs_extract.py" "$1" "$2" 2>"$data_dir/extract.log"
    return
  fi
  "$qdbus" $ref showCancelButton true >/dev/null 2>&1 || true
  python3 -I "$here/usr/bin/xdvdfs_extract.py" "$1" "$2" --progress \
    >"$data_dir/extract.progress" 2>"$data_dir/extract.log" &
  pid=$!
  while kill -0 $pid 2>/dev/null; do
    sleep 0.5
    percent=$(tail -1 "$data_dir/extract.progress" 2>/dev/null)
    [ -n "$percent" ] && "$qdbus" $ref Set "" value "$percent" >/dev/null 2>&1
    if [ "$("$qdbus" $ref wasCancelled 2>/dev/null)" = true ]; then
      kill $pid 2>/dev/null; wait $pid 2>/dev/null
      "$qdbus" $ref close >/dev/null 2>&1
      rm -f "$data_dir/extract.progress"
      return 130
    fi
  done
  wait $pid; local status=$?
  "$qdbus" $ref close >/dev/null 2>&1 || true
  rm -f "$data_dir/extract.progress"
  return $status
}

# The game files: NG2_GAME_DIR, the remembered folder, or ask: default.xex in
# an extracted copy, or a disc image or Games on Demand package (its header
# file, next to the .data folder), which is extracted into the data folder.
game=${NG2_GAME_DIR:-}
if [ -z "$game" ] && [ -f "$config_dir/game_dir" ]; then
  game=$(cat "$config_dir/game_dir")
  # The copy in the data folder, where the remembered folder is gone (a
  # portable folder moved, a drive mounted elsewhere).
  if [ ! -f "$game/default.xex" ] && [ -f "$data_dir/game/default.xex" ]; then
    game=$data_dir/game
    printf '%s\n' "$game" > "$config_dir/game_dir"
  fi
fi
# An extracted copy next to the AppImage is taken without asking (unless
# NG2_GAME_DIR names another folder).
if [ -z "${NG2_GAME_DIR:-}" ] && { [ -z "$game" ] || [ ! -f "$game/default.xex" ]; } && [ -n "$beside" ]; then
  for dir in "$beside" "$beside/game_files" "$beside/game"; do
    if [ -f "$dir/default.xex" ]; then game=$dir; printf '%s\n' "$game" > "$config_dir/game_dir"; break; fi
  done
fi
if [ -z "$game" ] || [ ! -f "$game/default.xex" ]; then
  [ -n "$ui" ] ||
    error "Put the extracted game (the folder with default.xex) next to the AppImage, set NG2_GAME_DIR to it, or start the AppImage from a terminal to be asked."
  ui_message "Ninja Gaiden II" \
    "Choose your copy of the game: a disc image (.iso), the Games on Demand package (the file in 544307D5/00007000, next to its .data folder), or default.xex in an extracted copy. A disc image or package is copied into $data_dir/game." || exit 0
  choice=$(ui_file "Ninja Gaiden II: game files" \
    "*|Disc image, Games on Demand package or default.xex") || exit 0
  [ -e "$choice" ] || error "There's no $choice."
  name=$(basename "$choice")
  case "${name,,}" in
    default.xex) game=$(dirname "$choice") ;;
    *)
      game=$data_dir/game
      rm -rf "$game.part"
      extract "$choice" "$game.part" || {
        status=$?
        rm -rf "$game.part"
        [ $status -eq 130 ] && exit 0  # cancelled
        error "Could not read the game from that file: $(tail -1 "$data_dir/extract.log")"
      }
      [ -f "$game.part/default.xex" ] || { rm -rf "$game.part"; error "There's no default.xex on that disc."; }
      # The new copy replaces the old one, which is set aside first and only
      # deleted once its downloadable content and title update are moved over.
      # One left by a swap that failed halfway: its content first.
      for keep in dlc default.xexp; do
        if [ ! -e "$game/$keep" ] && [ -e "$game.old/$keep" ]; then mv "$game.old/$keep" "$game.part/$keep"; fi
      done
      rm -rf "$game.old"
      if [ -e "$game" ]; then
        mv "$game" "$game.old" || { rm -rf "$game.part"; error "Can't replace $game."; }
      fi
      mv "$game.part" "$game" || { mv "$game.old" "$game" 2>/dev/null; error "Can't write to $game."; }
      kept=1
      for keep in dlc default.xexp; do
        if [ -e "$game.old/$keep" ]; then mv "$game.old/$keep" "$game/$keep" || kept=; fi
      done
      [ -n "$kept" ] && rm -rf "$game.old"
      ;;
  esac
  [ -f "$game/default.xex" ] || error "No default.xex in $game."
  printf '%s\n' "$game" > "$config_dir/game_dir"
fi

tu3_sha1=e9df578781355d9a25e1c0adba8fbb859333456a  # the World disc's (media ID 69F555CB)
tu2_sha1=22d3be744cc43316e6f93d3d17834a814dd28062  # what earlier builds ran: replaced
# Title Update 3 (default.xexp next to default.xex): ask for the update
# package (or an extracted default.xexp) once and put it in the game folder.
if [ -f "$game/default.xexp" ] && [ "$(sha1sum "$game/default.xexp" | cut -d' ' -f1)" = "$tu2_sha1" ]; then
  mv "$game/default.xexp" "$game/default.xexp.tu2" 2>/dev/null || true
fi
if [ ! -f "$game/default.xexp" ]; then
  [ -n "$ui" ] ||
    error "Title Update 3 is missing: put default.xexp in $game, or start the AppImage from a terminal to be asked."
  ui_message "Ninja Gaiden II: Title Update 3" \
    "This port runs the game with Title Update 3 (for the World release, media ID 69F555CB). Choose the update: its package (TU_1A461UL_000000G000000.00000000000O4) or an extracted default.xexp." || exit 0
  update=$(ui_file "Ninja Gaiden II: Title Update 3" "TU_* default.xexp|Title update or default.xexp") || exit 0
  [ -f "$update" ] || error "There's no $update."
  [ -w "$game" ] || error "Can't write to $game: copy default.xexp there yourself."
  case "$(basename "$update")" in
    *.xexp|*.XEXP) cp "$update" "$game/default.xexp.part" || error "Can't copy to $game." ;;
    *) python3 -I "$here/usr/bin/stfs_extract.py" "$update" default.xexp "$game/default.xexp.part" ||
         error "That file isn't the title update." ;;
  esac
  # Title Update 3's default.xexp for this disc, nothing else (the Japanese
  # release's TU3, another update or game's wouldn't fit this build).
  if [ "$(sha1sum "$game/default.xexp.part" | cut -d' ' -f1)" != "$tu3_sha1" ]; then
    rm -f "$game/default.xexp.part"
    error "That isn't Title Update 3 for this disc (the World release's: TU_1A461UL_000000G000000.00000000000O4)."
  fi
  mv "$game/default.xexp.part" "$game/default.xexp"
fi
[ "$(sha1sum "$game/default.xexp" | cut -d' ' -f1)" = "$tu3_sha1" ] ||
  error "$game/default.xexp isn't Title Update 3 for this disc. Remove it to choose the update again."

# Downloadable content, offered once: the packages in a chosen folder (at any
# depth, as the console keeps them) copied into the game folder's dlc/, which
# the game installs from at start.
if [ ! -f "$config_dir/dlc_asked" ] && [ -n "$ui" ]; then
  touch "$config_dir/dlc_asked"
  if ui_yesno "Ninja Gaiden II: downloadable content" "Add DLC" "Skip" \
      "Add downloadable content? Choose the folder with the packages (for example 544307D5/00000002). You can also put them in the dlc folder inside the game folder later."; then
    from=$(ui_folder "Ninja Gaiden II: downloadable content") || from=
    if [ -n "$from" ]; then
      # One of each name, none from the game's own dlc folder.
      target=$(realpath -m "$game/dlc")
      packages=()
      declare -A seen=()
      while IFS= read -r package; do
        name=$(basename "$package")
        case "$(realpath -m "$package")" in "$target"/*) continue ;; esac
        [ -n "${seen[$name]:-}" ] && continue
        seen[$name]=1
        packages+=("$package")
      done < <(python3 -I "$here/usr/bin/find_dlc.py" "$from")
      if [ ${#packages[@]} -eq 0 ]; then
        ui_error "There's no downloadable content for this game in $from."
      elif ! { mkdir -p "$game/dlc" && cp -f "${packages[@]}" "$game/dlc/"; }; then
        ui_error "Can't copy the downloadable content to $game/dlc."
      fi
    fi
  fi
fi

export NG2_CONFIG=${NG2_CONFIG:-$config_dir/ng2.toml}
exec "$here/usr/bin/ng2" --game_data_root="$game" --user_data_root="$data_dir/user" \
  --log_file="$data_dir/ng2.log" "$@"
