#!/bin/sh
#
# Copyright (C) 2012, 2014  Etersoft
# Copyright (C) 2012, 2014  Vitaly Lipatov <lav@etersoft.ru>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

# copied from /etc/init.d/outformat (ALT Linux)


# FIXME on Android: FIX ME! implement ttyname_r() bionic/libc/bionic/stubs.c:366
inputisatty()
{
    # check stdin
    #tty -s 2>/dev/null
    test -t 0
}

isatty()
{
    # check stdout
    test -t 1
}

isatty2()
{
    # check stderr
    test -t 2
}

check_tty()
{
    isatty2 || return

    # Set a sane TERM required for tput
    [ -n "$TERM" ] || TERM=dumb
    export TERM

    check_core_commands

    # grep -E from busybox may not --color
    # grep -E from MacOS print help to stderr
    if grep -E --help 2>&1 | grep -q -- "--color" ; then
        export EGREPCOLOR="--color"
    fi

    is_command tput || return
    # FreeBSD does not support tput -S
    echo | a= tput -S >/dev/null 2>/dev/null || return
    USETTY="tput -S"
}

: ${BLACK:=0} ${RED:=1} ${GREEN:=2} ${YELLOW:=3} ${BLUE:=4} ${MAGENTA:=5} ${CYAN:=6} ${WHITE:=7}

set_boldcolor()
{
    [ -n "$USETTY" ] || return
    {
        echo bold
        echo setaf $1
    } | $USETTY
}

set_color()
{
    [ -n "$USETTY" ] || return
    {
        echo setaf $1
    } | $USETTY
}

restore_color()
{
    [ -n "$USETTY" ] || return
    {
        echo op; # set Original color Pair.
        echo sgr0; # turn off all special graphics mode (bold in our case).
    } | $USETTY
}

# Check if terminal supports OSC 8 hyperlinks (cached)
is_osc8_supported()
{
    # Return cached result
    [ -n "$__OSC8_SUPPORTED" ] && return 0
    [ -n "$__OSC8_NOT_SUPPORTED" ] && return 1
    # Check terminal
    if [ -z "$USETTY" ] ; then
        __OSC8_NOT_SUPPORTED=1 ; return 1
    fi
    # VTE-based terminals (GNOME Terminal, Tilix, etc.)
    if [ -n "$VTE_VERSION" ] && [ "$VTE_VERSION" -ge 5000 ] 2>/dev/null ; then
        __OSC8_SUPPORTED=1 ; return 0
    fi
    # iTerm2
    [ "$TERM_PROGRAM" = "iTerm.app" ] && { __OSC8_SUPPORTED=1 ; return 0 ; }
    # Windows Terminal
    [ -n "$WT_SESSION" ] && { __OSC8_SUPPORTED=1 ; return 0 ; }
    # Konsole
    [ -n "$KONSOLE_VERSION" ] && { __OSC8_SUPPORTED=1 ; return 0 ; }
    # kitty
    [ -n "$KITTY_WINDOW_ID" ] && { __OSC8_SUPPORTED=1 ; return 0 ; }
    # foot
    { [ "$TERM" = "foot" ] || [ "$TERM" = "foot-extra" ] ; } && { __OSC8_SUPPORTED=1 ; return 0 ; }
    # WezTerm
    [ "$TERM_PROGRAM" = "WezTerm" ] && { __OSC8_SUPPORTED=1 ; return 0 ; }
    __OSC8_NOT_SUPPORTED=1
    return 1
}

# Make clickable link: make_osc8_link URL [TEXT]
# If TEXT is omitted, URL is used as text
# Without OSC 8 support: "TEXT (URL)" or just "URL" if no text
make_osc8_link()
{
    local url="$1"
    local text="$2"
    if is_osc8_supported ; then
        printf '\033]8;;%s\033\\%s\033]8;;\033\\' "$url" "${text:-$url}"
    elif [ -n "$text" ] ; then
        printf '%s (%s)' "$text" "$url"
    else
        printf '%s' "$url"
    fi
}

echover()
{
    [ -z "$verbose" ] && return
    echog "$*" >&2
}

# echo string without EOL
echon()
{
    # default /bin/sh on MacOS does not recognize -n
    echo -n "$*" 2>/dev/null || a= /bin/echo -n "$*"
}


# Print command line and run command line
showcmd()
{
    if [ -z "$quiet" ] ; then
        set_boldcolor $GREEN
        local PROMTSIG="\$"
        is_root && PROMTSIG="#"
        echo " $PROMTSIG $*" | tr '\t' ' '
        restore_color
    fi >&2
}

# Print command
echocmd()
{
    set_boldcolor $GREEN
    local PROMTSIG="\$"
    is_root && PROMTSIG="#"
    echo -n "$PROMTSIG $*"
    restore_color
}

# Print command line and run command line
docmd()
{
    showcmd "$*$EXTRA_SHOWDOCMD"
    "$@"
}

# Run every arg with docmd
docmd_foreach()
{
    local cmd pkg
    cmd="$1"
    #showcmd "$@"
    shift
    for pkg in "$@" ; do
        docmd $cmd $pkg
    done
}

# run command line with SUDO
sudorun()
{
    set_sudo
    if [ -z "$SUDO" ] ; then
        "$@"
        return
    fi
    $SUDO "$@"
}

# Print command line and run command line with SUDO
sudocmd()
{
    set_sudo
    [ -n "$SUDO" ] && showcmd "$SUDO $*" || showcmd "$*"
    sudorun "$@"
}

# Run every arg with sudocmd
# Returns on any error
sudocmd_foreach()
{
    local cmd pkg
    cmd="$1"
    #showcmd "$@"
    shift
    for pkg in "$@" ; do
        # don't quote $cmd here: it can be a command with an args
        sudocmd $cmd $pkg || return
    done
}

# print full path to files
make_filepath()
{
    local i
    for i in "$@" ; do
        [ -f "$i" ] || continue
        echo "$i" | grep -q "/" && echo "$i" && continue
        echo "./$i"
    done
}

get_firstarg()
{
    echon "$1"
}

get_lastarg()
{
    local lastarg
    eval "lastarg=\${$#}"
    echon "$lastarg"
}

# TODO: see etersoft-build-utils/tests/test_isnumber.sh
isnumber()
{
    echo "$*" | filter_strip_spaces | grep -q "^[0-9]\+$"
}

# copied from strings
# CHECKME: the same like estrlist has ?
# Note: used grep -E! write '[0-9]+(first|two)', not '[0-9]\+...'
rhas()
{
    echo "$1" | grep -E -q -- "$2"
}

rihas()
{
    echo "$1" | grep -E -i -q -- "$2"
}

# bash specific
startwith()
{
    # rhas "$1" "^$2"
    [ "${1:0:${#2}}" = "$2" ]
    #[[ "$1" = ${2}* ]]
}

is_abs_path()
{
    #echo "$1" | grep -q "^/"
    startwith "$1" "/"
}

# copied from strings
is_dirpath()
{
    [ "$1" = "." ] && return $?
    # rhas "$1" "/"
    startwith "$1" "/"
}

is_wildcard()
{
    echo "$1" | grep -q "[*?]" && return
    echo "$1" | grep -q "\[.*\]" && return
}

filter_strip_spaces()
{
        # possible use just
        #xargs echo
        sed -e "s| \+| |g" | \
                sed -e "s|^ ||" | sed -e "s| \$||"
}

strip_spaces()
{
        echo "$*" | filter_strip_spaces
}

firstupper()
{
    # FIXME: works with GNU sed only
    echo "$*" | sed 's/.*/\u&/'
}

tolower()
{
    # tr is broken in busybox (checked with OpenWrt)
    #echo "$*" | tr "[:upper:]" "[:lower:]"
    echo "$*" | awk '{print tolower($0)}'
}

firstword()
{
        echo "$*" | awk '{print $1}'
}

lastword()
{
        echo "$*" | xargs -n1 echo 2>/dev/null | tail -n1
}

# https://superuser.com/questions/422459/substitution-in-text-file-without-regular-expressions
# http://stackoverflow.com/a/2705678/120999
# use for subst complex string with symbols treating as regexp
sed_escape()
{
    echo "$*" | sed -e 's|[][()$*.^|/]|\\&|g'
}


# param true false
subst_option()
{
    eval "[ -n \"\$$1\" ]" && echo "$2" || echo "$3"
}

store_output()
{
    # use make_temp_file from etersoft-build-utils
    RC_STDOUT="$(mktemp)" || fatal
    remove_on_exit $RC_STDOUT
    local CMDSTATUS=$RC_STDOUT.pipestatus
    echo 1 >$CMDSTATUS
    #RC_STDERR=$(mktemp)
    ( LC_ALL=C $@ 2>&1 ; echo $? >$CMDSTATUS ) | tee $RC_STDOUT
    return "$(cat $CMDSTATUS)"
    # bashism
    # http://tldp.org/LDP/abs/html/bashver3.html#PIPEFAILREF
    #return $PIPESTATUS
}

showcmd_store_output()
{
    showcmd "$@"
    store_output "$@"
}

clean_store_output()
{
    rm -f $RC_STDOUT $RC_STDOUT.pipestatus
}

# run epm, possible from side repo
epm()
{
    if [ "$EPMMODE" = "pipe" ] ; then
        epm_main --inscript "$@"
        return
    fi

    # run epm again to full initialization
    local bashopt=''
    [ -n "$debug" ] && bashopt='-x'

    $CMDSHELL $bashopt $PROGDIR/$PROGNAME --inscript "$@"
}

# run $SUDO epm, possible from side repo
sudoepm()
{
    [ "$EPMMODE" = "pipe" ] && fatal "Can't use sudo epm call from the piped script"

    local bashopt=''
    [ -n "$debug" ] && bashopt='-x'

    sudorun $CMDSHELL $bashopt $PROGDIR/$PROGNAME --inscript "$@"
}

echog()
{
	if [ "$1" = "-n" ] ; then
		shift
		[ -n "$1" ] && eval_gettext "$*"
	else
		[ -n "$1" ] && eval_gettext "$*"
		echo
	fi
}

message()
{
    echog "$*"
}


__promo_message()
{
    local PROMOMESSAGE="$EPMPROMOMESSAGE"
    if [ -z "$PROMOMESSAGE" ] ; then
        local tg_link=$(make_osc8_link "https://t.me/useepm")
        PROMOMESSAGE=" (you can discuss this problem (epm $EPMVERSION on $DISTRNAME/$DISTRVERSION) in Telegram: $tg_link)"
    fi
    echo "$PROMOMESSAGE"
}

# Print error message and stop the program
fatal()
{
    set_color $RED >&2
    echog -n "ERROR: " >&2
    restore_color >&2
    echog "$* $(__promo_message)" >&2
#    [ "$TERM" = "screen" ] && echo "(screen detected: waiting ten seconds to exit ...)" >&2 && sleep 10
    exit 1
}

# Print error message and stop the program, skippimg translate
fixme()
{
    set_color $RED >&2
    echo -n "ERROR: " >&2
    restore_color >&2
    echog "$* $(__promo_message)" >&2
#    [ "$TERM" = "screen" ] && echo "(screen detected: waiting ten seconds to exit ...)" >&2 && sleep 10
    exit 1
}

# Print debug message
debug()
{
    [ -z "$debug" ] && return

    set_color $YELLOW >&2
    echog -n "WARNING: " >&2
    restore_color >&2
    echog "$*" >&2
}


# Print warning message
warning()
{
    set_color $YELLOW >&2
    echog -n "WARNING: " >&2
    restore_color >&2
    echog "$*" >&2
}

fatal_warning()
{
    [ -n "$force" ] || fatal "$@"
    warning "$@"
}

info()
{
    [ -n "$quiet" ] && return

    # print message to stderr if stderr forwarded to (a file)
    if isatty2 ; then
        isatty || return 0
        echog "$*"
    else
        echog "$*" >&2
    fi
}


check_su_root()
{
    #[ "$BASEDISTRNAME" = "alt" ] || return 0

    is_root || return 0

    echo "$PATH" | grep -q "/usr/sbin" && return 0

    fatal "There is missed /usr/sbin path in PATH. Probably you have used 'su' without '-' to get root access. Use 'esu' or 'su -' command to get root permissions."
}


# if we have not sudo, returns 1 and set SUDO variable to fatal
SUDO_TESTED=''
SUDO_CMD='sudo'
set_sudo()
{
    local nofail="$1"

    # cache the result
    [ -n "$SUDO_TESTED" ] && return "$SUDO_TESTED"
    SUDO_TESTED="0"

    SUDO=""
    # skip SUDO if disabled
    [ -n "$EPMNOSUDO" ] && return
    if [ "$DISTRNAME" = "Cygwin" ] || [ "$DISTRNAME" = "Windows" ] ; then
        # skip sudo using on Windows
        return
    fi

    check_su_root

    # if we are root, do not need sudo
    is_root && return

    # start error section
    SUDO_TESTED="1"

    if is_command doas && ! is_command sudo > /dev/null 2>&1 ; then
        SUDO="doas"
        SUDO_TESTED="0"
        return "$SUDO_TESTED"
    fi

    if ! is_command $SUDO_CMD ; then
        [ "$nofail" = "nofail" ] || SUDO="fatal 'For this operation run epm under root, or install and tune sudo (http://altlinux.org/sudo)'"
        SUDO_TESTED="2"
        return "$SUDO_TESTED"
    fi

    # if /dev/tty is available, sudo can ask for password (it opens /dev/tty directly)
    if [ -r /dev/tty ] && [ -w /dev/tty ] ; then
        if ! $SUDO_CMD -n true ; then
            # show message only if stderr is a tty (otherwise it might be lost)
            isatty2 && info "Please enter sudo user password to use sudo for all privileged operations in the current session." >&2
            if ! $SUDO_CMD -l >/dev/null ; then
                [ "$nofail" = "nofail" ] || SUDO="fatal 'For this operation run epm under root, or install and tune sudo (http://altlinux.org/sudo)'"
                SUDO_TESTED="3"
                return "$SUDO_TESTED"
            fi
        fi
    else
        # TODO: check user_can_sudo in https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh
        # use sudo if one is tuned and tuned without password
        # hack: check twice
        $SUDO_CMD -l -n >/dev/null 2>/dev/null
        if ! $SUDO_CMD -l -n >/dev/null 2>/dev/null ; then
            [ "$nofail" = "nofail" ] || SUDO="fatal 'Can't use sudo (only passwordless sudo is supported here). Please run epm under root or check http://altlinux.org/sudo '"
            SUDO_TESTED="4"
            return "$SUDO_TESTED"
        fi
    fi

    SUDO_TESTED="0"
    # FIXME: does not work: sudo -- VARIABLE=some command
    SUDO="$SUDO_CMD"
    #SUDO="$SUDO_CMD --"
    # check for < 1.7 version which do not support -- (and --help possible too)
    #$SUDO_CMD -h 2>/dev/null | grep -q "  --" || SUDO="$SUDO_CMD"

}

# return TRUE if we can run privileged command
sudo_allowed()
{
    set_sudo nofail
}

# wait for n seconds (if possible) during executing command
# args: seconds command
withtimeout()
{
    local TO=$(print_command_path timeout || print_command_path gtimeout)
    if [ -x "$TO" ] ; then
        $TO "$@"
        return
    fi
    fatal "Possible indefinite wait due timeout command is missed"
    # fallback: drop time arg and run without timeout
    #shift
    #"$@"
}

set_eatmydata()
{
    # don't use eatmydata (useless)
    return 0
    # skip if disabled
    [ -n "$EPMNOEATMYDATA" ] && return
    # use if possible
    is_command eatmydata || return
    set_sudo
    # FIXME: check if SUDO already has eatmydata
    [ -n "$SUDO" ] && SUDO="$SUDO eatmydata" || SUDO="eatmydata"
    [ -n "$verbose" ] && info "Uwaga! eatmydata is installed, we will use it for disable all sync operations."
    return 0
}

# 
__get_package_for_command()
{
    case "$1" in
        equery|revdep-rebuild)
            echo 'gentoolkit'
            ;;
        update-kernel|remove-old-kernels)
            echo 'update-kernel'
            ;;
    esac
}

# read from /dev/tty if available, otherwise from stdin
read_tty() {
    if [ -c /dev/tty ] ; then
        read -r "$@" </dev/tty
    else
        read -r "$@"
    fi
}

confirm() {
    local response
    local prompt
    if [ -n "$1" ] ; then
        prompt="$(eval_gettext "$1")"
    else
        prompt="$(eval_gettext "Are you sure? [y/N]")"
    fi
    printf "%s " "$prompt" >&2
    read_tty response || return 1
    case $response in
        [yY][eE][sS]|[yY])
            true
            ;;
        *)
            false
            ;;
    esac
}

# confirm with default Yes
confirm_yes() {
    local response
    local prompt
    if [ -n "$1" ] ; then
        prompt="$(eval_gettext "$1")"
    else
        prompt="$(eval_gettext "Are you sure? [Y/n]")"
    fi
    printf "%s " "$prompt" >&2
    read_tty response || return 1
    case $response in
        [nN][oO]|[nN])
            false
            ;;
        *)
            true
            ;;
    esac
}


confirm_info()
{
    info "$*" >&2
    if [ -z "$non_interactive" ] ; then
        confirm "Are you sure? [y/N]" || fatal "Exiting"
    fi

}


is_root()
{
    local EFFUID="$(id -u)"
    [ "$EFFUID" = "0" ]
}

assure_root()
{
    is_root || fatal "run me only under root"
}

check_su_access()
{
    is_command su && return
    [ ! -f /bin/su ] && warning "/bin/su is missed. Try install su package (http://altlinux.org/su)." && return 1
    local group="$(stat -c '%G' /bin/su)" || fatal
    warning "Check if you are in $group group to have access to su command."
    return 1
}

check_sudo_access()
{
    is_command sudo && return
    local cmd=''
    local i
    for i in /bin/sudo /usr/bin/sudo ; do
        [ -f $i ] && cmd="$i"
    done
    [ ! -f "$cmd" ] && warning "sudo command is missed. Try install sudo package (http://altlinux.org/sudo)." && return 1
    local group="$(stat -c '%G' "$cmd")" || fatal
    warning "Check if you are in $group group to have access to sudo command."
    return 1
}

check_sudo_access_only()
{
    is_command sudo && return
    local cmd=''
    local i
    for i in /bin/sudo /usr/bin/sudo ; do
        [ -f $i ] && cmd="$i"
    done
    [ ! -f "$cmd" ] && return 1
    local group="$(stat -c '%G' "$cmd")" || fatal
    warning "sudo command is presence, but is not accessible for you. Check if you are in $group group to have access to sudo command."
    return 1
}

esu()
{
    if is_root ; then
        if [ -n "$*" ] ; then
            [ -n "$quiet" ] || showcmd "$*"
            exec "$@"
        else
            # just shell
            showcmd "su -"
            a= exec su -
        fi
    fi

    set_pm_type


# TODO:
#quote() {
#    for arg in "$@"; do
#        printf '%s\n' "$arg" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/"
#    done
#}

    escape_args()
    {
        local output=''
        while [ -n "$1" ] ; do
            if has_space "$1" ; then
                [ -n "$output" ] && output="$output '$1'" || output="'$1'"
            else
                [ -n "$output" ] && output="$output $1" || output="$1"
            fi
            shift
        done
        echo "$output"
    }

    escaped="$(escape_args "$@")"

    check_sudo_access_only
    # sudo is not accessible, will ask root password
    if ! set_sudo ; then
        check_su_access
        #info "Enter root password:"
        if [ -n "$*" ] ; then
            [ -n "$quiet" ] || showcmd "su - -c $escaped"
            a= exec su - -c "$escaped"
        else
            # just shell
            showcmd "su -"
            a= exec su -
        fi
    fi

    check_sudo_access

    #info "You can be asked about your password:"
    if [ -n "$*" ] ; then
        [ -n "$quiet" ] || showcmd "$SUDO su - -c $escaped"
        $SUDO su - -c "$escaped"
    else
        showcmd "$SUDO su -"
        $SUDO su -
    fi
}

# Escape regex special characters (except * ? | which are used in patterns)
__escape_regex_special()
{
    # escape: \ . ^ $ ( ) [ ] { } +
    # keep | for OR patterns in search
    echo "$1" | sed -e 's|[\\.^$(){}+]|\\&|g' -e 's|\[|\\[|g' -e 's|\]|\\]|g'
}

# copied from eget's filter_glob
# check man glob
__convert_glob__to_regexp()
{
    # first escape regex special chars, then translate glob to regexp
    __escape_regex_special "$1" | sed -e "s|\*|.*|g" -e "s|?|.|g"
}

regexp_subst()
{
    local expression="$1"
    shift
    sed -i -r -e "$expression" "$@"
}

get_filesize()
{
    if stat -f%z "$1" >/dev/null 2>&1; then
        stat -f%z "$1"
    else
        # coreutils
        stat -c%s "$1"
    fi
}

is_file_greater_2gb() {
    local size
    size=$(get_filesize "$1") || return
    local limit=2147483648
    [ $size -ge $limit ]
}

is_file_greater_4gb() {
    local size
    size=$(get_filesize "$1") || return
    local limit=4294967296
    [ $size -ge $limit ]
}


# TODO: why we can't use epm directly?
try_assure_exists()
{
    load_helper epm-assure
    local package="$2"
    [ -n "$package" ] || package="$(__get_package_for_command "$1")"

    # ask for install: https://bugzilla.altlinux.org/42240
    local ask=''
    [ -n "$non_interactive" ] || ask=1

    ( verbose='' direct='' interactive=$ask epm_assure "$1" $package $3 )
}

assure_exists()
{
    try_assure_exists "$@" || fatal
}


assure_exists_erc()
{
    load_helper epm-assure
    local package="erc"
    ( direct='' epm_assure "$package" ) || epm ei erc || fatal "erc is not available to install."
}

# will replaced within disabled_eget in packaged version
eget()
{
    # use internal eget only if exists
    if [ -s $SHAREDIR/tools_eget ] ; then
        ( EGET_BACKEND="$eget_backend" $CMDSHELL "$SHAREDIR"/tools_eget "$@" )
        return
    fi
    fatal "Internal error: missed tools_eget"

    local EGET
    # FIXME: we need disable output here, eget can be used for get output
    assure_exists eget eget 3.3 >/dev/null
    # run external command, not the function
    EGET=$(print_command_path eget) || fatal "Missed command eget from installed package eget"
    $EGET "$@"
}

fetch_url()
{
    local url="$1"
    info 'Fetching $url ...'
    eget -q -O- "$url"
}

# FIXME:
sudocmd_eget()
{
    # use internal eget only if exists
    if [ -s $SHAREDIR/tools_eget ] ; then
        sudocmd env EGET_BACKEND="$eget_backend" $CMDSHELL "$SHAREDIR"/tools_eget "$@"
        return
    fi
}

calc_sha256sum()
{
    sha256sum "$1" | awk '{print $1}'
}

# Get hash value from checksum spec (strips algo: prefix if present)
# Usage: get_checksum_value "md5hash:abc123" -> "abc123"
get_checksum_value()
{
    echo "$1" | sed 's/^[^:]*://'
}

# Calculate checksum for file using algorithm from spec
# Usage: calc_checksum <algo:hash or hash> <file>
# Supported: md5, md5hash, sha1, sha256, sha512 (default: sha256)
calc_checksum()
{
    local spec="$1"
    local file="$2"
    case "${spec%%:*}" in
        md5|md5hash)
            md5sum "$file" ;;
        sha1|sha1hash)
            sha1sum "$file" ;;
        sha512|sha512hash)
            sha512sum "$file" ;;
        *)
            sha256sum "$file" ;;
    esac | awk '{print $1}'
}

print_sha256sum()
{
    local checksum=''
    if [ "$1" = "--checksum" ] ; then
        checksum="$2"
        shift 2
        local expected="$(get_checksum_value "$checksum")"
        local pcs="$(calc_checksum "$checksum" "$1")"
        [ "$expected" = "$pcs" ] || fatal "Checksum verification failed (${checksum%%:*}). Awaited: $expected, got: $pcs"
    fi

    local files="$*"
    local i

    local ch_ok=''
    [ -n "$checksum" ] && ch_ok='OK'

    echo "sha256sum:"
    for i in $files ; do
            echo "    $(calc_sha256sum $i) $ch_ok $(basename $i) $(du -Lh $i | cut -f1)"
    done
}

parse_json_value()
{
    local field="$1"
    echo "$field" | grep -q -E "^\[" || field='["'$field'"]'
    epm --inscript --quiet tool json -b | grep -m1 -F "$field" | sed -e 's|.*\][[:space:]]||' | sed -e 's|"\(.*\)"|\1|g'
}

# URL/file ["version"]
get_json_value()
{
    local src="$1"
    if is_url "$src" ; then
        local toutput
        toutput="$(fetch_url "$src")" || return
        echo "$toutput" | parse_json_value "$2"
    else
        [ -s "$src" ] || fatal 'File $src is missing, can'\''t get JSON'
        parse_json_value "$2" < "$src"
    fi
}

parse_json_values()
{
    local field="$1"
    echo "$field" | grep -q -E "^\[" || field="\[$(echo "$field" | sed 's/[^ ]*/"&"/g' | sed 's/ /,/g'),[0-9]*\]"
    epm --inscript --quiet tool json -b | grep "^$field" | sed -e 's|.*\][[:space:]]||' | sed -e 's|"\(.*\)"|\1|g'
}

# get keys of JSON object (not array)
# stdin: JSON, arg: object name
# example: parse_json_object_keys metapackage_variants < file.json
parse_json_object_keys()
{
    local field="$1"
    epm --inscript --quiet tool json -b | grep "^\[\"$field\"," | sed -e 's|^\[\"[^\"]*\",\"\([^\"]*\)\"\].*|\1|'
}

# URL/file ["version"]
get_json_values()
{
    local src="$1"
    if is_url "$src" ; then
        local toutput
        toutput="$(fetch_url "$src")" || return
        echo "$toutput" | parse_json_values "$2"
    else
        [ -s "$src" ] || fatal 'File $src is missing, can'\''t get JSON'
        parse_json_values "$2" < "$src"
    fi
}


__epm_assure_7zip()
{
    # install 7zip in any case (can be used)
    if is_command 7z || is_command 7za || is_command 7zr || is_command 7zz ; then
        :
    else
        epm install 7-zip || epm install p7zip
    fi
}

# will replaced within disabled_erc in packaged version
erc()
{

    __epm_assure_7zip

    # use internal eget only if exists
    if [ -s "$SHAREDIR"/tools_erc ] ; then
        $CMDSHELL "$SHAREDIR"/tools_erc "$@"
        return
    fi
    fatal "Internal error: missed tools_erc"

    # FIXME: we need disable output here, ercat can be used for get output
    assure_exists_erc >/dev/null
    # run external command, not the function
    local ERC
    ERC=$(print_command_path erc) || fatal "Missed command erc from installed package erc"
    $ERC "$@"
}

# will replaced within disabled_ercat in packaged version
ercat()
{
    local ERCAT
    # use internal eget only if exists
    if [ -s "$SHAREDIR"/tools_ercat ] ; then
        $CMDSHELL "$SHAREDIR"/tools_ercat "$@"
        return
    fi
    fatal "Internal error: missed tools_ercat"

    # FIXME: we need disable output here, ercat can be used for get output
    assure_exists_erc >/dev/null
    # run external command, not the function
    ERCAT=$(print_command_path ercat) || fatal "Missed command ercat from installed package erc"
    $ERCAT "$@"
}

estrlist()
{
    if [ -s "$SHAREDIR"/tools_estrlist ] ; then
        $CMDSHELL "$SHAREDIR"/tools_estrlist "$@"
        return
    fi
    fatal "missed tools_estrlist"
}

onefile_estrlist()
{
    internal_tools_estrlist "$@"
}

# will replaced within eget() in packed version
onefile_eget()
{
    # check for both
    # we really need that cross here,
    is_command curl || try_assure_exists wget
    is_command wget || try_assure_exists curl
    internal_tools_eget "$@"
}

# Check if file is a package file (rpm, deb, etc.), not ELF binary or other
is_package_file()
{
    local t
    t="$(get_package_type "$1")" || return 1
    case "$t" in
        ELF|exe|msi|AppImage)
            return 1
            ;;
    esac
    return 0
}

# TODO: improve and drop!
get_package_type()
{
    local i
    case $1 in
        *.deb)
            echo "deb"
            return
            ;;
        *.rpm)
            echo "rpm"
            return
            ;;
        *.txz)
            echo "txz"
            return
            ;;
        *.tbz)
            echo "tbz"
            return
            ;;
        *.exe)
            echo "exe"
            return
            ;;
        *.msi)
            echo "msi"
            return
            ;;
        *.AppImage|*.appimage)
            echo "AppImage"
            return
            ;;
        *.pkg.tar.*)
            echo "pkg.tar.$(echo "$1" | sed -e 's|.*\.pkg\.tar\.||')"
            return
            ;;
        *)
            if [ -r "$1" ] && file -L "$1" | grep -q " ELF " ; then
                echo "ELF"
                return
            fi
            # print extension by default
            basename "$1" | sed -e 's|.*\.||'
            return 1
            ;;
    esac
}


# print options description from HELPCMD/HELPOPT lines in the code
# args: section_name, [file with code]
get_help()
{
    if [ "$0" = "/dev/stdin" ] || [ "$0" = "sh" ] ; then
        return
    fi
    local F="$0"
    if [ -n "$2" ] ; then
        is_dirpath "$2" && F="$2" || F="$(dirname $0)/$2"
    fi

    cat "$F" | grep -- "# $1" | while read -r n ; do
        if echo "$n" | grep -q "# $1: PART: " ; then
            echo
            echo "$n" | sed -e "s|# $1: PART: ||"
            continue
        fi
        echo "$n" | grep -q "^ *#" && continue
        opt=`echo $n | sed -e "s|) # $1:.*||g" -e 's|"||g' -e 's@^|@@'`
        desc=`echo $n | sed -e "s|.*) # $1:||g"`
        printf "    %-20s %s\n" "$opt" "$desc"
    done
}

set_bigtmpdir()
{
    # TODO: improve BIGTMPDIR conception
    # https://bugzilla.mozilla.org/show_bug.cgi?id=69938
    # https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch05s15.html
    # https://geekpeach.net/ru/%D0%BA%D0%B0%D0%BA-systemd-tmpfiles-%D0%BE%D1%87%D0%B8%D1%89%D0%B0%D0%B5%D1%82-tmp-%D0%B8%D0%BB%D0%B8-var-tmp-%D0%B7%D0%B0%D0%BC%D0%B5%D0%BD%D0%B0-tmpwatch-%D0%B2-centos-rhel-7
    if [ -z "$BIGTMPDIR" ] ; then
        BIGTMPDIR="/var/tmp"
        [ -d "$BIGTMPDIR" ] || BIGTMPDIR="$TMPDIR"
    fi
    export BIGTMPDIR
}

assure_tmpdir()
{
    if [ -z "$TMPDIR" ] ; then
        export TMPDIR="/tmp"
        debug "Your have no TMPDIR defined. Using $TMPDIR as fallback."
    fi

    if [ ! -d "$TMPDIR" ] ; then
        fatal "TMPDIR $TMPDIR does not exist."
    fi

    if [ ! -w "$TMPDIR" ] ; then
        fatal "TMPDIR $TMPDIR is not writable."
    fi
}

test_shell()
{
    local R
    R="$($CMDSHELL /dev/null 2>&1)"
    [ -n "$R" ] && fatal "$CMDSHELL is broken (bash wrongly printing out '$R'). Check ~/.bashrc and /etc/bashrc, run $CMDSHELL manually for test."
}


set_distro_info()
{

    test_shell

    # TODO: return when we will not ask run under root
    #[ -n "$SUDO_USER" ] && warning "It is not necessary to run epm using sudo."

    assure_tmpdir

    set_bigtmpdir

    # don't run again in subprocesses
    [ -n "$DISTRVENDOR" ] && return 0

    DISTRVENDOR="$PROGDIR"/distr_info

    # export pack of variables, see epm print info --print-eepm-env
    [ -n "$verbose" ] && $DISTRVENDOR --print-eepm-env
    eval $($DISTRVENDOR --print-eepm-env | grep -v '^ *#')
}

# FIXME: detect if not recognized
set_pm_type()
{
    local CMD
    set_distro_info

# override package manager detection result
if [ -n "$EPM_BACKEND" ] ; then
    PMTYPE="$EPM_BACKEND"
    return
fi
# obsoleted
if [ -n "$FORCEPM" ] ; then
    PMTYPE="$FORCEPM"
    return
fi

}

is_active_systemd()
{
    [ "$DISTRCONTROL" = "systemd" ]
}

assure_distr()
{
    local distr="$1"
    local TEXT="this option"
    [ -n "$2" ] && TEXT="$2"
    [ "$DISTRNAME" = "$distr" ] || fatal '$TEXT supported only for $distr distro'
}

# return delimiter sign in depend of package type
get_pkg_name_delimiter()
{
   local pkgtype="$1"
   [ -n "$pkgtype" ] || pkgtype="$PKGFORMAT"

   [ "$pkgtype" = "deb" ] && echo "_" && return
   echo "-"
}

# used via remove_on_exit
__epm_remove_tmp_files()
{
    trap "-" EXIT
    [ -n "$DEBUG" ] && return 0

    [ -n "$verbose" ] && info "Removing tmp files on exit ..."

    if [ -n "$to_clean_tmp_dirs" ] ; then
        echo "$to_clean_tmp_dirs" | while read p ; do
            [ -n "$p" ] || continue
            [ -n "$verbose" ] && echo "rm -rf '$p'"
            rm -rf "$p" 2>/dev/null
        done
    fi

    if [ -n "$to_clean_tmp_files" ] ; then
        echo "$to_clean_tmp_files" | while read p ; do
            [ -n "$p" ] || continue
            rm $verbose -f "$p" 2>/dev/null
        done
    fi

    if [ -n "$to_sudo_clean_tmp_files" ] ; then
        echo "$to_sudo_clean_tmp_files" | while read p ; do
            [ -n "$p" ] || continue
            sudo rm $verbose -f "$p" 2>/dev/null
        done
    fi

    return 0
}


remove_on_exit()
{
    if [ -z "$set_remove_on_exit" ] ; then
        trap "__epm_remove_tmp_files" EXIT
        set_remove_on_exit=1
    fi
    while [ -n "$1" ] ; do
        if [ -d "$1" ] ; then
            to_clean_tmp_dirs="$to_clean_tmp_dirs
$1"
        else
            to_clean_tmp_files="$to_clean_tmp_files
$1"
        fi
        shift
    done
}

sudo_remove_on_exit()
{
    # set trap if needed
    remove_on_exit

    while [ -n "$1" ] ; do
            # don't check, it can be non accessible
            to_sudo_clean_tmp_files="$to_sudo_clean_tmp_files
$1"
        shift
    done
}

#has_space()
#{
#    estrlist -- has_space "$@"
#}
# use internal implementation for speed
has_space()
{
        case "$1" in
            *[[:space:]]*) return 0 ;;
        esac
        return 1
}

# Filter glob pattern list, return only existing files
# Usage: filter_glob_list /etc/file /etc/dir/*.ext
filter_glob_list()
{
    local f
    for f in "$@" ; do
        [ -e "$f" ] && printf "%s " "$f"
    done
}


is_url()
{
    echo "$1" | grep -qE "^(file|ftp|http|https|ipfs|rsync):/" && return 0
    # SSH/rsync URL: host:/path or user@host:/path (but not scheme://)
    echo "$1" | grep -qE '^[^:]+:/' && ! echo "$1" | grep -q "://"
}

# detect bash
is_bash()
{
    [ -n "$BASH_VERSION" ]
}

# print a path to the command if exists in $PATH
if is_bash ; then
print_command_path()
{
    type -fpP -- "$1" 2>/dev/null
}
else
print_command_path()
{
    command -v "$1" 2>/dev/null
}
fi

# check if <arg> is a real command
is_command()
{
    print_command_path "$1" >/dev/null
}

# compatibility layer

# add realpath if missed (with -s support)
if ! is_command realpath ; then
realpath()
{
    [ -n "$*" ] || return
    if [ "$1" = "-s" ] ; then
        shift
        echo "$(cd "$(dirname "$1")" && pwd -P)/$(basename "$1")" #"
        return
    fi
    readlink -f "$@"
}
fi


# TODO: use perl if sed -i is not accessible
# sed -i is only supported in GNU sed.
#  sed -i "s/$find/$replace/g" "$@"
#  perl -p -i -e "s/$find/$replace/g" "$@"

# add subst if missed
if ! is_command subst ; then
subst()
{
    sed -i -e "$@"
}
fi

# Suggest similar packages using fzf fuzzy search
# Args: package_name
# Returns: selected package name (if interactive) or empty
__epm_suggest_similar_packages()
{
    local pkg="$1"
    local cache="$epm_vardir/available-packages"
    local count="${suggest_count:-7}"

    # need cache file
    [ -s "$cache" ] || return 1

    # need fzf for fuzzy search
    is_command fzf || return 1

    local similar
    similar="$(fzf -f "$pkg" < "$cache" 2>/dev/null | head -$count)"
    [ -z "$similar" ] && return 1

    # Interactive selection if enabled (via config or --interactive flag)
    if [ -n "$suggest_interactive$interactive" ] && inputisatty ; then
        local selected
        selected="$(echo "$similar" | fzf --prompt="$(eval_gettext "Select package"): " --height=10 --reverse)"
        if [ -n "$selected" ] ; then
            echo "$selected"
            return 0
        fi
        return 1
    fi

    # Non-interactive: just show suggestions (to stderr)
    echo "" >&2
    echog "Perhaps you meant:" >&2
    echo "$similar" | sed 's/^/  /' >&2
    return 1
}

# Suggest similar packages for a list of packages
# Args: package_names...
# Returns: selected packages (if interactive) or empty
__epm_suggest_similar_packages_by_list()
{
    local pkg
    local selected_all=""
    for pkg in "$@" ; do
        local selected
        selected="$(__epm_suggest_similar_packages "$pkg")"
        [ -n "$selected" ] && selected_all="$selected_all $selected"
    done
    echo $selected_all
}

check_core_commands()
{
    #which which >/dev/null || fatal "Can't find which command (which or debianutils package is missed?)"
    is_command grep || fatal "Can't find grep command (coreutils package is missed?)"
    is_command sed || fatal "Can't find sed command (sed package is missed?)"
}

export TEXTDOMAIN=eepm
if [ "$EPMMODE" = "git" ] ; then
    TEXTDOMAINDIR=$PROGDIR/../po
else
    TEXTDOMAINDIR='/usr/share/locale'
fi
export TEXTDOMAINDIR

if [ -d "$TEXTDOMAINDIR" ] && is_command gettext.sh ; then
	. gettext.sh
else
	eval_gettext()
	{
		eval "echo -n \"$@\""
	}
fi

# Load YAML fields into shell variables safely (prevents command injection)
# Usage: yaml_load_vars file.yaml field1 field2 field3 ...
yaml_load_vars()
{
    local file="$1"
    shift
    local data field value
    data="$(epm tool yaml "$file" 2>/dev/null)"
    for field in "$@" ; do
        value="$(printf '%s\n' "$data" | grep "^${field}=" | head -n1 | sed "s/^[^=]*=\"\(.*\)\"$/\1/")"
        # skip if key is not present in yaml (preserve existing value)
        [ -n "$value" ] || continue
        # Use single quotes to prevent command execution in values
        eval "$field='$(printf '%s' "$value" | sed "s/'/'\\\\''/g")'"
    done
}
