#!/usr/bin/env bash

#
# ## log()
#
# Log arguments to the calling environments STDOUT.
#
# ### Input Parameters
#
# Strings to be logged.
#
# ### Stream Outputs
#
# Prints arguments passed in to the calling environments STDOUT with a newline
# character appended.
#
# ### Environmental effects
#
# None.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# none.
#
# ### Usage Examples
#
# user$ log "Hello there! "
# Hello there!
#
core_log()
{
  printf "$*\n"
}
log()
{
  core_log "$@"
}

#
# ## warn()
#
# Log arguments to the calling environments STDOUT preceeded by 'WARNING .
#
# ### Input Parameters
#
# Strings to be logged.
#
# ### Stream Outputs
#
# Prints arguments passed in to the calling environments STDOUT with a newline
# character appended and 'WARNING ' prepended.
#
# ### Environmental effects
#
# None.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# none.
#
# ### Usage Examples
#
# user$ info "Bad Monkeys are Typing! "
# WARNING: Bad Monkeys are Typing!
#
core_warn()
{
  printf "WARNING: $*\n"
}
warn()
{
  core_warn "$@"
}
#
# ## debug()
#
# Log arguments to the calling environments STDOUT preceeded by 'DEBUG: .
#
# ### Input Parameters
#
# Debug type, Strings to be logged.
#
# ### Stream Outputs
#
# Prints arguments passed in to the calling environments STDOUT with a newline
# character appended and 'DEBUG: ' prepended.
#
# ### Environmental effects
#
# None.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# Fails if no arguments are given.
#
# ### Usage Examples
#
# The following will show when bdsm is called with --debug=[welcome|all]
# or if debug_flags set to welcome|all.
# Code:
#
# debug welcome "Hello there! "
# Output:
#
# DEBUG welcome: Hello there!
#
core_debug()
{
  local type="$1"
  shift

  if (( debug_flag == 1 )) && options_check "$debug_flags" "$type"
  then
    printf "DEBUG $type: $*\n"
  else
    return 0
  fi
}
debug()
{
  core_debug "$@"
}

#
# ## log_search()
#
# Log arguments to the calling environments STDOUT preceeded by 'DEBUG: .
#
# ### Input Parameters
#
# Search type, Search result, Strings to be logged.
#
# ### Stream Outputs
#
# Prints arguments passed in to the calling environments STDOUT with a newline
# character appended and 'DEBUG: ' prepended.
#
# ### Environmental effects
#
# None.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# Fails if no arguments are given.
#
# ### Usage Examples
#
# user$ ./install --debug=search help
# DEBUG search:   module bdsm/help            in builtin/bdsm/modules/shell/help.
#
core_log_search()
{
  local name=$1 value=$2
  shift
  shift

  if (( debug_flag == 1 )) && options_check "$debug_flags" "search"
  then
    printf "DEBUG search: %8s %-20s in $*.\n" "$name" "$value"
  else
    return 0
  fi
}
log_search()
{
  core_log_search "$@"
}

#
# ## log_todo()
#
# Log arguments to the calling environments STDOUT preceeded by 'DEBUG: and caller file:line.
#
# ### Input Parameters
#
# Search type, Search result, Strings to be logged.
#
# ### Stream Outputs
#
# Prints arguments passed in to the calling environments STDOUT with a newline
# character appended and 'DEBUG: caller_file:line' prepended.
#
# ### Environmental effects
#
# None.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# Fails if no arguments are given.
#
# ### Usage Examples
#
# user$ ./install --debug=search help
# DEBUG search:   module bdsm/help            in builtin/bdsm/modules/shell/help.
#
core_log_todo()
{
  if (( debug_flag == 1 )) && options_check "$debug_flags" "todo"
  then
    local file="${BASH_SOURCE[2]}" line="${BASH_LINENO[1]}"
    printf "DEBUG todo: ${file##${bdsm_path}\/extensions\/}:${line}:\n"
    printf "$*" | xargs -s 72 | xargs -I {} printf "DEBUG todo: # {}\n"
  else
    return 0
  fi
}

log_todo()
{
  core_log_todo "$@"
}

#
# ## error()
#
# Log arguments to the calling environments STDERR preceeded by 'ERROR '.
# Exits with status code 1.
#
# ### Input Parameters
#
# Strings to be logged.
#
# ### Stream Outputs
#
# Prints arguments passed in to the calling environments STDOUT with a newline
# character appended and 'ERROR ' prepended.
#
# ### Environmental effects
#
# None.
#
# ### Return Codes
#
# 1 for failure.
#
# ### Failure Scenarios
#
# Fails if no arguments are given.
#
# ### Usage Examples
#
# user$ error "Hello there! "
# ERROR Hello there!
# *poof* shell closed...
#
core_error()
{
  set +o xtrace
  printf "\nERROR: $*\n" >&2
  set +o errtrace
  trap - ERR
  exit 1
}
error()
{
  core_error "$@"
}

#
# ## succeed()
#
# Log arguments to the calling environments STDOUT. Exits with status code 0.
#
# ### Input Parameters
#
# Strings to be logged.
#
# ### Stream Outputs
#
# Prints arguments passed in to the calling environments STDOUT with a newline
# character appended.
#
# ### Environmental effects
#
# None.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# Fails if no arguments are given.
#
# ### Usage Examples
#
# user$ succeed "Hello there! "
# Hello there!
# *poof* shell closed...
#
core_succeed() {
  set +o xtrace
  if [[ -n "$*" ]]
  then
    printf "$*\n"
  fi
  exit 0
}
succeed()
{
  core_succeed "$@"
}

#
# ## fail()
#
# Log arguments to the calling environments STDERR preceeded by 'ERROR '.
# Exits with status code 1.
#
# ### Input Parameters
#
# Strings to be logged.
#
# ### Stream Outputs
#
# Prints arguments passed in to the calling environments STDOUT with a newline
# character appended and 'ERROR ' prepended.
#
# ### Environmental effects
#
# None.
#
# ### Return Codes
#
# 1 for failure.
#
# ### Failure Scenarios
#
# Fails if no arguments are given.
#
# ### Usage Examples
#
# user$ error "Hello there! "
# ERROR Hello there!
# *poof* shell closed...
#
core_fail()
{
  trace_flag=0
  backtrace "$*"
  exit 1
}
fail()
{
  core_fail "$@"
}

#
# ## set_ps4()
#
# Set the environmental PS4 variable for informative tracing.
#
# ### Input Parameters
#
# None.
#
# ### Stream Outputs
#
# None.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# No failure scenarios currently.
#
# ### Usage Examples
#
#     user$ echo "'$PS4'"
#     '+ '
#     user$ set_ps4
#     user$ echo "'$PS4'"
#     ' > ${BASH_SOURCE##${bdsm_path}\/} ${FUNCNAME[0]:+${FUNCNAME[0]}()} ${LINENO} $ '
#
# ### Code Walkthrough
set_ps4()
{
  # Set the PS4 variable. Then export the PS4 variabel so that this script and
  # called bash scripts will display a more detailed and formatted trace output.
  # Finally return true, 0, for success.
  PS4="# \${BASH_SOURCE##\${bdsm_path}\/extensions\/} \${FUNCNAME[0]:+\${FUNCNAME[0]}()} \${LINENO} $ "
  export PS4
  return 0
}

#
# ## trace_filter()
#
# Enables tracing depending on the filter
# If enabled  then every line of code execution after the function returns
# will be displayed in the format specified by the PS4 environment variable
# (see set_ps4).
#
# ### Input Parameters
#
# type of trace to filter
# if none given restores default state
#
# ### Stream Outputs
#
# None.
#
# ### Return Codes
#
# 0 for success.
#
# ### Failure Scenarios
#
# No failure scenarios currently.
#
# ### Usage Examples
#
#     trace_filter search
#     <search code>
#     trace_filter
#
# ### Code Walkthrough
core_trace_filter()
{
  local _action="$1" old
  shift
  case "${_action}" in
    start)
      filter="$1"
      trace_filter_stack=( "$filter" "${trace_filter_stack[@]}" )
      ;;
    stop)
      old="${trace_filter_stack[0]}"
      old="${old%%=*}"
      if [[ "${FUNCNAME[2]}" == "${old}" ]]
      then
        shift || true
        trace_filter_stack=( "$@" )
      fi
      ;;
    *)
      fail "Unknown action ${_action}"
      ;;
  esac
  filter="${trace_filter_stack[0]}"

  if [[ -n "$filter" ]]
  then
    if options_check "$trace_flags" "${filter##*=}"
    then
      set -o xtrace
    fi
  else
    # No param, restore default state
    if (( trace_flag != 0 ))
    then
      set -o xtrace
    fi
  fi
}
trace_filter()
{
  set +o xtrace
  if [[ -n "$1" ]]
  then
    core_trace_filter start $state "${FUNCNAME[1]}=$1"
  else
    core_trace_filter stop $state "${trace_filter_stack[@]}"
  fi
}

action()
{
  trace_filter action
  "$@"
}

#
# ## backtrace()
#
# Display a backtrace of the current call stack to the point that backtrace was
# called from. Currently trace is turned on as well so that execution after
# calling backtrace is traced.
#
# ### Input Parameters
#
# None.
#
# ### Stream Outputs
#
# The call stack up to the current point of execution is printed to STDOUT of
# the calling environment.
#
# ### Return Codes
#
# 0 for success
#
# ### Failure Scenarios
#
# No failure scenarios currently.
#
# ### Usage Examples
#
#     user$ cat ./test
#     #!/usr/bin/env bash
#     source "/usr/local/bdsm/extensions/builtin/core/modules/shell/core/initialize" # Load BDSM framework core.
#     modules trace # Load the trace module.
#
#     functiona()
#     {
#       echo "function a"
#       functionb
#       echo "function a"
#     }
#
#     functionb()
#     {
#       echo "function b"
#       functionc
#       echo "function b"
#     }
#
#     functionc()
#     {
#       echo "function c"
#       backtrace
#       echo "function c"
#     }
#
#     functiona
#
#     user$ $PWD/test
#     function a
#     function b
#     function c
#     Error Backtrace:
#        Trace   Line Function             File
#           4.     25 functionc()          /Users/wayneeseguin/test
#           3.     18 functionb()          /Users/wayneeseguin/test
#           2.     11 functiona()          /Users/wayneeseguin/test
#           1.     29 main()               /Users/wayneeseguin/test
#      > /Users/wayneeseguin/test functionc() 26 $ echo 'function c'
#     function c
#      > /Users/wayneeseguin/test functionb() 19 $ echo 'function b'
#     function b
#      > /Users/wayneeseguin/test functiona() 12 $ echo 'function a'
#     function a
#
# ### Code Walkthrough
backtrace()
{
  # Turn off shell tracing
  trace_filter backtrace #backtrace is disabled by default

  # Declare separator and columns variables. Initialize columns to the terminal
  local _separator _columns

  # If setting the columns via tput does not work then default the columns to 80
  if ! _columns=$(tput cols)
  then
    _columns=80
  fi

  # Set the separator variable contents to the separator filled out to the
  # number of columns.
  printf -v _separator '%*s' "${_columns:-${COLUMNS:-80}}"

  # Store the passed in message, if given as the first parameter, in the
  # message variable, default to empty string.
  local _message="${1:-}" _flag="$2"

  # If a message was given print the message to the calling environment's STDERR
  if [[ -n _message ]]
  then
    printf "\n${_message}\n\n" >&2
  fi

  # Declare function local variables
  local _source _function _line _index _largest _format _digits _longest

  # Loop over the function names in the function array stack to determine
  # which has the longest length.
  # Note that we do not use array_largest here as we are in an error function.
  for (( _index=0 ; _index < ${#FUNCNAME[@]} ; _index++ ))
  do
    # For each function name in the stack if the function name is longer than
    # the length of current largest then
    if (( ${#FUNCNAME[${_index}]} > ${#_largest} ))
    then
      # set it as the new largest element
      _largest=${FUNCNAME[${_index}]}
    fi
  done

  # Loop over the bash source files array in order to determine
  # which has the the longest length.
  # Note that we do not use array_largest here as we are in an error function.
  for (( _index=0 ; _index < ${#BASH_SOURCE[@]} ; _index++ ))
  do
    # For each source file in the source files array, if the file name/path
    # is longer than the current longest then set it as the new longest element
    if (( ${#BASH_SOURCE[${_index}]} > ${#_longest} ))
    then
      _longest=${BASH_SOURCE[${_index}]}
    fi
  done

  for (( _index=0 ; _index < ${#BASH_LINENO[@]} ; _index++ ))
  do
    # For each line number in the line numbers array, if the number of digits
    # is longer than the current longest digits then set it as the new largest
    if (( ${#BASH_LINENO[${_index}]} > ${#_digits} ))
    then
      _digits=${BASH_LINENO[${_index}]}
    fi
  done

  # Determine the format string output for the backtrace based on the users
  # set editor. If an editor has not been set, default the editor variable to
  # the pager or empty string.
  case "${EDITOR:=${PAGER:-}}" in
    (mvim*|mate*)
      # Set the format based on GUI editing tools format for opening
      # a file to a given line using a file URI.
      _format="%-60s # %-$((${#_largest} + 2))s \n"
      # Print the backtrace header to the calling environment's STDERR
      printf "${_format}" "Source" "function()"  1>&2
      ;;
    (vi*|gvim*|emacs*|*)
      # Set the format based on the console editing tools format for opening
      # a file to a given line.
      # _format="%-$((${#_largest} + 2))s $ %${#EDITOR}s +%-$((${#_digits}))s %-$((${#_longest} + 2))s %s\n"
      _format="%${#EDITOR}s +%-$((${#_digits}))s %-$((${#_longest} + 2))s # %s %-$((${#_largest} + 2))s\n"
      # Print the backtrace header to the calling environment's STDERR
      printf "${_format}" ' ' "#" "source file" "function()"  1>&2
      ;;
  esac

  # Print the backtrace separator line.
  printf "${_separator// /=}\n" 1>&2

  # Iterate over the functions array stack skipping this backtrace function [0]
  for (( _index=1 ; _index < ${#FUNCNAME[@]} ; ++_index ))
  do
    _source="${BASH_SOURCE[${_index}]}"
    _function="${FUNCNAME[${_index}]:+${FUNCNAME[${_index}]}()}"
    _line=${BASH_LINENO[$(( _index - 1 ))]}

    case "$_function" in
      fail\(\)|error\(\)|command_not_found_handle\(\)|command_not_found\(\))
        continue
        ;;
    esac

    # Determine the format string output for the backtrace based on the users
    # set editor. Note that if an editor was not set then previously we have
    # defualted it to the pager. If no pager is set we simply output a standard
    # unix utility trace line.
    case "${EDITOR:-}" in
      # Print with the format and content for GUI editing tools a file to open a
      # given line using a URI. print backtrace to calling environment's STDERR
      (mvim*|mate*)
        printf "${_format}" "open ${EDITOR// *}://open?url=file://${_source}&line=${_line}" "${_function}" 1>&2
        ;;
      # Print with the format and content for console editing tools a to open a
      # file to a given line print backtrace to calling environment's STDERR
      (vi*|gvim*|emacs*|*)
        printf "${_format}" "${EDITOR}" "${_line}" "${_source} " "${_function}" 1>&2
        ;;
    esac
  done

  # Print the backtrace separator line and if the trace flag is nonnegative
  # then turn tracing back on
  printf "${_separator// /=}\n" 1>&2

  if [[ "${_flag}" != "no_exit" ]]
  then
    kill -s USR2 $APP_PID #to exit application not current subshell
  fi
}
