#!/usr/bin/env sh

set -eu
printf '\n'

BOLD="$(tput bold 2>/dev/null || printf '')"
GREY="$(tput setaf 0 2>/dev/null || printf '')"
UNDERLINE="$(tput smul 2>/dev/null || printf '')"
RED="$(tput setaf 1 2>/dev/null || printf '')"
GREEN="$(tput setaf 2 2>/dev/null || printf '')"
YELLOW="$(tput setaf 3 2>/dev/null || printf '')"
BLUE="$(tput setaf 4 2>/dev/null || printf '')"
MAGENTA="$(tput setaf 5 2>/dev/null || printf '')"
NO_COLOR="$(tput sgr0 2>/dev/null || printf '')"
PROJECT="rasusa"
GH_USER="mbhall88"

SUPPORTED_TARGETS="x86_64-unknown-linux-musl x86_64-unknown-linux-gnu \
                   aarch64-unknown-linux-musl aarch64-unknown-linux-gnu \
                   aarch64-apple-darwin x86_64-apple-darwin"

info() {
  printf '%s\n' "${BOLD}${GREY}>${NO_COLOR} $*"
}

warn() {
  printf '%s\n' "${YELLOW}! $*${NO_COLOR}"
}

error() {
  printf '%s\n' "${RED}x $*${NO_COLOR}" >&2
}

completed() {
  printf '%s\n' "${GREEN}✓${NO_COLOR} $*"
}

has() {
  command -v "$1" 1>/dev/null 2>&1
}

# Make sure user is not using zsh or non-POSIX-mode bash, which can cause issues
verify_shell_is_posix_or_exit() {
  if [ -n "${ZSH_VERSION+x}" ]; then
    error "Running installation script with \`zsh\` is known to cause errors."
    error "Please use \`sh\` instead."
    exit 1
  elif [ -n "${BASH_VERSION+x}" ] && [ -z "${POSIXLY_CORRECT+x}" ]; then
    error "Running installation script with non-POSIX \`bash\` may cause errors."
    error "Please use \`sh\` instead."
    exit 1
  else
    true # No-op: no issues detected
  fi
}

# Gets path to a temporary file
get_tmpfile() {
  suffix="$1"
  if has mktemp; then
    mktemp -t "${PROJECT}.XXXXXX.${suffix}"
  else
    # Fallback to a PID-based filename if mktemp is unavailable
    printf "/tmp/%s.%s.%s" "${PROJECT}" "$$" "${suffix}"
  fi
}

# Makes a temporary directory
get_tmpdir() {
  if has mktemp; then
    mktemp -d -t "${PROJECT}.XXXXXX"
  else
    dir="/tmp/${PROJECT}.$$"
    mkdir -p "$dir"
    printf "%s" "$dir"
  fi
}

# Gets the latest github release tag (version)
get_version_from_github() {
  tmp_json=$(get_tmpfile "json")
  
  # We always need the version to construct URLs, even in dry-run mode
  if has curl; then
    curl --fail --silent --location --output "$tmp_json" "https://api.github.com/repos/${GH_USER}/${PROJECT}/releases/latest"
  elif has wget; then
    wget --quiet --output-document="$tmp_json" "https://api.github.com/repos/${GH_USER}/${PROJECT}/releases/latest"
  elif has fetch; then
    fetch --quiet --output="$tmp_json" "https://api.github.com/repos/${GH_USER}/${PROJECT}/releases/latest"
  else
    error "No HTTP download program (curl, wget, fetch) found, exiting…"
    exit 1
  fi
  
  ver=$(grep '"tag_name":' "$tmp_json" | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
  rm -f "$tmp_json"
  
  if [ -z "$ver" ]; then
    error "Could not parse version from GitHub API response"
    exit 1
  fi
  echo "$ver"
}

# Test if a location is writeable by trying to write to it. Windows does not let
# you test writeability other than by writing: https://stackoverflow.com/q/1999988
test_writeable() {
  path="${1:-}"
  if [ -d "$path" ] && touch "${path}/.rasusa_test" 2>/dev/null; then
    rm "${path}/.rasusa_test"
    return 0
  else
    return 1
  fi
}

download() {
  file="$1"
  url="$2"

  if has curl; then
    cmd="curl --fail --silent --location --output $file $url"
  elif has wget; then
    cmd="wget --quiet --output-document=$file $url"
  elif has fetch; then
    cmd="fetch --quiet --output=$file $url"
  else
    error "No HTTP download program (curl, wget, fetch) found, exiting…"
    return 1
  fi

  if [ -n "${DRY_RUN-}" ]; then
    info "[Dry-run] Would run: $cmd"
    return 0
  fi

  $cmd && return 0 || rc=$?

  error "Command failed (exit code $rc): ${BLUE}${cmd}${NO_COLOR}"
  printf "\n" >&2
  info "This is likely due to $PROJECT not yet supporting your configuration."
  info "If you would like to see a build for your configuration,"
  info "please create an issue requesting a build for ${MAGENTA}${TARGET}${NO_COLOR}:"
  info "${BOLD}${UNDERLINE}https://github.com/${GH_USER}/${PROJECT}/issues/new/${NO_COLOR}"
  return $rc
}

unpack() {
  archive=$1
  bin_dir=$2
  temp_dir=$(get_tmpdir)
  sudo=${3-}

  if [ -n "${DRY_RUN-}" ]; then
    info "[Dry-run] Would unpack $archive to $bin_dir using $sudo"
    return 0
  fi

  case "$archive" in
  *.tar.gz)
    flags=$(test -n "${VERBOSE-}" && echo "-xzvof" || echo "-xzof")
    ${sudo} tar "${flags}" "${archive}" -C "${temp_dir}" --strip-components=1
    ${sudo} mv "${temp_dir}/${PROJECT}" "${bin_dir}/${PROJECT}"
    rm -rf "${temp_dir}"
    return 0
    ;;
  *.zip)
    flags=$(test -z "${VERBOSE-}" && echo "-qqo" || echo "-o")
    UNZIP="${flags}" ${sudo} unzip "${archive}" -d "${temp_dir}"
    # Zip files might have a different internal structure, but usually it's project-version/rasusa
    ${sudo} find "${temp_dir}" -name "${PROJECT}" -type f -exec mv {} "${bin_dir}/${PROJECT}" \;
    rm -rf "${temp_dir}"
    return 0
    ;;
  esac

  error "Unknown package extension."
  printf "\n"
  info "This almost certainly results from a bug in this script--please file a"
  info "bug report at https://github.com/${GH_USER}/${PROJECT}/issues"
  return 1
}

usage() {
  printf "%s\n" \
    "install.sh [option]" \
    "" \
    "Fetch and install the latest version of $PROJECT, if $PROJECT is already" \
    "installed it will be updated to the latest version."

  printf "\n%s\n" "Options"
  printf "\t%s\n\t\t%s\n\n" \
    "-V, --verbose" "Enable verbose output for the installer" \
    "-d, --dry-run" "Display the actions that would be taken without performing them" \
    "-f, -y, --force, --yes" "Skip the confirmation prompt during installation" \
    "-p, --platform" "Override the platform identified by the installer [default: ${PLATFORM}]" \
    "-b, --bin-dir" "Override the bin installation directory [default: ${BIN_DIR}]" \
    "-a, --arch" "Override the architecture identified by the installer [default: ${ARCH}]" \
    "-B, --base-url" "Override the base URL used for downloading releases [default: ${BASE_URL}]" \
    "-h, --help" "Display this help message"
}

elevate_priv() {
  if ! has sudo; then
    error 'Could not find the command "sudo", needed to get permissions for install.'
    info "If you are on Windows, please run your shell as an administrator, then"
    info "rerun this script. Otherwise, please run this script as root, or install"
    info "sudo."
    exit 1
  fi
  
  if [ -n "${DRY_RUN-}" ]; then
    info "[Dry-run] Would elevate privileges using sudo"
    return 0
  fi

  if ! sudo -v; then
    error "Superuser not granted, aborting installation"
    exit 1
  fi
}

verify_checksum() {
  archive=$1
  url=$2
  
  if [ -n "${DRY_RUN-}" ]; then
    info "[Dry-run] Would verify checksum for $archive using ${url}.sha256"
    return 0
  fi

  checksum_file=$(get_tmpfile "sha256")
  if ! download "$checksum_file" "${url}.sha256"; then
    warn "Could not download checksum file. Skipping verification."
    rm -f "$checksum_file"
    return 0
  fi

  info "Verifying checksum…"
  if has sha256sum; then
    # sha256sum expects "hash filename" format, which is what we have in the file
    (cd "$(dirname "$archive")" && sha256sum -c "$checksum_file") >/dev/null 2>&1
  elif has shasum; then
    (cd "$(dirname "$archive")" && shasum -a 256 -c "$checksum_file") >/dev/null 2>&1
  elif has openssl; then
    expected_hash=$(awk '{print $1}' "$checksum_file")
    actual_hash=$(openssl dgst -sha256 "$archive" | awk '{print $NF}')
    if [ "$expected_hash" != "$actual_hash" ]; then
      error "Checksum verification failed!"
      exit 1
    fi
  else
    warn "No checksum verification tool (sha256sum, shasum, openssl) found. Skipping verification."
    rm -f "$checksum_file"
    return 0
  fi

  if [ $? -ne 0 ]; then
    error "Checksum verification failed! The downloaded file may be corrupted or tampered with."
    rm -f "$checksum_file"
    exit 1
  fi

  completed "Checksum verified"
  rm -f "$checksum_file"
}

install() {
  ext="$1"
  url="$2"

  if test_writeable "${BIN_DIR}"; then
    sudo=""
    msg="Installing $PROJECT, please wait…"
  else
    warn "Escalated permissions are required to install to ${BIN_DIR}"
    elevate_priv
    sudo="sudo"
    msg="Installing $PROJECT as root, please wait…"
  fi
  info "$msg"

  archive=$(get_tmpfile "$ext")

  # download to the temp file
  if ! download "${archive}" "${url}"; then
    rm -f "${archive}"
    exit 1
  fi

  # verify checksum
  verify_checksum "${archive}" "${url}"

  # unpack the temp file to the bin dir, using sudo if required
  if ! unpack "${archive}" "${BIN_DIR}" "${sudo}"; then
    rm -f "${archive}"
    exit 1
  fi
  
  rm -f "${archive}"
}

# Currently supporting:
#   - win (Git Bash)
#   - darwin
#   - linux
#   - linux_musl (Alpine)
#   - freebsd
detect_platform() {
  platform="$(uname -s | tr '[:upper:]' '[:lower:]')"

  case "${platform}" in
  msys_nt*) platform="pc-windows-msvc" ;;
  cygwin_nt*) platform="pc-windows-msvc" ;;
  # mingw is Git-Bash
  mingw*) platform="pc-windows-msvc" ;;
  # use the statically compiled musl bins on linux to avoid linking issues.
  linux) platform="unknown-linux-musl" ;;
  darwin) platform="apple-darwin" ;;
  freebsd) platform="unknown-freebsd" ;;
  esac

  printf '%s' "${platform}"
}

# Currently supporting:
#   - x86_64
#   - i386
#   - arm
#   - arm64
detect_arch() {
  arch="$(uname -m | tr '[:upper:]' '[:lower:]')"

  case "${arch}" in
  amd64) arch="x86_64" ;;
  armv*) arch="arm" ;;
  arm64) arch="aarch64" ;;
  esac

  # `uname -m` in some cases mis-reports 32-bit OS as 64-bit, so double check
  if [ "${arch}" = "x86_64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then
    arch=i686
  elif [ "${arch}" = "aarch64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then
    arch=arm
  fi

  printf '%s' "${arch}"
}

detect_target() {
  arch="$1"
  platform="$2"
  target="$arch-$platform"

  if [ "${target}" = "arm-unknown-linux-musl" ]; then
    target="${target}eabihf"
  fi

  printf '%s' "${target}"
}

confirm() {
  if [ -z "${FORCE-}" ]; then
    printf "%s " "${MAGENTA}?${NO_COLOR} $* ${BOLD}[y/N]${NO_COLOR}"
    set +e
    read -r yn </dev/tty
    rc=$?
    set -e
    if [ $rc -ne 0 ]; then
      error "Error reading from prompt (please re-run with the '--yes' option)"
      exit 1
    fi
    if [ "$yn" != "y" ] && [ "$yn" != "yes" ]; then
      error 'Aborting (please answer "yes" to continue)'
      exit 1
    fi
  fi
}

check_bin_dir() {
  bin_dir="${1%/}"

  if [ ! -d "$BIN_DIR" ]; then
    error "Installation location $BIN_DIR does not appear to be a directory"
    info "Make sure the location exists and is a directory, then try again."
    usage
    exit 1
  fi

  # https://stackoverflow.com/a/11655875
  good=$(
    IFS=:
    for path in $PATH; do
      if [ "${path%/}" = "${bin_dir}" ]; then
        printf 1
        break
      fi
    done
  )

  if [ "${good}" != "1" ]; then
    warn "Bin directory ${bin_dir} is not in your \$PATH"
  fi
}

is_build_available() {
  arch="$1"
  platform="$2"
  target="$3"

  good=$(
    IFS=" "
    for t in $SUPPORTED_TARGETS; do
      if [ "${t}" = "${target}" ]; then
        printf 1
        break
      fi
    done
  )

  if [ "${good}" != "1" ]; then
    error "${arch} builds for ${platform} are not yet available for $PROJECT"
    printf "\n" >&2
    info "If you would like to see a build for your configuration,"
    info "please create an issue requesting a build for ${MAGENTA}${target}${NO_COLOR}:"
    info "${BOLD}${UNDERLINE}https://github.com/${GH_USER}/${PROJECT}/issues/new/${NO_COLOR}"
    printf "\n"
    exit 1
  fi
}

# defaults
if [ -z "${PLATFORM-}" ]; then
  PLATFORM="$(detect_platform)"
fi

if [ -z "${BIN_DIR-}" ]; then
  BIN_DIR=/usr/local/bin
fi

if [ -z "${ARCH-}" ]; then
  ARCH="$(detect_arch)"
fi

if [ -z "${BASE_URL-}" ]; then
  BASE_URL="https://github.com/${GH_USER}/${PROJECT}/releases"
fi

# Non-POSIX shells can break once executing code due to semantic differences
verify_shell_is_posix_or_exit

# parse argv variables
while [ "$#" -gt 0 ]; do
  case "$1" in
  -p | --platform)
    PLATFORM="$2"
    shift 2
    ;;
  -b | --bin-dir)
    BIN_DIR="$2"
    shift 2
    ;;
  -a | --arch)
    ARCH="$2"
    shift 2
    ;;
  -B | --base-url)
    BASE_URL="$2"
    shift 2
    ;;

  -V | --verbose)
    VERBOSE=1
    shift 1
    ;;
  -d | --dry-run)
    DRY_RUN=1
    shift 1
    ;;
  -f | -y | --force | --yes)
    FORCE=1
    shift 1
    ;;
  -h | --help)
    usage
    exit
    ;;

  -p=* | --platform=*)
    PLATFORM="${1#*=}"
    shift 1
    ;;
  -b=* | --bin-dir=*)
    BIN_DIR="${1#*=}"
    shift 1
    ;;
  -a=* | --arch=*)
    ARCH="${1#*=}"
    shift 1
    ;;
  -B=* | --base-url=*)
    BASE_URL="${1#*=}"
    shift 1
    ;;
  -V=* | --verbose=*)
    VERBOSE="${1#*=}"
    shift 1
    ;;
  -d=* | --dry-run=*)
    DRY_RUN="${1#*=}"
    shift 1
    ;;
  -f=* | -y=* | --force=* | --yes=*)
    FORCE="${1#*=}"
    shift 1
    ;;

  *)
    error "Unknown option: $1"
    usage
    exit 1
    ;;
  esac
done

TARGET="$(detect_target "${ARCH}" "${PLATFORM}")"

is_build_available "${ARCH}" "${PLATFORM}" "${TARGET}"

printf "  %s\n" "${UNDERLINE}Configuration${NO_COLOR}"
info "${BOLD}Bin directory${NO_COLOR}: ${GREEN}${BIN_DIR}${NO_COLOR}"
info "${BOLD}Platform${NO_COLOR}:      ${GREEN}${PLATFORM}${NO_COLOR}"
info "${BOLD}Arch${NO_COLOR}:          ${GREEN}${ARCH}${NO_COLOR}"

# non-empty VERBOSE enables verbose untarring
if [ -n "${VERBOSE-}" ]; then
  VERBOSE=v
  info "${BOLD}Verbose${NO_COLOR}: yes"
else
  VERBOSE=
fi

printf '\n'

EXT=tar.gz
if [ "${PLATFORM}" = "pc-windows-msvc" ]; then
  EXT=zip
fi

VERSION=$(get_version_from_github)
URL="${BASE_URL}/latest/download/${PROJECT}-${VERSION}-${TARGET}.${EXT}"
info "Tarball URL: ${UNDERLINE}${BLUE}${URL}${NO_COLOR}"
confirm "Install $PROJECT ${GREEN}v${VERSION} (latest)${NO_COLOR} to ${BOLD}${GREEN}${BIN_DIR}${NO_COLOR}?"
check_bin_dir "${BIN_DIR}"

install "${EXT}" "$URL"
completed "$PROJECT (v${VERSION}) installed"
