#!/bin/sh
set -eu

# code-server's automatic install script.
# See https://coder.com/docs/code-server/v3.10.2/install

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 or Alpine, 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://coder.com/docs/code-server/v3.10.2/install
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

  if [ "$OS" = "linux" ] && [ "$(distro)" = "alpine" ]; then
    if [ "$METHOD" = standalone ]; then
      echoerr "No precompiled releases available for alpine."
      echoerr 'Please rerun without the "--method standalone" flag to install from npm.'
      exit 1
    fi
    echoh "No precompiled releases available for alpine."
    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://coder.com/docs/code-server/v3.10.2/install#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 "$@"
