#!/bin/bash

VERBOSITY=0
TEMP_D=""
PAYLOAD_MARKER="_____PAYLOAD_____"

error() { echo "$@" 1>&2; }
fail() { [ $# -eq 0 ] || error "$@"; exit 1; }

bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; return 1; }
cleanup() {
    [ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
}

debug() {
    local level=${1}; shift;
    [ "${level}" -gt "${VERBOSITY}" ] && return
    error "${@}"
}

print_vars() {
    local k karray t="" cur="" tic="'" repltic="'\''"
    for k in "$@"; do
        karray="$k[@]"
        karray=( "${!karray}" )
        if [ ${#karray[@]} -gt 1 ]; then
            cur="("
            for t in "${karray[@]}"; do
                cur="${cur} '${t//${tic}/${repltic}}'"
            done
            cur="${cur} )"
        else
            t=${!k}
            cur="'${t//${tic}/${repltic}}'"
            #printf "%s=\"%s\"" "$k" "${t//'/'\''}"
        fi
        printf "%s=%s\n" "$k" "$cur"
    done
}

write_extractor() {
    echo "#!/bin/bash"
    echo "# vi: ts=4 expandtab syntax=sh"
    print_vars "$@"
    echo "CREATE_TIME='$(date -R)'"
    echo "PAYLOAD_MARKER='$PAYLOAD_MARKER'"
    cat <<"END_EXTRACTOR"
VERBOSITY=0
INFO_KEYS=("LABEL" "PREFIX" "COMMAND" "CREATE_TIME" "MD5SUM")

error() { echo "$@" 1>&2; }
fail() { [ $# -eq 0 ] || error "$@"; exit 1; }

Usage() {
    cat <<EOF
Usage: ${0##*/} [ options ] [mode]

   extract embedded archive

   options:
      -p | --prefix P    override extraction dir (default: $PREFIX)
      -v | --verbose     be more verbose
           --no-execute  extract only, do not execute any bundled command

   mode is one of:
     info:    dump information about archive
     check:   check archive against expected checksum
     dump:    dump archive to stdout
     extract: extract the archive under $PREFIX (default mode)
EOF
}

bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; return 1; }

debug() {
    local level=${1}; shift;
    [ "${level}" -gt "${VERBOSITY}" ] && return
    error "${@}"
}

print_vars() {
    local k karray t="" cur="" tic="'" repltic="'\''"
    for k in "$@"; do
        karray="$k[@]"
        karray=( "${!karray}" )
        if [ ${#karray[@]} -gt 1 ]; then
            cur="("
            for t in "${karray[@]}"; do
                cur="${cur} '${t//${tic}/${repltic}}'"
            done
            cur="${cur} )"
        else
            t=${!k}
            cur="'${t//${tic}/${repltic}}'"
            #printf "%s=\"%s\"" "$k" "${t//'/'\''}"
        fi
        printf "%s=%s\n" "$k" "$cur"
    done
}

dump_b64() {
   sed -n "1,/^${PAYLOAD_MARKER}$/!p" "$1"
}

dump_bin() {
    dump_b64 "$@" | base64 --decode
}

extract() {
    mkdir "$2" || { error "failed to make '$2'"; return 1; }
    dump_bin "$1" | tar -Sxzf - -C "$2"
}

main() {
    local short_opts="hp:v"
    local long_opts="help,no-execute,prefix:,verbose"
    local getopt_out=$(getopt --name "${0##*/}" \
        --options "${short_opts}" --long "${long_opts}" -- "$@") &&
        eval set -- "${getopt_out}" ||
        { bad_Usage; return; }

    local cur="" next="" 
    local prefix="$PREFIX" execute=true

    while [ $# -ne 0 ]; do
        cur="$1"; next="$2";
        case "$cur" in
            -h|--help) Usage ; exit 0;;
               --no-execute) execute=false;;
            -p|--prefix) prefix="$next"; shift;;
            -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
            --) shift; break;;
        esac
        shift;
    done

    [ $# -lt 2 ] || { bad_Usage "got $# args. expected only 1"; return; }
    mode="$1"

    case "${mode:-extract}" in
        info) print_vars "${INFO_KEYS[@]}"; return;;
        check)
            found=$(dump_bin "$0" | md5sum) ||
                { error "failed to calculate checksum"; return 1; }
            found=${found%  -}
            [ "$found" = "$MD5SUM" ] &&
                error "found = expected = $found" && return 0
            error "found = ${found}. expected = ${MD5SUM}"
            return 1;;
        dump) dump_bin "$0"; return;;
        extract)
            extract "$0" "$prefix" || { error "failed extraction"; return 1; }
            if [ "${#COMMAND[@]}" = "1" -a -z "${COMMAND}" ]; then
                error "extracted to '$PWD/$prefix'. no command to run."
                return 0;
            fi
            if ! $execute; then
                error "extracted to '$PWD/$prefix'. disabled command"
                return 0;
            fi
            cd "$prefix" ||
                { error "failed to change to $prefix"; return 1; }

            local np=""
            if [ -n "$PYPATH" ]; then
                np="${PYPATH//_pwd_/$PWD}${PYTHONPATH:+:${PYTHONPATH}}"
                export PYTHONPATH="$np"
            fi

            if [ -n "$BINPATH" ]; then
                np="${BINPATH//_pwd_/$PWD}${PATH:+:${PATH}}"
                export PATH="$np"
            fi

            debug 1 "executing: ${COMMAND[*]}"
            exec "${COMMAND[@]}"
            ;;
    esac
    
    return 0
}

set -o pipefail
main "$@"

exit
END_EXTRACTOR
    echo "$PAYLOAD_MARKER"
}

Usage() {
    cat <<EOF
Usage: ${0##*/} [ options ] archive_dir label [ command [ args ] ]

   create a self extracting executable of archive_dir.

   options:
      -d | --extract-dir D    extract top level dir named D
                              default: dirname(dir)
      -o | --output      F    output to 'F'. default: - (stdout)
           --environ     E=N  set environment before execution
EOF
}

main() {
    local short_opts="hd:o:v"
    local long_opts="bin-path:,extract-dir:,environ:,help,output:,python-path:,verbose"
    local getopt_out=$(getopt --name "${0##*/}" \
        --options "${short_opts}" --long "${long_opts}" -- "$@") &&
        eval set -- "${getopt_out}" ||
        { bad_Usage; return; }

    local cur="" next="" prefix=""
    local pypath="" binpath=""
    local output="-"

    while [ $# -ne 0 ]; do
        cur="$1"; next="$2";
        case "$cur" in
               --bin-path)
                [ "$next" = "." ] && next="_pwd_";
                binpath="$next${binpath:+:${binpath}}"; shift;;
               --python-path)
                [ "$next" = "." ] && next="_pwd_";
                pypath="$next${pypath:+:${pypath}}"; shift;;
            -h|--help) Usage ; exit 0;;
            -d|--extract-dir) prefix=$next; shift;;
            -o|--output) output=$next; shift;;
            -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
            --) shift; break;;
        esac
        shift;
    done

    local archive_d="$1" label="$2" adir
    [ $# -ge 2 ] || {
        bad_Usage "expected archive and label. got only $# args";
        return 1;
    }

    shift 2
    local MD5SUM PREFIX LABEL COMMAND 
    local PYPATH="$pypath" BINPATH="$binpath"
    COMMAND=( "$@" )
    adir=$(cd "${archive_d}" && echo "$PWD") ||
        { error "failed to change dir to ${archive_d}"; return 1; }
    PREFIX="${prefix:-${adir##*/}}"
    LABEL="$label"

    TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") ||
        fail "failed to make tempdir"
    trap cleanup EXIT

    local payload="${TEMP_D}/payload.tar.gz" md5=""
    tar -C "$archive_d" -Sczf "${payload}" . ||
        { error "failed to create archive from '${archive_d}'"; return 1; }

    md5=$(md5sum < "$payload") ||
        { error "failed to get checksum of ${payload}"; return 1; }
    md5=${md5%  -}
    MD5SUM="$md5"

    if [ "$output" != "-" ]; then
        exec > "$output" ||
            { error "failed to redirect output to $output"; return 1; }
    fi
        
    write_extractor MD5SUM PREFIX LABEL COMMAND PYPATH BINPATH ||
        { error "failed to write extractro"; return 1; }
    base64 < "$payload" ||
        { error "failed to base64 encode payload"; return 1; }

    return 0
}

main "$@"

# vi: ts=4 expandtab syntax=sh
