# -*- shell-script -*-
# break.sh - Debugger Break and Watch routines
#
#   Copyright (C) 2002-2003, 2006-2011, 2014-2019, 2023 Rocky
#   Bernstein <rocky@gnu.org>
#
#   This program is free software; you can redistribute it and/or
#   modify it under the terms of the GNU General Public License as
#   published by the Free Software Foundation; either version 2, 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
#   General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; see the file COPYING.  If not, write to
#   the Free Software Foundation, 59 Temple Place, Suite 330, Boston,
#   MA 02111 USA.

#================ VARIABLE INITIALIZATIONS ====================#

typeset -a _Dbg_keep
_Dbg_keep=('keep' 'del')

# Note: we loop over possibly sparse arrays with _Dbg_brkpt_max by adding one
# and testing for an entry. Could add yet another array to list only
# used indices. Bash is kind of primitive.

# Breakpoint data structures

# Line number of breakpoint $i
typeset -a _Dbg_brkpt_line; _Dbg_brkpt_line=()

# Number of breakpoints.
typeset -i _Dbg_brkpt_count=0

# filename of breakpoint $i
typeset -a  _Dbg_brkpt_file; _Dbg_brkpt_file=()

# 1/0 if enabled or not
typeset -a  _Dbg_brkpt_enable; _Dbg_brkpt_enable=()

# Number of times hit
typeset -a _Dbg_brkpt_counts; _Dbg_brkpt_counts=()

# Is this a onetime break?
typeset -a _Dbg_brkpt_onetime; _Dbg_brkpt_onetime=()

# Condition to eval true in order to stop.
typeset -a  _Dbg_brkpt_cond; _Dbg_brkpt_cond=()

# Needed because we can't figure out what the max index is and arrays
# can be sparse.
typeset -i  _Dbg_brkpt_max=0

# Maps a resolved filename to a list of beakpoint line numbers in that file
typeset -A _Dbg_brkpt_file2linenos; _Dbg_brkpt_file2linenos=()

# Maps a resolved filename to a list of breakpoint entries.
typeset -A _Dbg_brkpt_file2brkpt; _Dbg_brkpt_file2brkpt=()

# Note: we loop over possibly sparse arrays with _Dbg_brkpt_max by adding one
# and testing for an entry. Could add yet another array to list only
# used indices. Bash is kind of primitive.

# Watchpoint data structures
typeset -a  _Dbg_watch_exp=() # Watchpoint expressions
typeset -a  _Dbg_watch_val=() # values of watchpoint expressions
typeset -ai _Dbg_watch_arith=()  # 1 if arithmetic expression or not.
typeset -ai _Dbg_watch_count=()  # Number of times hit
typeset -ai _Dbg_watch_enable=() # 1/0 if enabled or not
typeset -i  _Dbg_watch_max=0     # Needed because we can't figure out what
                                    # the max index is and arrays can be sparse

typeset     _Dbg_watch_pat="${_Dbg_int_pat}[wW]"

#========================= FUNCTIONS   ============================#

_Dbg_save_breakpoints() {
  typeset file
  typeset -p _Dbg_brkpt_file         >> "$_Dbg_statefile"
  typeset -p _Dbg_brkpt_cond         >> "$_Dbg_statefile"
  typeset -p _Dbg_brkpt_line         >> "$_Dbg_statefile"
  typeset -p _Dbg_brkpt_count        >> "$_Dbg_statefile"
  typeset -p _Dbg_brkpt_enable       >> "$_Dbg_statefile"
  typeset -p _Dbg_brkpt_onetime      >> "$_Dbg_statefile"
  typeset -p _Dbg_brkpt_max          >> "$_Dbg_statefile"
  typeset -p _Dbg_brkpt_file2linenos >> "$_Dbg_statefile"
  typeset -p _Dbg_brkpt_file2brkpt   >> "$_Dbg_statefile"

}

_Dbg_save_watchpoints() {
  typeset -p _Dbg_watch_exp    >> "$_Dbg_statefile"
  typeset -p _Dbg_watch_val    >> "$_Dbg_statefile"
  typeset -p _Dbg_watch_arith  >> "$_Dbg_statefile"
  typeset -p _Dbg_watch_count  >> "$_Dbg_statefile"
  typeset -p _Dbg_watch_enable >> "$_Dbg_statefile"
  typeset -p _Dbg_watch_max    >> "$_Dbg_statefile"
}

# Start out with general break/watchpoint functions first...

# Enable/disable breakpoint or watchpoint by entry numbers.
_Dbg_enable_disable() {
    if (($# <= 1)) ; then
	_Dbg_errmsg "_Dbg_enable_disable error - need at least 1 arg, got $#"
	return 1
    fi
    typeset -i on=$1
    typeset en_dis=$2
    shift; shift

  typeset to_go
  if [[ $1 == 'display' ]] ; then
    shift
    to_go="$*"
    typeset i
    eval "$_Dbg_seteglob"
    for i in $to_go ; do
      case $i in
	$_Dbg_int_pat )
	  _Dbg_enable_disable_display $on "$en_dis" $i
	;;
	* )
	  _Dbg_errmsg "Invalid entry number skipped: $i"
      esac
    done
    eval "$_Dbg_resteglob"
    return 0
  elif [[ $1 == 'action' ]] ; then
    shift
    to_go="$*"
    typeset i
    eval "$_Dbg_seteglob"
    for i in $to_go ; do
      case $i in
	$_Dbg_int_pat )
	  _Dbg_enable_disable_action $on "$en_dis" $i
	;;
	* )
	  _Dbg_errmsg "Invalid entry number skipped: $i"
      esac
    done
    eval "$_Dbg_resteglob"
    return 0
  elif [[ $1 == 'breakpoints' ]] ; then
    shift
    to_go="$@"
    if (( 0 == $# )) ; then
	to_go=${!_Dbg_brkpt_enable[@]}
    fi
  else
    to_go="$@"
    if (( 0 == $# )) ; then
	to_go=${!_Dbg_brkpt_enable[@]}
    fi
  fi

  typeset i
  eval "$_Dbg_seteglob"
  for i in $to_go ; do
    case $i in
      $_Dbg_watch_pat )
        _Dbg_enable_disable_watch $on $en_dis ${del:0:${#del}-1}
        ;;
      $_Dbg_int_pat )
        _Dbg_enable_disable_brkpt $on "$en_dis" $i
	;;
      * )
      _Dbg_errmsg "Invalid entry number skipped: $i"
    esac
  done
  eval "$_Dbg_resteglob"
  return 0
}

# Print a message regarding how many times we've encountered
# breakpoint number $1 if the number of times is greater than 0.
# Uses global array _Dbg_brkpt_counts.
function _Dbg_print_brkpt_count {
  typeset -i i; i=$1
  if (( _Dbg_brkpt_counts[i] != 0 )) ; then
    if (( _Dbg_brkpt_counts[i] == 1 )) ; then
      _Dbg_printf "\tbreakpoint already hit 1 time"
    else
      _Dbg_printf "\tbreakpoint already hit %d times" ${_Dbg_brkpt_counts[$i]}
    fi
  fi
}

#======================== BREAKPOINTS  ============================#

# clear all brkpts
_Dbg_clear_all_brkpt() {
  _Dbg_write_journal_eval "_Dbg_brkpt_file2linenos=()"
  _Dbg_write_journal_eval "_Dbg_brkpt_file2brkpt=()"
  _Dbg_write_journal_eval "_Dbg_brkpt_line=()"
  _Dbg_write_journal_eval "_Dbg_brkpt_cond=()"
  _Dbg_write_journal_eval "_Dbg_brkpt_file=()"
  _Dbg_write_journal_eval "_Dbg_brkpt_enable=()"
  _Dbg_write_journal_eval "_Dbg_brkpt_counts=()"
  _Dbg_write_journal_eval "_Dbg_brkpt_onetime=()"
  _Dbg_write_journal_eval "_Dbg_brkpt_count=0"
}

# Internal routine to a set breakpoint unconditonally.

_Dbg_set_brkpt() {
    (( $# < 3 || $# > 4 )) && return 1
    typeset source_file
    source_file=$(_Dbg_expand_filename "$1")
    typeset -ri lineno=$2
    typeset -ri is_temp=$3
    typeset -r  condition=${4:-1}

    # Increment brkpt_max here because we are 1-origin
    ((_Dbg_brkpt_max++))
    ((_Dbg_brkpt_count++))

    _Dbg_brkpt_line[_Dbg_brkpt_max]=$lineno
    _Dbg_brkpt_file[_Dbg_brkpt_max]="$source_file"
    _Dbg_brkpt_cond[_Dbg_brkpt_max]="$condition"
    _Dbg_brkpt_onetime[_Dbg_brkpt_max]=$is_temp
    _Dbg_brkpt_counts[_Dbg_brkpt_max]=0
    _Dbg_brkpt_enable[_Dbg_brkpt_max]=1

    typeset dq_source_file
    dq_source_file=$(_Dbg_esc_dq "$source_file")
    typeset dq_condition=$(_Dbg_esc_dq "$condition")

    # Make sure we are not skipping over functions.
    _Dbg_old_set_opts="${_Dbg_old_set_opts%% *} -o functrace"
    _Dbg_write_journal_eval "_Dbg_old_set_opts='$_Dbg_old_set_opts'"

    _Dbg_write_journal_eval "_Dbg_brkpt_line[$_Dbg_brkpt_max]=$lineno"
    _Dbg_write_journal_eval "_Dbg_brkpt_file[$_Dbg_brkpt_max]=\"$dq_source_file\""
    _Dbg_write_journal "_Dbg_brkpt_cond[$_Dbg_brkpt_max]=\"$dq_condition\""
    _Dbg_write_journal "_Dbg_brkpt_onetime[$_Dbg_brkpt_max]=$is_temp"
    _Dbg_write_journal "_Dbg_brkpt_counts[$_Dbg_brkpt_max]=\"0\""
    _Dbg_write_journal "_Dbg_brkpt_enable[$_Dbg_brkpt_max]=1"

    # Add line number with a leading and trailing space. Delimiting the
    # number with space helps do a string search for the line number.
    _Dbg_write_journal_eval "_Dbg_brkpt_file2linenos[\"$source_file\"]+=\" $lineno \""
    _Dbg_write_journal_eval "_Dbg_brkpt_file2brkpt[\"$source_file\"]+=\" $_Dbg_brkpt_max \""

    source_file=$(_Dbg_adjust_filename "$source_file")
    if (( is_temp == 0 )) ; then
	_Dbg_msg "Breakpoint $_Dbg_brkpt_max set in file ${source_file}, line $lineno."
    else
	_Dbg_msg "One-time breakpoint $_Dbg_brkpt_max set in file ${source_file}, line $lineno."
    fi
    _Dbg_write_journal "_Dbg_brkpt_max=$_Dbg_brkpt_max"
    return 0
}

# Internal routine to unset the actual breakpoint arrays.
# 0 is returned if successful
_Dbg_unset_brkpt_arrays() {
    (( $# != 1 )) && return 1
    typeset -i del=$1
    _Dbg_write_journal_eval "unset _Dbg_brkpt_line[$del]"
    _Dbg_write_journal_eval "unset _Dbg_brkpt_counts[$del]"
    _Dbg_write_journal_eval "unset _Dbg_brkpt_file[$del]"
    _Dbg_write_journal_eval "unset _Dbg_brkpt_enable[$del]"
    _Dbg_write_journal_eval "unset _Dbg_brkpt_cond[$del]"
    _Dbg_write_journal_eval "unset _Dbg_brkpt_onetime[$del]"
    ((_Dbg_brkpt_count--))
    return 0
}

# Internal routine to delete a breakpoint by file/line.
# We return the number of breakpoints found or zero if we didn't find
# a breakpoint
function _Dbg_unset_brkpt {
    (( $# != 2 )) && return 0

    typeset    filename=$1
    typeset -i lineno=$2
    typeset -i found=0
    typeset    fullname
    fullname=$(_Dbg_expand_filename "$filename")

    # FIXME: combine with _Dbg_hook_breakpoint_hit
    typeset -a linenos
    eval "linenos=(${_Dbg_brkpt_file2linenos["$fullname"]})"
    typeset -a brkpt_nos
    eval "brkpt_nos=(${_Dbg_brkpt_file2brkpt["$fullname"]})"

    typeset -i i
    # Note: <= rather than < looks funny below, but that is correct.
    for ((i=0; i <= ${#linenos[@]}; i++)); do
	if (( linenos[i] == lineno )) ; then
	    # Got a match, find breakpoint entry number
	    typeset -i brkpt_num
	    (( brkpt_num = brkpt_nos[i] ))
	    _Dbg_unset_brkpt_arrays $brkpt_num
	    unset linenos[i]
	    _Dbg_brkpt_file2linenos["$fullname"]=" ${linenos[@]} "
	    typeset -a brkpt_nos
	    eval "brkpt_nos=(${_Dbg_brkpt_file2brkpt["$filename"]})"
	    unset brkpt_nos[$i]
	    _Dbg_brkpt_file2brkpt["$filename"]="${brkpt_nos[@]}"
	    (( found ++ ))
	fi
    done
    if (( found == 0 )) ; then
	filename=$(_Dbg_file_canonic "$filename")
	_Dbg_errmsg "No breakpoint found at $filename, line ${lineno}."
    fi
    return $found
}

# Routine to a delete breakpoint by entry number: $1.
# Returns whether or not anything was deleted.
function _Dbg_delete_brkpt_entry {
    (( $# == 0 )) && return 0
    typeset -r  del="$1"
    typeset -i  i
    typeset -i  found=0

    if [[ -z "${_Dbg_brkpt_file[$del]}" ]] ; then
	_Dbg_errmsg "No breakpoint number $del."
	return 1
    fi
    typeset    source_file="$(_Dbg_expand_filename "${_Dbg_brkpt_file[$del]}")"
    typeset -i lineno=${_Dbg_brkpt_line[$del]}
    typeset -i try
    typeset    new_lineno_val=''
    typeset    new_brkpt_nos=''
    typeset -i i=-1
    typeset -a brkpt_nos
    brkpt_nos=(${_Dbg_brkpt_file2brkpt["$source_file"]})
    for try in ${_Dbg_brkpt_file2linenos["$source_file"]} ; do
	((i++))
	if (( brkpt_nos[i] == del )) ; then
	    if (( try != lineno )) ; then
		    _Dbg_errmsg 'internal brkpt structure inconsistency'
		    return 1
	    fi
	    _Dbg_unset_brkpt_arrays $del
	    ((found++))
	else
	    new_lineno_val+=" $try "
	    new_brkpt_nos+=" ${brkpt_nos[$i]} "
	fi
    done
    if (( found > 0 )) ; then
	if (( ${#new_lineno_val[@]} == 0 )) ; then
	    # Remove array entirely
	    _Dbg_write_journal_eval "unset '_Dbg_brkpt_file2linenos[\"$source_file\"]'"
	    _Dbg_write_journal_eval "unset '_Dbg_brkpt_file2brkpt[\"$source_file\"]'"
	else
	    # Replace array entries with reduced set.
	    _Dbg_write_journal_eval "_Dbg_brkpt_file2linenos[\"$source_file\"]=\"${new_lineno_val}\""
	    _Dbg_write_journal_eval "_Dbg_brkpt_file2brkpt[\"$source_file\"]=\"$new_brkpt_nos\""
	fi
	return 0
    fi
    return 1
}

# Enable/disable aciton(s) by entry numbers.
function _Dbg_enable_disable_action {
    (($# != 3)) && return 1
    typeset -i on=$1
    typeset en_dis=$2
    typeset -i i=$3
    if [[ -n "${_Dbg_brkpt_file[$i]}" ]] ; then
	if [[ ${_Dbg_action_enable[$i]} == $on ]] ; then
	    _Dbg_errmsg "Breakpoint entry $i already ${en_dis}, so nothing done."
	    return 1
	else
	    _Dbg_write_journal_eval "_Dbg_brkpt_enable[$i]=$on"
	    _Dbg_msg "Action entry $i $en_dis."
	    return 0
	fi
    else
	_Dbg_errmsg "Action entry $i doesn't exist, so nothing done."
	return 1
    fi
}

# Enable/disable breakpoint(s) by entry numbers.
function _Dbg_enable_disable_brkpt {
    (($# < 2)) && return 1
    typeset -i on=$1
    typeset en_dis=$2
    typeset -a brkpts=("$3")
    typeset -i rc=0

    for i in "${brkpts[@]}";  do
	if [[ -n "${_Dbg_brkpt_file[$i]}" ]] ; then
	    if [[ ${_Dbg_brkpt_enable[$i]} == "$on" ]] ; then
		_Dbg_errmsg "Breakpoint entry $i already ${en_dis}, so nothing done."
		rc=1
	    else
		_Dbg_write_journal_eval "_Dbg_brkpt_enable[$i]=$on"
		_Dbg_msg "Breakpoint entry $i $en_dis."
	    fi
	else
	    _Dbg_errmsg "Breakpoint entry $i doesn't exist, so nothing done."
	    rc=1
	fi
    done
    return $rc

}

#======================== WATCHPOINTS  ============================#

_Dbg_get_watch_exp_eval() {
  typeset -i i=$1
  typeset new_val

  if [[ $(eval echo \"${_Dbg_watch_exp[$i]}\") == "" ]]; then
      new_val=''
  elif (( _Dbg_watch_arith[$i] == 1 )) ; then
      . "${_Dbg_libdir}/set-d-vars.sh"
      eval let new_val=\"${_Dbg_watch_exp[$i]}\"
  else
      . "${_Dbg_libdir}/set-d-vars.sh"
      eval new_val="${_Dbg_watch_exp[$i]}"
  fi
  echo $new_val
}

# Enable/disable watchpoint(s) by entry numbers.
_Dbg_enable_disable_watch() {
  typeset -i on=$1
  typeset en_dis=$2
  typeset -i i=$3
  if [ -n "${_Dbg_watch_exp[$i]}" ] ; then
    if [[ ${_Dbg_watch_enable[$i]} == $on ]] ; then
      _Dbg_msg "Watchpoint entry $i already $en_dis so nothing done."
    else
      _Dbg_write_journal_eval "_Dbg_watch_enable[$i]=$on"
      _Dbg_msg "Watchpoint entry $i $en_dis."
    fi
  else
    _Dbg_msg "Watchpoint entry $i doesn't exist so nothing done."
  fi
}

_Dbg_list_watch() {
  if [ ${#_Dbg_watch_exp[@]} != 0 ]; then
    typeset i=0 j
    _Dbg_section "Num Type       Enb  Expression"
    for (( i=0; (( i < _Dbg_watch_max )); i++ )) ; do
      if [ -n "${_Dbg_watch_exp[$i]}" ] ;then
	_Dbg_printf '%-3d watchpoint %-4s %s' $i \
	  ${_Dbg_yn[${_Dbg_watch_enable[$i]}]} \
          "${_Dbg_watch_exp[$i]}"
	_Dbg_print_brkpt_count ${_Dbg_watch_count[$i]}
      fi
    done
  else
    _Dbg_msg "No watch expressions have been set."
  fi
}

_Dbg_delete_watch_entry() {
  typeset -i del=$1

  if [ -n "${_Dbg_watch_exp[$del]}" ] ; then
    _Dbg_write_journal_eval "unset _Dbg_watch_exp[$del]"
    _Dbg_write_journal_eval "unset _Dbg_watch_val[$del]"
    _Dbg_write_journal_eval "unset _Dbg_watch_enable[$del]"
    _Dbg_write_journal_eval "unset _Dbg_watch_count[$del]"
  else
    _Dbg_msg "Watchpoint entry $del doesn't exist so nothing done."
  fi
}

_Dbg_clear_watch() {
  if (( $# < 1 )) ; then
    typeset _Dbg_prompt_output=${_Dbg_tty:-/dev/null}
    read $_Dbg_edit -p "Delete all watchpoints? (y/n): " \
      <&$_Dbg_input_desc 2>>$_Dbg_prompt_output

    if [[ $REPLY == [Yy]* ]] ; then
      _Dbg_write_journal_eval unset _Dbg_watch_exp[@]
      _Dbg_write_journal_eval unset _Dbg_watch_val[@]
      _Dbg_write_journal_eval unset _Dbg_watch_enable[@]
      _Dbg_write_journal_eval unset _Dbg_watch_count[@]
      _Dbg_msg "All Watchpoints have been cleared"
    fi
    return 0
  fi

  eval "$_Dbg_seteglob"
  if [[ $1 == "$_Dbg_int_pat" ]]; then
    _Dbg_write_journal_eval "unset _Dbg_watch_exp[$1]"
    _msg "Watchpoint $i has been cleared"
  else
    _Dbg_list_watch
    _basdhb_msg "Please specify a numeric watchpoint number"
  fi

  eval "$_Dbg_resteglob"
}
