#
# Bash Functions for sourcing in scripts.
#
# This is a copy of some of the functions for HPR project use.
#
#       ${HOME}/HPR/InternetArchive/function_lib.sh
#
# Revised: 2024-06-15 18:13:28
#


#===  FUNCTION  ================================================================
#        NAME:  abs_diff
# DESCRIPTION:  Compute the difference between two numbers, returning an
#               absolute value
#  PARAMETERS:  $1      first number
#               $2      second number
#     RETURNS:  The absolute value as a string
#===============================================================================
function abs_diff {
    local p1="${1:?Usage: abs_diff n1 n2}"
    local p2="${2:?Usage: abs_diff n1 n2}"

    echo $((p1 >= p2 ? p1 - p2 : p2 - p1))
}

#===  FUNCTION  ================================================================
#          NAME:  cleanup_temp
#   DESCRIPTION:  Cleanup temporary files in case of a keyboard interrupt
#                 (SIGINT) or a termination signal (SIGTERM) and at script
#                 exit
#    PARAMETERS:  * - names of temporary files to delete
#       RETURNS:  Nothing
#===============================================================================
function cleanup_temp {
    for tmp in "$@"; do
        [ -e "$tmp" ] && rm --force "$tmp"
    done
    exit 0
}

#===  FUNCTION  ================================================================
#         NAME: _DEBUG
#  DESCRIPTION: Writes one or more messages if in DEBUG mode. Each argument is
#               seen as a message and is written on a separate line.
#               References the global variable 'DEBUG' which is expected to be
#               True if debug output is wanted.
#   PARAMETERS: List of messages
#      RETURNS: Nothing
#===============================================================================
_DEBUG () {
    [ "$DEBUG" == 0 ] && return
    for msg in "$@"; do
        printf 'D> %s\n' "$msg"
    done
}

#===  FUNCTION  ================================================================
#         NAME: yes_no
#  DESCRIPTION: Read a Yes or No response from STDIN (only these values are
#               accepted) and return a suitable numeric value.
#   PARAMETERS: 1 - Prompt string for the read
#               2 - Default value (optional)
#      RETURNS: 0 for a response of Y or YES, 1 otherwise
#===============================================================================
yes_no () {
    local prompt="${1:?Usage: yes_no prompt [default]}"
    local default="${2^^}"
    local ans res

    if [[ $prompt =~ %s ]]; then
        if [[ -n $default ]]; then
            default=${default:0:1}
            # shellcheck disable=SC2059
            # {
            case "$default" in
                Y) printf -v prompt "$prompt" "[Y/n]" ;;
                N) printf -v prompt "$prompt" "[y/N]" ;;
                *) echo "Error: ${FUNCNAME[0]} @ line ${BASH_LINENO[0]}: Default must be 'Y' or 'N'"
                   exit 1
                   ;;
            esac
            # }
        else
            echo "Error: ${FUNCNAME[0]} @ line ${BASH_LINENO[0]}: Default required"
            exit 1
        fi
    fi

    #
    # Loop until a valid input is received
    #
    while true; do
        #
        # Read and handle CTRL-D (EOF)
        #
        read -r -e -p "$prompt" ans
        res="$?"
        if [[ $res -ne 0 ]]; then
            echo "Read aborted"
            return 1
        fi

        [ -z "$ans" ] && ans="$default"

        #
        # Look for valid replies and return appropriate values. Print an error
        # message otherwise and loop around for another go
        #
        if [[ ${ans^^} =~ ^Y(E|ES)?$ ]]; then
            return 0
        elif [[ ${ans^^} =~ ^NO?$ ]]; then
            return 1
        else
            echo "Invalid reply; please use 'Y' or 'N'"
        fi
    done
}

#===  FUNCTION  ================================================================
#         NAME: make_file_list
#  DESCRIPTION: Given an array of filenames select from them and make
#               an array of choices in a different order if required
#   PARAMETERS: 1 - input array name
#               2 - output array name
#      RETURNS: True (0) if a new list was generated, false (1) otherwise
#===============================================================================
make_file_list () {
    local -n inarr=${1:?Usage make_file_list input_array out_array}
    local -n outarr=${2:?Usage make_file_list input_array out_array}
    local i msg prompt range lines choices

    #
    # Print the array contents
    #
    lines="${#inarr[@]}"
    for ((i = 0; i < lines; i++)); do
        printf '%-2d %s\n' "$((i+1))" "${inarr[$i]}"
    done

    msg="Enter a range like '1,2' or '4-7'"
    if [[ $lines -eq 1 ]]; then
        prompt="1"
    else
        prompt="1-$lines"
        msg+=" (between 1 and $lines)"
    fi

    #
    # Choose array elements by number where order is significant
    #
    echo "$msg"
    if read_value "Files to add to list: " range "$prompt"; then
        #
        # If there's a range then parse it and write the relevant elements to the
        # target array, otherwise exit with an error
        #
        if [[ -n "$range" ]]; then
            range_parse "$lines" "$range" choices 0
            for i in $choices; do
                outarr+=("${inarr[$((i-1))]}")
            done
        else
            echo "No files chosen; no list created"
            return 1
        fi

        return 0
    else
        return 1
    fi

}

#===  FUNCTION  ================================================================
#          NAME:  range_parse
#   DESCRIPTION:  Parse a collection of ranges
#    PARAMETERS:  - maximum limit of the range
#                 - entered range expression (e.g. 1-3,7,14)
#                 - name of variable to receive the result
#       RETURNS:  Writes a list of values to the nominated variable
#===============================================================================
function range_parse {
    local max=${1?range_parse: arg 1 missing}
    local range=${2?range:parse: arg2 missing}
    local result=${3?range:parse: arg3 missing}
    local tosort=${4:-1}

    local selection=

    #
    # Turn this off. It affects how some code below behaves
    #
    set +o nounset

    #
    # Remove spaces from the range
    #
    range=${range// /}

    #
    # Check for invalid characters
    #
    if [[ $range =~ [^0-9,-] ]]; then
        echo "Invalid range: $range"
        exit
    fi

    #
    # Slice up the sub-ranges separated by commas and turn all n-m expressions
    # into the intermediate values. Trim the trailing space from the
    # concatenation.
    #
    until [[ -z $range ]]; do
        if [[ $range =~ [,] ]]; then
            item=${range%%,*}
            range=${range#*,}
        else
            item=$range
            range=
        fi
        if [[ $item =~ [-] ]]; then
            item=${item//-/ }
            if [[ $item =~ ^([0-9]{1,}).([0-9]{1,})$ ]]; then
                # shellcheck disable=SC2086
                item=$(seq -s' ' $item)
            else
                echo "Invalid sequence: ${item/ /-}"
                item=
            fi
        fi
        selection+="$item "
    done
    selection=${selection%% }

    #
    # Check for out of bounds problems, sort values and and make unique
    #
    if [[ ${#selection} -gt 0 ]]; then
        #
        # Validate the resulting range
        #
        declare -a sel err msg
        for i in $selection; do
            if [[ $i -lt 1 || $i -gt $max ]]; then
                err+=( $i )
            else
                sel+=( $i )
            fi
        done

        #
        # Report any errors
        #
        if [[ ${#err[*]} -gt 0 ]]; then
            msg=( $(for i in ${err[*]}; do echo $i; done | sort -un) )
            if [[ ${#msg[*]} -gt 1 ]]; then
                printf "Values out of range: %s .. %s\n" ${msg[0]} ${msg[${#msg[*]}-1]}
            else
                printf "Value out of range: %s\n" ${msg[0]}
            fi
        fi

        #
        # Rebuild the selection
        #
        selection=
        if [[ ${#sel[*]} -gt 0 ]]; then
            if [[ $tosort -eq 1 ]]; then
                selection="$(for i in ${sel[*]}; do echo $i; done | sort -un)"
            else
                selection="$(for i in ${sel[*]}; do echo $i; done)"
            fi
        fi
    fi

    #
    # Return the result
    #
    eval $result=\"$selection\"

    return
}

#===  FUNCTION  ================================================================
#         NAME: pad
#  DESCRIPTION: Pad $text to the left with $char characters to length $length
#   PARAMETERS: 1 - the text string to pad (no default)
#               2 - how long the padded string is to be (default 80)
#               3 - the character to pad with (default '-')
#      RETURNS: Nothing
#===============================================================================
pad () {
    local text=${1?Usage: pad text [length] [character]}
    local length=${2:-80}
    local char=${3:--}

    local line
    printf -v line "%0*d%s\n" $((length - ${#text})) 0 "$text"
    char=${char:0:1}
    echo "${line//0/$char}"
}

#===  FUNCTION  ================================================================
#         NAME: read_value
#  DESCRIPTION: Read a value from STDIN and handle errors.
#   PARAMETERS: 1 - Prompt string for the read
#               2 - Name of variable to receive the result
#               3 - Default value (optional)
#      RETURNS: 1 on error, otherwise 0
#===============================================================================
read_value () {
    local prompt="${1:?Usage: read_value prompt outputname [default]}"
    local outputname="${2:?Usage: read_value prompt outputname [default]}"
    local default="${3:-}"
    local var

    #
    # Make an option for the 'read' if there's a default
    #
    if [[ -n $default ]]; then
        default="-i '$default'"
    fi

    #
    # Read and handle CTRL-D (EOF). Use 'eval' to deal with the argument being
    # a variable
    #
    eval "read -r -e $default -p '$prompt' var"
    res="$?"
    if [[ $res -ne 0 ]]; then
        echo "Read aborted"
        return 1
    fi

    #
    # Return the value in the nominated variable
    #
    eval "$outputname='$var'"
    return 0
}

#===  FUNCTION  ================================================================
#         NAME: define_colours
#  DESCRIPTION: Defines terminal colours
#   PARAMETERS: None
#      RETURNS: Nothing; justs sets the variables in the namespace
#===============================================================================
define_colours () {

    #
    # Colour codes
    #
    red=$(tput setaf 1)
    export red
    green=$(tput setaf 2)
    export green
    yellow=$(tput setaf 3)
    export yellow
    blue=$(tput setaf 4)
    export blue
    purple=$(tput setaf 5)
    export purple
    reset=$(tput sgr0)
    export reset

    return
}

#===  FUNCTION  ================================================================
#         NAME: undefine_colours
#  DESCRIPTION: Remove all the colour codes from the colour variables
#   PARAMETERS: None
#      RETURNS: Nothing; justs sets the variables in the namespace
#===============================================================================
undefine_colours () {

    #
    # Colour codes being removed
    #
    for code in red green yellow blue purple reset; do
        eval "$code=''"
        eval "export $code"
    done

    return
}

#===  FUNCTION  ================================================================
#         NAME: coloured
#  DESCRIPTION: Write a message with colour codes
#   PARAMETERS: 1 - colour name
#               2 - message
#      RETURNS: Nothing
#===============================================================================
coloured () {
    local colour="${1:-green}"
    local message="${2:-no message}"

    printf '%s%s%s\n' "${!colour}" "$message" "${reset}"
}

#===  FUNCTION  ================================================================
#         NAME: plural
#  DESCRIPTION: Adds an 's' to a word depending on the size of a number (based
#               on an example from O'Reilly). Also look at ngettext for a much
#               more sophisticated alternative.
#   PARAMETERS: 1 - the word to pluralise
#               2 - the number used to decide
#      RETURNS: Nothing
#===============================================================================
plural () {
    if [[ $2 -eq 1 || $2 -eq -1 ]]; then
        echo "${1}"
    else
        echo "${1}s"
    fi
}

#===  FUNCTION  ================================================================
#         NAME: trimpath
#  DESCRIPTION: Trim a file path to its last few components for display
#   PARAMETERS: $1 - path to trim
#               $2 - number of elements to retain
#      RETURNS: Trimmed path
#===============================================================================
trimpath () {
    local path=${1:?Usage: mpath path elements}
    local elements=${2:?Usage: mpath path elements}
    local -a arr1 arr2
    local result

    # Split the path
#     oldifs="$IFS"
#     IFS='/' mapfile -d'/' -t arr1 <<<"$path"
    IFS='/' read -r -a arr1 <<<"$path"
#     IFS="$oldifs"

    if [[ ${#arr1[@]} -gt $elements ]]; then
        # Put the last elements in another array
        mapfile -t arr2 < <(printf '%s\n' "${arr1[@]: -$elements}")

        # Return the second array interleaved with '/'
        result="${arr2[*]/%//}"
        result="${result//\/ /\/}"
        echo "${result:0:-1}"
    else
        echo "$path"
    fi
}

#===============================================================================
#         NAME: stampfile
#  DESCRIPTION: Rename a file by adding a timestamp to its name. The timestamp
#               is the modification time. It is added at the end by default or
#               wherever the caller places a '%s'. Note there are not enough
#               error checks to deal with really weird requests :-)
#   PARAMETERS: $1 - name of file to rename
#               $2 - optional template string
#      RETURNS: Nothing
#===============================================================================
function stampfile () {
    if [[ $# = 0  || $# -gt 2 ]]; then
        echo "Usage: ${FUNCNAME[0]} filename [template]"
        return
    fi
    local fname=${1}
    local tp=${2:-"${fname}_%s"}
    [ -e "$fname" ] || { echo "$fname does not exist"; return; }
    # shellcheck disable=SC2059
    new=$(printf "$tp" "$(date -r "$fname" +%Y%m%d_%H%M%S)")
    mv "$fname" "$new" || { echo "Unable to mv $fname to $new"; return; }
    echo "Renamed $fname to $new"
}

#
# vim: syntax=sh:ts=8:sw=4:et:tw=78:fo=tcrqn21