#!/bin/bash
#
# Bash error library            @version: 1.2.4
# 
# This is an error handler for any kind of bash 
# script. The library catches defaults script errors
# as syntax erros by an trap and displays/logs them.
#
# All needed settings can be done in head of this file.
# It is also posible to export the vars in an external
# file, than you have to source them before this
# library.
#
# For more details read&visit 
# https://github.com/iptoux/bash_error_lib/wiki

MTSTART=$(date +%s.%N)


############ LIBRARY VARIABLES ############

# Error
ERROR_EXIT=true
ERROR_LOG=true
ERROR_DISP_SNIPPED=true
ERROR_DISP_STACK=true

# Debug
DEBUG_AUTO=false
DEBUG_OPT="-debug"

############ RUNTIME SETS ############ 
set +o errexit  # Deactivate default Exit after first command failure.
set -E


# DATE for log file names.
DATE=$(date +%m.%d.%y)

LOG_DIR="logs/"
LOG_MAXCOUNT=5                                      # How many logs per day' should be keep?
LOG_MAXDAY=7                                        # How many day' should be keep?


######################## BEGIN OF LIBRARY ########################
##################################################################
############ DONT ############ EDIT ############ HERE ############
##################################################################

# Redirect stderr to file for parsing errors....
exec 2> 'stderr'

DEBUG_PID=$$
ERROR_FILE=${LOG_DIR}${DATE}'_error.log'
DEBUG_FILE=${LOG_DIR}${DATE}'_debug_'${DEBUG_PID}'.log'

# Unsetting ARGS to start fresh
unset TARGS 
unset FARGS
unset ECONT
unset EARGS

############ HANDLER VARS/SETS ############
# Declaring all needed Container array's variables, lists
declare -A TARGS                                        # ARGS FROM TRAP
declare -gA FARGS                                       # ARGS FROME FILE
declare -gA ECONT                                       # CONTAINER ERROR VARIABLES (MERGE)
declare -A BECD                                          # CONTAINER ERROR VARIABLE CODE NAMES

OUTMSG=""
OUT_COLOR=false

BECD[1]="General/External script error."
BECD[2]="Bash script error."
BECD[126]="Command invoked cannot be exec."
BECD[127]="Command (func) not found."
BECD[128]="Invalid argument to exit."
BECD[130]="Script terminated by Control-C."


# An simple function that handels the debug output.
# Checks if $SDEBUG is true. When @param "false" is
# given on call, you can disable debug mode on any file/line.
#
# @param bool "false"
bs_debug() {

    switch="${1}"

    if [ "${switch}" == "auto" ] || [ "${switch}" == true ]; then
        # open a new file descriptor for logging
        exec 5>> "${DEBUG_FILE}"
        # redirect trace logs to fd 5
        BASH_XTRACEFD=5
        # format output
        PS4='- [T]: $(date "+%H:%M:%S") [F]: $(basename ${BASH_SOURCE[0]}) [M]: ${FUNCNAME[0]} [L]: $LINENO [C]: '
        # activate debug to file
        set -o xtrace
    elif [ "$1" == false ]; then
        set +o xtrace
    fi

}


#### READ FROM FILE ###
#######################
#
# This function loads the erros information from external error file.
# After loading, the array lenght will be checked. This is needed to 
# get the all informations of an error, also if it is external.
error_read_file() {

    # Read content from file
    IFS=':' read -d '' -ra err_array < 'stderr'

        # 4 (internal) ->= default error message (with arg)
        #-> ./test.sh: line 75: lol: command not found         <- 
        #
        # 3 (external) ->= needs check, if trap gives needed vales.        
        #-> cat: lol: No such file or directory            <- 
        #        
        # 3 (internal) ->= default error message (without arg)
        #./test.sh: line 83: syntax error near unexpected token `}'
        if [[ ${#err_array[@]} -eq 5 ]]; then
            
            #echo "Multiline error/errors"
            FARGS["file"]=${err_array[0]}
            FARGS["line"]=${err_array[1]##* }
            FARGS["call"]=${err_array[4]//$'\n'/} # ${dt//$'\n'/} # Remove all newlines. ${dt%$'\n'}  # Remove a trailing newline. 
            FARGS["msg"]=$(echo "${err_array[2]}" | cut -d '.' -f1)
            # Add new line
            FARGS["msg"]="${FARGS["msg"]}
"
        elif [[ ${#err_array[@]} -eq 4 ]]; then
            # "Sinleline default (4) with argument/cmd"
            FARGS["file"]=${err_array[0]}
            FARGS["line"]=${err_array[1]##* }
            FARGS["call"]=${err_array[2]}
            FARGS["msg"]=${err_array[3]}
        elif [ -n "${err_array[1]##* }" ] && [ "${err_array[1]##* }" -eq "${err_array[1]##* }" ] 2>/dev/null; then
            if [[ ${#err_array[@]} -gt 5 ]]; then
                # "multiline args bigger than 5 / some external call"
                FARGS["file"]=${err_array[0]}
                FARGS["line"]=${err_array[1]##* }
                FARGS["call"]=${err_array[2]}
                FARGS["msg"]=$(echo "${err_array[3]}" | cut -d $'\n' -f1)
                FARGS["msg"]+="\n"

            else
                # "Singleline default (3) without argument/cmd"
                FARGS["file"]=${err_array[0]}
                FARGS["line"]=${err_array[1]##* }
                FARGS["msg"]=${err_array[2]}
            fi
        else
                
                # "Singleline default external error by internal call, no line, source, func."
                FARGS["file"]=${err_array[0]}
                FARGS["call"]=${err_array[1]}
                FARGS["msg"]=${err_array[2]}

                # this is an external error.
                declare -g isExt=true

        fi    

    #delete tmp_err file
    #rm 'stderr'
}

# Merge ARG VARS
#
# This function merges the VARS - TARGS & FARGS into an error conatiner var
# Check for empty args, check for matching lines
error_merge_vars(){

    #TARGS - CODE // FILE // LINE // FUNC
    #FARGS - FILE // LINE // CALL // MSG

    # FIll empty args with N/A
    if [[ -v "${FARGS["call"]}" ]]; then
        echo "Set"
        FARGS["call"]="N/A"
    fi

    # Fill container...
    ECONT["code"]="${TARGS["code"]}"

    ECONT["type"]="${BECD[${TARGS["code"]}]}"
    ECONT["msg"]="${FARGS["msg"]}"  #//\\n/
    
    
    # Check if it is an Syntax error and re-order vars
    # If true, we read from external error file.
    if [ "${FARGS["call"]}" == "syntax error" ]; then
        ECONT["call"]=" Script"
        ECONT["cause"]="${FARGS["call"]}"
    elif [ "${TARGS["func"]}" == "error_read_file" ]; then
        ECONT["call"]="${FARGS["call"]}"
        ECONT["cause"]="main"
    else
        ECONT["call"]="${FARGS["call"]}"
        ECONT["cause"]="${TARGS["func"]}"
    fi
    
    if [ "${FARGS["file"]}" != "${TARGS["file"]}" ]; then
        if [[ ${TARGS["file"]} =~ ${BASH_SOURCE[0]} ]]; then
            ECONT["source"]="${FARGS["file"]}"
        else
            ECONT["source"]="${TARGS["file"]}"
        fi
    else
         ECONT["source"]="${FARGS["file"]}"
    fi

    # Match lines to get always the same
    if [[ "${TARGS["line"]}" -eq "${FARGS["line"]}" ]]; then
        ECONT["line"]="${TARGS["line"]}"
    else
        # Checks if FARGS-line is empty
        if [[ -z ${FARGS["line"]} ]]; then
            ECONT["line"]="${TARGS["line"]}"
        else
            ECONT["line"]="${FARGS["line"]}"
        fi
    fi
    
    # Check if is external command error
    if [ "${isExt}" == true ]; then
        ECONT["cause"]="${FARGS["file"]}"
    fi

    # done
    return 0
}

# This is the function to get and build stack
error_stacktrace() {

    declare -a libfuncs

    # Define an array that holds all library own function names, so they are excluded in stack call.
    libfuncs=("bs_error_trap" "error_read_file" "bs_debug" "error_merge_vars" "error_stacktrace")

    #define local function vars
    local frame=0 LINE SUB FILE prfx


        if [ ${OUT_COLOR} == true ]; then
            prfx="\n${C['RED']}>> ${F['END']}${C['CYN']}${F['UBE']}FULLSTACK:${F['END']}\n"
        else
            prfx="\n>> FULLSTACK:\n\n"
        fi

    # add blank line
    OUTMSG+="${prfx}"

    # loop through frames (func calls)
    while read -r LINE SUB FILE < <(caller "${frame}"); do
        
        # Skip library own names in stack output.
        if [[ ${libfuncs[*]} =~ ${SUB} ]]; then
                ((frame++))
        else
            if [ ${OUT_COLOR} == true ]; then
                OUTMSG+="    ${SUB} @ ${FILE}:${LINE}\n"
            else
                OUTMSG+="    ${SUB} @ ${FILE}:${LINE}\n"
            fi
            ((frame++))
        fi
    done

}


error_out_msg() {

    local prfx

    if [ ${OUT_COLOR} == true ]; then
        prfx="${C['RED']}>> ${F['END']}"
        
        OUTMSG="\n------------------------------------------------\n"
        OUTMSG+="${prfx}${C['RED']}${F['BLD']}ERROR (${ECONT["code"]})${C['WHT']} - ${ECONT["type"]}${F['END']}\n\n"
        OUTMSG+="${prfx}${C['CYN']}MSG:${F['END']}${C['GRN']}${ECONT["msg"]}${F['END']}"
        OUTMSG+="${prfx}${C['CYN']}CALL/CMD/ARG:${F['END']}${C['GRN']}${ECONT["call"]}${F['END']}\n"
        OUTMSG+="${prfx}${C['CYN']}CAUSE BY: ${F['END']}${C['RED']}${ECONT["cause"]} ${C['WHT']}IN: ${C['RED']}${F['UBE']}${ECONT["source"]}${F['UEN']} ${C['WHT']}ON LINE: ${C['GRN']}${ECONT["line"]}${F['END']}\n"
    
    else
        prfx=">> "
        
        OUTMSG="\n------------------------------------------------\n"
        OUTMSG+="${prfx}ERROR (${ECONT["code"]}) - ${ECONT["type"]}\n\n"
        OUTMSG+="${prfx}MSG:${ECONT["msg"]}"
        OUTMSG+="${prfx}CALL/CMD/ARG:${ECONT["call"]}\n"
        OUTMSG+="${prfx}CAUSE BY: ${ECONT["cause"]} IN: ${ECONT["source"]} ON LINE: ${ECONT["line"]}\n"
    fi
    
}

# Bash error logging, the output printed in an error log file.
# This function builds the style of the log entry.
error_out_log() {

    local logtime mode

    unset mode logtime

    mode=$1

        case ${mode} in
            fancy)
                # load (user) theme frome file
            ;;
            *)  # default output to logfile (same as onscreen)
                # adding Logtime
                logtime=$(date +%d.%m.%Y' - '%H:%M:%S)
                OUTMSG_LOG="\n>> Date/Time: ${logtime}"
                OUTMSG_LOG+="${OUTMSG}"
            ;;
        esac

        # Write output to logfile...
        echo -e "${OUTMSG_LOG}" >> "${ERROR_FILE}"

}


error_read_src() {

    local prfx

    if [ ${OUT_COLOR} == true ]; then
            prfx="${C['RED']}>> ${F['END']}"
            OUTMSG+="\n${prfx}${C['CYN']}${F['UBE']}SNIPPED:${F['END']}\n"
            OUTMSG+="\n${prfx}-> 06 lines of source from ${F['UBE']}${ECONT["source"]}${F['UEN']} <-\n"
            OUTMSG+="${prfx}{\n"
        else
            prfx=">> "
            OUTMSG+="\n>> SNIPPED:\n"
            OUTMSG+="\n>> -> 06 lines of source from ${ECONT["source"]} <-\n"
            OUTMSG+=">> {\n"
        fi

        regfile=${ECONT["source"]}
        regex=$(awk 'NR>L-4 && NR<L+4 { printf "L:%-5d%3s%s\n",NR,(NR==L?">>>":""),$regfile }' L="${ECONT["line"]}" "${ECONT["source"]}")
                    
        if [ ${OUT_COLOR} == true ]; then
            OUTMSG+="${regex}\n${prfx}${F['END']}}\n"
        else
            OUTMSG+="${regex}\n>> }\n"
        fi
}

error_msg_color() {

    # Set var(switch) to store bool
    switch=${1}
    
    # Check if (var) is true, if yes colors will be enabled.
    if [ "${switch}" == true ]; then
        
        OUT_COLOR=true
        
        # Declare an global associative array
        declare -gA C                                   # color array
        declare -gA F                                   # formating array

        # Fill color array with values (tput command output)
        C['BLK']="$(tput setaf 0)"
        C+=(['RED']="$(tput setaf 1)" ['GRN']="$(tput setaf 2)" ['YEL']="$(tput setaf 3)" ['BLU']="$(tput setaf 4)")
        C+=(['MAG']="$(tput setaf 5)" ['CYN']="$(tput setaf 6)" ['WHT']="$(tput setaf 7)")
        C['END']="$(tput sgr0)"         # F['END'] can also be used.

        # Fill formating array with values (tput command output)
        F['BLD']="$(tput bold)"
        F['UBE']="$(tput smul)"         # Underline BEgin
        F['UEN']="$(tput rmul)"         # Underline ENd
        F['DIM']="$(tput dim)"
        F['END']="$(tput sgr0)"         # C['END'] can also be used.
    
    elif [ "${switch}" == false ]; then
        
        # Set color output false, destroy arrays.
        OUT_COLOR=false
        unset C F

    fi

}


# Bash error library handler
# This is an own self written error handler, to handle function
# and command errors during the runtime/developemnt.
#
# It uses an typical trap, but can also display simple user
# error messages. These messages have to be piped.
# 
# Errors will logged in an file if enabled.
#
# @param $errmsg | @output err
bs_error_trap() {

    # oldvars, needed?
    #err_time=$(date "+%H:%M:%S")

    declare -g rerun

    # get lenght of TARGS (for function name (4))
    TARGS_LEN=${#*}
    TARGS["code"]="${1}"

        # Check if script is called by default script 
        # exit (0) or error -> jump to cleanup if true
        if [[ ${TARGS["code"]} -eq 0 ]] || [[ ${rerun} -eq 1 ]]; then
            unset rerun
            error_exit_clean
            return 0
        fi

    TARGS["file"]="${2}"
    TARGS["line"]="${3}"
       
        # Check if lenght of args is 4 (function is given?)
        if [[ ${TARGS_LEN} -eq 4 ]]; then
            if [[ -z "$4" ]]; then
                TARGS["func"]="Script"
            else
                TARGS["func"]="${4}"
            
            fi
        else
            TARGS["func"]="N/A"
        fi

    # Load data from file
    error_read_file

    # Merge vars
    error_merge_vars

    # Generate default output msg
    error_out_msg
    
    # Check if stacktrace should be displayd
    if [ "${ERROR_DISP_STACK}" == true ]; then
        error_stacktrace
    fi

    # Check if file source snipped should be displayd
    if [ "${ERROR_DISP_SNIPPED}" == true ]; then
        error_read_src
    fi

    # Check if logging of errors in enabled.
    if [ "${ERROR_LOG}" == true ]; then
        error_out_log ""
    fi

    echo -e "${OUTMSG}"

    # don't re_run on script exit call
    rerun=1

    # Check if Exit on error is true.
    if [ "${ERROR_EXIT}" == true ]; then
        exit 1
    fi

}

error_exit_clean() {

    local dur

    if [ -f 'stderr' ]; then
         rm 'stderr'
    fi

    count_debug_logs=$(find ${LOG_DIR}$(date +%m.%d.%y)_debug* | wc -l)
    
        if [[ ${count_debug_logs} -gt ${LOG_MAXCOUNT} ]]; then
            printf ">> There are %s debug log files in folder, cleaning....\n" "${count_debug_logs}"

            ls -t logs/*debug* | tail -n +2 | xargs rm --
        fi
    
    dur=$(echo "$(date +%s.%N)"-"${MTSTART}" | node -p)

    if [ ${OUT_COLOR} == true ]; then
        prfx="${C['RED']}>> ${F['END']}"
        dur=$(printf "%0.6f" "${dur}")
        echo -e "${prfx} ${F['DIM']}Execution time: ${dur} seconds${F['END']}\n"
    else
        prfx=">> "
        echo -e "${prfx} Execution time: ${dur} seconds\n"
        
    fi

    #echo -e "${prfx} Execution time: ${dur} seconds\n"
    #printf "%s ${C['RED']} Execution time: %.6f seconds\n" "${prfx}" ""
    return 0
}


_error() {

    # Fill ECONT container with args and user message...
    ECONT["code"]="1"
    ECONT["type"]="(M): ${BECD[${ECONT["code"]}]}"
    
    ECONT["msg"]=" ${1}\n"

    ECONT["call"]=" N/A (see message)"
    ECONT["cause"]="$(caller 0 | cut -d ' ' -f2)"
    ECONT["source"]="$(caller 0 | cut -d ' ' -f3)"
    ECONT["line"]="$(caller 0 | cut -d ' ' -f1)"

    # Generate default output msg
    error_out_msg
    
    # Check if file source snipped should be displayd
    if [ "${ERROR_DISP_SNIPPED}" == true ]; then
        error_read_src
    fi

    # Check if logging of errors in enabled.
    if [ "${ERROR_LOG}" == true ]; then
        error_out_log ""
    fi

    echo -e "${OUTMSG}"


}


# Function to call Error manualy 
# Name is specily for short calling 
# _log(){

#     unset LARGS

#     declare -A LARGS

#     # Store passed vars
#     EARGS["msg"]=${1}
    
#     if [[ -n ${2} ]]; then EARGS["type"]=${2}; fi


#     # Check if type is given
#     if [[ -v ${EARGS["type"]} ]]; then
#             echo -e "\n${EARGS["msg"]}\n"
#     else
#         # convert short level to name
#         case ${EARGS["type"]} in
#             i)  etype="INFO";;
#             w)  etype="WARN";;
#             e)  etype="ERROR";;
#             *)  etype="INFO";;
#         esac
#             echo "${etype}::${EARGS["msg"]}"
#     fi

#     # check for color mode

        
#     # check for log

#     # check for exit?

# }



#### DONT #### EDIT #### DONT #### EDIT #### DONT #### EDIT #### DONT #### EDIT ##
## DONT #### EDIT #### DONT #### EDIT #### DONT #### EDIT #### DONT #### EDIT #### 
# DONT #### EDIT #### DONT #### EDIT #### DONT #### EDIT #### DONT #### EDIT ####  

# Checks if debug mode is set to auto enabled or if an
# command option (-youropt) is given on script call
if [ ${DEBUG_AUTO} == true ]; then    
    # call debug function
    bs_debug auto
fi

if [ ${OUT_COLOR} == true ]; then    
    # call color function
    # load colors in array.
    error_msg_color true
fi


# check if arg is DEBUG_OPT
for i in "${@}"
do 
    if [ "${i}" == "${DEBUG_OPT}" ];then    
    # call debug function
        bs_debug true
    fi

    if [ "${i}" == "-c" ] || [ "${i}" == "--color" ]; then    
        # call color function
        # load colors in array.
        error_msg_color true
    fi 
done

