#!/bin/sh
set -eu

# code-server's automatic install script.
# See https://github.com/cdr/code-server/blob/main/docs/install.md

usage() {
  arg0="$0"
  if [ "$0" = sh ]; then
    arg0="curl -fsSL https://code-server.dev/install.sh | sh -s --"
  else
    not_curl_usage="The latest script is available at https://code-server.dev/install.sh
"
  fi

  cath <<EOF
Installs code-server for Linux, macOS and FreeBSD.
It tries to use the system package manager if possible.
After successful installation it explains how to start using code-server.

Pass in user@host to install code-server on user@host over ssh.
The remote host must have internet access.
${not_curl_usage-}
Usage:

  $arg0 [--dry-run] [--version X.X.X] [--method detect] \
        [--prefix ~/.local] [--rsh ssh] [user@host]

  --dry-run
      Echo the commands for the install process without running them.

  --version X.X.X
      Install a specific version instead of the latest.

  --method [detect | standalone]
      Choose the installation method. Defaults to detect.
      - detect detects the system package manager and tries to use it.
        Full reference on the process is further below.
      - standalone installs a standalone release archive into ~/.local
        Add ~/.local/bin to your \$PATH to use it.

  --prefix <dir>
      Sets the prefix used by standalone release archives. Defaults to ~/.local
      The release is unarchived into ~/.local/lib/code-server-X.X.X
      and the binary symlinked into ~/.local/bin/code-server
      To install system wide pass ---prefix=/usr/local

  --rsh <bin>
      Specifies the remote shell for remote installation. Defaults to ssh.

- For Debian, Ubuntu and Raspbian it will install the latest deb package.
- For Fedora, CentOS, RHEL and openSUSE it will install the latest rpm package.
- For Arch Linux it will install the AUR package.
- For any unrecognized Linux operating system it will install the latest standalone
  release into ~/.local

- For macOS it will install the Homebrew package.
  - If Homebrew is not installed it will install the latest standalone release
    into ~/.local

- For FreeBSD, it will install the npm package with yarn or npm.

- If ran on an architecture with no releases, it will install the
  npm package with yarn or npm.
  - We only have releases for amd64 and arm64 presently.
  - The npm package builds the native modules on postinstall.

It will cache all downloaded assets into ~/.cache/code-server

More installation docs are at https://github.com/cdr/code-server/blob/main/docs/install.md
EOF
}

echo_latest_version() {
  # https://gist.github.com/lukechilds/a83e1d7127b78fef38c2914c4ececc3c#gistcomment-2758860
  version="$(curl -fsSLI -o /dev/null -w "%{url_effective}" https://github.com/cdr/code-server/releases/latest)"
  version="${version#https://github.com/cdr/code-server/releases/tag/}"
  version="${version#v}"
  echo "$version"
}

echo_npm_postinstall() {
  echoh
  cath <<EOF
The npm package has been installed successfully!
Please extend your path to use code-server:
  PATH="$NPM_BIN_DIR:\$PATH"
Please run with:
  code-server
EOF
}

echo_standalone_postinstall() {
  echoh
  cath <<EOF
Standalone release has been installed into $STANDALONE_INSTALL_PREFIX/lib/code-server-$VERSION
Please extend your path to use code-server:
  PATH="$STANDALONE_INSTALL_PREFIX/bin:\$PATH"
Then you can run:
  code-server
EOF
}

echo_systemd_postinstall() {
  echoh
  cath <<EOF
To have systemd start code-server now and restart on boot:
  sudo systemctl enable --now code-server@\$USER
Or, if you don't want/need a background service you can run:
  code-server
EOF
}

main() {
  if [ "${TRACE-}" ]; then
    set -x
  fi

  unset \
    DRY_RUN \
    METHOD \
    STANDALONE_INSTALL_PREFIX \
    VERSION \
    OPTIONAL \
    ALL_FLAGS \
    RSH_ARGS \
    RSH

  ALL_FLAGS=""
  while [ "$#" -gt 0 ]; do
    case "$1" in
    -*)
      ALL_FLAGS="${ALL_FLAGS} $1"
      ;;
    esac

    case "$1" in
    --dry-run)
      DRY_RUN=1
      ;;
    --method)
      METHOD="$(parse_arg "$@")"
      shift
      ;;
    --method=*)
      METHOD="$(parse_arg "$@")"
      ;;
    --prefix)
      STANDALONE_INSTALL_PREFIX="$(parse_arg "$@")"
      shift
      ;;
    --prefix=*)
      STANDALONE_INSTALL_PREFIX="$(parse_arg "$@")"
      ;;
    --version)
      VERSION="$(parse_arg "$@")"
      shift
      ;;
    --version=*)
      VERSION="$(parse_arg "$@")"
      ;;
    --rsh)
      RSH="$(parse_arg "$@")"
      shift
      ;;
    --rsh=*)
      RSH="$(parse_arg "$@")"
      ;;
    -h | --h | -help | --help)
      usage
      exit 0
      ;;
    --)
      shift
      # We remove the -- added above.
      ALL_FLAGS="${ALL_FLAGS% --}"
      RSH_ARGS="$*"
      break
      ;;
    -*)
      echoerr "Unknown flag $1"
      echoerr "Run with --help to see usage."
      exit 1
      ;;
    *)
      RSH_ARGS="$*"
      break
      ;;
    esac

    shift
  done

  if [ "${RSH_ARGS-}" ]; then
    RSH="${RSH-ssh}"
    echoh "Installing remotely with $RSH $RSH_ARGS"
    curl -fsSL https://code-server.dev/install.sh | prefix "$RSH_ARGS" "$RSH" "$RSH_ARGS" sh -s -- "$ALL_FLAGS"
    return
  fi

  VERSION="${VERSION-$(echo_latest_version)}"
  METHOD="${METHOD-detect}"
  if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then
    echoerr "Unknown install method \"$METHOD\""
    echoerr "Run with --help to see usage."
    exit 1
  fi
  STANDALONE_INSTALL_PREFIX="${STANDALONE_INSTALL_PREFIX-$HOME/.local}"

  OS="$(os)"
  if [ ! "$OS" ]; then
    echoerr "Unsupported OS $(uname)."
    exit 1
  fi

  distro_name

  ARCH="$(arch)"
  if [ ! "$ARCH" ]; then
    if [ "$METHOD" = standalone ]; then
      echoerr "No precompiled releases for $(uname -m)."
      echoerr 'Please rerun without the "--method standalone" flag to install from npm.'
      exit 1
    fi
    echoh "No precompiled releases for $(uname -m)."
    install_npm
    return
  fi

  if [ "$OS" = "freebsd" ]; then
    if [ "$METHOD" = standalone ]; then
      echoerr "No precompiled releases available for $OS."
      echoerr 'Please rerun without the "--method standalone" flag to install from npm.'
      exit 1
    fi
    echoh "No precompiled releases available for $OS."
    install_npm
    return
  fi

  CACHE_DIR="$(echo_cache_dir)"

  if [ "$METHOD" = standalone ]; then
    install_standalone
    return
  fi

  case "$(distro)" in
  macos)
    install_macos
    ;;
  debian)
    install_deb
    ;;
  fedora | opensuse)
    install_rpm
    ;;
  arch)
    install_aur
    ;;
  *)
    echoh "Unsupported package manager."
    install_standalone
    ;;
  esac
}

parse_arg() {
  case "$1" in
  *=*)
    # Remove everything after first equal sign.
    opt="${1%%=*}"
    # Remove everything before first equal sign.
    optarg="${1#*=}"
    if [ ! "$optarg" ] && [ ! "${OPTIONAL-}" ]; then
      echoerr "$opt requires an argument"
      echoerr "Run with --help to see usage."
      exit 1
    fi
    echo "$optarg"
    return
    ;;
  esac

  case "${2-}" in
  "" | -*)
    if [ ! "${OPTIONAL-}" ]; then
      echoerr "$1 requires an argument"
      echoerr "Run with --help to see usage."
      exit 1
    fi
    ;;
  *)
    echo "$2"
    return
    ;;
  esac
}

fetch() {
  URL="$1"
  FILE="$2"

  if [ -e "$FILE" ]; then
    echoh "+ Reusing $FILE"
    return
  fi

  sh_c mkdir -p "$CACHE_DIR"
  sh_c curl \
    -#fL \
    -o "$FILE.incomplete" \
    -C - \
    "$URL"
  sh_c mv "$FILE.incomplete" "$FILE"
}

install_macos() {
  if command_exists brew; then
    echoh "Installing from Homebrew."
    echoh

    sh_c brew install code-server

    return
  fi

  echoh "Homebrew not installed."

  install_standalone
}

install_deb() {
  echoh "Installing v$VERSION deb package from GitHub releases."
  echoh

  fetch "https://github.com/cdr/code-server/releases/download/v$VERSION/code-server_${VERSION}_$ARCH.deb" \
    "$CACHE_DIR/code-server_${VERSION}_$ARCH.deb"
  sudo_sh_c dpkg -i "$CACHE_DIR/code-server_${VERSION}_$ARCH.deb"

  echo_systemd_postinstall
}

install_rpm() {
  echoh "Installing v$VERSION rpm package from GitHub releases."
  echoh

  fetch "https://github.com/cdr/code-server/releases/download/v$VERSION/code-server-$VERSION-$ARCH.rpm" \
    "$CACHE_DIR/code-server-$VERSION-$ARCH.rpm"
  sudo_sh_c rpm -i "$CACHE_DIR/code-server-$VERSION-$ARCH.rpm"

  echo_systemd_postinstall
}

install_aur() {
  echoh "Installing from the AUR."
  echoh

  sh_c mkdir -p "$CACHE_DIR/code-server-aur"
  sh_c "curl -#fsSL https://aur.archlinux.org/cgit/aur.git/snapshot/code-server.tar.gz | tar -xzC $CACHE_DIR/code-server-aur --strip-components 1"
  echo "+ cd $CACHE_DIR/code-server-aur"
  if [ ! "${DRY_RUN-}" ]; then
    cd "$CACHE_DIR/code-server-aur"
  fi
  sh_c makepkg -si

  echo_systemd_postinstall
}

install_standalone() {
  echoh "Installing standalone release archive v$VERSION from GitHub releases."
  echoh

  fetch "https://github.com/cdr/code-server/releases/download/v$VERSION/code-server-$VERSION-$OS-$ARCH.tar.gz" \
    "$CACHE_DIR/code-server-$VERSION-$OS-$ARCH.tar.gz"

  sh_c="sh_c"
  if [ ! -w "$STANDALONE_INSTALL_PREFIX" ]; then
    sh_c="sudo_sh_c"
  fi

  if [ -e "$STANDALONE_INSTALL_PREFIX/lib/code-server-$VERSION" ]; then
    echoh
    echoh "code-server-$VERSION is already installed at $STANDALONE_INSTALL_PREFIX/lib/code-server-$VERSION"
    echoh "Remove it to reinstall."
    exit 0
  fi

  "$sh_c" mkdir -p "$STANDALONE_INSTALL_PREFIX/lib" "$STANDALONE_INSTALL_PREFIX/bin"
  "$sh_c" tar -C "$STANDALONE_INSTALL_PREFIX/lib" -xzf "$CACHE_DIR/code-server-$VERSION-$OS-$ARCH.tar.gz"
  "$sh_c" mv -f "$STANDALONE_INSTALL_PREFIX/lib/code-server-$VERSION-$OS-$ARCH" "$STANDALONE_INSTALL_PREFIX/lib/code-server-$VERSION"
  "$sh_c" ln -fs "$STANDALONE_INSTALL_PREFIX/lib/code-server-$VERSION/bin/code-server" "$STANDALONE_INSTALL_PREFIX/bin/code-server"

  echo_standalone_postinstall
}

install_npm() {
  if command_exists yarn; then
    sh_c="sh_c"
    if [ ! -w "$(yarn global bin)" ]; then
      sh_c="sudo_sh_c"
    fi
    echoh "Installing with yarn."
    echoh
    "$sh_c" yarn global add code-server --unsafe-perm
    NPM_BIN_DIR="$(yarn global bin)" echo_npm_postinstall
    return
  elif command_exists npm; then
    sh_c="sh_c"
    if [ ! -w "$(npm config get prefix)" ]; then
      sh_c="sudo_sh_c"
    fi
    echoh "Installing with npm."
    echoh
    "$sh_c" npm install -g code-server --unsafe-perm
    NPM_BIN_DIR="$(npm bin -g)" echo_npm_postinstall
    return
  fi
  echoh
  echoerr "Please install npm or yarn to install code-server!"
  echoerr "You will need at least node v12 and a few C dependencies."
  echoerr "See the docs https://github.com/cdr/code-server/blob/v3.10.2/docs/install.md#yarn-npm"
  exit 1
}

os() {
  case "$(uname)" in
  Linux)
    echo linux
    ;;
  Darwin)
    echo macos
    ;;
  FreeBSD)
    echo freebsd
    ;;
  esac
}

# distro prints the detected operating system including linux distros.
# Also parses ID_LIKE for common distro bases.
#
# Example outputs:
# - macos -> macos
# - freebsd -> freebsd
# - ubuntu, raspbian, debian ... -> debian
# - amzn, centos, rhel, fedora, ... -> fedora
# - opensuse-{leap,tumbleweed} -> opensuse
# - alpine -> alpine
# - arch -> arch
#
# Inspired by https://github.com/docker/docker-install/blob/26ff363bcf3b3f5a00498ac43694bf1c7d9ce16c/install.sh#L111-L120.
distro() {
  if [ "$OS" = "macos" ] || [ "$OS" = "freebsd" ]; then
    echo "$OS"
    return
  fi

  if [ -f /etc/os-release ]; then
    (
      . /etc/os-release
      if [ "${ID_LIKE-}" ]; then
        for id_like in $ID_LIKE; do
          case "$id_like" in debian | fedora | opensuse)
            echo "$id_like"
            return
            ;;
          esac
        done
      fi

      echo "$ID"
    )
    return
  fi
}

# os_name prints a pretty human readable name for the OS/Distro.
distro_name() {
  if [ "$(uname)" = "Darwin" ]; then
    echo "macOS v$(sw_vers -productVersion)"
    return
  fi

  if [ -f /etc/os-release ]; then
    (
      . /etc/os-release
      echo "$PRETTY_NAME"
    )
    return
  fi

  # Prints something like: Linux 4.19.0-9-amd64
  uname -sr
}

arch() {
  case "$(uname -m)" in
  aarch64)
    echo arm64
    ;;
  x86_64)
    echo amd64
    ;;
  amd64) # FreeBSD.
    echo amd64
    ;;
  esac
}

command_exists() {
  command -v "$@" >/dev/null
}

sh_c() {
  echoh "+ $*"
  if [ ! "${DRY_RUN-}" ]; then
    sh -c "$*"
  fi
}

sudo_sh_c() {
  if [ "$(id -u)" = 0 ]; then
    sh_c "$@"
  elif command_exists sudo; then
    sh_c "sudo $*"
  elif command_exists su; then
    sh_c "su - -c '$*'"
  else
    echoh
    echoerr "This script needs to run the following command as root."
    echoerr "  $*"
    echoerr "Please install sudo or su."
    exit 1
  fi
}

echo_cache_dir() {
  if [ "${XDG_CACHE_HOME-}" ]; then
    echo "$XDG_CACHE_HOME/code-server"
  elif [ "${HOME-}" ]; then
    echo "$HOME/.cache/code-server"
  else
    echo "/tmp/code-server-cache"
  fi
}

echoh() {
  echo "$@" | humanpath
}

cath() {
  humanpath
}

echoerr() {
  echoh "$@" >&2
}

# humanpath replaces all occurrences of " $HOME" with " ~"
# and all occurrences of '"$HOME' with the literal '"$HOME'.
humanpath() {
  sed "s# $HOME# ~#g; s#\"$HOME#\"\$HOME#g"
}

# We need to make sure we exit with a non zero exit if the command fails.
# /bin/sh does not support -o pipefail unfortunately.
prefix() {
  PREFIX="$1"
  shift
  fifo="$(mktemp -d)/fifo"
  mkfifo "$fifo"
  sed -e "s#^#$PREFIX: #" "$fifo" &
  "$@" >"$fifo" 2>&1
}

main "$@"