#!/bin/bash

## Bash wrap script to launch the ``compose`` docker with right options.
##
##

## Launcher
##  - should need minimum requirement to run
##    - no shell libs
##

[[ "${BASH_SOURCE[0]}" != "${0}" ]] && SOURCED=true

##
## From kal-shlib
##


_sed_compat_load() {
    if get_path sed >/dev/null; then
        if sed --version >/dev/null 2>&1; then  ## GNU
            sed_compat() { sed -r "$@"; }
            sed_compat_i() { sed -r -i "$@"; }
        else                                    ## BSD
            sed_compat() { sed -E "$@"; }
            sed_compat_i() { sed -E -i "" "$@"; }
        fi
    else
        ## Look for ``gsed``
        if (get_path gsed && gsed --version) >/dev/null 2>&1; then
            sed_compat() { gsed -r "$@"; }
            sed_compat_i() { gsed -r -i "$@"; }
        else
            die "$exname: required GNU or BSD sed not found"
        fi
    fi
}

## BSD / GNU sed compatibility layer
sed_compat() { _sed_compat_load; sed_compat "$@"; }


hash_get() {
    if get_path sha256sum >/dev/null; then
        hash_get() { local x; x=$(sha256sum) || return 1; echo "${x:0:32}"; }
    elif get_path md5sum >/dev/null; then
        hash_get() { local x; x=$(md5sum) || return 1; echo "${x:0:32}"; }
    elif get_path md5 >/dev/null; then
        hash_get() { md5; }
    else
        err "required GNU md5sum or BSD md5 not found"
        return 1
    fi
    hash_get
}


## output on stdout the next record on stdin separated by a '\0'
next-0() {
    local ans IFS=''
    read -r -d '' ans &&
    printf "%s" "$ans"
}


array_read-0() {
    local elt aname
    while true; do
        for aname in "$@"; do
            declare -n cur="$aname"
            elt="$(next-0)" || return 0
            cur+=("$elt")
        done
    done
}


str_pattern_matches() {
    local str="$1"
    shift
    for pattern in "$@"; do
        eval "[[ \"$str\" == $pattern ]]" && return 0
    done
    return 1
}


ansi_color() {
    local choice="$1"

    if [ "$choice" == "tty" ]; then
        if [ -t 1 ]; then
            choice="yes"
        else
            choice="no"
        fi
    fi

    ANSI_ESC=$'\e['

    if [ "$choice" != "no" ]; then

        NORMAL="${ANSI_ESC}0m"

        GRAY="${ANSI_ESC}1;30m"
        RED="${ANSI_ESC}1;31m"
        GREEN="${ANSI_ESC}1;32m"
        YELLOW="${ANSI_ESC}1;33m"
        BLUE="${ANSI_ESC}1;34m"
        PINK="${ANSI_ESC}1;35m"
        CYAN="${ANSI_ESC}1;36m"
        WHITE="${ANSI_ESC}1;37m"

        DARKGRAY="${ANSI_ESC}0;30m"
        DARKRED="${ANSI_ESC}0;31m"
        DARKGREEN="${ANSI_ESC}0;32m"
        DARKYELLOW="${ANSI_ESC}0;33m"
        DARKBLUE="${ANSI_ESC}0;34m"
        DARKPINK="${ANSI_ESC}0;35m"
        DARKCYAN="${ANSI_ESC}0;36m"
        DARKWHITE="${ANSI_ESC}0;37m"

    else

        NORMAL=
        RED=
        GREEN=
        YELLOW=
        BLUE=
        GRAY=
        WHITE=

        DARKGRAY=
        DARKRED=
        DARKGREEN=
        DARKYELLOW=
        DARKBLUE=
        DARKPINK=
        DARKCYAN=
        DARKWHITE=

    fi

    ansi_color="$choice"

    export NORMAL GRAY RED GREEN YELLOW BLUE PINK CYAN WHITE DARKGRAY \
           DARKRED DARKGREEN DARKYELLOW DARKBLUE DARKPINK DARKCYAN \
           DARKWHITE SUCCESS WARNING FAILURE NOOP ON OFF ERROR ansi_color
}


e() { printf "%s" "$*"; }

warn() { e "${YELLOW}Warning:$NORMAL" "$*"$'\n' >&2 ; }
info() { e "${BLUE}II$NORMAL" "$*"$'\n' >&2 ; }
verb() { [ -z "$VERBOSE" ] || e "$*"$'\n' >&2; }
debug() { [ -z "$DEBUG" ] || e "$*"$'\n' >&2; }
err() { e "${RED}Error:$NORMAL $*"$'\n' >&2 ; }
die() { err "$@" ; exit 1; }

## equivalent of 'xargs echo' with builtins
nspc() {
    local content
    content=$(printf "%s " $(cat -))
    printf "%s" "${content::-1}"
}


get_path() { (
    IFS=:
    for d in $PATH; do
        filename="$d/$1"
        [ -f "$filename" -a -x "$filename" ] && {
            echo "$d/$1"
            return 0
        }
    done
    return 1
) }


depends() {
    ## Avoid colliding with variables that are created with depends.
    local __i __path __new_name

    for __i in "$@"; do
        if ! __path=$(get_path "$__i"); then
            __new_name="$(echo "${__i//-/_}")"
            if [ "$__new_name" != "$__i" ]; then
                depends "$__new_name"
            else
                err "dependency check: couldn't find '$__i' required command."
                exit 1
            fi
        else
            if ! test -z "$__path" ; then
                export "$(echo "${__i//- /__}")"="$__path"
            fi
        fi
    done
}


get_os() {
    local uname_output
    uname_output="$(uname -s)"
    case "${uname_output}" in
        Linux*)
            if [[ "$(< /proc/version)" =~ "@(Microsoft|WSL)" ]]; then
                e wsl
            # elif [[ "$(< /proc/version)" =~ "@(microsoft|WSL)" ]]; then
            #     e wsl2
            else
                e linux
            fi
            ;;
        Darwin*)  e mac;;
        CYGWIN*)  e cygwin;;
        MINGW*)   e mingw;;
        *)        e "UNKNOWN:${uname_output}";;
    esac
}


read-0() {
    local eof= IFS=''
    while [ "$1" ]; do
        read -r -d '' -- "$1" || eof=1
        shift
    done
    [ -z "$eof" ]
}


read-0a() {
    local eof= IFS=''
    while [ "$1" ]; do
        IFS='' read -r -d $'\n' -- "$1" || eof=1
        shift
    done
    [ -z "$eof" ]
}


p0() {
    printf "%s\0" "$@"
}


cla.normalize() {
    local letters arg i
    while [ "$#" != 0 ]; do
        arg=$1
        case "$arg" in
            --)
                p0 "$@"
                return 0
                ;;
            --*=*|-*=*)
                shift
                set -- "${arg%%=*}" "${arg#*=}" "$@"
                continue
                ;;
            --*|-?) :;;
            -*)
                letters=${arg:1}
                shift
                i=${#letters}
                while ((i--)); do
                    set -- -${letters:$i:1} "$@"
                done
                continue
                ;;
        esac
        p0 "$arg"
        shift
    done
}


docker_has_image() {
    local image="$1"
    images=$(docker images -q "$image" 2>/dev/null) || {
        err "docker images call has failed unexpectedly."
        return 1
    }
    [ -n "$images" ]
}


docker_image_id() {
    local image="$1"
    image_id=$(docker inspect "$image" --format='{{.Id}}') || return 1
    echo "$image_id"
}


##
## Compose-core common functions
##


list_compose_vars() {
    while read-0a def; do
        def="${def##* }"
        def="${def%=*}"
        p0 "$def"
    done < <(declare -p | grep "^declare -x COMPOSE_[A-Z_]\+=\"")
}


get_running_compose_containers() {
    ## XXXvlab: docker bug: there will be a final newline anyway
    docker ps --filter label="compose.service" --format='{{.ID}}'
}


get_volumes_for_container() {
    local container="$1"

    docker inspect \
           --format '{{range $mount := .Mounts}}{{$mount.Source}}{{"\x00"}}{{$mount.Destination}}{{"\x00"}}{{end}}' \
           "$container"
}


is_volume_used() {
    local volume="$1"

    while read container_id; do
        while read-0 src dst; do
            [ "$src" == "$volume" ] && return 0
        done < <(get_volumes_for_container "$container_id")
    done < <(get_running_compose_containers)
    return 1
}


_MULTIOPTION_REGEX='^((-[a-zA-Z]|--[a-zA-Z0-9-]+)(, )?)+'
_MULTIOPTION_REGEX_LINE_FILTER=$_MULTIOPTION_REGEX'(\s|=)'


##
## compose launcher functions
##


clean_unused_sessions() {
    for f in "$SESSION_DIR/"*; do
        [ -e "$f" ] || continue
        is_volume_used "$f" && continue
        ## XXXvlab: the second rmdir should not be useful
        rm -f "$f" >/dev/null || rmdir "$f" >/dev/null || {
            debug "Unexpected session remnants $f"
        }
    done
}


check_no_links_subdirs() {
    local dir
    for dir in "$@"; do
        [ -d "$dir" ] || continue
        if [ -L "$dir" ]; then
            err "Unfortunately, this compose launcher do not support yet symlinks in charm-store."
            echo "  Found symlink in charm-store: $dir" >&2
            return 1
        fi
        [ -e "$dir/metadata.yml" ] && continue
        check_no_links_subdirs "$dir"/* || return 1
    done
}


get_override() {
    local override
    override=$(get_volume_opt "$@") || return 1
    if [ -n "$override" ]; then
        if ! [ -f "$override" ]; then
            err "Invalid override of 'compose-core' detected." \
                "File '$override' does not exist on host."
            return 1
        fi
        echo "$override"
    fi
}


get_hash_image() {
    local compose_docker_image="$1" override="$2"
    {
        docker_image_id "$compose_docker_image" || {
            err "Failed to get docker image id of image '$compose_docker_image'."
            return 1
        }
        p0 ""
        [ -n "$override" ] && cat "$override"
        true
    } | hash_get
    return "${PIPESTATUS[0]}"
}


get_compose_file_opt() {
    local hash_bin="$1" override="$2" \
          cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$(p0 "$@" | hash_get)"
    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi
    shift 2
    DC_MATCH_MULTI=$(get_compose_multi_opts_list "$hash_bin" "$override") || return 1
    DC_MATCH_SINGLE=$(get_compose_single_opts_list "$hash_bin" "$override") || return 1

    while read-0 arg; do
        case "$arg" in
            "-f"|"--file")
                read-0 value
                e "$value"
                return 0
            ;;
            --*|-*)
                if str_pattern_matches "$arg" $DC_MATCH_MULTI; then
                    read-0 value
                    opts+=("$arg" "$value")
                    shift
                elif str_pattern_matches "$arg" $DC_MATCH_SINGLE; then
                    opts+=("$arg")
                else
                    debug "Unknown option '$arg'. Didn't manage to pre-parse correctly options."
                    return 1
                fi
                ;;
            *)
                return 1
                ;;
        esac
    done < <(cla.normalize "$@") | tee "$cache_file"
}


replace_compose_file_opt() {
    local hash_bin="$1" override="$2" args arg \
          cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$(p0 "$@" | hash_get)"
    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi
    debug "Replacing '-f|--file' argument in command line."
    shift 2
    DC_MATCH_MULTI=$(get_compose_multi_opts_list "$hash_bin" "$override") || return 1
    DC_MATCH_SINGLE=$(get_compose_single_opts_list "$hash_bin" "$override") || return 1

    args=()
    while read-0 arg; do
        case "$arg" in
            "-f"|"--file")
                read-0 value
                args+=("$arg" "${value##*/}")
            ;;
            --*|-*)
                if str_pattern_matches "$arg" $DC_MATCH_MULTI; then
                    read-0 value
                    args+=("$arg" "$value")
                    shift
                elif str_pattern_matches "$arg" $DC_MATCH_SINGLE; then
                    args+=("$arg")
                else
                    err "Unknown option '$arg'. Didn't manage to pre-parse correctly options."
                    return 1
                fi
                ;;
            *)
                args+=("$arg")
                while read-0 arg; do
                    args+=("$arg")
                done
                ;;
        esac
    done < <(cla.normalize "$@")
    p0 "${args[@]}" | tee "$cache_file"
}


get_compose_opts_list() {
    local hash_bin="$1" override="$2" \
          cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$1"

    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi
    debug "Pre-Launching docker to retrieve command line argument definitions."
    opts_list=()
    if [ -n "$override" ]; then
        opts_list+=("-v" "$override:/usr/local/bin/compose-core:ro")
    fi
    compose_opts_help=$(docker run "${opts_list[@]}" "$COMPOSE_DOCKER_IMAGE" --help 2>/dev/null)

    echo "$compose_opts_help" |
        grep '^Options:' -A 20000 |
        tail -n +2 |
        { cat ; echo; } |
        grep -E -m 1 "^\S*\$" -B 10000 |
        head -n -1 |
        grep -E "^\s+-" |
        sed_compat 's/\s+((((-[a-zA-Z]|--[a-zA-Z0-9-]+)( [A-Z=]+|=[^ ]+)?)(, )?)+)\s+.*$/\1/g' |
        tee "$cache_file" || return 1
}


multi_opts_filter() {
     grep -E "$_MULTIOPTION_REGEX_LINE_FILTER" |
        sed_compat "s/^($_MULTIOPTION_REGEX)(\s|=).*$/\1/g" |
        tr ',' "\n" | nspc
}


single_opts_filter() {
    grep -E -v "$_MULTIOPTION_REGEX_LINE_FILTER" |
        tr ',' "\n" | nspc
}


get_compose_multi_opts_list() {
    local hash_bin="$1" override="$2" \
          cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$1"  opts_list
    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi

    opts_list=$(get_compose_opts_list "$hash_bin" "$override") || return 1
    echo "$opts_list" | multi_opts_filter | tee "$cache_file"
}


get_compose_single_opts_list() {
    local hash_bin="$1" override="$2" \
          cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$1"  opts_list
    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi
    opts_list=$(get_compose_opts_list "$hash_bin" "$override") || return 1
    echo "$opts_list" | single_opts_filter | tee "$cache_file"
}


get_volume_opt() {
    local cache_file="$COMPOSE_LAUNCHER_CACHE/$FUNCNAME.cache.$(p0 "$@" | hash_get)"
    if [ -e "$cache_file" ]; then
        cat "$cache_file" &&
        touch "$cache_file" || return 1
        return 0
    fi

    while [ "$#" != 0 ]; do
        case "$1" in
            "-v")
                dst="${2#*:}"
                dst="${dst%:*}"
                if [ "$dst" == "/usr/local/bin/compose-core" ]; then
                    override="${2%%:*}"
                    debug "Override of compose-core found: $override"
                fi
                shift;;
            "-e"|"-w")
                shift;;
            *)
                :
                ;;
        esac
        shift
    done
    { [ -n "$override" ] && echo "$override"; } | tee "$cache_file"
}


get_tz() {
    if [ -n "$TZ" ]; then
        :
    elif [ -n "$COMPOSE_LOCAL_ROOT" ] &&  ## previous compose run
           [ -e "$COMPOSE_LOCAL_ROOT/etc/timezone" ]; then
        read -r TZ < "$COMPOSE_LOCAL_ROOT/etc/timezone"
    elif [ -e "/etc/timezone" ]; then  ## debian host system timezone
        read -r TZ < /etc/timezone
    elif [ -e "/etc/localtime" ]; then  ## redhat and macosx sys timezone
        local fullpath dirname
        fullpath="$(readlink -f /etc/localtime)"
        dirname="${fullpath%/*}"
        TZ=${TZ:-${fullpath##*/}/${dirname##*/}}
    else
        err "Timezone not found nor inferable !"
        echo "   compose relies on '/etc/timezone' or '/etc/localtime' to be present " >&2
        echo "   so as to ensure same timezone for all containers that need it." >&2
        echo >&2
        echo "   You can set a default value for compose by create issuing:" >&2
        echo >&2
        if [ -n "$COMPOSE_LOCAL_ROOT" ] && [ "$UID" != 0 ]; then
            echo "      mkdir -p $COMPOSE_LOCAL_ROOT/etc &&" >&2
            echo "      echo \"Europe/Paris\" > $COMPOSE_LOCAL_ROOT/etc/timezone" >&2
        else
            echo "      echo \"Europe/Paris\" > /etc/timezone" >&2
        fi
        echo >&2
        echo "   Of course, you can change 'Europe/Paris' value by any other valid timezone." >&2
        echo "   timezone." >&2
        echo >&2
        echo "   Notice you can also use \$TZ environment variable, but the value" >&2
        echo "   will be necessary each time you launch compose." >&2
        echo >&2
        return 1
    fi
    e "$TZ"
}


pretty_print() {
    while [ "$#" != 0 ]; do
        case "$1" in
            "-v"|"-e"|"-w")
                e "$1" "$2" "\\"$'\n'
                shift;;
            *)
                e "$1 ";;
        esac
        shift
    done
}


win_env() { "$CMD" /c "<nul set /p=%${1}%" 2>/dev/null; }
wsl_path_env() { wslpath "$(win_env "${1}")"; }


set_os() {
    OS="$(get_os)"
    case "$OS" in
        linux)
            ## Order matters, files get to override vars
            compose_config_files=(
                ## DEFAULT LINUX VARS
                /etc/default/charm
                /etc/default/datastore
                /etc/default/compose

                ## COMPOSE SYSTEM-WIDE FILES
                /etc/compose.conf
                /etc/compose.local.conf
                /etc/compose/local.conf

                ## COMPOSE USER FILES
                ~/.compose/etc/local.conf
                ~/.compose.conf
            )

            COMPOSE_LOCAL_ROOT=${COMPOSE_LOCAL_ROOT:-"$HOME/.compose"}
            COMPOSE_VAR=${COMPOSE_VAR:-/var/lib/compose}
            COMPOSE_CACHE=${COMPOSE_CACHE:-/var/cache/compose}
            DATASTORE=${DATASTORE:-/srv/datastore/data}
            CONFIGSTORE=${CONFIGSTORE:-/srv/datastore/config}
            if [ "$UID" == 0 ]; then
                SESSION_DIR=${SESSION_DIR:-"$COMPOSE_VAR"/sessions}
                CHARM_STORE=${CHARM_STORE:-/srv/charm-store}
                TZ_PATH=${TZ_PATH:-"$COMPOSE_VAR"/timezones}
                COMPOSE_LAUNCHER_CACHE=${COMPOSE_LAUNCHER_CACHE:-"$COMPOSE_CACHE"}
            else
                SESSION_DIR=${SESSION_DIR:-"$COMPOSE_LOCAL_ROOT"/sessions}
                CHARM_STORE=${CHARM_STORE:-"$HOME"/.charm-store}
                TZ_PATH=${TZ_PATH:-"$COMPOSE_LOCAL_ROOT"/timezones}
                COMPOSE_LAUNCHER_CACHE=${COMPOSE_LAUNCHER_CACHE:-"$COMPOSE_LOCAL_ROOT"/cache}
            fi
            ;;
        mac)
            ## Order matters, files get to override vars
            compose_config_files=(
                ## COMPOSE SYSTEM-WIDE FILES
                /etc/compose.conf
                /etc/compose.local.conf
                /etc/compose/local.conf

                ## COMPOSE USER FILES
                ~/.compose/etc/local.conf
                ~/.compose.conf
            )

            COMPOSE_LOCAL_ROOT=${COMPOSE_LOCAL_ROOT:-"$HOME/.compose"}
            COMPOSE_VAR=${COMPOSE_VAR:-"$COMPOSE_LOCAL_ROOT"/lib}
            COMPOSE_CACHE=${COMPOSE_CACHE:-"$COMPOSE_LOCAL_ROOT"/cache}
            SESSION_DIR=${SESSION_DIR:-"$COMPOSE_LOCAL_ROOT"/sessions}
            DATASTORE=${DATASTORE:-"$COMPOSE_LOCAL_ROOT"/data}
            CONFIGSTORE=${CONFIGSTORE:-"$COMPOSE_LOCAL_ROOT"/config}
            CHARM_STORE=${CHARM_STORE:-"$HOME"/.charm-store}
            TZ_PATH=${TZ_PATH:-"$COMPOSE_LOCAL_ROOT"/timezones}
            COMPOSE_LAUNCHER_CACHE=${COMPOSE_LAUNCHER_CACHE:-"$COMPOSE_LOCAL_ROOT"/cache}
            ;;
        wsl)
            type -p cmd.exe >/dev/null || {
                die "cmd.exe is not found in \$PATH."
            }

            CMD=$(type -p cmd.exe >/dev/null) || {
                for p in {/mnt,}/c/WINDOWS/SYSTEM32; do
                    if [ -x "$p"/cmd.exe ]; then
                        CMD="$p"/cmd.exe
                    fi
                done
                if [ -z "$CMD" ]; then
                    die "cmd.exe is not found in \$PATH." \
                        "And could not find it in standard directories."
                fi
            }

            WIN_HOME="$(wsl_path_env UserProfile)"
            WIN_PROGRAM_FILES="$(wsl_path_env ProgramFiles)"

            ## Order matters, files get to override vars
            compose_config_files=(
                ## COMPOSE SYSTEM-WIDE FILES
                /etc/compose.conf
                /etc/compose.local.conf
                /etc/compose/local.conf

                ## COMPOSE USER FILES
                ~/.compose/etc/local.conf
                ~/.compose.conf

                ## COMPOSE USER FILES
                {~,"$WIN_HOME"}/.compose/etc/local.conf
                {~,"$WIN_HOME"}/.compose.conf
            )

            APPDATA_BASE="$WIN_HOME/AppData/Local/Compose"

            COMPOSE_LAUNCHER_APP_DIR="$WIN_PROGRAM_FILES/Compose"
            COMPOSE_LOCAL_ROOT=${COMPOSE_LOCAL_ROOT:-"$APPDATA_BASE/Launcher"}

            COMPOSE_VAR=${COMPOSE_VAR:-"$APPDATA_BASE/lib"}
            COMPOSE_CACHE=${COMPOSE_CACHE:-"$APPDATA_BASE/cache"}

            DATASTORE=${DATASTORE:-"$APPDATA_BASE/data"}
            CONFIGSTORE=${CONFIGSTORE:-"$APPDATA_BASE/config"}

            mkdir -p "$COMPOSE_VAR" "$COMPOSE_CACHE" "$DATASTORE" "$CONFIGSTORE" || return 1
            SESSION_DIR=${SESSION_DIR:-"$COMPOSE_LOCAL_ROOT"/sessions}
            CHARM_STORE=${CHARM_STORE:-"$COMPOSE_LAUNCHER_APP_DIR/charm-store"}
            TZ_PATH=${TZ_PATH:-"$COMPOSE_LOCAL_ROOT"/timezones}
            COMPOSE_LAUNCHER_CACHE=${COMPOSE_LAUNCHER_CACHE:-"$COMPOSE_LOCAL_ROOT"/cache}
            ;;
        *)
            echo "System '$os' not supported yet." >&2
            return 1
            ;;
    esac
}


mk_docker_run_options() {

    set_os || return 1

    docker_run_opts=("-v" "/var/run/docker.sock:/var/run/docker.sock")

    ##
    ## Load config files
    ##

    if [ -z "$DISABLE_SYSTEM_CONFIG_FILE" ]; then
        ## XXXvlab: should provide YML config opportunities in possible parent dirs ?
        ##  userdir ? and global /etc/compose.yml ?
        for cfgfile in "${compose_config_files[@]}"; do
            [ -e "$cfgfile" ] || continue
            docker_run_opts+=("-v" "$cfgfile:$cfgfile:ro")
            debug "Loading config file '$cfgfile'."
            . "$cfgfile"
        done
    else
        docker_run_opts+=("-e" "DISABLE_SYSTEM_CONFIG_FILE=$DISABLE_SYSTEM_CONFIG_FILE")
    fi

    mkdir -p "$COMPOSE_LAUNCHER_CACHE" || return 1

    ## get TZ value and prepare TZ_PATH
    TZ=$(get_tz) || return 1
    mkdir -p "${TZ_PATH}"
    TZ_PATH="${TZ_PATH}/$(e "$TZ" | hash_get | cut -c 1-8)" || return 1
    [ -e "$TZ_PATH" ] || e "$TZ" > "$TZ_PATH"

    ## CACHE/DATA DIRS
    docker_run_opts+=("-v" "$COMPOSE_VAR:/var/lib/compose")
    docker_run_opts+=("-v" "$COMPOSE_CACHE:/var/cache/compose")

    docker_run_opts+=("-v" "$TZ_PATH:/etc/timezone:ro")

    ##
    ## Checking vars
    ##

    ## CHARM_STORE
    [ -e "$CHARM_STORE" ] || mkdir -p "$CHARM_STORE" || return 1
    [ -L "$CHARM_STORE" ] && {
        CHARM_STORE=$(readlink -f "$CHARM_STORE") || return 1
    }

    docker_run_opts+=(
        "-v" "$CHARM_STORE:/srv/charm-store:ro"
        "-e" "CHARM_STORE=/srv/charm-store"
        "-e" "HOST_CHARM_STORE=$CHARM_STORE"
    )
    check_no_links_subdirs "$CHARM_STORE"/* || return 1

    ## DATASTORE and CONFIGSTORE
    docker_run_opts+=(

        "-v" "$DATASTORE:/srv/datastore/data:rw"
        "-e" "DATASTORE=/srv/datastore/data"
        "-e" "HOST_DATASTORE=$DATASTORE"

        "-v" "$CONFIGSTORE:/srv/datastore/config:rw"
        "-e" "CONFIGSTORE=/srv/datastore/config"
        "-e" "HOST_CONFIGSTORE=$CONFIGSTORE"
    )

    if [ "$OS" == "linux" ]; then
        [ -d "$HOME/.docker" ] && \
            docker_run_opts+=("-v" "$HOME/.docker:/root/.docker:ro")
        [ -d "$HOME/.ssh" ] && \
            docker_run_opts+=("-v" "$HOME/.ssh:/root/.ssh:ro")
        [ -d "/etc/ssh" ] && \
            docker_run_opts+=("-v" "/etc/ssh:/etc/ssh:ro")
    fi

    COMPOSE_DOCKER_IMAGE=${COMPOSE_DOCKER_IMAGE:-docker.0k.io/compose}
    docker_run_opts+=("-e" "COMPOSE_DOCKER_IMAGE=$COMPOSE_DOCKER_IMAGE")

    if ! docker_has_image "$COMPOSE_DOCKER_IMAGE"; then
        docker pull "$COMPOSE_DOCKER_IMAGE" || return 1
    fi



    COMPOSE_LAUNCHER_BIN=$(readlink -f "${BASH_SOURCE[0]}")
    docker_run_opts+=("-v" "$COMPOSE_LAUNCHER_BIN:/usr/local/bin/compose")
    COMPOSE_LAUNCHER_BIN_OVERRIDE=$(get_override "${docker_run_opts[@]}") || return 1
    COMPOSE_LAUNCHER_HASH=$(
        get_hash_image "$COMPOSE_DOCKER_IMAGE" "$COMPOSE_LAUNCHER_BIN_OVERRIDE") || return 1

    while read-0 var; do
        case "$var" in
            COMPOSE_YML_FILE|COMPOSE_LAUNCHER_BIN|COMPOSE_DOCKER_IMAGE|\
                COMPOSE_LAUNCHER_OPTS|COMPOSE_VAR|COMPOSE_CACHE)
                :
                ;;
            *)
                docker_run_opts+=("-e" "$var=${!var}")
                ;;
        esac
    done < <(list_compose_vars)

    ARG_COMPOSE_FILE=$(
        get_compose_file_opt "$COMPOSE_LAUNCHER_HASH" "$COMPOSE_LAUNCHER_BIN_OVERRIDE" \
        "$@") || return 1

    compose_file="${ARG_COMPOSE_FILE:-$COMPOSE_YML_FILE}"
    if [ -z "$compose_file" ]; then

        ## Find a compose.yml in parents
        debug "No config file specified on command line arguments"
        debug "Looking for 'compose.yml' in self and parents.."
        if parent=$(while true; do
                        [ -e "./compose.yml" ] && {
                            echo "$PWD"
                            exit 0
                        }
                        [ "$PWD" == "/" ] && return 1
                        cd ..
                    done
                 ); then
            compose_file="$(realpath "$parent"/"compose.yml")"
            debug "  .. found '$compose_file'"
        else
            debug "  .. not found."
        fi
    fi

    if [ -z "$compose_file" ] && [ "${DEFAULT_COMPOSE_FILE+x}" ]; then
        debug "Using \$DEFAULT_COMPOSE_FILE value '$DEFAULT_COMPOSE_FILE' as compose file."
        compose_file="$DEFAULT_COMPOSE_FILE"
    fi

    if [ -n "$compose_file" ]; then
        if ! [ -f "$compose_file" ]; then
            die "Specified compose file '$compose_file' not found."
        fi
        compose_file="$(realpath "$compose_file")"
        if [ "$OS" == "wsl" ]; then
            ## Docker host is not same linux than WSL, so
            ## access to root files are not the same.
            ##YYYvlab, check on cp where is the base
            dst="$COMPOSE_LAUNCHER_CACHE/compose.$(hash_get < "$compose_file").yml"
            cp "$compose_file" "$dst"
            ## docker host start with /c/... whereas WSL could start with /mnt/c/...
            local="$dst"
        else
            local="$compose_file"
        fi

        parent_dir="${compose_file%/*}"
        docker_path=/var/lib/compose/root/${parent_dir##*/}/${compose_file##*/}
        docker_run_opts+=(
            "-e" "COMPOSE_YML_FILE=${compose_file##*/}"
            "-v" "${local}:${docker_path}:ro"
            "-w" "${docker_path%/*}"
        )
    else
        docker_path=/var/lib/compose/root
        docker_run_opts+=(
            "-w" "${docker_path}"
        )
    fi

    clean_unused_sessions
    filename=$(mktemp -p /tmp/ -t launch_opts-XXXXXXXXXXXXXXXX)
    p0 "${docker_run_opts[@]}" > "$filename"
    hash=$(hash_get < "$filename") || return 1
    src="$SESSION_DIR/$UID-$hash"
    dest="/var/lib/compose/sessions/$UID-$hash"
    additional_docker_run_opts=(
        "-v" "$SESSION_DIR/$UID-$hash:$dest:ro"
        "-e" "COMPOSE_LAUNCHER_OPTS=$dest"
        "-e" "COMPOSE_LAUNCHER_BIN=$COMPOSE_LAUNCHER_BIN"
    )
    p0 "${additional_docker_run_opts[@]}" >> "$filename"
    docker_run_opts+=("${additional_docker_run_opts[@]}")

    ## keep also some env vars:
    for var in ARG_COMPOSE_FILE COMPOSE_DOCKER_IMAGE COMPOSE_LAUNCHER_{BIN_OVERRIDE,HASH}; do
        p0 "!env:$var=${!var}"
    done >> "$filename"

    mkdir -p "$SESSION_DIR" || return 1
    mv -f "$filename" "$src" || return 1

}


load_env() {
    docker_run_opts=()
    if [ -z "$COMPOSE_LAUNCHER_OPTS" ]; then
        mk_docker_run_options "$@" || return 1
    else
        set_os || return 1
        while read-0 opt; do
            if [[ "$opt" == "!env:"* ]]; then
                opt="${opt##!env:}"
                var="${opt%%=*}"
                value="${opt#*=}"
                debug "Loading var: $var=$value"
                export "$var"="$value"
            else
                docker_run_opts+=("$opt")
            fi
        done < <(cat "$COMPOSE_LAUNCHER_OPTS")
    fi
}

show_env() {
    echo "${WHITE}Environment:${NORMAL}"
    echo "  COMPOSE_DOCKER_IMAGE: $COMPOSE_DOCKER_IMAGE"
    echo "  CHARM_STORE: $CHARM_STORE"
    echo "  DATASTORE: $DATASTORE"
    echo "  CONFIGSTORE: $CONFIGSTORE"
    echo "  COMPOSE_VAR: $COMPOSE_VAR"
    echo "  COMPOSE_CACHE: $COMPOSE_CACHE"
    echo "  COMPOSE_LAUNCHER_CACHE: $COMPOSE_LAUNCHER_CACHE"
    echo "  SESSION_DIR: $SESSION_DIR"
    echo "  TZ_PATH: $TZ_PATH"
}


run() {
    local os docker_run_opts
    load_env "$@" || return 1

    [ -n "$DEBUG" ] && show_env >&2

    if [ -n "$ARG_COMPOSE_FILE" ]; then
        array_read-0 cmd_args < \
            <(replace_compose_file_opt "$COMPOSE_LAUNCHER_HASH" \
                                       "$COMPOSE_LAUNCHER_BIN_OVERRIDE" \
                                       "$@")
        set -- "${cmd_args[@]}"
    fi

    [ -t 0 ] && docker_run_opts+=("-i")
    [ -t 1 ] && docker_run_opts+=("-t")

    debug "${WHITE}Launching:${NORMAL}"
    if [ -n "$DEBUG" ] || [ -n "$DRY_RUN" ]; then
        echo "docker run --rm \\"
        pretty_print "${docker_run_opts[@]}" | sed_compat 's/^/    /g;s/([^\])$/\1\\\n/g'
        if [ -z "$ENTER" ]; then
            printf "%s\n" "    ${COMPOSE_DOCKER_IMAGE} \\"
            printf "       "
            printf "%s " "$@"
            printf "\n"
        else
            echo "    --entrypoint bash \\"
            echo "    ${COMPOSE_DOCKER_IMAGE}"
        fi
    fi | { if [ -n "$DEBUG" ]; then sed_compat 's/^/  /g'; else cat; fi } >&2
    if [ -z "$DRY_RUN" ]; then
        debug "${WHITE}Execution:${NORMAL}"
        if [ -z "$ENTER" ]; then
            exec docker run --rm "${docker_run_opts[@]}" "${COMPOSE_DOCKER_IMAGE}" "$@"
        else
            exec docker run --rm "${docker_run_opts[@]}" \
                 --entrypoint bash \
                 "${COMPOSE_DOCKER_IMAGE}"
        fi
    fi
}


[ -n "$SOURCED" ] && return 0


##
## Code
##


depends docker cat readlink sed realpath tee sed grep tail

ansi_color "${ansi_color:-tty}"

if [ "$SHOW_ENV" ]; then
    load_env "$@" || return 1
    show_env >&2
    exit 0
fi

if [ "$SHOW_CONFIG_LOCATIONS" ]; then
    set_os || return 1
    echo "compose will read these files if existing in the given order:"
    for loc in "${compose_config_files[@]}"; do
        echo " - $loc"
    done
    exit 0
fi

run "$@"