#!/usr/bin/env bash

set -ueo pipefail
shopt -s dotglob extglob nullglob

# Constants
VERSION=1.7.0
SOURCE=$(readlink -f "${BASH_SOURCE[0]}")
DISABLE_UPGRADE=

# Options
cmd=status
verbosity=0
scan=(/opt /usr/share /usr/lib64)
flavors=('' canary ptb)
discord=
modules=
bd_repo='https://github.com/rauenzi/BetterDiscordApp'
bd_repo_branch=injector
bd=
copy_bd=
snap=
flatpak=
upgrade_url='https://github.com/bb010g/betterdiscordctl/raw/master/betterdiscordctl'

# Variables
flavor=
core=
xdg_config=${XDG_CONFIG_HOME:-$HOME/.config}
data=${XDG_DATA_HOME:-$HOME/.local/share}/betterdiscordctl
snap_bin=snap
flatpak_bin=flatpak

show_help() {
  cat << EOF
Usage: ${0##*/} [COMMAND] [OPTION...]

Options:
  -V, --version                  Display version info and exit
  -h, --help                     Display this help message and exit
  -v, --verbose                  Increase verbosity
  -s, --scan=DIRECTORIES         Colon-separated list of directories to scan for
                                 a Discord installation
                                 (default '/opt:/usr/share')
  -f, --flavors=FLAVORS          Colon-separated list of Discord flavors
                                 (default ':canary:ptb')
  -d, --discord=DIRECTORY        Use the specified Discord directory
                                 (requires --modules)
  -m, --modules=DIRECTORY        Use the specified Discord modules directory
  -r, --bd-repo=REPOSITORY       Use the specified Git repo for BetterDiscord
      --bd-repo-branch=BRANCH    Use the specified Git branch for BetterDiscord
                                 (default 'stable16')
  -b, --betterdiscord=DIRECTORY  Use the specified BetterDiscord directory
  -c, --copy-bd                  Copy BD directory instead of symlinking
      --snap[=COMMAND]           Use the Snap version of Discord (optionally
                                 using the specified snap command)
      --flatpak[=COMMAND]        Use the Flatpak version of Discord (optionally
                                 using the specified flatpak command)
      --upgrade-url=URL          Custom URL to upgrade betterdiscordctl with

Commands:
  status (default)               Show the current Discord patch state.
  install                        Install BetterDiscord.
  reinstall                      Reinstall BetterDiscord.
  update                         Update BetterDiscord.
  uninstall                      Uninstall BetterDiscord.
  upgrade                        Update betterdiscordctl.
EOF
}

verbose() {
  if (( verbosity >= $1 )); then
    shift
    printf '%s\n' "$1" >&2
  fi
}

die() {
  while [ $# -gt 0 ]; do
    printf '%s\n' "$1" >&2
    shift
  done
  exit 1
}

die_with_help() {
  die "$@" 'Use "--help" for more information.'
}

die_non_empty() {
  die_with_help "ERROR: \"$1\" requires a non-empty option argument."
}

while :; do
  if [[ -z ${1+x} ]]; then break; fi
  case $1 in
    status|install|reinstall|update|uninstall|upgrade)
      cmd=$1
      ;;
    -V|--version)
      printf 'betterdiscordctl %s\n' "$VERSION" >&2
      exit
      ;;
    -h|-\?|--help)
      show_help; exit
      ;;
    -v|--verbose)
      ((++verbosity))
      ;;
    -s|--scan)
      if [[ ${2+x} ]]; then IFS=':' read -ra scan <<< "$2"; shift
      else die_non_empty '--scan'; fi
      ;;
    --scan=?*)
      IFS=':' read -ra scan <<< "${1#*=}"
      ;;
    --scan=)
      die_non_empty '--scan'
      ;;
    -f|--flavors)
      if [[ ${2+x} ]]; then IFS=':' read -ra flavors <<< "$2"; shift
      else die_non_empty '--flavors'; fi
      ;;
    --flavors=?*)
      IFS=':' read -ra flavors <<< "${1#*=}"
      ;;
    --flavors=)
      die_non_empty '--flavors'
      ;;
    -d|--discord)
      if [[ ${2+x} ]]; then discord=$2; shift
      else die_non_empty '--discord'; fi
      ;;
    --discord=?*)
      discord=${1#*=}
      ;;
    --discord=)
      die_non_empty '--discord'
      ;;
    -m|--modules)
      if [[ ${2+x} ]]; then modules=$2; shift
      else die_non_empty '--modules'; fi
      ;;
    --modules=?*)
      modules=${1#*=}
      ;;
    --modules=)
      die_non_empty '--modules'
      ;;
    --bd-repo-branch)
      if [[ ${2+x} ]]; then bd_repo_branch=$2; shift
      else die_non_empty '--bd-repo-branch'; fi
      ;;
    --bd-repo-branch=?*)
      bd_repo_branch=${1#*=}
      ;;
    --bd-repo-branch=)
      die_non_empty '--bd-repo-branch'
      ;;
    -r|--bd-repo)
      if [[ ${2+x} ]]; then bd_repo=$2; shift
      else die_non_empty '--bd-repo'; fi
      ;;
    --bd-repo=?*)
      bd_repo=${1#*=}
      ;;
    --bd-repo=)
      die_non_empty '--bd-repo'
      ;;
    -b|--betterdiscord)
      if [[ ${2+x} ]]; then bd=$2; shift
      else die_non_empty '--betterdiscord'; fi
      ;;
    --betterdiscord=?*)
      bd=${1#*=}
      ;;
    --betterdiscord=)
      die_non_empty '--betterdiscord'
      ;;
    -c|--copy-bd)
      copy_bd=yes
      ;;
    --snap)
      snap=yes
      copy_bd=yes
      ;;
    --snap=?*)
      snap=yes
      copy_bd=yes
      snap_bin=${1#*=}
      ;;
    --snap=)
      die_non_empty '--snap'
      ;;
    --flatpak)
      flatpak=yes
      copy_bd=yes
      ;;
    --flatpak=?*)
      flatpak=yes
      copy_bd=yes
      flatpak_bin=${1#*=}
      ;;
    --flatpak=)
      die_non_empty '--flatpak'
      ;;
    --upgrade-url)
      if [[ ${2+x} ]]; then upgrade_url=$2; shift
      else die_non_empty '--upgrade-url'; fi
      ;;
    --upgrade-url=?*)
      upgrade_url=${1#*=}
      ;;
    --upgrade-url=)
      die_non_empty '--upgrade-url'
      ;;
    --)
      shift
      break
      ;;
    -?*)
      printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
      ;;
    *)
      break
  esac
  shift
done

mkdir -p "$data"
[[ -f $data/bd_map ]] || touch "$data/bd_map"

# Commands

bdc_status() {
  index_mod=no
  linked_dir=no
  linked_repo=no
  if [[ -d $core/injector ]]; then
    if [[ -h $core/injector ]]; then
      linked_dir=$(readlink "$core/injector")
      if pushd "$core/injector" >/dev/null; then
        linked_repo=$(git remote get-url origin 2>/dev/null || printf 'no\n')
        popd >/dev/null
      else
        linked_dir="(broken link) $linked_dir"
      fi
    fi
  fi
  if [[ ! -f $core/index.js ]]; then
    index_mod='(missing) no'
  else
    grep -q 'injector' "$core/index.js" && index_mod=yes
  fi

  printf 'Discord: %s
Modules: %s
Index modified: %s
Linked injector directory: %s
Linked injector repository: %s\n' \
    "$discord" "$modules" "$index_mod" "$linked_dir" "$linked_repo"
}

bdc_install() {
  [[ -d $core/injector ]] && die 'ERROR: Already installed.'

  # Clean up legacy cruft
  if [[ -d $core/core ]]; then
    printf 'Removing legacy core directory...\n' >&2
    rm -rf "$core/core"
  fi

  bd_patch
  bd_injector

  printf 'Installed. (Restart Discord if necessary.)\n' >&2
}

bdc_reinstall() {
  [[ -d $core/injector ]] || die 'Not installed.'

  bdc_kill

  verbose 1 'V: Removing old injector folder.'
  rm -rf "$core/injector"

  bd_patch
  bd_injector

  printf 'Reinstalled.\n' >&2
}

bdc_update() {
  [[ -d $core/injector ]] || die 'Not installed.'

  if ! pushd "$core/injector" >/dev/null; then
    if [[ -h $core/injector ]]; then
      die 'ERROR: BetterDiscord injector symbolic link is broken.'
    else
      die 'ERROR: BetterDiscord injector location is not a directory.'
    fi
  fi
  if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    printf 'Updating Git repository...\n' >&2
    git fetch origin "$bd_repo_branch"
    git reset --hard FETCH_HEAD
  else
    printf 'WARN: No Git repository found.\n' >&2
  fi
  popd >/dev/null
}

bdc_uninstall() {
  [[ -d $core/injector ]] || die 'Not installed.'

  bdc_kill
  bd_unpatch

  # Remove managed BD repo if applicable
  bd_n=$(bd_map_get_dir "$discord" | bd_map_entry_n)
  bd_map_remove "$discord"
  if [[ -z $(bd_map_get_n "$bd_n") ]]; then
    verbose 2 "VV: Removing $data/bd/$bd_n"
    rm -rf "$data/bd/$bd_n"
  fi

  printf 'Uninstalled.\n' >&2
}

bdc_upgrade() {
  if [[ $DISABLE_UPGRADE ]]; then
    die 'ERROR: Upgrading has been disabled.' \
        'If you installed this from a package, its maintainer should keep it up to date.'
  fi

  github_version=$(curl -NLSs "$upgrade_url" | sed -n 's/^VERSION=//p')
  if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    die "ERROR: GitHub couldn't be reached to check the version."
  fi
  verbose 2 "VV: Script location: $SOURCE"
  verbose 2 "VV: Upgrade URL: $upgrade_url"
  verbose 1 "V: Local version: $VERSION"
  verbose 1 "V: GitHub version: $github_version"
  semver_diff=$(Semver::compare "$github_version" "$VERSION")
  if [[ $semver_diff -eq 1 ]]; then
    printf 'Downloading betterdiscordctl...\n' >&2
    if curl -LSso "$SOURCE" "$upgrade_url"; then
      printf 'Successfully updated betterdiscordctl.\n' >&2
    else
      die 'ERROR: Failed to update betterdiscordctl.' \
          'You may want to rerun this command with sudo.'
    fi
  else
    if [[ $semver_diff -eq 0 ]]; then
      printf 'betterdiscordctl is already the latest version (%s).\n' "$VERSION" >&2
    else
      printf 'Local version (%s) is higher than GitHub version (%s).\n' "$VERSION" "$github_version" >&2
    fi
  fi
}

# Implementation functions

bdc_main() {
  if [[ -z $discord ]]; then
    if [[ $snap ]]; then bdc_snap
    elif [[ $flatpak ]]; then bdc_flatpak
    else bdc_scan; fi
  else
    flavor=$flavors
    # --discord and --modules
    [[ -z $modules ]] && die_with_help 'ERROR: "--discord" requires "--modules" to also be set.'
    [[ -d $discord ]] || die 'ERROR: Discord installation not found.'
    [[ -d $modules ]] || die 'ERROR: Discord modules directory not found.'
  fi
  [[ -d $discord ]] || die 'ERROR: Discord installation not found. Try specifying it with "--discord".'
  core=$modules/discord_desktop_core
  [[ -d $core ]] || die "ERROR: Directory 'discord_desktop_core' not found in $(readlink -f "$modules")"
}

bdc_scan() {
  for scandir in "${scan[@]}"; do
    verbose 2 "VV: Scanning $scandir"
    for flavor in "${flavors[@]}"; do
      verbose 2 "VV: Trying flavor '$flavor'"
      shopt -s nocaseglob
      for discord in "$scandir"/discord?(-)"$flavor"; do
        shopt -u nocaseglob
        if [[ -d $discord ]]; then
          verbose 1 "V: Using Discord at $discord"
          discord_config=$xdg_config/discord${flavor,,}
          if [[ ! -d $discord_config ]]; then
            printf 'WARN: Config directory not found for Discord %s (%s, %s).\n' \
              "$flavor" "$discord" "$discord_config" >&2
            continue 2
          fi
          if [[ -z $modules ]]; then
            bdc_find_modules
          else
            # --modules
            [[ -d $modules ]] || die 'ERROR: Discord modules directory not found.'
          fi
          break 3
        fi
      done
    done
  done
}

bdc_find_modules() {
  declare -a all_modules
  all_modules=("$discord_config/"+([0-9]).+([0-9]).+([0-9])/modules)
  ((${#all_modules[@]})) || die 'ERROR: Discord modules directory not found.'
  modules=${all_modules[-1]}
  verbose 1 "V: Found modules in $modules"
}

bdc_snap() {
  # shellcheck disable=SC2016
  # Expansion should happen inside snap's shell.
  snap_location=$("$snap_bin" run --shell discord <<< $'printf -- \'%s\n\' "$SNAP" 1>&3' 3>&1)
  discord=${snap_location:?}/usr/share/discord
  verbose 2 "VV: Checking $discord"
  if [[ -d $discord ]]; then
    verbose 1 "V: Using Discord at $discord"
    # shellcheck disable=SC2016
    # Expansion should happen inside snap's shell.
    xdg_config=$("$snap_bin" run --shell discord <<< $'printf -- \'%s/.config\n\' "$SNAP_USER_DATA" 1>&3' 3>&1)
    discord_config=$xdg_config/discord
    bdc_find_modules
  else
    die 'ERROR: Discord installation not found.'
  fi
}

bdc_flatpak() {
  flatpak_version=$("$flatpak_bin" --version | sed -n 's/Flatpak //p')
  if [[ $(Semver::compare "$flatpak_version" '1.0.0') -eq -1 ]]; then
    die 'ERROR: You are using an unsupported version of Flatpak.' \
        'See https://github.com/bb010g/betterdiscordctl/issues/45'
  fi
  # flatpak sucks and doesn't use stderr for warnings.
  # https://github.com/flatpak/flatpak/blob/13e449b/app/flatpak-main.c#L259-L286
  # This really should be better for directories with newlines, but...
  # We're just going to grab the last line and hope for the best.
  flatpak_location=$("$flatpak_bin" info --show-location com.discordapp.Discord)
  flatpak_location=${flatpak_location##*$'\n'}
  if [[ -d ${flatpak_location:?}/files/discord ]]; then
    discord=$flatpak_location/files/discord
  else
    discord=$flatpak_location/files/extra
  fi
  verbose 2 "VV: Checking $discord"
  if [[ -d $discord ]]; then
    verbose 1 "V: Using Discord at $discord"
    # shellcheck disable=SC2016
    # Expansion should happen inside flatpak's shell.
    flatpak_config=$("$flatpak_bin" run --command=sh com.discordapp.Discord -c $'printf -- \'%s\n\' "$XDG_CONFIG_HOME"')
    discord_config=${flatpak_config:-$HOME/.var/app/com.discordapp.Discord/config}/discord
    if [[ ! -d $discord_config ]]; then
      printf 'WARN: Config directory not found for Discord (%s, %s).\n' "$discord" "$discord_config" >&2
    fi
    bdc_find_modules
  else
    die 'ERROR: Discord installation not found.'
  fi
}

bdc_kill() {
  declare process_name=Discord
  [[ $flavor ]] && process_name+=" $flavor"
  printf 'Killing %s processes...\n' "$process_name" >&2
  pkill -exi -KILL "discord-?$flavor" || printf 'No active processes found.\n' >&2
}

bd_injector() {
  if [[ -z $bd ]]; then
    bd=$data/bd/$(bd_map_add "$discord" "$bd_repo")
    if [[ ! -d $bd ]]; then
      printf 'Cloning %s...\n' "$bd_repo" >&2
      git clone "$bd_repo" -b "$bd_repo_branch" --depth=1 --single-branch "$bd"
    fi
  fi

  if [[ $copy_bd ]]; then
    verbose 1 'V: Copying BetterDiscord injector...'
    cp -r "$bd" "$core/injector"
  else
    verbose 1 'V: Linking BetterDiscord injector...'
    ln -s "$bd" "$core/injector"
  fi
}

bd_patch() {
  if ! grep -q 'injector' "$core/index.js"; then
    verbose 1 'V: Injecting into index.js...'
    sed -i "$core/index.js" \
      -e "1i require('./injector');" \
      -e "s/core'/core.asar'/"
  fi
}

bd_unpatch() {
  verbose 1 'V: Removing BetterDiscord injection...'
  sed -i "$core/index.js" \
    -e '/injector/d' \
    -e "s/core'/core.asar'/"
  rm -rf "$core/injector"
}

bd_map_entry_n() {
  sed 's/^.*\t\t.*\t\(.*\)$/\1/' "$@"
}

bd_map_fresh() {
  verbose 2 'VV: Generating fresh bd_map number...'
  bd_map_entry_n "$data/bd_map" | sort | awk \
    'BEGIN {max=-1} NF != 0 {if ($1>max+1) {exit}; max=$1} END {print max+1}'
}

bd_map_add() {
  entry=$(bd_map_get_repo "$2")
  if [[ $entry ]]; then
    num=$(head -n1 <<< "$entry" | bd_map_entry_n)
  else
    num=$(bd_map_fresh)
  fi
  printf '%s\t\t%s\t%s\n' "$1" "$2" "$num" >> "$data/bd_map"
  printf '%s\n' "$num"
}

bd_map_get_dir() {
  grep -F "$1"$'\t\t' "$data/bd_map"
}

bd_map_get_repo() {
  grep -F $'\t\t'"$1"$'\t' "$data/bd_map"
}

bd_map_get_n() {
  grep $'\t'"$1\$" "$data/bd_map"
}

bd_map_remove() {
  sed -i "$data/bd_map" -e "\\%$1\\t\\t%d"
}

# Included from https://github.com/bb010g/Semver.sh , under the MIT License.

Semver::validate() {
  # shellcheck disable=SC2064
  trap "$(shopt -p extglob)" RETURN
  shopt -s extglob

  declare normal=${1%%[+-]*}
  declare extra=${1:${#normal}}

  declare major=${normal%%.*}
  if [[ $major != +([0-9]) ]]; then echo "Semver::validate: invalid major: $major" >&2; return 1; fi
  normal=${normal:${#major}+1}
  declare minor=${normal%%.*}
  if [[ $minor != +([0-9]) ]]; then echo "Semver::validate: invalid minor: $minor" >&2; return 1; fi
  declare patch=${normal:${#minor}+1}
  if [[ $patch != +([0-9]) ]]; then echo "Semver::validate: invalid patch: $patch" >&2; return 1; fi

  declare -r ident="+([0-9A-Za-z-])"
  declare pre=${extra%%+*}
  declare pre_len=${#pre}
  if [[ $pre_len -gt 0 ]]; then
    pre=${pre#-}
    if [[ $pre != $ident*(.$ident) ]]; then echo "Semver::validate: invalid pre-release: $pre" >&2; return 1; fi
  fi
  declare build=${extra:pre_len}
  if [[ ${#build} -gt 0 ]]; then
    build=${build#+}
    if [[ $build != $ident*(.$ident) ]]; then echo "Semver::validate: invalid build metadata: $build" >&2; return 1; fi
  fi

  if [[ $2 ]]; then
    echo "$2=(${major@Q} ${minor@Q} ${patch@Q} ${pre@Q} ${build@Q})"
  else
    echo "$1"
  fi
}

Semver::compare() {
  declare -a x y
  eval "$(Semver::validate "$1" x)"
  eval "$(Semver::validate "$2" y)"

  declare x_i y_i i
  for i in 0 1 2; do
    x_i=${x[i]}; y_i=${y[i]}
    if [[ $x_i -eq $y_i ]]; then continue; fi
    if [[ $x_i -gt $y_i ]]; then echo 1; return; fi
    if [[ $x_i -lt $y_i ]]; then echo -1; return; fi
  done

  x_i=${x[3]}; y_i=${y[3]}
  if [[ -z $x_i && $y_i ]]; then echo 1; return; fi
  if [[ $x_i && -z $y_i ]]; then echo -1; return; fi

  declare -a x_pre; declare x_len
  declare -a y_pre; declare y_len
  IFS=. read -ra x_pre <<< "$x_i"; x_len=${#x_pre[@]}
  IFS=. read -ra y_pre <<< "$y_i"; y_len=${#y_pre[@]}

  if (( x_len > y_len )); then echo 1; return; fi
  if (( x_len < y_len )); then echo -1; return; fi

  for (( i=0; i < x_len; i++ )); do
    x_i=${x_pre[i]}; y_i=${y_pre[i]}
    if [[ $x_i = "$y_i" ]]; then continue; fi

    declare num_x num_y
    num_x=$([[ $x_i = +([0-9]) ]] && echo "$x_i")
    num_y=$([[ $y_i = +([0-9]) ]] && echo "$y_i")
    if [[ $num_x && $num_y ]]; then
      if [[ $x_i -gt $y_i ]]; then echo 1; return; fi
      if [[ $x_i -lt $y_i ]]; then echo -1; return; fi
    else
      if [[ $num_y ]]; then echo 1; return; fi
      if [[ $num_x ]]; then echo -1; return; fi
      if [[ $x_i > $y_i ]]; then echo 1; return; fi
      if [[ $x_i < $y_i ]]; then echo -1; return; fi
    fi
  done

  echo 0
}

# Run command

case "$cmd" in
  status)
    bdc_main
    bdc_status
    ;;
  install)
    bdc_main
    bdc_install
    ;;
  reinstall)
    bdc_main
    bdc_reinstall
    ;;
  update)
    bdc_main
    bdc_update
    ;;
  uninstall)
    bdc_main
    bdc_uninstall
    ;;
  upgrade)
    bdc_upgrade
    ;;
  *)
    die "ERROR: Unknown command: $cmd"
    ;;
esac