#!/usr/bin/env bash

function http_get { # Adapted from http://github.com/technomancy/leiningen
    local url="$1" path="$2"

    if [[ -z $http_get ]]; then
        if type -p curl >/dev/null 2>&1; then
            http_get='curl -fL# -o'
        else
            http_get='wget -O'
        fi
    fi
    mkdir -p "$(dirname $path)"
    echo "Downloading $url..." >&2
    $http_get "$path.pending" "$url" || return 1
    mv -- "$path.pending" "$path"
}

function bootstrap {
    drip_root=$DRIP_HOME/$DRIP_VERSION
    drip_src=$drip_root/src
    mkdir -p "$drip_root"

    script="$0"

    # Adapted from http://github.com/technomancy/leiningen
    # resolve symlinks to the script itself portably
    while [[ -h $script ]] ; do
        local ls=$(ls -ld -- "$script")
        local link=$(expr -- "$ls" : '.*-> \(.*\)$')
        if expr -- "$link" : '/.*' > /dev/null; then
            script=$link
        else
            script=$(dirname "$script"$)/$link
        fi
    done

    local bin_dir=$(dirname $script)
    drip_dir=$bin_dir/..
    if true; then
        # Checkout or package
        if [[ -d $drip_dir/.git ]]; then
            drip_install=checkout
        else
            drip_install=package
        fi

        drip_jar=$drip_dir/drip.jar
        drip_proxy=$bin_dir/drip_proxy
        drip_daemon=$bin_dir/drip_daemon
    else
        # Standalone
        drip_dir=
        drip_install=standalone
        drip_jar=$(mvn_get 'drip' 'org/flatland' "$DRIP_VERSION" "$DRIP_JAR_URL")

        drip_proxy=$(compile 'drip_proxy')
        drip_daemon=$(compile 'drip_daemon')
        [[ -e $drip_proxy && -e $drip_daemon ]] || exit 1
    fi
}

function compile {
    local base=$1
    local src_file
    local bin_file="$drip_root/$base"

    if [[ -z $drip_dir ]]; then
        mkdir -p "$drip_src"
        src_file=$drip_src/$base.c
        [[ -e $src_file ]] || http_get "$DRIP_SRC_URL/$base.c" "$src_file"
    else
        src_file=$drip_dir/src/$base.c
    fi
    if [[ $src_file -nt $bin_file ]]; then
        gcc $src_file -o $bin_file
    fi
    echo "$bin_file"
}

function mvn_get {
    local artifact="$1" group="$2" version="$3" url="$4"
    local jar="$MVN_REPO/$group/$artifact/$version/$artifact-$version.jar"
    [[ -e $jar ]] || http_get "$url" "$jar"
    printf '%s' "$jar"
}

declare -a jvm_args
declare -a main_args
declare -a runtime_args

function jar_main_class {
    local jar=$1
    local line=$(unzip -p $jar META-INF/MANIFEST.MF | grep ^Main-Class:)
    local main_class=$(expr -- "$line" : 'Main-Class: \([^[:space:]]*\)')
    echo $main_class
}

function parse_args {
    unset drip_command java_command
    if [[ $# -eq 0 ]]; then
        echo 'Usage: drip [command | same args as java]'
        echo
        echo 'drip supports the following commands:'
        echo '    version          print drip version and exit'
        echo '    upgrade          upgrade drip to the latest version'
        echo '    kill [-signal]   kill all idle drip JVMs (with signal)'
        echo '    ps               print a list of all drip processes'
        exit 0
    elif ! expr -- "$1" : '.*[.-]' > /dev/null; then
        drip_command=$1
        return
    elif [[ $# -eq 1 && $1 == -* ]]; then
        java_command=$1
        return
    fi

    unset classpath main_class main_args runtime_args context
    for arg in "$@"; do
        if [[ -z $main_class ]]; then
            if [[ -z $context ]]; then
                if [[ $arg == "-cp" || $arg == "-classpath" ]]; then
                    context='-cp'
                elif [[ $arg == "-jar" ]]; then
                    context='-jar'
                elif [[ $arg == --* ]]; then
                    runtime_args+=("${arg:1}")
                elif [[ $arg != -* ]]; then
                    main_class=$arg
                else
                    jvm_args+=("$arg")
                fi
            else
                classpath=$arg
                if [[ $context == '-jar' ]]; then
                    main_class=$(jar_main_class $arg)
                fi
                unset context
            fi
        else
            main_args+=("$arg")
        fi
    done
    main_class=${main_class//\//.}
}

function make_sha_path {
    opts="$PWD ${jvm_args[*]} $classpath $main_class"
    sha=$(echo -n "$opts" | git hash-object --stdin)
    sha_path=$drip_root/$sha
    mkdir -p "$sha_path"
    echo -n "$opts" > $sha_path/opts
}

function default_init {
    local f='%s\n%s'
    case $main_class in
        clojure.main)
            DRIP_INIT_CLASS='clojure.main'
            DRIP_INIT=$(printf $f '-e' 'nil');;
        org.jruby.Main)
            DRIP_INIT_CLASS='org.jruby.main.DripMain'
            DRIP_INIT=$(printf $f '1 + 1');;
        scala.tools.nsc.MainGenericRunner)
            DRIP_INIT_CLASS='scala.tools.nsc.MainGenericRunner'
            DRIP_INIT=$(printf $f '-e' 'null');;
    esac
}

function launch_jvm {
    jvm_dir=$sha_path/$$-$n

    if mkdir "$jvm_dir"; then
        mkfifo "$jvm_dir/control"

        if [[ -z $DRIP_INIT_CLASS ]]; then
            default_init
        fi
        export DRIP_INIT_CLASS=${DRIP_INIT_CLASS//\//.}
        export DRIP_INIT

        $drip_daemon $DRIP_JAVA_CMD "${jvm_args[@]}" -Djava.awt.headless=true \
                                    "-classpath" "$drip_jar:$classpath" \
                                    org.flatland.drip.Main "$main_class" "$jvm_dir" \
                                    > /dev/null 2> $drip_root/error.log
    fi
}

function kill_jvm {
    local signal=$1
    local pid=$(cat "$jvm_dir/jvm.pid" 2> /dev/null)
    kill "$signal" $pid 2> /dev/null
}

function lock_dir {
    if mkdir "$jvm_dir/lock" 2> /dev/null; then
        if kill_jvm -0; then
            echo $$ > "$jvm_dir/client.pid"
            active_jvm_dir=$jvm_dir
            return 0
        else
            rm -rf "$jvm_dir"
        fi
    fi
    return 0
}

function find_jvm {
    make_sha_path

    for jvm_dir in $sha_path/*-*; do
        if [[ -z $active_jvm_dir ]]; then
            lock_dir
        elif [[ ! -e $jvm_dir/lock ]]; then
            let n=$n+1
        fi
    done

    n=${n:-0}

    while (( $n < $DRIP_POOL )); do
        let n=$n+1
        launch_jvm
    done

    if [[ -z $active_jvm_dir ]]; then
        exec "$DRIP_JAVA_CMD" "${jvm_args[@]}" "${runtime_args[@]}" \
                              "-classpath" "$classpath" \
                              "$main_class" "${main_args[@]}"
    fi
}

function kill_jvms {
    local killed=false

    for version_dir in $DRIP_HOME/*; do
        [[ -d $version_dir ]] || continue
        for sha_dir in $version_dir/*; do
            [[ -d $sha_dir ]] || continue
            [[ $sha_dir != $version_dir/src ]] || continue
            for jvm_dir in $sha_dir/*-*; do
                [[ -d $jvm_dir ]] || continue
                if lock_dir; then
                    kill_jvm "$1"
                    rm -rf "$jvm_dir"
                    killed=true
                fi
            done
            local dirs=($sha_dir/*)
            if [[ "${dirs[*]}" == "$sha_dir/opts" ]]; then
                rm -rf "$sha_dir"
            fi
        done
    done

    $killed || echo "No idle Drip JVM running"
}

function send_array {
    local string=$*
    local length=${#string}
    (( $length != 0 )) && let length=length+1

    printf '%s:' "$length"
    for e; do
        printf -- '%s\0' "$e"
    done
    printf ','
}

function send_env {
    declare -a vars

    # Call declare in a bash subprocess to get rid of variables that aren't exported.
    while read -r line; do
        [[ $line == *=\(*\) ]] && continue # Filter bash arrays
        [[ $line != *=*     ]] && break    # Filter function definitions

        # Filter extra variables that were added by the bash subprocess but are not exported.
        for var in BASH BASHOPTS BASH_EXECUTION_STRING BASH_VERSION \
                   SHELLOPTS IFS PS4 UID EUID PPID; do
            [[ $line == $var=*  ]] && continue 2
        done

        vars+=("$(eval echo $line)")
    done <<< "$(bash -c declare)"

    send_array "${vars[@]}"
}

function send_args {
    mkfifo "$active_jvm_dir/status"
    exec 4> "$active_jvm_dir/control"
    send_array "${main_args[@]}" >&4
    send_array "${runtime_args[@]}" >&4
    send_env >&4
    exec 4>&-
}

function wait_for_exit {
    status=$(cat "$active_jvm_dir/status")
    rm -rf "$active_jvm_dir"
}

function run_main {
    send_args
    $drip_proxy "$active_jvm_dir"

    wait_for_exit
}

function run_drip_command {
    case $drip_command in
        version)
            if [[ $drip_install == checkout ]]; then
                git_sha="$(cd "$drip_dir"; git rev-parse --short HEAD)"
            fi
            printf 'drip version "%s" %s %s\n' $DRIP_VERSION $drip_install $git_sha;;
        kill)
            kill_jvms "$2";;
        ps)
            jps -vlm | grep org.flatland.drip.Main;;
        upgrade)
            case $drip_install in
                standalone)
                    rm -rf "$drip_src" # need to refetch in case it has changed
                    http_get  "$DRIP_BIN_URL" "$script" || exit $?
                    chmod +x "$script";;
                checkout)
                    (cd "$drip_dir" && git pull);;
                package)
                    echo Please use your package manager to upgrade Drip.
                    echo For example:
                    echo '  port selfupdate && port upgrade drip'
            esac
            bootstrap
            exit $?;;
        *)
            echo Unknown command: $drip_command
            exit 1;;
    esac
}

# Let's go.

DRIP_VERSION=0.2.4-17-ga4bd00d
DRIP_POOL=${DRIP_POOL:-1}
DRIP_HOME=${DRIP_HOME:-~/.drip}
DRIP_BRANCH=${DRIP_BRANCH:-master}
DRIP_REPO=${DRIP_REPO:-http://clojars.org/repo}
DRIP_JAR_URL=${DRIP_JAR_URL:-$DRIP_REPO/org/flatland/drip/$DRIP_VERSION/drip-$DRIP_VERSION.jar}
DRIP_BIN_URL=${DRIP_BIN_URL:-https://raw.github.com/flatland/drip/$DRIP_BRANCH/bin/drip}
DRIP_SRC_URL=${DRIP_SRC_URL:-https://raw.github.com/flatland/drip/$DRIP_BRANCH/src}
DRIP_JAVA_CMD=${DRIP_JAVA_CMD:-java}
DRIP_JAVA_CMD=$(which $DRIP_JAVA_CMD)
MVN_REPO=${MVN_REPO:-~/.m2/repository}

bootstrap

parse_args "$@"

if [[ -z $drip_command ]]; then
    [[ -z $java_command ]] || exec "$DRIP_JAVA_CMD" $java_command
    find_jvm
    run_main

    exit $status
else
    run_drip_command
fi
