#! /bin/bash
# vim: shiftwidth=4 tabstop=4 expandtab

#
# Bash helpers
#
AUTO=0
FORCE=0
function confirm() {
    local question="$*"
    [[ -z "$question" ]] && question="Do you want to continue"
    [[ $AUTO -eq 1 ]] && return 0
    echo -ne "$question [y/N]? "
    local a
    read -r a
    [[ "$a" == "y" ]] || [[ "$a" == "Y" ]] && echo && return 0
    return 1
}

function check_hyp() {
    local hyp
    for hyp in ${HYPS:-}; do
        [[ "$hyp" == "$1" ]] && return 0
    done
    return 1
}

function check_regex() {
  [[ $(grep -Ec "$2" <<< "$1") -eq 1 ]] && return 0
  return 1
}

function check_int() {
    check_regex "$1" '^-?[0-9]+$' || return 1
    [[ -n "$2" ]] && [[ $1 -lt $2 ]] && return 1
    [[ -n "$3" ]] && [[ $1 -gt $3 ]] && return 1
    return 0
}

function check_volume_name() {
    check_regex "$1" '^[a-zA-Z0-9_\-]+$' && return 0
    return 1
}

function check_volume_path() {
    check_regex "$1" '^[a-zA-Z0-9_\-]+/[a-zA-Z0-9_\-]+$' && return 0
    return 1
}

function generate_mac() {
    printf '00:16:3e:%02X:%02X:%02X' $(( RANDOM%256 )) $(( RANDOM%256 )) $(( RANDOM%256 ))
}

function in_array() {
    local needle=$1 el
    shift
    for el in "$@"; do
        [[ "$el" = "$needle" ]] && return 0
    done
    return 1
}

function implode() {
    local d=${1-} f=${2-}
    if shift 2; then
        printf %s "$f" "${@/#/$d}"
    fi
}

function explode() {
    local output_var=$1 separator=$2
    declare -ga "$output_var=()"
    mapfile -t "$output_var" < <( tr "$separator" '\n' <<< "${@:3}" | grep -v '^$' )
}

function array_filter() {
    local values=() x=0 v
    for v in "$@"; do
        if [[ "$v" == "--" ]]; then
            x=1
        elif [[ $x -eq 0 ]]; then
            values+=( "$v" )
        else
            mapfile -t values < <( printf '%s\n' "${values[@]}" | grep -Ev "^${v}$" )
        fi
    done
    printf '%s\n' "${values[@]}"
}

function array_grep() {
    local output_var=$1 arg args=() values=() x=0
    for arg in "${@:2}"; do
        if [[ "$arg" == "--" ]]; then
            x=1
        elif [[ $x -eq 0 ]]; then
            args+=( "$arg" )
        else
            values+=( "$arg" )
        fi
    done
    [[ ${#values[@]} -eq 0 ]] && eval "values=( \"\${${output_var}[@]}\" )"
    mapfile -t "$output_var" < <( { IFS=$'\n'; echo "${values[*]}"; } | grep "${args[@]}" )
}

declare -ra _FORMAT_SIZE_UNITS=( tb gb mb kb b )
declare -rA _FORMAT_SIZE_UNITS_FACTOR=(
    ["tb"]=1099511627776 ["gb"]=1073741824 ["mb"]=1048576 ["kb"]=1024 ["b"]=1 )
function format_size() {
    local size="" unit=kb allow_zero=0 negative=0 opt
    for opt in "$@"; do
        opt="${opt,,}"
        if [[ ${#opt} -gt 2 ]] && \
            [[ "${_FORMAT_SIZE_UNITS_FACTOR[${opt:2}]:-null}" != "null" ]]; then
            unit=${opt:2}
        elif [[ ${#opt} -gt 1 ]] && \
            [[ "${_FORMAT_SIZE_UNITS_FACTOR[${opt:1}]:-null}" != "null" ]]; then
            unit=${opt:1}
        elif [[ "$opt" == "--allow-zero" ]] || [[ "$opt" == "-z" ]]; then
            allow_zero=1
        elif [[ -z "$size" ]]; then
            size=$opt
            [[ "$size" == "null" ]] && echo -n null && return
            check_int "$size" || { echo -n "format_size: invalid value '$size'"; return 1; }
        else
            echo -n "format_size: invalid parameter '$opt'"
            return 1
        fi
    done
    if [[ $size -eq 0 ]]; then
        [[ $allow_zero -eq 0 ]] && return
        echo -n "0${_FORMAT_SIZE_UNITS[${#_FORMAT_SIZE_UNITS[@]} - 1]}"
    elif [[ $size -lt 0 ]]; then
        (( size=size*-1 ))
        negative=1
    fi

    (( size=size*${_FORMAT_SIZE_UNITS_FACTOR[$unit]} ))
    for unit in "${_FORMAT_SIZE_UNITS[@]}"; do
        [[ $size -lt ${_FORMAT_SIZE_UNITS_FACTOR[$unit]} ]] && continue
        if [[ $size -eq ${_FORMAT_SIZE_UNITS_FACTOR[$unit]} ]]; then
            size=1
        else
            size=$( echo "scale=1; $size/${_FORMAT_SIZE_UNITS_FACTOR[$unit]}"|bc|sed 's/\.0$//' )
        fi
        [[ $negative -eq 1 ]] && size=$( echo "$size*-1"|bc )
        echo -n "${size}${unit^^}"
        return
    done
}

function format_duration {
    local t=$1
    local d=$((t/60/60/24))
    local h=$((t/60/60%24))
    local m=$((t/60%60))
    local s=$((t%60))
    [[ $d -ne 0 ]] && printf '%d days and ' $d
    printf '%02d:%02d:%02d' $h $m $s
}

function format_vm_state() {
    case "$1" in
        1)
            echo "running"
            ;;
        3)
            echo "paused"
            ;;
        5)
            echo "shut off"
            ;;
        *)
            echo "UNKNOWN ($1)"
            ;;
    esac
}

#
# JSON data helpers
#

function json_get() {
    local variable key default json=0 filepath idx=1
    while [[ $idx -le $# ]]; do
        arg=${!idx}
        case "$arg" in
            -j|--json)
                json=1
                ;;
            -f|--filepath)
                ((idx++))
                filepath=${!idx}
                ;;
            -h|--help)
                echo "usage: json_get [variable|-f /path/to/file.json] [key] [default]"
                echo "  variable                           Input/output JSON global variable name"
                echo "  -f|--filepath /path/to/file.json   Input/output JSON file path"
                echo "  [key]                              The JSON key to set"
                echo "  [default]                          The default value (default: empty string)"
                echo "  -j|--json                          Consider specified default value as a JSON parsable"
                return 0
                ;;
            *)
                if [[ ! -v filepath ]] && [[ ! -v variable ]]; then
                    local -n variable="$arg"
                elif [[ ! -v key ]]; then
                    key="$arg"
                elif [[ ! -v default ]]; then
                    default="$arg"
                fi
        esac
        ((idx++))
    done

    local -a jq_args=(-r)
    if [[ "$json" -eq 1 ]]; then
        jq_args+=( "--argjson" "default" "$default" )
    else
        jq_args+=( "--arg" "default" "$default" )
    fi

    jq_args+=( "$key ? // \$default" )

    if [[ -v filepath ]]; then
        jq "${jq_args[@]}" < "$filepath"
    else
        jq "${jq_args[@]}" <<< "$variable"
    fi
}

function json_set() {
    local variable key value json=0 append=0 extend=0 filepath multiple=0 idx=1
    while [[ $idx -le $# ]]; do
        arg=${!idx}
        case "$arg" in
            -j|--json)
                json=1
                ;;
            -a|--append)
                append=1
                ;;
            -e|--extend)
                extend=1
                if [[ "$json" -eq 0 ]]; then
                    local -a value=( "$value" )
                    multiple=1
                    json=1
                fi
                ;;
            -f|--filepath)
                ((idx++))
                filepath=${!idx}
                ;;
            -h|--help)
                echo "usage: json_set [variable|-f /path/to/file.json] [key] [value1 value2 ...]"
                echo "  variable                           Input/output JSON global variable name"
                echo "  -f|--filepath /path/to/file.json   Input/output JSON file path"
                echo "  [key]                              The JSON key to set"
                echo "  [value]                            The value. If multiple values provided, consider as an array of values."
                echo "  -j|--json                          Consider specified value as a JSON parsable"
                echo "  -a|--append                        Append value to an existing array"
                echo "  -e|--extend                        Extend an existing array (Auto-consider specified value as a JSON parsable)"
                return 0
                ;;
            *)
                if [[ ! -v filepath ]] && [[ ! -v variable ]]; then
                    local -n variable="$arg"
                elif [[ ! -v key ]]; then
                    key="$arg"
                elif [[ ! -v value ]]; then
                    value="$arg"
                else
                    if [[ "$multiple" -eq 0 ]]; then
                        local -a value=( "$value" )
                        multiple=1
                    fi
                    value+=( "$arg" )
                fi
        esac
        ((idx++))
    done

    local -a jq_args
    if [[ "$multiple" -eq 1 ]]; then
        value=$( jo -a "${value[@]}" )
        json=1
    fi
    if [[ "$json" -eq 1 ]]; then
        jq_args+=( "--argjson" "value" "$value" )
    else
        jq_args+=( "--arg" "value" "$value" )
    fi

    if [[ "$append" -eq 1 ]]; then
        jq_args+=( "$key += [\$value]" )
    elif [[ "$extend" -eq 1 ]]; then
        jq_args+=( "$key += \$value" )
    else
        jq_args+=( "$key = \$value" )
    fi

    if [[ "${filepath:-UNDEFINED}" == "UNDEFINED" ]]; then
        new_value=$( jq "${jq_args[@]}" <<< "$variable" <<< "$variable" ) || \
            return 1
        variable=$new_value
        return 0
    fi

    new_value=$( jq "${jq_args[@]}" < "$filepath" ) || \
        return 1
    cat <<< "$new_value" > "$filepath"
}

function dump_assoc_array_to_json() {
    declare -n dict=$1
    local key
    for key in "${!dict[@]}"; do
        printf '%s\0%s\0' "$key" "${dict[$key]}"
    done |
    jq -Rs '
          split("\u0000")
          | . as $a
          | reduce range(0; length/2) as $i
              ({}; . + {($a[2*$i]): ($a[2*$i + 1]|fromjson? // .)})'
}

function dump_assoc_array_to_json_file() {
    dump_assoc_array_to_json "$1" > "$2"
}

function load_assoc_array_from_json() {
    local input="$1"
    local output_var=$2
    local filter='[to_entries[]|"["+(.key|@sh)+"]="+(.value|@sh)]|"("+join(" ")+")"'
    declare -Ag "$output_var=$(jq --join-output "${filter}" <<< "${input}")"
}

function load_assoc_array_from_json_file() {
    load_assoc_array_from_json "$( cat "$1" )" "$2"
}

function print_json_table() {
    if [[ $# -eq 0 ]]; then
        jq -r '
            def capitalize: (.[0:1] | ascii_upcase) + .[1:];
            (.[0] | ([keys[] | capitalize | .] | (., map(length*"-")))),
            (.[] | ([keys[] as $k | .[$k]]))
        | @tsv' | column -t -s $'\t'
    else
        jq -r "
            ([\"$( implode "\", \"" "${@^}" )\"] | (., map(length*\"-\"))),
            (.[] | [.\"$( implode "\", .\"" "$@" )\"])
        | @tsv" | column -t -s$'\t'
    fi
}

JQ_FORMAT_SIZE='
    def format_size:
        if . < 1024 then
        . | tostring + "KB"
        elif . < 1024 * 1024 then
        (. / 1024) | floor | tostring + "MB"
        elif . < 1024 * 1024 * 1024 then
        (. / (1024 * 1024)) | floor | tostring + "GB"
        else
        (. / (1024 * 1024 * 1024)) | floor | tostring + "TB"
        end;
'

#
# Styled text printing helper
#
declare -rA COLORS=(
    [black]=30 [red]=31 [green]=32 [brown]=33 [blue]=34 [purple]=35 [cyan]=36 [light_grey]=37
    [default]=39 [dark_grey]=90 [light_red]=91 [light_green]=92 [yellow]=93 [light_blue]=94
    [light_purple]=95 [light_cyan]=96 [white]=97
)
declare -rA BACKGROUND_COLORS=(
    [black]=40 [red]=41 [green]=42 [brown]=43 [blue]=44 [purple]=45 [cyan]=46 [ligth_grey]=47
    [default]=49 [dark_grey]=100 [light_red]=101 [light_green]=102 [yellow]=103 [light_blue]=104
    [light_purple]=105 [light_cyan]=106 [white]=107
)
declare -rA TEXT_STYLES=(
    [normal]=0 [bold]=1 [dim]=2 [italic]=3 [underline]=4 [blink]=5 [inverted_colors]=7 [hidden]=8
    [strikethrough]=9
)
declare -rA RESET_STYLES=(
    [all]=0 [bold]=21 [dim]=22 [underline]=24 [blink]=25 [inverted_colors]=27 [hidden]=28
)

function sprint() {
    local idx=1 opt value no_newline=0 output=""
    local -a styles=() text=()

    __sprint() {
        [[ -n "$output" ]] && output+=" "
        [[ "${#styles}" -gt 0 ]] && \
            output+="\e[$(implode ';' "${styles[@]}")m" && \
            styles=()
        output+="${text[*]}"
        text=()
    }
    while [[ $idx -le $# ]]; do
        opt=${!idx}
        case $opt in
            -c|--color)
                [[ "${#text}" -gt 0 ]] && __sprint
                ((idx++))
                value="${!idx,,}"
                if [[ "${COLORS[$value]:-null}" == "null" ]]; then
                    echo -n "sprint: invalid color '${!idx}'"
                    return 1
                fi
                styles+=( "${COLORS[$value]}" )
                ;;
            -b|--bg)
                [[ "${#text}" -gt 0 ]] && __sprint
                ((idx++))
                value="${!idx,,}"
                if [[ "${BACKGROUND_COLORS[$value]:-null}" == "null" ]]; then
                    echo -n "sprint: invalid background color '${!idx}'"
                    return 1
                fi
                styles+=( "${BACKGROUND_COLORS[$value]}" )
                ;;
            -s|--style)
                [[ "${#text}" -gt 0 ]] && __sprint
                ((idx++))
                value="${!idx,,}"
                if [[ "${TEXT_STYLES[$value]:-null}" == "null" ]]; then
                    echo -n "sprint: invalid text style '${!idx}'"
                    return 1
                fi
                styles+=( "${TEXT_STYLES[$value]}" )
                ;;
            -r|--reset)
                [[ "${#text}" -gt 0 ]] && __sprint
                ((idx++))
                value="${!idx,,}"
                if [[ "${RESET_STYLES[$value]:-null}" == "null" ]]; then
                    echo -n "sprint: invalid reset option '${!idx}'"
                    return 1
                fi
                output+="\e[${RESET_STYLES[$value]}m"
                ;;
            -n)
                no_newline=1
                ;;
            -h|--help)
                echo "usage: sprint [-n] [-c color] [-b color] [-s style] [words] [-r what] [...]"
                echo "  -c / --color [color]   Colored text. Available colors:"
                implode ", " "${!COLORS[@]}" | \
                    fold -w 53 -s | sed "s/^/                         /g"
                echo
                echo "  -b / --bg [color]      Text background color. Available colors:"
                implode ", " "${!BACKGROUND_COLORS[@]}" | \
                    fold -w 53 -s | sed "s/^/                         /g"
                echo
                echo "  -s / --style [style]   Text style. Available styles:"
                implode ", " "${!TEXT_STYLES[@]}" | \
                    fold -w 53 -s | sed "s/^/                         /g"
                echo
                echo "  -r / --reset [what]    Reset some previously specified styles. Available reset clauses:"
                implode ", " "${!RESET_STYLES[@]}" | \
                    fold -w 53 -s | sed "s/^/                         /g"
                echo
                echo "  -n                     Do not add new line"
                echo "  [words]                Words that compose the text"
                ;;
            *)
                text+=( "$opt" )
        esac
        ((idx++))
    done
    __sprint
    output+="\e[${RESET_STYLES[all]}m"
    if [[ "$no_newline" -eq 1 ]]; then
        echo -en "$output"
    else
        echo -e "$output"
    fi
}
