#! /usr/bin/env bash

set -e

if ! docker > /dev/null 2>&1 ; then
    echo "Docker does not seem to be installed."
    exit 1
fi

docker_version="$(docker version -f "{{ .Server.Version }}")"
docker_major="$(echo "$docker_version" | cut -d . -f 1)"
docker_minor="$(echo "$docker_version" | cut -d . -f 2)"

if ([ "$docker_major" -gt 1 ] ||
    ( [ "$docker_major" -eq 1 ] && [ "$docker_minor" -ge 13 ] )) ; then
    docker_1_13=true
else
    docker_1_13=false
fi

current_dir="$(pwd -P)"
src_dir="$(cd "$(dirname "$0")" && echo "$current_dir/")"
cd "$src_dir"

default_port=9732
port="$default_port"

docker_image=docker.io/tezos/tezos:alphanet
docker_volume=tezos-alphanet-data$suffix
suffix=

data_dir="$HOME/.tezos-alphanet$suffix"
docker_container="tezos-alphanet$suffix"

if [ ! -z "$ALPHANET_EMACS" ]; then
    interactive_flags="-t"
else
    interactive_flags="-it"
fi


## Saving state ############################################################

save_identity() {
    if [ ! -f "$data_dir/identity.json" ]; then
        echo "Saving the generated identity into '$data_dir/identity.json'..."
        mkdir -p "$data_dir/"
    fi
    docker cp "$docker_container:var/run/tezos/node/identity.json" \
              "$data_dir/"
}


may_restore_identity() {
    if [ -f "$data_dir/identity.json" ]; then
        echo "Restoring the peer identity from '$data_dir/identity.json'..."
        docker exec "$docker_container" mkdir -p /var/run/tezos/node/
        docker cp "$data_dir/identity.json" \
                  "$docker_container:var/run/tezos/node/"
        docker exec "$docker_container" \
               sudo chown tezos "/var/run/tezos/node/identity.json"
    fi
}

may_save_client_file() {
    if docker exec "$docker_container" \
              test -f "/var/run/tezos/client/$1" ; then
        docker cp "$docker_container:var/run/tezos/client/$1" \
               "$data_dir/$1"
    elif [ -f "$data_dir/$1" ] ; then
        mv "$data_dir/$1" "$data_dir/$1.bak"
    fi
}

may_restore_client_file() {
    if [ -f "$data_dir/$1" ]; then
        docker cp "$data_dir/$1" \
                  "$docker_container:var/run/tezos/client/"
        docker exec "$docker_container" \
               sudo chown tezos "/var/run/tezos/client/$1"
    fi
}

save_accounts() {
    if ! docker exec "$docker_container" \
         test -f "/var/run/tezos/client/secret keys" ; then
        return
    fi
    if [ ! -f "$data_dir/secret keys" ]; then
        echo "Saving the secrets into '$data_dir/secret keys'..."
        echo
        echo -e "\033[33mWARNING: THE SECRET KEYS FILE IS UNENCRYPTED!!!\033[0m"
        echo
        mkdir -p "$data_dir/"
    fi
    docker cp "$docker_container:var/run/tezos/client/secret keys" \
           "$data_dir/"
    may_save_client_file "public key hashs"
    may_save_client_file "public keys"
    may_save_client_file "contracts"
}

may_restore_accounts() {
    docker exec "$docker_container" mkdir -p /var/run/tezos/client/
    if [ -f "$data_dir/secret keys" ]; then
        echo "Restoring the secret keys from '$data_dir/secret keys'..."
        may_restore_client_file "secret keys"
    fi
    may_restore_client_file "public key hashs"
    may_restore_client_file "public keys"
    may_restore_client_file "contracts"
}


## Container ###############################################################

pull_image() {
    if [ "$TEZOS_ALPHANET_DO_NOT_PULL" = "yes" ] ||  [ "$ALPHANET_EMACS" ] ; then
        return ;
    fi
    docker pull "$docker_image"
}

check_container() {
    res=$(docker inspect \
                 --format="{{ .State.Running }}" \
                 --type=container "$docker_container" 2>/dev/null \
              || echo false)
    [ "$res" = true ]
}

check_volume() {
    docker volume inspect "$docker_volume" > /dev/null 2>&1
}

clear_volume() {
    if check_volume ; then
        docker volume rm "$docker_volume" > /dev/null
        echo -e "\033[32mThe blockchain data has been removed from the disk.\033[0m"
    else
        echo -e "\033[32mNo remaining data to be removed from the disk.\033[0m"
    fi
}

uptodate_container() {
    running_image=$(docker inspect \
                           --format="{{ .Image }}" \
                           --type=container "$docker_container")
    latest_image=$(docker inspect \
                          --format="{{ .Id }}" \
                          --type=image "$docker_image")
    [ "$latest_image" = "$running_image" ]
}

assert_container_uptodate() {
    pull_image > /dev/null
    if ! uptodate_container; then
        echo "The current container is not the latest available."
        echo "Please restart."
        exit 1
    fi
}

assert_container() {
    if ! check_container; then
        echo -e "\033[31mNo container currently running!\033[0m"
        exit 1
    fi
}

start_container() {
    if [ "$#" -ge 2 ] && [ "$1" = "--rpc-port" ] ; then
        docker_export_rpc="-p $2:8732"
    fi
    if check_container; then
        assert_container_uptodate
    else
        if ! check_volume; then
            docker volume create "$docker_volume"
        fi
        docker rm "$docker_container" || true > /dev/null 2>&1
        echo "Launching the docker container..."
        docker run --rm -dit -p "$port:$port" $docker_export_rpc \
               -v "$docker_volume:/var/run/tezos" \
               --entrypoint /bin/sh \
               --name "$docker_container" \
               "$docker_image" > /dev/null
        may_restore_identity
        may_restore_accounts
    fi
}

stop_container() {
    if ! check_container; then
        echo -e "\033[31mNo container to kill!\033[0m"
        exit 1
    fi
    save_identity ## Saving again, just in case...
    save_accounts
    printf "Stopping the container... "
    docker stop "$docker_container" >/dev/null
    echo " done"
}


## Node ####################################################################

init_node() {
    docker exec "$docker_container" tezos init \
           "$@" --net-addr "[::]:$port"
    save_identity
}

check_node() {
    check_container && docker exec "$interactive_flags" "$docker_container" tezos check_node
}

assert_node() {
    if ! check_node; then
        echo -e "\033[31mNode is not running!\033[0m"
        exit 0
    fi
}

status_node() {
    if check_node; then
        echo -e "\033[32mNode is running\033[0m"
    else
        echo -e "\033[33mNode is not running\033[0m"
    fi
}

start_node() {
    if check_node; then
        echo -e "\033[31mCannot run two nodes in the same container!\033[0m"
        exit 1
    fi
    if $docker_1_13; then
        tezos_log_env="-eTEZOS_LOG=${TEZOS_LOG:=* -> info}"
    fi
    docker exec -d "${tezos_log_env}" \
           "$docker_container" tezos run_node
    sleep 1
    docker exec "$docker_container" tezos wait_node
    echo -e "\033[32mThe node is now running.\033[0m"
}

log_node() {
    docker exec "$interactive_flags" "$docker_container" tezos log_node
}

stop_node() {
    docker exec "$docker_container" tezos stop_node
}


## Baker ###################################################################

check_baker() {
    check_node && docker exec "$interactive_flags" "$docker_container" tezos check_baker
}

assert_baker() {
    if ! check_baker; then
        echo -e "\033[31mBaker is not running!\033[0m"
        exit 0
    fi
}

status_baker() {
    if check_baker; then
        echo -e "\033[32mBaker is running\033[0m"
    else
        echo -e "\033[33mBaker is not running\033[0m"
    fi
}

start_baker() {
    if check_baker; then
        echo -e "\033[31mCannot run two bakers in the same container!\033[0m"
        exit 1
    fi
    TEZOS_LOG="${TEZOS_LOG:=* -> info}"
    docker exec -d "$docker_container" tezos run_baker
    echo -e "\033[32mThe baker is now running.\033[0m"
}

log_baker() {
    docker exec "$interactive_flags" "$docker_container" tezos log_baker
}

stop_baker() {
    docker exec "$docker_container" tezos stop_baker
}

## Baker ###################################################################

check_endorser() {
    check_node && docker exec "$interactive_flags" "$docker_container" tezos check_endorser
}

assert_endorser() {
    if ! check_endorser; then
        echo -e "\033[31mEndorser is not running!\033[0m"
        exit 0
    fi
}

status_endorser() {
    if check_endorser; then
        echo -e "\033[32mEndorser is running\033[0m"
    else
        echo -e "\033[33mEndorser is not running\033[0m"
    fi
}

start_endorser() {
    if check_endorser; then
        echo -e "\033[31mCannot run two endorsers in the same container!\033[0m"
        exit 1
    fi
    TEZOS_LOG="${TEZOS_LOG:=* -> info}"
    docker exec -d "$docker_container" tezos run_endorser
    echo -e "\033[32mThe endorser is now running.\033[0m"
}

log_endorser() {
    docker exec "$interactive_flags" "$docker_container" tezos log_endorser
}

stop_endorser() {
    docker exec "$docker_container" tezos stop_endorser
}

## Misc ####################################################################


run_client() {
    declare -a container_args=();
    for arg in "$@"; do
        if [[ "$arg" == 'container:'* ]]; then
            local_path="${arg#container:}"
            if [[ "$local_path" != '/'* ]]; then
                local_path="$current_dir/$local_path"
            fi
            docker exec "$docker_container" mkdir -p -m 777 /tmp/copied/
            file_name=$(basename "${local_path}")
            docker_path="/tmp/copied/$file_name"
            docker cp "${local_path}" "$docker_container:${docker_path}"
            docker exec "$docker_container" sudo chmod 644 "${docker_path}"
            container_args+=("file:$docker_path");
        else
            container_args+=("${arg}");
        fi
    done
    docker exec "$interactive_flags" "$docker_container" tezos client "${container_args[@]}"
    docker exec "$docker_container" rm -rf /tmp/copied # Remove copied files
    save_accounts
}

run_shell() {
    if [ $# -eq 0 ]; then
        docker exec -it "$docker_container" bash
    else
        docker exec -it "$docker_container" bash -c "$@"
    fi
    save_accounts
}

display_head() {
    docker exec "$interactive_flags" "$docker_container" tezos \
           client rpc call /blocks/head with '{}'
    docker exec "$interactive_flags" "$docker_container" tezos \
           client rpc call /blocks/head/proto/context/level with '{}'
}

## Main ####################################################################

start() {
    pull_image
    start_container "$@"
    init_node "$@"
    start_node
    start_baker
    start_endorser
    save_accounts
    warn_script_uptodate
}

go_alpha_go() {
    docker exec "$interactive_flags" "$docker_container" tezos client \
           activate \
           protocol ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK \
           with fitness 1 \
           and key dictator
}

stop() {
    stop_node || true
    stop_container
}

status() {
    pull_image
    if ! uptodate_container; then
        echo -e "\033[31mThe container is running but not the latest available.\033[0m"
        exit 1
    fi
    echo -e "\033[32mThe container is running and up to date.\033[0m"
    warn_script_uptodate verbose
    status_node
    status_baker
    status_endorser
}

warn_script_uptodate() {
    if [[ $ALPHANET_EMACS ]]; then
       return
    fi
    docker cp "$docker_container:home/tezos/scripts/alphanet.sh" \
              ".alphanet.sh.new"
    if ! diff .alphanet.sh.new  "$0" >/dev/null 2>&1 ; then
        echo -e "\033[33mWarning: the container contains a new version of 'alphanet.sh'.\033[0m"
        echo -e "\033[33mYou might run '$0 update_script' to synchronize.\033[0m"
    elif [ "$1" = "verbose" ] ; then
        echo -e "\033[32mThe script is up to date.\033[0m"
    fi
    rm .alphanet.sh.new
}

assert_uptodate() {
    assert_container
    assert_container_uptodate
    warn_script_uptodate
}

update_script() {
    pull_image
    tmp="$(docker run --rm -dit --entrypoint /bin/sleep "$docker_image" 20)"
    docker cp "$tmp:home/tezos/scripts/alphanet.sh" ".alphanet.sh.new"
    docker stop "$tmp" > /dev/null
    if ! diff .alphanet.sh.new  "$0" >/dev/null 2>&1 ; then
        mv .alphanet.sh.new "$0"
        echo -e "\033[32mThe script has been updated.\033[0m"
    else
        rm .alphanet.sh.new
        echo -e "\033[32mThe script is up to date.\033[0m"
    fi
}

usage() {
    echo "Usage: $0 [GLOBAL_OPTIONS] <command> [OPTIONS]"
    echo "  Main commands:"
    echo "    $0 start [--rpc-port <int>] [OPTIONS]"
    echo "       Launch a full Tezos alphanet node in a docker container"
    echo "       automatically generating a new network identity."
    echo "       An account 'my_account' for a manager 'my_identity' is also"
    echo "       created to be used via the client."
    echo "       OPTIONS (others than --rpc-port) are directly passed to the"
    echo "       Tezos node, see '$0 shell tezos-node config --help'"
    echo "       for more details."
    echo "       By default, the RPC port is not exported outside the docker"
    echo "       container. WARNING: when exported some RPCs could be harmful"
    echo "       (e.g. 'inject_block', 'force_validation', ...), it is"
    echo "       advised not to export them publicly."
    echo "    $0 <stop|kill>"
    echo "       Friendly or brutally stop the node."
    echo "    $0 restart"
    echo "       Friendly stop the node, fetch the latest docker image and "
    echo "       update this script, then start the node again."
    echo "       The blockchain data are preserved."
    echo "    $0 clear"
    echo "       Remove all the blockchain data from the disk (except"
    echo "       for secret keys and other configuration backup)."
    echo "    $0 status"
    echo "       Check that the running node is running and up to date."
    echo "       Upgrade is automatically done by the start command."
    echo "    $0 head"
    echo "       Display info about the current head of the blockchain."
    echo "    $0 client <COMMAND>"
    echo "       Pass a command to the tezos client."
    echo "    $0 update_script"
    echo "       Replace 'alphanet.sh' with the one found in the docker image."
    echo "  Advanced commands:"
    echo "    $0 container <start|stop|status>"
    echo "    $0 node <start|stop|status|log>"
    echo "    $0 baker <start|stop|status|log>"
    echo "    $0 endorser <start|stop|status|log>"
    echo "    $0 shell"
    echo "Node configuration backup directory: $data_dir"
    echo "Global options are currently limited to:"
    echo "  --port <int>"
    echo "      change public the port Tezos node"
    echo "Container prefix:"
    echo "    container:<FILE>"
    echo "      can be used anywhere 'file:<FILE>' is permitted in client commands."
    echo "      It will cause the referenced file to be copied into the docker conainer."
    echo "      Files will be renamed, which may make errors difficult to read"
}

## Dispatch ################################################################

if [ "$#" -ge 2 ] && [ "$1" = "--port" ] ; then
    port="$2"
    suffix=".$port"
    shift 2
fi

command="$1"
if [ "$#" -eq 0 ] ; then usage ; exit 1;  else shift ; fi


case "$command" in

    ## Main

    start)
        start "$@"
        ;;
    restart)
        if check_container; then
            stop_container
        fi
        update_script
        export TEZOS_ALPHANET_DO_NOT_PULL=yes
        exec "$0" start "$@"
        ;;
    clear)
        if check_container; then
            echo -e "\033[31mCannot clear data while the container is running.\033[0m"
            exit 1
        fi
        clear_volume
        ;;
    status)
        assert_container
        status
        ;;
    stop)
        assert_container
        stop
        ;;
    kill)
        stop_container
        ;;

    ## Container

    container)
        subcommand="$1"
        if [ "$#" -eq 0 ] ; then usage ; exit 1 ; else shift ; fi
        case "$subcommand" in
            start)
                start_container "$@"
                warn_script_uptodate
                ;;
            status)
                if check_container; then
                    echo -e "\033[32mContainer is running\033[0m"
                else
                    echo -e "\033[33mContainer is not running\033[0m"
                fi
                ;;
            stop)
                stop_container
                ;;
            *)
                usage
                exit 1
        esac ;;

    ## Node

    node)
        subcommand="$1"
        if [ "$#" -eq 0 ] ; then usage ; exit 1;  else shift ; fi
        case "$subcommand" in
            start)
                assert_uptodate
                start_node
                ;;
            status)
                assert_uptodate
                status_node
                ;;
            log)
                assert_uptodate
                log_node
                ;;
            stop)
                assert_uptodate
                stop_node
                ;;
            *)
                usage
                exit 1
        esac ;;
    ## Baker

    baker)
        subcommand="$1"
        if [ "$#" -eq 0 ] ; then usage ; exit 1;  else shift ; fi
        case "$subcommand" in
            status)
                assert_uptodate
                status_baker
                ;;
            start)
                assert_uptodate
                assert_node
                start_baker
                ;;
            log)
                assert_uptodate
                log_baker
                ;;
            stop)
                assert_uptodate
                stop_baker
                ;;
            *)
                usage
                exit 1
        esac ;;

    ## Endorser

    endorser)
        subcommand="$1"
        if [ "$#" -eq 0 ] ; then usage ; exit 1;  else shift ; fi
        case "$subcommand" in
            status)
                assert_uptodate
                status_endorser
                ;;
            start)
                assert_uptodate
                assert_node
                start_endorser
                ;;
            log)
                assert_uptodate
                log_endorser
                ;;
            stop)
                assert_uptodate
                stop_endorser
                ;;
            *)
                usage
                exit 1
        esac ;;

    ## Misc.

    head)
        assert_uptodate
        assert_node
        display_head
        ;;
    go_alpha_go)
        assert_uptodate
        assert_node
        go_alpha_go
        ;;
    shell)
        assert_uptodate
        run_shell "$@"
        ;;
    client)
        assert_uptodate
        assert_node
        run_client "$@"
        ;;
    check_script)
        assert_uptodate
        ;;
    update_script)
        update_script
        ;;
    *)
        usage
        exit 1
        ;;
esac
