#!/usr/bin/env bash

#
# Setup.
#

VERSION="2.1.12"
N_PREFIX=${N_PREFIX-/usr/local}
BASE_VERSIONS_DIR=$N_PREFIX/n/versions

#
# Log <type> <msg>
#

log() {
  printf "  \033[36m%10s\033[0m : \e[2m%s\e[22m\033[0m\n" $1 $2
}

#
# Exit with the given <msg ...>
#

abort() {
  printf "\n  \033[31mError: $@\033[0m\n\n" && exit 1
}

#
# All Bin(node/io) configurations
#

BINS=("node"
      "io")
MIRROR=(${NODE_MIRROR-https://nodejs.org/dist/}
        ${IO_MIRROR-https://iojs.org/dist/})
BIN_NAME=("node"
          "iojs")
VERSIONS_DIR=($BASE_VERSIONS_DIR/node
              $BASE_VERSIONS_DIR/io)

if [ -n "$PROJECT_NAME" ]; then
  BINS+=($PROJECT_NAME)
  BIN_NAME+=($PROJECT_NAME)
  if [ -z "$PROJECT_URL" ]; then
    abort "Must specify PROJECT_URL when supplying PROJECT_NAME"
  fi
  MIRROR+=(${PROJECT_URL})
  VERSIONS_DIR+=($BASE_VERSIONS_DIR/$PROJECT_NAME)
fi

#
# Ensure we have curl or wget support.
#

CURL_PARAMS=( "-L"
              "-#")

WGET_PARAMS=( "--no-check-certificate"
              "-q"
              "-O-")

if [ -n "$HTTP_USER" ];then
  if [ -z "$HTTP_PASSWORD" ]; then
    abort "Must specify HTTP_PASSWORD when supplying HTTP_USER"
  fi
  CURL_PARAMS+=("-u $HTTP_USER:$HTTP_PASSWORD")
  WGET_PARAMS+=("--http-password=$HTTP_PASSWORD"
                "--http-user=$HTTP_USER")
elif [ -n "$HTTP_PASSWORD" ]; then
  abort "Must specify HTTP_USER when supplying HTTP_PASSWORD"
fi

GET=

# wget support
command -v wget > /dev/null && GET="wget ${WGET_PARAMS[@]}"

command -v curl > /dev/null && GET="curl ${CURL_PARAMS[@]}" && QUIET=false

test -z "$GET" && abort "curl or wget required"

#
# State
#

DEFAULT=0
QUIET=true
ACTIVATE=true
ARCH=

#
# set_default <BIN_NAME>
#


set_default() {
  for (( i=0 ; i<${#BINS[@]} ; i++ )); do
    if test ${BINS[$i]} = $1; then
      DEFAULT=$i
    fi
  done
}

for dir in ${VERSIONS_DIR[@]}; do
  test -d $dir || mkdir -p $dir
done

#
# set_arch <arch> to override $(uname -a)
#

set_arch() {
  if test ! -z $1; then
    ARCH=$1
  else
    abort "missing -a|--arch value"
  fi
}

#
# Functions used when showing versions installed
#

enter_fullscreen() {
  tput smcup
  stty -echo
}

leave_fullscreen() {
  tput rmcup
  stty echo
}

handle_sigint() {
  leave_fullscreen
  S="$?"
  kill 0
  exit $S
}

handle_sigtstp() {
  leave_fullscreen
  kill -s SIGSTOP $$
}

#
# Output usage information.
#

display_help() {
  cat <<-EOF

  Usage: n [options/env] [COMMAND] [args]

  Environments:
    n [COMMAND] [args]            Uses default env (node)
    n io [COMMAND]                Sets env as io
    n project [COMMAND]           Uses custom env-variables to use non-official sources

  Commands:

    n                              Output versions installed
    n latest                       Install or activate the latest node release
    n -a x86 latest                As above but force 32 bit architecture
    n stable                       Install or activate the latest stable node release
    n lts                          Install or activate the latest LTS node release
    n <version>                    Install node <version>
    n use <version> [args ...]     Execute node <version> with [args ...]
    n bin <version>                Output bin path for <version>
    n rm <version ...>             Remove the given version(s)
    n prune                        Remove all versions except the current version
    n --latest                     Output the latest node version available
    n --stable                     Output the latest stable node version available
    n --lts                        Output the latest LTS node version available
    n ls                           Output the versions of node available

  (iojs):
    n io latest                    Install or activate the latest iojs release
    n io -a x86 latest             As above but force 32 bit architecture
    n io <version>                 Install iojs <version>
    n io use <version> [args ...]  Execute iojs <version> with [args ...]
    n io bin <version>             Output bin path for <version>
    n io rm <version ...>          Remove the given version(s)
    n io --latest                  Output the latest iojs version available
    n io ls                        Output the versions of iojs available

  Options:

    -V, --version   Output current version of n
    -h, --help      Display help information
    -q, --quiet     Disable curl output (if available)
    -d, --download  Download only
    -a, --arch      Override system architecture

  Aliases:

    which   bin
    use     as
    list    ls
    -       rm

EOF
}

err_no_installed_print_help() {
  printf "\n  \033[31mError: no installed version\033[0m\n"
  display_help
  exit 1
}

#
# Hide cursor.
#

hide_cursor() {
  printf "\e[?25l"
}

#
# Show cursor.
#

show_cursor() {
  printf "\e[?25h"
}

#
# Output version after selected.
#

next_version_installed() {
  list_versions_installed | grep $selected -A 1 | tail -n 1
}

#
# Output version before selected.
#

prev_version_installed() {
  list_versions_installed | grep $selected -B 1 | head -n 1
}

#
# Output n version.
#

display_n_version() {
  echo $VERSION && exit 0
}

#
# Check for installed version, and populate $active
#

check_current_version() {
  command -v node &> /dev/null
  if test $? -eq 0; then
    local current=$(node --version)
    if [ -n "$PROJECT_VERSION_CHECK" ]; then
      current=$(node -p "$PROJECT_VERSION_CHECK || process.exit(1)" || node --version)
    fi
    current=${current#v}
    for bin in ${BINS[@]}; do
      if diff &> /dev/null \
        $BASE_VERSIONS_DIR/$bin/$current/bin/node \
        $(which node) ; then
        active=$bin/$current
      fi
    done
  fi
}

#
# Check the operation is supported for io.
#

check_io_supported() {
  test $DEFAULT -eq 1 && abort "$1 not supported for io.js"
}

#
# Display sorted versions directories paths.
#

versions_paths() {
  find $BASE_VERSIONS_DIR -maxdepth 2 -type d \
    | sed 's|'$BASE_VERSIONS_DIR'/||g' \
    | egrep "/[0-9]+\.[0-9]+\.[0-9]+$" \
    | sed 's|/|.|' \
    | sort -k 1,1 -k 2,2n -k 3,3n -k 4,4n -t . \
    | sed 's|\.|/|'
}

#
# Display installed versions with <selected>
#

display_versions_with_selected() {
  selected=$1
  echo
  for version in $(versions_paths); do
    if test "$version" = "$selected"; then
      printf "  \033[36mο\033[0m $version\033[0m\n"
    else
      printf "    \e[2m$version\e[22m\n"
    fi
  done
  echo
}

#
# List installed versions.
#

list_versions_installed() {
  for version in $(versions_paths); do
    echo $version
  done
}

#
# Display current node --version and others installed.
#

display_versions() {
  enter_fullscreen
  check_current_version
  clear
  display_versions_with_selected $active

  trap handle_sigint INT
  trap handle_sigtstp SIGTSTP

  ESCAPE_SEQ=$'\033'
  UP=$'A'
  DOWN=$'B'

  while true; do
    read -rsn 1 key
    case "$key" in
      $ESCAPE_SEQ)
        # Handle ESC sequences followed by other characters, i.e. arrow keys
        read -rsn 1 -t 1 tmp
        if  [[ "$tmp" == "[" ]]; then
          read -rsn 1 -t 1 arrow
          case "$arrow" in
            $UP)
              clear
              display_versions_with_selected $(prev_version_installed)
              ;;
            $DOWN)
              clear
              display_versions_with_selected $(next_version_installed)
              ;;
          esac
        fi
        ;;
      "k")
        clear
        display_versions_with_selected $(prev_version_installed)
        ;;
      "j")
        clear
        display_versions_with_selected $(next_version_installed)
        ;;
      "q")
        clear
        leave_fullscreen
        exit
        ;;
      "")
        # enter key returns empty string
        activate $selected
        leave_fullscreen
        echo $selected
        exit
        ;;
    esac
  done
}

#
# Move up a line and erase.
#

erase_line() {
  printf "\033[1A\033[2K"
}

#
# Check if the HEAD response of <url> is 200.
#
is_ok() {
  if command -v curl > /dev/null; then
    $GET -Is $1 | head -n 1 | grep 200 > /dev/null
  else
    $GET -S --spider 2>&1 $1 | head -n 1 | grep 200 > /dev/null
  fi
}

#
# Check if the OSS(Object Storage Service) mirror is ok.
#
is_oss_ok() {
  if command -v curl > /dev/null; then
    if $GET -Is $1 | head -n 1 | grep 302 > /dev/null; then
      is_oss_ok $GET -Is $1 | grep Location | awk -F ': ' '{print $2}'
    else
      $GET -Is $1 | head -n 1 | grep 200 > /dev/null
    fi
  else
    if $GET -S --spider 2>&1 $1 | head -n 1 | grep 302 > /dev/null; then
      is_oss_ok $GET -S --spider 2>&1 $1 | grep Location | awk -F ': ' '{print $2}'
    else
      $GET -S --spider 2>&1 $1 | head -n 1 | grep 200 > /dev/null
    fi
  fi
}

#
# Determine tarball url for <version>
#

tarball_url() {
  local version=$1
  local uname="$(uname -a)"
  local arch=x86
  local os=

  # from nave(1)
  case "$uname" in
    Linux*) os=linux ;;
    Darwin*) os=darwin ;;
    SunOS*) os=sunos ;;
  esac

  case "$uname" in
    *x86_64*) arch=x64 ;;
    *armv6l*) arch=armv6l ;;
    *armv7l*) arch=armv7l ;;
  esac

  if [ ${arch} = "armv6l" -a ${BIN_NAME[$DEFAULT]} = node ]; then
    local semver=${version//./ }
    local major=$(echo $semver | grep -o -E '[0-9]+' | head -1 | sed -e 's/^0\+//')
    local minor=$(echo $semver | awk '{print $2}' | grep -o -E '[0-9]+' | head -1 | sed -e 's/^0\+//')
    [[ $major -eq "" && $minor -lt 12 ]] && arch=arm-pi
  fi

  [ ! -z $ARCH ] && arch=$ARCH

  echo "${MIRROR[$DEFAULT]}v${version}/${BIN_NAME[$DEFAULT]}-v${version}-${os}-${arch}.tar.gz"

}

#
# Disable PaX mprotect for <binary>
#

disable_pax_mprotect() {
  test -z $1 && abort "binary required"
  local binary=$1

  # try to disable mprotect via XATTR_PAX header
  local PAXCTL=$(PATH="/sbin:/usr/sbin:$PATH" which paxctl-ng 2>&1)
  local PAXCTL_ERROR=1
  if [ -x "$PAXCTL" ]; then
    $PAXCTL -l && $PAXCTL -m "$binary" >/dev/null 2>&1
    PAXCTL_ERROR="$?"
  fi

  # try to disable mprotect via PT_PAX header
  if [ $PAXCTL_ERROR != 0 ]; then
    PAXCTL=$(PATH="/sbin:/usr/sbin:$PATH" which paxctl 2>&1)
    if [ -x "$PAXCTL" ]; then
      $PAXCTL -Cm "$binary" >/dev/null 2>&1
    fi
  fi
}

#
# Activate <version>
#

activate() {
  local version=$1
  check_current_version
  if test "$version" != "$active"; then
    local dir=$BASE_VERSIONS_DIR/$version
    # Remove old npm to avoid potential issues with simple overwrite.
    if test -d "$dir/lib/node_modules/npm"; then
      if test -d "$N_PREFIX/lib/node_modules/npm"; then
        rm -rf "$N_PREFIX/lib/node_modules/npm"
      fi
    fi
    # Copy (lib before bin to avoid error messages on Darwin when cp over dangling link)
    for subdir in lib bin include share; do
      if test -L "$N_PREFIX/$subdir"; then
        find "$dir/$subdir" -mindepth 1 -maxdepth 1 -exec cp -fR "{}" "$N_PREFIX/$subdir" \;
      else
        cp -fR "$dir/$subdir" $N_PREFIX
      fi
    done
    disable_pax_mprotect "$N_PREFIX/bin/node"
  fi
}

#
# Install latest version.
#

install_latest() {
  install $(display_latest_version)
}

#
# Install latest stable version.
#

install_stable() {
  check_io_supported "stable"
  install $(display_latest_stable_version)
}

#
# Install latest LTS version.
#

install_lts() {
  check_io_supported "lts"
  install $(display_latest_lts_version)
}

#
# Install <version>
#

install() {
  local version=${1#v}

  local dots=$(echo $version | sed 's/[^.]*//g')
  if test ${#dots} -lt 2; then
    version=$($GET 2> /dev/null ${MIRROR[DEFAULT]} \
      | egrep "</a>" \
      | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \
      | egrep -v '^0\.[0-7]\.' \
      | egrep -v '^0\.8\.[0-5]$' \
      | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \
      | egrep ^$version \
      | tail -n1)

    test $version || abort "invalid version ${1#v}"
  fi

  local dir=${VERSIONS_DIR[$DEFAULT]}/$version

  if test -d $dir; then
    if [[ ! -e $dir/n.lock ]] ; then
      if $ACTIVATE ; then
        activate ${BINS[$DEFAULT]}/$version
      fi
      exit
    fi
  fi

  echo
  log install ${BINS[$DEFAULT]}-v$version

  local url=$(tarball_url $version)
  is_ok $url || is_oss_ok $url || abort "invalid version $version"

  log mkdir $dir
  mkdir -p $dir
  if [ $? -ne 0 ] ; then
    abort "sudo required"
  else
    touch $dir/n.lock
  fi

  cd $dir

  log fetch $url
  $GET $url | tar -zx --strip-components=1
  [ $QUIET == false ] && erase_line
  rm -f $dir/n.lock

  disable_pax_mprotect bin/node

  if $ACTIVATE ; then
    activate ${BINS[$DEFAULT]}/$version
    log installed $(node --version)
  fi
  echo
}

#
# Set curl to quiet (silent) mode.
#

set_quiet() {
  command -v curl > /dev/null && GET="$GET -s" && QUIET=true
}

#
# Remove <version ...>
#

remove_versions() {
  test -z $1 && abort "version(s) required"
  check_current_version
  while test $# -ne 0; do
    local version=${1#v}
    [ "${BINS[$DEFAULT]}/$version" == "$active" ] && abort "cannot remove currently active version ($active)"
    rm -rf ${VERSIONS_DIR[$DEFAULT]}/$version
    shift
  done
}

#
# Prune non-active versions
#

prune_versions() {
  check_current_version
  for version in $(versions_paths); do
    if [ $version != $active ]
    then
      echo $version
      rm -rf ${BASE_VERSIONS_DIR[$DEFAULT]}/$version
      shift
    fi
  done
}

#
# Output bin path for <version>
#

display_bin_path_for_version() {
  test -z $1 && abort "version required"
  local version=${1#v}

  if [ "$version" = "latest" ]; then
    version=$(display_latest_version)
  fi

  if [ "$version" = "stable" ]; then
    version=$(display_latest_stable_version)
  fi

  if [ "$version" = "lts" ]; then
    version=$(display_latest_lts_version)
  fi

  local bin=${VERSIONS_DIR[$DEFAULT]}/$version/bin/node
  if test -f $bin; then
    printf "$bin \n"
  else
    abort "$1 is not installed"
  fi
}

#
# Execute the given <version> of node with [args ...]
#

execute_with_version() {
  test -z $1 && abort "version required"
  local version=${1#v}

  if [ "$version" = "latest" ]; then
    version=$(display_latest_version)
  fi

  if [ "$version" = "stable" ]; then
    version=$(display_latest_stable_version)
  fi

  if [ "$version" = "lts" ]; then
    version=$(display_latest_lts_version)
  fi

  local bin=${VERSIONS_DIR[$DEFAULT]}/$version/bin/node

  shift # remove version

  if test -f $bin; then
    exec $bin "$@"
  else
    abort "$version is not installed"
  fi
}

#
# Display the latest release version.
#

display_latest_version() {
  $GET 2> /dev/null ${MIRROR[$DEFAULT]} \
    | egrep "</a>" \
    | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \
    | egrep -v '^0\.[0-7]\.' \
    | egrep -v '^0\.8\.[0-5]$' \
    | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \
    | tail -n1
}

#
# Display the latest stable release version.
#

display_latest_stable_version() {
  check_io_supported "--stable"
  $GET 2> /dev/null ${MIRROR[$DEFAULT]} \
    | egrep "</a>" \
    | egrep -o '[0-9]+\.[0-9]*[02468]\.[0-9]+' \
    | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \
    | tail -n1
}

#
# Display the latest lts release version.
#

display_latest_lts_version() {
  check_io_supported "--lts"
  local folder_name=$($GET 2> /dev/null ${MIRROR[$DEFAULT]} \
    | egrep "</a>" \
    | egrep -o 'latest-[a-z]{2,}' \
    | sort \
    | tail -n1)

  $GET 2> /dev/null ${MIRROR[$DEFAULT]}/$folder_name/ \
    | egrep "</a>" \
    | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \
    | head -n1
}

#
# Display the versions available.
#

display_remote_versions() {
  check_current_version
  local versions=""
  versions=$($GET 2> /dev/null ${MIRROR[$DEFAULT]} \
    | egrep "</a>" \
    | egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \
    | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \
    | awk '{ print "  " $1 }')

  echo
  local bin=${BINS[$DEFAULT]}
  for v in $versions; do
    if test "$active" = "$bin/$v"; then
      printf "  \033[36mο\033[0m $v \033[0m\n"
    else
      if test -d $BASE_VERSIONS_DIR/$bin/$v; then
        printf "    $v \033[0m\n"
      else
        printf "    \e[2m$v\e[22m\n"
      fi
    fi
  done
  echo
}

#
# Handle arguments.
#

if test $# -eq 0; then
  test -z "$(versions_paths)" && err_no_installed_print_help
  display_versions
else
  while test $# -ne 0; do
    case $1 in
      -V|--version) display_n_version ;;
      -h|--help|help) display_help; exit ;;
      -q|--quiet) set_quiet ;;
      -d|--download) ACTIVATE=false ;;
      --latest) display_latest_version; exit ;;
      --stable) display_latest_stable_version; exit ;;
      --lts) display_latest_lts_version; exit ;;
      io) set_default $1 ;; # set bin and continue
      project) DEFAULT=2 ;;
      -a|--arch) shift; set_arch $1;; # set arch and continue
      bin|which) display_bin_path_for_version $2; exit ;;
      as|use) shift; execute_with_version $@; exit ;;
      rm|-) shift; remove_versions $@; exit ;;
      prune) prune_versions; exit ;;
      latest) install_latest; exit ;;
      stable) install_stable; exit ;;
      lts) install_lts; exit ;;
      ls|list) display_remote_versions; exit ;;
      *) install $1; exit ;;
    esac
    shift
  done
fi
