#!/usr/bin/env bash
#
# A more capable sbt runner, coincidentally also called sbt.
# Author: Paul Phillips <paulp@typesafe.com>

# todo - make this dynamic
declare -r sbt_release_version=0.11.3
declare -r sbt_snapshot_version=0.13.0-SNAPSHOT

unset sbt_jar sbt_dir sbt_create sbt_snapshot sbt_launch_dir
unset scala_version java_home sbt_explicit_version
unset verbose debug quiet

build_props_sbt () {
  if [[ -f project/build.properties ]]; then
    versionLine=$(grep ^sbt.version project/build.properties)
    versionString=${versionLine##sbt.version=}
    echo "$versionString"
  fi
}

update_build_props_sbt () {
  local ver="$1"
  local old=$(build_props_sbt)

  if [[ $ver == $old ]]; then
    return
  elif [[ -f project/build.properties ]]; then
    perl -pi -e "s/^sbt\.version=.*\$/sbt.version=${ver}/" project/build.properties
    grep -q '^sbt.version=' project/build.properties || echo "sbt.version=${ver}" >> project/build.properties

    echo !!!
    echo !!! Updated file project/build.properties setting sbt.version to: $ver
    echo !!! Previous value was: $old
    echo !!!
  fi
}

sbt_version () {
  if [[ -n $sbt_explicit_version ]]; then
    echo $sbt_explicit_version
  else
    local v=$(build_props_sbt)
    if [[ -n $v ]]; then
      echo $v
    else
      echo $sbt_release_version
    fi
  fi
}

echoerr () {
  echo 1>&2 "$@"
}
vlog () {
  [[ $verbose || $debug ]] && echoerr "$@"
}
dlog () {
  [[ $debug ]] && echoerr "$@"
}

# this seems to cover the bases on OSX, and someone will
# have to tell me about the others.
get_script_path () {
  local path="$1"
  [[ -L "$path" ]] || { echo "$path" ; return; }

  local target=$(readlink "$path")
  if [[ "${target:0:1}" == "/" ]]; then
    echo "$target"
  else
    echo "$(dirname $path)/$target"
  fi
}

# a ham-fisted attempt to move some memory settings in concert
# so they need not be dicked around with individually.
get_mem_opts () {
  local mem=${1:-1536}
  local perm=$(( $mem / 4 ))
  (( $perm > 256 )) || perm=256
  (( $perm < 1024 )) || perm=1024
  local codecache=$(( $perm / 2 ))
  
  echo "-Xms${mem}m -Xmx${mem}m -XX:MaxPermSize=${perm}m -XX:ReservedCodeCacheSize=${codecache}m"
}

die() {
  echo "Aborting: $@"
  exit 1
}

make_url () {
  groupid="$1"
  category="$2"
  version="$3"
  
  echo "http://typesafe.artifactoryonline.com/typesafe/ivy-$category/$groupid/sbt-launch/$version/sbt-launch.jar"
}

declare -r default_jvm_opts="-Dfile.encoding=UTF8"
declare -r default_sbt_opts="-XX:+CMSClassUnloadingEnabled"
declare -r default_sbt_mem=1536
declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy"
declare -r sbt_opts_file=".sbtopts"
declare -r jvm_opts_file=".jvmopts"
declare -r latest_28="2.8.2"
declare -r latest_29="2.9.1"
declare -r latest_210="2.10.0-SNAPSHOT"

declare -r script_path=$(get_script_path "$BASH_SOURCE")
declare -r script_dir="$(dirname $script_path)"
declare -r script_name="$(basename $script_path)"

# some non-read-onlies set with defaults
declare java_cmd=java
declare sbt_launch_dir="$script_dir/.lib"
declare sbt_mem=$default_sbt_mem

# pull -J and -D options to give to java.
declare -a residual_args
declare -a java_args
declare -a scalac_args
declare -a sbt_commands

build_props_scala () {
  if [[ -f project/build.properties ]]; then
    versionLine=$(grep ^build.scala.versions project/build.properties)
    versionString=${versionLine##build.scala.versions=}
    echo ${versionString%% .*}
  fi
}

execRunner () {
  # print the arguments one to a line, quoting any containing spaces
  [[ $verbose || $debug ]] && echo "# Executing command line:" && {
    for arg; do
      if printf "%s\n" "$arg" | grep -q ' '; then
        printf "\"%s\"\n" "$arg"
      else
        printf "%s\n" "$arg"
      fi
    done
    echo ""
  }

  exec "$@"
}

sbt_groupid () {
  case $(sbt_version) in
        0.7.*) echo org.scala-tools.sbt ;;
       0.10.*) echo org.scala-tools.sbt ;;
    0.11.[12]) echo org.scala-tools.sbt ;;
            *) echo org.scala-sbt ;;
  esac
}

sbt_artifactory_list () {
  local version0=$(sbt_version)
  local version=${version0%-SNAPSHOT}
  local url="http://typesafe.artifactoryonline.com/typesafe/ivy-snapshots/$(sbt_groupid)/sbt-launch/"
  dlog "Looking for snapshot list at: $url "
  
  curl -s --list-only "$url" | \
    grep -F $version | \
    perl -e 'print reverse <>' | \
    perl -pe 's#^<a href="([^"/]+).*#$1#;'
}

make_release_url () {
  make_url $(sbt_groupid) releases $(sbt_version)
}

# argument is e.g. 0.13.0-SNAPSHOT
# finds the actual version (with the build id) at artifactory
make_snapshot_url () {
  for ver in $(sbt_artifactory_list); do
    local url=$(make_url $(sbt_groupid) snapshots $ver)
    dlog "Testing $url"
    curl -s --head "$url" >/dev/null
    dlog "curl returned: $?"
    echo "$url"
    return
  done
}

jar_url () {
  case $(sbt_version) in
             0.7.*) echo "http://simple-build-tool.googlecode.com/files/sbt-launch-0.7.7.jar" ;;
        *-SNAPSHOT) make_snapshot_url ;;
                 *) make_release_url ;;
  esac
}

jar_file () {
  echo "$sbt_launch_dir/$1/sbt-launch.jar"
}

download_url () {
  local url="$1"
  local jar="$2"
  
  echo "Downloading sbt launcher $(sbt_version):"
  echo "  From  $url"
  echo "    To  $jar"

  mkdir -p $(dirname "$jar") && {
    if which curl >/dev/null; then
      curl --fail --silent "$url" --output "$jar"
    elif which wget >/dev/null; then
      wget --quiet -O "$jar" "$url"
    fi
  } && [[ -f "$jar" ]]
}

acquire_sbt_jar () {
  sbt_url="$(jar_url)"
  sbt_jar="$(jar_file $(sbt_version))"

  [[ -f "$sbt_jar" ]] || download_url "$sbt_url" "$sbt_jar"
}

usage () {
  cat <<EOM
Usage: $script_name [options]

  -h | -help         print this message
  -v | -verbose      this runner is chattier
  -d | -debug        set sbt log level to Debug
  -q | -quiet        set sbt log level to Error
  -no-colors         disable ANSI color codes
  -sbt-create        start sbt even if current directory contains no sbt project
  -sbt-dir   <path>  path to global settings/plugins directory (default: ~/.sbt/<version>)
  -sbt-boot  <path>  path to shared boot directory (default: ~/.sbt/boot in 0.11 series)
  -ivy       <path>  path to local Ivy repository (default: ~/.ivy2)
  -mem    <integer>  set memory options (default: $sbt_mem, which is
                       $(get_mem_opts $sbt_mem) )
  -no-share          use all local caches; no sharing
  -offline           put sbt in offline mode
  -jvm-debug <port>  Turn on JVM debugging, open at the given port.
  -batch             Disable interactive mode

  # sbt version (default: from project/build.properties if present, else latest release)
  !!! The only way to accomplish this pre-0.12.0 if there is a build.properties file which
  !!! contains an sbt.version property is to update the file on disk.  That's what this does.
  -sbt-version  <version>   use the specified version of sbt 
  -sbt-jar      <path>      use the specified jar as the sbt launcher
  -sbt-snapshot             use a snapshot version of sbt
  -sbt-launch-dir <path>    directory to hold sbt launchers (default: $sbt_launch_dir)

  # scala version (default: as chosen by sbt)
  -28                       use $latest_28
  -29                       use $latest_29
  -210                      use $latest_210
  -scala-home <path>        use the scala build at the specified directory
  -scala-version <version>  use the specified version of scala

  # java version (default: java from PATH, currently $(java -version |& grep version))
  -java-home <path>         alternate JAVA_HOME

  # jvm options and output control
  JAVA_OPTS     environment variable holding jvm args, if unset uses "$default_jvm_opts"
  SBT_OPTS      environment variable holding jvm args, if unset uses "$default_sbt_opts"
  .jvmopts      if file is in sbt root, it is prepended to the args given to the jvm
  .sbtopts      if file is in sbt root, it is prepended to the args given to **sbt**
  -Dkey=val     pass -Dkey=val directly to the jvm
  -J-X          pass option -X directly to the jvm (-J is stripped)
  -S-X          add -X to sbt's scalacOptions (-S is stripped)

In the case of duplicated or conflicting options, the order above
shows precedence: JAVA_OPTS lowest, command line options highest.
EOM
}

addJava () {
  dlog "[addJava] arg = '$1'"
  java_args=( "${java_args[@]}" "$1" )
}
addSbt () {
  dlog "[addSbt] arg = '$1'"
  sbt_commands=( "${sbt_commands[@]}" "$1" )
}
addScalac () {
  dlog "[addScalac] arg = '$1'"
  scalac_args=( "${scalac_args[@]}" "$1" )
}
addResidual () {
  dlog "[residual] arg = '$1'"
  residual_args=( "${residual_args[@]}" "$1" )
}
addResolver () {
  addSbt "set resolvers in ThisBuild += $1"
}
addDebugger () {
  addJava "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1"
}
get_jvm_opts () {
  # echo "${JAVA_OPTS:-$default_jvm_opts}"
  # echo "${SBT_OPTS:-$default_sbt_opts}"

  [[ -f "$jvm_opts_file" ]] && cat "$jvm_opts_file"
}

process_args ()
{
  require_arg () {
    local type="$1"
    local opt="$2"
    local arg="$3"
    
    if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then
      die "$opt requires <$type> argument"
    fi
  }
  while [[ $# -gt 0 ]]; do
    case "$1" in
       -h|-help) usage; exit 1 ;;
    -v|-verbose) verbose=1 && shift ;;
      -d|-debug) debug=1 && shift ;;
      -q|-quiet) quiet=1 && shift ;;

           -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;;
           -mem) require_arg integer "$1" "$2" && sbt_mem="$2" && shift 2 ;;
     -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;;
      -no-share) addJava "$noshare_opts" && shift ;;
      -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;;
       -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;;
     -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;;
       -offline) addSbt "set offline := true" && shift ;;
     -jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;;
         -batch) exec </dev/null && shift ;;

    -sbt-create) sbt_create=true && shift ;;
  -sbt-snapshot) sbt_explicit_version=$sbt_snapshot_version && shift ;;
       -sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;;
   -sbt-version) require_arg version "$1" "$2" && sbt_explicit_version="$2" && shift 2 ;;
-sbt-launch-dir) require_arg path "$1" "$2" && sbt_launch_dir="$2" && shift 2 ;;
 -scala-version) require_arg version "$1" "$2" && addSbt "set scalaVersion := \"$2\"" && shift 2 ;;
    -scala-home) require_arg path "$1" "$2" && addSbt "set scalaHome in ThisBuild := Some(file(\"$2\"))" && shift 2 ;;
     -java-home) require_arg path "$1" "$2" && java_cmd="$2/bin/java" && shift 2 ;;

            -D*) addJava "$1" && shift ;;
            -J*) addJava "${1:2}" && shift ;;
            -S*) addScalac "${1:2}" && shift ;;
            -28) addSbt "++ $latest_28" && shift ;;
            -29) addSbt "++ $latest_29" && shift ;;
           -210) addSbt "++ $latest_210" && shift ;;

              *) addResidual "$1" && shift ;;
    esac
  done
  
  [[ $debug ]] && {
    case $(sbt_version) in
     0.7.*) addSbt "debug" ;; 
         *) addSbt "set logLevel in Global := Level.Debug" ;;
    esac
  }
  [[ $quiet ]] && {
    case $(sbt_version) in
     0.7.*) ;; 
         *) addSbt "set logLevel in Global := Level.Error" ;;
    esac
  }
}

# if .sbtopts exists, prepend its contents to $@ so it can be processed by this runner
[[ -f "$sbt_opts_file" ]] && {
  sbtargs=()
  while IFS= read -r arg; do
    sbtargs=( "${sbtargs[@]}" "$arg" )
  done <"$sbt_opts_file"

  set -- "${sbtargs[@]}" "$@"
}

# process the combined args, then reset "$@" to the residuals
process_args "$@"
set -- "${residual_args[@]}"
argumentCount=$#

# set scalacOptions if we were given any -S opts
[[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[@]}\""

# Update build.properties no disk to set explicit version - sbt gives us no choice
[[ -n "$sbt_explicit_version" ]] && update_build_props_sbt "$sbt_explicit_version"
echo "Detected sbt version $(sbt_version)"

[[ -n "$scala_version" ]] && echo "Overriding scala version to $scala_version"

# no args - alert them there's stuff in here
(( $argumentCount > 0 )) || echo "Starting $script_name: invoke with -help for other options"

# verify this is an sbt dir or -create was given
[[ -f ./build.sbt || -d ./project || -n "$sbt_create" ]] || {
  cat <<EOM
$(pwd) doesn't appear to be an sbt project.
If you want to start sbt anyway, run:
  $0 -sbt-create

EOM
  exit 1
}

# pick up completion if present; todo
[[ -f .sbt_completion.sh ]] && source .sbt_completion.sh

# no jar? download it.
[[ -f "$sbt_jar" ]] || acquire_sbt_jar || {
  # still no jar? uh-oh.
  echo "Download failed. Obtain the jar manually and place it at $sbt_jar"
  exit 1
}

[[ -n "$sbt_dir" ]] || {
  sbt_dir=~/.sbt/$(sbt_version)
  addJava "-Dsbt.global.base=$sbt_dir"
  echo "Using $sbt_dir as sbt dir, -sbt-dir to override."
}

# since sbt 0.7 doesn't understand iflast
(( ${#residual_args[@]} == 0 )) && residual_args=( "shell" )

# run sbt
execRunner "$java_cmd" \
  $(get_mem_opts $sbt_mem) \
  $(get_jvm_opts) \
  ${java_args[@]} \
  -jar "$sbt_jar" \
  "${sbt_commands[@]}" \
  "${residual_args[@]}"
