#!/bin/bash

# Process Doxygen log to generate sorted list of top offenders.
#

me=$(basename $0)
DIR="$(dirname $0)"
# Trick to get the absolute path, since Doxygen prefixes errors that way
ROOT=$(cd "$DIR/.."; pwd -P)

# Known log files
STANDARDLOGFILE=$DIR/doxygen.log
WARNINGSLOGFILE=$DIR/doxygen.warnings.log
# Default choice:  generate it
LOG=$WARNINGSLOGFILE
# Verbose log
VERBLOG="$DIR/doxygen.verbose.log"

# Doxygen config file
DOXYFILE=$DIR/doxygen.conf


# Options ------------------------------
#

# One line synopsis, continue
function synopsis_short
{
    echo "Usage: $me [-behiStv] [-s <log-file> | -l | -w] [-m <module>] [-f <regex>] [-F <regex>]"
}

# Two line synopsis, then exit with error
function synopsis
{
    synopsis_short
    echo "    -h  For detailed usage"
    exit 1
}

# Full help, then exit with no error
function usage
{
    synopsis_short
    cat <<-EOF

    Run Doxygen to generate all errors; report error counts
    by module and file.

    -i  Skip the build and introspection (introspected-doxygen, command-line)
    -s  Skip Doxygen run; use existing <log-file>
    -w  Skip Doxygen run; use existing warnings log $WARNINGSLOGFILE
    -l  Skip Doxygen run; use the normal Doxygen log $STANDARDLOGFILE

    -b  Omit the blacklist filter of files whose warnings we ignore
    -e  Filter out warnings from */examples/*
    -t  Filter out warnings from */test/*
    -m  Only include files matching src/<module>
    -f  Only include files matching the <regex>
    -F  Exclude files matching the <regex>
    -S  Just show the summary counts, not the full listing

    -v  Show detailed output from each step.
    -h  Print this usage message

    The default behavior is to modify $DOXYFILE temporarily to
    report all undocumented elements, and to reduce the run time.
    The output of this special run is kept in $WARNINGSLOGFILE.
    To further reduce the run time, the -i option also skips
    the build, so ns3 doesn\'t have to compile any modified files at all.
    This option necessarily skips re-generation of introspected-doxygen.h and
    introspected-command-line.h, which rely on a clean build,
    but these files only change when executable code changes, so this
    isn't usually significant when just amending Doxygen comments.

    The -l, -s, and -w options skip the Doxygen run altogether.
    The first two use a specified or the standard log file;
    the -w option uses the warnings log from a prior run.
    Only the first of -s <log-file>, -w, or -l will have effect.

    The -e and -t options exclude examples and test directories
    from the counts.  The -m option only includes a specific module.
    The -F option only includes files (or warnings) matching the <regex>.
    The -m and -F options append the relevant warnings after the
    numerical report.  These can be used in any combination.

    Some warnings are just nuisances, typically from third-party code
    or constructs Doxygen has trouble with.  These are pre-loaded in to
    the black list filter.  If you want to see what files and warnings these
    are use the -b option.

EOF
    exit 0
}

# Messaging ----------------------------
#

# Arg -v Verbosity level
verbosity=0

# Log message, possibly verbosely if -v option given
# Non-verbose:  Print the msg (without newline), then report status of cmd
#   verbose -n msg
#   ...
#   status_report $? cmd
# Verbose:  prefix with script name
function verbose
{
    if [ "$1" == "-n" ]; then
        if [ $verbosity -eq 1 ]; then
            echo "$me:  ${2}..."
        else
            echo -n "${2}..."
        fi
    elif [ $verbosity -eq 1 ]; then
        echo "$me: ${1:+ }$1 $2"
    else
        echo "$2"
    fi
}

# Use file handle 6 for verbose output
rm -f $VERBLOG
exec 6>$VERBLOG

# Swap back to original DOXYFILE
# Define here so status_report can use it if needed
function restore_doxyfile()
{
    if [ -e $DOXYFILE.bak ]; then
        rm -f $DOXYFILE
        mv -f $DOXYFILE.bak $DOXYFILE
    fi
}

# Report status of last command
#   status_report $? long_msg [exitonerr]
# If status ($?) was success (0)
#   If verbose
#     Show detailed log of last command
#     Verbosely log long_msg
#   Log "done."
# If error
#   If verbose log FAILED and detail log
#   If exitonerr exit the script
function status_report
{
    local status="$1"
    local long_msg="$2"
    local exitonerr="${3:-yes}"
    if [ $status -eq 0 ]; then
        [[ $verbosity -eq 1 && -e $VERBLOG ]] && cat $VERBLOG
        verbose "$long_msg "  "done."
        [[ -e $VERBLOG ]] && rm -f $VERBLOG
    else
        if [ $exitonerr == "yes" ]; then
            verbose "$long_msg "  "FAILED.  Details:"
            [[ -e $VERBLOG ]] && cat $VERBLOG && rm -f $VERBLOG
            restore_doxyfile
            exit 1
        else
            verbose "$long_msg "  "FAILED, continuing"
            [[ -e $VERBLOG ]] && cat $VERBLOG && rm -f $VERBLOG
        fi
    fi
}


# Argument processing ------------------
#

# -f argument
use_filearg=0
logfile_arg=
# -l
use_standard=0
# skip Doxygen run; using existing log file
skip_doxy=0
# skip build, print-introspected-doxygen and introspected-command-line
skip_intro=0

# Filtering flags, 1 means on/true
filter_blacklist=1
filter_examples=0
filter_test=0
explicit_m_option=0
filter_module=""
explicit_f_option=0
filter_in=""
filter_out=""

# Summary only
summary_only=0

while getopts :bef:F:hilm:Ss:tvw option ; do

    case $option in
    (b)  filter_blacklist=0       ;;

    (e)  filter_examples=1        ;;

    (f)  filter_in="$OPTARG"
         explicit_f_option=1
         ;;

    (F)  filter_out="$OPTARG"     ;;

    (h)  usage                    ;;

    (i)  skip_intro=1             ;;

    (l)  use_standard=1           ;;

    (m)  filter_module="$OPTARG"
         explicit_m_option=1
         ;;

    (s)  use_filearg=1
         logfile_arg="$OPTARG"
         ;;

    (S)  summary_only=1           ;;

    (t)  filter_test=1            ;;

    (v)  verbosity=1
         exec 6>&1
         ;;

    (w)  use_filearg=1
         logfile_arg="$WARNINGSLOGFILE"
         ;;

    (:)  echo "$me: Missing argument to -$OPTARG" ; synopsis ;;

    (\?) echo "$me: Invalid option: -$OPTARG"     ; synopsis ;;

    esac
done

# Check for existence of alternate Doxygen log file
function checklogfile
{
    if [ -e "$1" ] ; then
        skip_doxy=1
        LOG="$1"
    else
        echo "$me: log file $1 does not exist."
        synopsis
    fi
}

# Log file -----------------------------
#

if [[ $use_filearg -eq 1 && "${logfile_arg:-}" != "" ]] ; then
    checklogfile "$logfile_arg"
elif [ $use_standard -eq 1 ]; then
    checklogfile "$STANDARDLOGFILE"
fi

# Log filters --------------------------
#

# Append a regular expression to a parameter
#  with '\|' alternation operator if the parameter wasn't empty to begin with.
function REappend
{
    local param="$1"
    local token="$2"

    eval "${param}=\"${!param:-}${!param:+\\|}$token\""
}

# Explicit -f or -m with empty args should filter out all, not pass all
[[ $explicit_f_option -eq 1 && "${filter_in:-}" == "" ]] && filter_out=".*"
[[ $explicit_m_option -eq 1 && "${filter_module:-}" == "" ]] && filter_out=".*"

# Filter in regular expression for -m and -f
filter_inRE=""
[[ "$filter_module" != "" ]] && REappend filter_inRE src/$filter_module
[[ "$filter_in"     != "" ]] && REappend filter_inRE "$filter_in"

# Blacklist filter of files whose warnings we ignore
filter_blacklistRE=""

#   External files: adding our own Doxygen makes diffs with upstream very hard
#     cairo-wideint
REappend filter_blacklistRE "cairo-wideint"
REappend filter_blacklistRE "valgrind\.h"

#   Functions with varying numbers of arguments
#   Explicit template instantiation declaration
# REappend filter_blacklistRE "MakeCallback< ObjectBase \\* >(ObjectBase \\*"

#   Global variable declarations which look like function prototypes
REappend filter_blacklistRE "generic-battery-discharge-example.cc.*battDischFile"

#   ATTRIBUTE_HELPER_CPP( and _HEADER(
REappend filter_blacklistRE "ATTRIBUTE_HELPER_\\(CPP\\|HEADER\\)"

#   NS_DEPRECATED_...(msg)
REappend filter_blacklistRE "return type .*NS_DEPRECATED_3_"

#   *.py:  we've included python files since 2012,
#          but they are now (2025) generating warnings
REappend filter_blacklistRE "\.py"

#
# Filter out regular expression for black list, -e, -t and -F
filter_outRE=""
[[ $filter_blacklist -eq 1 ]] && REappend filter_outRE "$filter_blacklistRE"
[[ $filter_examples  -eq 1 ]] && REappend filter_outRE "/examples/"
[[ $filter_test      -eq 1 ]] && REappend filter_outRE "/test/"
[[ "$filter_out"     != "" ]] && REappend filter_outRE "$filter_out"


# Configuration ------------------------
#

# Log switch values '0' as "off", not '0' as "ON"
function on_off
{
    if [[ "${!1:-}" != "" && "${!1}" != "0" ]] ; then
        echo "ON"
    else
        echo "off"
    fi
}

# Log all the filters
function show_filters
{
    echo
    echo "    Net result of all filters:"
    [[ "${filter_blacklistRE:-}" != "" ]] && echo "      Blacklist:     \"$filter_blacklistRE\""
    [[ "${filter_inRE:-}"  != "" ]] && echo "      Filtering in:  \"$filter_inRE\""
    [[ "${filter_outRE:-}" != "" ]] && echo "      Filtering out: \"$filter_out\""

    echo
}

if [ $verbosity -eq 1 ]; then
    echo
    echo "$me:"
    echo "    Verbose:          $(on_off verbosity)"
    echo "    Skip build:       $(on_off skip_intro)"
    echo "    Skip Doxygen:     $(on_off skip_doxy)"
    echo "    Log file to use:  $LOG"
    echo "    Module filter:    $(on_off filter_module)  $filter_module"
    echo "    Examples filter:  $(on_off filter_examples)"
    echo "    Tests filter:     $(on_off filter_test)"
    echo "    Blacklist filter: $(on_off filter_blacklist)"
    echo "    Filter in:        $(on_off filter_in)  $filter_in"
    echo "    Filter out:       $(on_off filter_out)  $filter_out"
    echo "    Summary only:     $(on_off summary_only)"

    #  Show the resulting filters here, in addition to below
    show_filters
fi


#  Run Doxygen -------------------------
#

if [ $skip_doxy -eq 1 ]; then
    echo
    echo "Skipping Doxygen run, using existing log file $LOG"

else

    # We're going to modify DOXYFILE
    # In case the user ^C's out of this we need to restore
    # DOXYFILE, otherwise weird things happen the next time
    # they build Doxygen
    trap restore_doxyfile INT

    # Modify DOXYFILE to generate all the warnings
    # We keep dot active to generate graphs in the documentation
    # (see for example PacketTagList) and warn about ill-formed
    # graphs, but we disable all the Doxygen-generated diagrams
    # to shorten the run time.

    cp $DOXYFILE ${DOXYFILE}.bak
    cat <<-EOF >> $DOXYFILE

    # doxygen.warnings.report.sh:
    EXTRACT_ALL = no
    WARNINGS = no
    WARN_LOGFILE = $WARNINGSLOGFILE
    SOURCE_BROWSER = no
    HTML_OUTPUT = html-warn
    CLASS_GRAPH = no
    COLLABORATION_GRAPH = no
    GROUP_GRAPHS = no
    INCLUDE_GRAPH = no
    INCLUDED_BY_GRAPH = no
    CALL_GRAPH = no
    CALLER_GRAPH = no
    GRAPHICAL_HIERARCHY = no
    DIRECTORY_GRAPH = no
EOF

    doxycmd="doxygen-no-build"
    if [ $skip_intro -eq 1 ]; then
        verbose "" "Skipping ./ns3 build, print-introspected-doxygen, and introspected-command-line"

    else
        # Run introspection, which may require a build
        verbose -n "Building"
        (cd "$ROOT" && ./ns3 build >&6 2>&6 )
        status_report $? "./ns3 build"

        doxycmd=doxygen
    fi

    echo "Doxygen@version:" `which doxygen` "@" `doxygen --version`
    verbose "" "Repo version: $(cd $ROOT && doc/ns3_html_theme/get_version.sh | cut -d ':' -f 2- )"
    verbose -n "Rebuilding Doxygen docs with full errors"
    (cd "$ROOT" && ./ns3 docs $doxycmd >&6 2>&6 )
    status_report $? "./ns3 docs $doxycmd"

    restore_doxyfile
fi

# Filter log file
function filter_log
{
    local flog;
    flog=$( cat "$LOG" | grep "^$ROOT" )

    [[ "${filter_inRE:-}"  != "" ]] && flog=$( echo "$flog" | grep "$filter_inRE" )
    [[ "${filter_outRE:-}" != "" ]] && flog=$( echo "$flog" | grep -v "$filter_outRE" )

    flog=$(                         \
        echo "$flog"              | \
        sed "s|^$ROOT/||g"         | \
        sort -t ':' -k1,1 -k2,2n  | \
        uniq                        \
        )

    echo "$flog"
}

# Analyze the log ----------------------
#
#  Show the resulting filters, if not already shown by verbose
if [[ $verbosity -ne 1 && ! -z "$filter_log_results" ]]; then
    show_filters
fi

verbose -n "Filtering the Doxygen log $LOG"

filter_log_results=$(filter_log)

# List of module directories (e.g, "src/core/model")
if [ ! -z "$filter_log_results" ]
then
    undocmods=$(                \
        filter_log            | \
        cut -d ':' -f 1       | \
        sed "s|$ROOT/||g"     | \
        cut -d '/' -f 1-3     | \
        sort                  | \
        uniq -c               | \
        sort -nr                \
        )
    modcount=$(                         \
        echo "$undocmods"             | \
        wc -l                         | \
        sed 's/^[ \t]*//;s/[ \t]*$//'   \
        )
    modwarncount=$(                               \
        echo "$undocmods"                       | \
        awk '{total += $1}; END {print total}'    \
        )
else
    undocmods=""
    modcount=0
    modwarncount=0
fi

# For a function with multiple undocumented parameters,
# Doxygen prints the additional parameters on separate lines,
# so they don't show up in the totals above.
# Rather than work too hard to get the exact number for each file,
# we just list the total here.
addlparam=$(                                  \
    grep "^  parameter '" "$LOG"            | \
    wc -l                                   | \
    sed 's/^[ \t]*//;s/[ \t]*$//'             \
    )

# Sometimes Doxygen can not pinpoint a warning to an exact file.
# In this case the output is of the form:
# "<operator==>:1: warning: parameters of member ns3::operator== are not documented"
# or
# "<operator==>:1: warning: return type of member ns3::operator== is not documented"
misplacedWarns=$(                             \
    grep ">:1:" "$LOG"                      | \
    wc -l                                   | \
    sed 's/^[ \t]*//;s/[ \t]*$//'             \
    )

# This is to catch LaTeX formula errors.
# In this case the output is of the form:
# "error: Problems running latex." followed by
# "Check your installation or look for typos in _formulas.tex and check _formulas.log!"
# and
# "Check your installation or look for typos in _formulas_dark.tex and check _formulas_dark.log!"
# Hence, the count can be two.
latexWarns=$(                                 \
    grep "Problems running latex" "$LOG"    | \
    wc -l                                   | \
    sed 's/^[ \t]*//;s/[ \t]*$//'             \
    )

# Total number of warnings
warncount=$((modwarncount + addlparam + misplacedWarns + latexWarns))

# List of files appearing in the log
if [ ! -z "$filter_log_results" ]
then
    undocfiles=$(               \
        filter_log            | \
        cut -d ':' -f 1       | \
        sed "s|$ROOT||g"      | \
        cut -d '/' -f 2-      | \
        sort                  | \
        uniq -c               | \
        sort -k 2               \
        )
else
    undocfiles=""
fi

# Sorted by number, decreasing
undocsort=$(echo "$undocfiles" | sort -k1nr,2 )

# Total number of files
if [ ! -z "$undocfiles" ]
then
    filecount=$(                        \
        echo "$undocfiles"            | \
        wc -l                         | \
        sed 's/^[ \t]*//;s/[ \t]*$//'   \
        )
else
    filecount=0
fi

# Filtered in warnings
filterin=
if [ "${filter_inRE:-}" != "" ] ; then
    filterin=$(              \
        filter_log         | \
        sed "s|$ROOT/||g"    \
        )
fi

status_report 0 "Filter"
echo


# Summarize the log --------------------
#
# Return status based on warnings
exit_status=$((warncount > 0))

if ((warncount == 0)); then
   echo "No warnings passed the filters."
   exit $exit_status
fi

echo
echo "Report of Doxygen warnings"
echo "----------------------------------------"
echo
echo "(All counts are approximate.)"
echo
echo "Warnings by module/directory:"
echo
echo "Count Directory"
echo "----- ----------------------------------"
echo "$undocmods"
echo " $addlparam additional undocumented parameters."
echo " $misplacedWarns additional warnings."
echo "----------------------------------------"
printf "%6d total warnings\n" $warncount
printf "%6d directories with warnings\n" $modcount
echo
echo
echo "Warnings by file (alphabetical)"
echo
echo "Count File"
echo "----- ----------------------------------"
echo "$undocfiles"
echo "----------------------------------------"
printf "%6d files with warnings\n" $filecount
echo
echo
echo "Warnings by file (numerical)"
echo
echo "Count File"
echo "----- ----------------------------------"
echo "$undocsort"
echo "----------------------------------------"
printf "%6d files with warnings\n" $filecount
echo
echo
if [ $latexWarns -ne 0 ] ; then
    html_output=`dirname $LOG`
    if [ $skip_doxy -eq 1 ]; then
        html_output+="/html"
    else
        html_output+="/html-warn"
    fi
    echo "----------------------------------------"
    echo "There are LaTeX warnings, probably math"
    echo "  Inspect the generated LaTeX input files"
    echo "  '$html_output/_formulas.tex'"
    echo "  (and '$html_output/_formulas_dark.tex')."
    echo "  as well as the LaTeX run log"
    echo "  '$html_output/_formulas.log'"
    echo
    echo "The LaTeX log is:"
    echo "$(<$html_output/_formulas.log )"
    echo "----------------------------------------"
    echo
    echo
fi
echo "Doxygen Warnings Summary"
echo "----------------------------------------"
printf "%6d directories\n" $modcount
printf "%6d files\n" $filecount
printf "%6d warnings\n" $warncount

# if [ "${filter_inRE:-}" != "" ] ; then
#     if [ "$filterin" != "" ] ; then
#         echo
#         echo
#         echo "Filtered Warnings"
#         echo "========================================"
#         echo "$filterin"
#         exit_status=1
#     else
#         exit_status=0
#     fi
# fi

# Summary only: quit now
if [ "$summary_only" == 1 ]; then
    status_report 0 $me
    exit $exit_status
fi

if [ "$filterin" != "" ] ; then
    echo
    echo
    echo "Filtered Warnings"
    echo "========================================"
    echo "$filterin"
elif [ "$filter_log_results" != "" ] ; then
    echo
    echo
    echo "Warnings"
    echo "========================================"
    echo "$filter_log_results"
fi

status_report 0 $me

exit $exit_status
