#!/usr/bin/env bash

set -e

install_dir=PREFIX
version=1.10.3.849

function join { local d=$1; shift; echo -n "$1"; shift; printf "%s" "${@/#/$d}"; }

# Extract opts
print_classpath=false
describe=false
verbose=false
trace=false
force=false
repro=false
tree=false
pom=false
help=false
prep=false
jvm_opts=()
resolve_aliases=()
classpath_aliases=()
repl_aliases=()
mode="repl"
while [ $# -gt 0 ]
do
  case "$1" in
    -version)
      >&2 echo "Clojure CLI version $version"
      exit 0
      ;;
    --version)
      echo "Clojure CLI version $version"
      exit 0
      ;;
    -J*)
      jvm_opts+=("${1:2}")
      shift
      ;;
    -R*)
      resolve_aliases+=("${1:2}")
      shift
      >&2 echo "-R is deprecated, use -A with repl, -M for main, or -X for exec"
      ;;
    -C*)
      classpath_aliases+=("${1:2}")
      shift
      >&2 echo "-C is deprecated, use -A with repl, -M for main, or -X for exec"
      ;;
    -O*)
      >&2 echo "-O is no longer supported, use -A with repl, -M for main, or -X for exec"
      exit 1
      ;;
    -T*)
      >&2 echo "-T is no longer supported, use -A with repl, -M for main, or -X for exec"
      exit 1
      ;;
    -A)
      >&2 echo "-A requires an alias"
      exit 1
      ;;
    -A*)
      repl_aliases+=("${1:2}")
      shift
      ;;
    -M)
      mode="main"
      shift
      break
      ;;
    -M*)
      mode="main"
      main_aliases="${1:2}"
      shift
      break
      ;;
    -X)
      mode="exec"
      shift
      break
      ;;
    -X*)
      mode="exec"
      exec_aliases="${1:2}"
      shift
      break
      ;;
    -P)
      prep=true
      shift
      ;;
    -Sdeps)
      shift
      deps_data="${1}"
      shift
      ;;
    -Scp)
      shift
      force_cp="${1}"
      shift
      ;;
    -Spath)
      print_classpath=true
      shift
      ;;
    -Sverbose)
      verbose=true
      shift
      ;;
    -Sthreads)
      shift
      threads="${1}"
      shift
      ;;
    -Strace)
      trace=true
      shift
      ;;
    -Sdescribe)
      describe=true
      shift
      ;;
    -Sforce)
      force=true
      shift
      ;;
    -Srepro)
      repro=true
      shift
      ;;
    -Stree)
      tree=true
      shift
      ;;
    -Spom)
      pom=true
      shift
      ;;
    -Sresolve-tags)
      >&2 echo "Option changed, use: clj -X:deps git-resolve-tags"
      exit 1
      ;;
    -S*)
      >&2 echo "Invalid option: $1"
      exit 1
      ;;
    -h|--help|"-?")
      if [[ -n "$main_aliases" ]] || [[ ${#repl_aliases[@]} -gt 0 ]]; then
        break
      else
        help=true
        shift
      fi
      ;;
    --)
      shift
      break
      ;;
    *)
      break
      ;;
  esac
done

# Find java executable
set +e
JAVA_CMD=${JAVA_CMD:-$(type -p java)}
set -e
if [[ -z "$JAVA_CMD" ]]; then
  if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
    JAVA_CMD="$JAVA_HOME/bin/java"
  else
    >&2 echo "Couldn't find 'java'. Please set JAVA_HOME."
    exit 1
  fi
fi

if "$help"; then
  cat <<-END
	Version: $version

	You use the Clojure tools ('clj' or 'clojure') to run Clojure programs
	on the JVM, e.g. to start a REPL or invoke a specific function with data.
	The Clojure tools will configure the JVM process by defining a classpath
	(of desired libraries), an execution environment (JVM options) and
	specifying a main class and args. 

	Using a deps.edn file (or files), you tell Clojure where your source code
	resides and what libraries you need. Clojure will then calculate the full
	set of required libraries and a classpath, caching expensive parts of this
	process for better performance.

	The internal steps of the Clojure tools, as well as the Clojure functions
	you intend to run, are parameterized by data structures, often maps. Shell
	command lines are not optimized for passing nested data, so instead you
	will put the data structures in your deps.edn file and refer to them on the
	command line via 'aliases' - keywords that name data structures.

	'clj' and 'clojure' differ in that 'clj' has extra support for use as a REPL
	in a terminal, and should be preferred unless you don't want that support,
	then use 'clojure'.

	Usage:
	  Start a REPL   clj     [clj-opt*] [-Aaliases] [init-opt*]
	  Exec function  clojure [clj-opt*] -X[aliases] [a/fn] [kpath v]*
	  Run main       clojure [clj-opt*] -M[aliases] [init-opt*] [main-opt] [arg*]
	  Prepare        clojure [clj-opt*] -P [other exec opts]

	exec-opts:
	 -Aaliases      Use concatenated aliases to modify classpath
	 -X[aliases]    Use concatenated aliases to modify classpath or supply exec fn/args
	 -M[aliases]    Use concatenated aliases to modify classpath or supply main opts
	 -P             Prepare deps - download libs, cache classpath, but don't exec

	clj-opts:
	 -Jopt          Pass opt through in java_opts, ex: -J-Xmx512m
	 -Sdeps EDN     Deps data to use as the last deps file to be merged
	 -Spath         Compute classpath and echo to stdout only
	 -Spom          Generate (or update) pom.xml with deps and paths
	 -Stree         Print dependency tree
	 -Scp CP        Do NOT compute or cache classpath, use this one instead
	 -Srepro        Ignore the ~/.clojure/deps.edn config file
	 -Sforce        Force recomputation of the classpath (don't use the cache)
	 -Sverbose      Print important path info to console
	 -Sdescribe     Print environment and command parsing info as data
	 -Sthreads      Set specific number of download threads
	 -Strace        Write a trace.edn file that traces deps expansion
	 --             Stop parsing dep options and pass remaining arguments to clojure.main
	 --version      Print the version to stdout and exit
	 -version       Print the version to stderr and exit

	init-opt:
	 -i, --init path     Load a file or resource
	 -e, --eval string   Eval exprs in string; print non-nil values
	 --report target     Report uncaught exception to "file" (default), "stderr", or "none"

	main-opt:
	 -m, --main ns-name  Call the -main function from namespace w/args
	 -r, --repl          Run a repl
	 path                Run a script from a file or resource
	 -                   Run a script from standard input
	 -h, -?, --help      Print this help message and exit

	Programs provided by :deps alias:
	 -X:deps mvn-install       Install a maven jar to the local repository cache
	 -X:deps git-resolve-tags  Resolve git coord tags to shas and update deps.edn

	For more info, see:
	 https://clojure.org/guides/deps_and_cli
	 https://clojure.org/reference/repl_and_main
END
  exit 0
fi

# Set tools classpath used for the various programs before the user's program
tools_cp="$install_dir/libexec/clojure-tools-$version.jar"

# Determine user config directory
if [[ -n "$CLJ_CONFIG" ]]; then
  config_dir="$CLJ_CONFIG"
elif [[ -n "$XDG_CONFIG_HOME" ]]; then
  config_dir="$XDG_CONFIG_HOME/clojure"
else
  config_dir="$HOME/.clojure"
fi

# If user config directory does not exist, create it
if [[ ! -d "$config_dir" ]]; then
  mkdir -p "$config_dir"
fi
if [[ ! -e "$config_dir/deps.edn" ]]; then
  cp "$install_dir/example-deps.edn" "$config_dir/deps.edn"
fi

# Determine user cache directory
if [[ -n "$CLJ_CACHE" ]]; then
  user_cache_dir="$CLJ_CACHE"
elif [[ -n "$XDG_CACHE_HOME" ]]; then
  user_cache_dir="$XDG_CACHE_HOME/clojure"
else
  user_cache_dir="$config_dir/.cpcache"
fi

# Chain deps.edn in config paths. repro=skip config dir
config_project="deps.edn"
if "$repro"; then
  config_paths=("$install_dir/deps.edn" "deps.edn")
else
  config_user="$config_dir/deps.edn"
  config_paths=("$install_dir/deps.edn" "$config_dir/deps.edn" "deps.edn")
fi
config_str=$(printf ",%s" "${config_paths[@]}")
config_str=${config_str:1}

# Determine whether to use user or project cache
if [[ -f deps.edn ]]; then
  cache_dir=.cpcache
else
  cache_dir="$user_cache_dir"
fi

# Construct location of cached classpath file
val="$(join '' ${resolve_aliases[@]})|$(join '' ${classpath_aliases[@]})|$(join '' ${repl_aliases[@]})|$exec_aliases|$main_aliases|$deps_data"
for config_path in "${config_paths[@]}"; do
  if [[ -f "$config_path" ]]; then
    val="$val|$config_path"
  else
    val="$val|NIL"
  fi
done
ck=$(echo "$val" | cksum | cut -d" " -f 1)

libs_file="$cache_dir/$ck.libs"
cp_file="$cache_dir/$ck.cp"
jvm_file="$cache_dir/$ck.jvm"
main_file="$cache_dir/$ck.main"
basis_file="$cache_dir/$ck.basis"

# Print paths in verbose mode
if "$verbose"; then
  echo "version      = $version"
  echo "install_dir  = $install_dir"
  echo "config_dir   = $config_dir"
  echo "config_paths =" "${config_paths[@]}"
  echo "cache_dir    = $cache_dir"
  echo "cp_file      = $cp_file"
  echo
fi

# Check for stale classpath file
stale=false
if "$force" || "$trace" || "$tree" || "$prep" || [ ! -f "$cp_file" ]; then
  stale=true
else
  for config_path in "${config_paths[@]}"; do
    if [ "$config_path" -nt "$cp_file" ]; then
      stale=true
      break
    fi
  done
fi

# Make tools args if needed
if "$stale" || "$pom"; then
  tools_args=()
  if [[ -n "$deps_data" ]]; then
    tools_args+=("--config-data" "$deps_data")
  fi
  if [[ ${#resolve_aliases[@]} -gt 0 ]]; then
    tools_args+=("-R$(join '' ${resolve_aliases[@]})")
  fi
  if [[ ${#classpath_aliases[@]} -gt 0 ]]; then
    tools_args+=("-C$(join '' ${classpath_aliases[@]})")
  fi
  if [[ -n "$main_aliases" ]]; then
    tools_args+=("-M$main_aliases")
  fi
  if [[ ${#repl_aliases[@]} -gt 0 ]]; then
    tools_args+=("-A$(join '' ${repl_aliases[@]})")
  fi
  if [[ -n "$exec_aliases" ]]; then
    tools_args+=("-X$exec_aliases")
  fi
  if [[ -n "$force_cp" ]]; then
    tools_args+=("--skip-cp")
  fi
  if [[ -n "$threads" ]]; then
    tools_args+=("--threads" "$threads")
  fi
  if "$trace"; then
    tools_args+=("--trace")
  fi
  if "$tree"; then
    tools_args+=("--tree")
  fi
fi

# If stale, run make-classpath to refresh cached classpath
if [[ "$stale" = true && "$describe" = false ]]; then
  if "$verbose"; then
    >&2 echo "Refreshing classpath"
  fi
  "$JAVA_CMD" -classpath "$tools_cp" clojure.main -m clojure.tools.deps.alpha.script.make-classpath2 --config-user "$config_user" --config-project "$config_project" --basis-file "$basis_file" --libs-file "$libs_file" --cp-file "$cp_file" --jvm-file "$jvm_file" --main-file "$main_file" "${tools_args[@]}"
fi

if "$describe"; then
  cp=
elif [[ -n "$force_cp" ]]; then
  cp="$force_cp"
else
  cp=$(cat "$cp_file")
fi

if "$prep"; then
  exit 0
elif "$pom"; then
  exec "$JAVA_CMD" -classpath "$tools_cp" clojure.main -m clojure.tools.deps.alpha.script.generate-manifest2 --config-user "$config_user" --config-project "$config_project" --gen=pom "${tools_args[@]}"
elif "$print_classpath"; then
  echo "$cp"
elif "$describe"; then
  for config_path in "${config_paths[@]}"; do
    if [[ -f "$config_path" ]]; then
      path_vector="$path_vector\"$config_path\" "
    fi
  done
  cat <<-END
	{:version "$version"
	 :config-files [$path_vector]
	 :config-user "$config_user"
	 :config-project "$config_project"
	 :install-dir "$install_dir"
	 :config-dir "$config_dir"
	 :cache-dir "$cache_dir"
	 :force $force
	 :repro $repro
	 :main-aliases "$main_aliases"
	 :repl-aliases "${repl_aliases[@]}"}
END
elif "$tree"; then
  exit 0
elif "$trace"; then
  >&2 echo "Wrote trace.edn"
else
  set -f
  if [[ -e "$jvm_file" ]]; then
    set +e
    IFS=$'\n' read -ra jvm_cache_opts -d '' <"$jvm_file"
    set -e
  fi

  if [ "$mode" == "exec" ]; then
    exec_args=()
    if [[ -n "$exec_aliases" ]]; then
      exec_args+=("--aliases" "$exec_aliases")
    fi
    exec "$JAVA_CMD" "${jvm_cache_opts[@]}" "${jvm_opts[@]}" "-Dclojure.basis=$basis_file" -classpath "$cp:$install_dir/libexec/exec.jar" clojure.main -m clojure.run.exec "${exec_args[@]}" "$@"
  else
    if [[ -e "$main_file" ]]; then
      set +e
      IFS=$'\n' read -ra main_cache_opts -d '' <"$main_file"
      set -e
    fi
    if [ "$#" -gt 0 ] && [ "$mode" == "repl" ]; then
      >&2 echo "WARNING: Use of -A with clojure.main is deprecated, use -M instead"
    fi
    exec "$JAVA_CMD" "${jvm_cache_opts[@]}" "${jvm_opts[@]}" "-Dclojure.basis=$basis_file" -classpath "$cp" clojure.main "${main_cache_opts[@]}" "$@"
  fi
fi
