#!/usr/bin/env bash
#
# chk_tree - verify that the source tree and git and Makefiles are in sync
#
# This tools is used by "make prep", "make full_debug", and "make debug".
#
# Copyright (C) 2023,2026 Landon Curt Noll
#
# Calc is open software; you can redistribute it and/or modify it under
# the terms of version 2.1 of the GNU Lesser General Public License
# as published by the Free Software Foundation.
#
# Calc 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 Lesser General
# Public License for more details.
#
# A copy of version 2.1 of the GNU Lesser General Public License is
# distributed with calc under the filename COPYING-LGPL.  You should have
# received a copy with calc; if not, write to Free Software Foundation, Inc.
# 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
#
# Under source code control:    2023/10/04 21:04:44
# File existed as early as:     2023
#
# chongo <was here> /\oo/\      http://www.isthe.com/chongo/
# Share and enjoy!  :-)         http://www.isthe.com/chongo/tech/comp/calc/


# setup
#
export VERSION="1.1 2026-02-04"
NAME=$(basename "$0")
export NAME
#
export V_FLAG=0
export M_FLAG=
#
GIT_TOOL=$(type -P git)
export GIT_TOOL
if [[ -z $GIT_TOOL ]]; then
    echo "$0: FATAL: git tool is not installed or not in \$PATH" 1>&2
    exit 5
fi
"$GIT_TOOL" rev-parse --is-inside-work-tree >/dev/null 2>&1
status="$?"
if [[ $status -eq 0 ]]; then
    TOPDIR=$("$GIT_TOOL" rev-parse --show-toplevel)
fi
export TOPDIR
#
EXIT_CODE=0


# usage
#
export USAGE="usage: $0 [-h] [-v level] [-V] [-d topdir] [-m]

    -h          print help message and exit
    -v level    set verbosity level (def level: $V_FLAG)
    -V          print version string and exit

    -m          used by make full_debug or make debug (def: run by hand)

    -d topdir   set topdir (def: $TOPDIR)

Exit codes:
     0         all OK
     2         -h and help string printed or -V and version string printed
     3         command line error
     5         some internal tool is not found or not an executable file
     6         problems found with or in the topdir
 >= 10         internal error

$NAME version: $VERSION"


# parse command line
#
while getopts :hv:Vd:m flag; do
  case "$flag" in
    h) echo "$USAGE"
        exit 2
        ;;
    v) V_FLAG="$OPTARG"
        ;;
    V) echo "$VERSION"
        exit 2
        ;;
    d) TOPDIR="$OPTARG"
        ;;
    m) M_FLAG="true"
        ;;
    \?) echo "$0: ERROR: invalid option: -$OPTARG" 1>&2
        echo 1>&2
        echo "$USAGE" 1>&2
        exit 3
        ;;
    :) echo "$0: ERROR: option -$OPTARG requires an argument" 1>&2
        echo 1>&2
        echo "$USAGE" 1>&2
        exit 3
        ;;
    *) echo "$0: ERROR: unexpected value from getopts: $flag" 1>&2
        echo 1>&2
        echo "$USAGE" 1>&2
        exit 3
        ;;
  esac
done
if [[ $V_FLAG -ge 1 ]]; then
    echo "$0: debug[1]: debug level: $V_FLAG" 1>&2
fi
#
# remove the options
#
shift $(( OPTIND - 1 ));
#
# verify arg count
#
if [[ $# -ne 0 ]]; then
    echo "$0: ERROR: expected 0 args, found: $#" 1>&2
    echo "$USAGE" 1>&2
    exit 3
fi


# cd to topdir
#
if [[ ! -e $TOPDIR ]]; then
    echo "$0: ERROR: cannot cd to non-existent path: $TOPDIR" 1>&2
    exit 6
fi
if [[ ! -d $TOPDIR ]]; then
    echo "$0: ERROR: cannot cd to a non-directory: $TOPDIR" 1>&2
    exit 6
fi
export CD_FAILED
if [[ $V_FLAG -ge 5 ]]; then
    echo "$0: debug[5]: about to: cd $TOPDIR" 1>&2
fi
cd "$TOPDIR" || CD_FAILED="true"
if [[ -n $CD_FAILED ]]; then
    echo "$0: ERROR: cd $TOPDIR failed" 1>&2
    exit 6
fi
if [[ $V_FLAG -ge 3 ]]; then
    echo "$0: debug[3]: now in directory: $(/bin/pwd)" 1>&2
fi


# print running info if verbose
#
# If -v 3 or higher, print exported variables in order that they were exported.
#
if [[ $V_FLAG -ge 3 ]]; then
    echo "$0: debug[3]: VERSION=$VERSION" 1>&2
    echo "$0: debug[3]: NAME=$NAME" 1>&2
    echo "$0: debug[3]: V_FLAG=$V_FLAG" 1>&2
    echo "$0: debug[3]: GIT_TOOL=$GIT_TOOL" 1>&2
    echo "$0: debug[3]: TOPDIR=$TOPDIR" 1>&2
fi


# firewall - verify that the "make distdir" directories exist
#
make distdir >/dev/null 2>&1
status="$?"
if [[ $status -ne 0 ]]; then
    echo "$0: ERROR: make distdir exit code: $status" 1>&2
    EXIT_CODE=10
    echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
fi

# collect make distdir directory list
#
# We ignore make action lines.
#
declare -a DISTDIR
mapfile -t DISTDIR < <(make -s distdir | grep -v '^make\[[0-9]*\]: ' | sort -u)
if [[ ${#DISTDIR[@]} -le 0 ]]; then
    echo "$0: ERROR: distdir is empty" 1>&2
    EXIT_CODE=11
    echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
fi

# collect directories
#
# We ignore NOTES because it contains notes that are not part of calc source.
# We ignore .git and GitHub because they are not part of the calc source tarball.
#
declare -a FINDDIR
mapfile -t FINDDIR < <(find . -type d \
        ! -path './NOTES/*' ! -name NOTES \
        ! -path './.git/*' ! -name .git \
        ! -path './.github/*' ! -name .github \
        ! -name picky ! -path './picky/*' | \
        sed -e 's/^\.\///' | sort -u)
if [[ ${#FINDDIR[@]} -le 0 ]]; then
    echo "$0: ERROR: find dir is empty" 1>&2
    EXIT_CODE=12
    echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
fi

# compare DISTDIR and FINDDIR
#
declare -a DIFF_DISTDIR_FINDDIR
mapfile -t DIFF_DISTDIR_FINDDIR < <(printf '%s\n' "${DISTDIR[@]}" "${FINDDIR[@]}" |
                                    tr ' ' '\n' | sort | uniq -u)
if [[ ${#DIFF_DISTDIR_FINDDIR[@]} -ne 0 ]]; then

    # report that DISTDIR and FINDDIR differ
    #
    echo "$0: ERROR: distdir and find dir differ for critical directories" 1>&2
    declare -a ONLY_FINDDIR
    mapfile -t ONLY_FINDDIR < <(printf '%s\n' "${DISTDIR[@]}" "${DISTDIR[@]}" "${FINDDIR[@]}" |
                                tr ' ' '\n' | sort | uniq -u)
    if [[ ${#ONLY_FINDDIR[@]} -ne 0 ]]; then
        echo "$0: ERROR: found only in find dir: ${ONLY_FINDDIR[*]}" 1>&2
    fi
    declare -a ONLY_DISTDIR
    mapfile -t ONLY_DISTDIR < <(printf '%s\n' "${FINDDIR[@]}" "${FINDDIR[@]}" "${DISTDIR[@]}" |
                                tr ' ' '\n' | sort | uniq -u)
    if [[ ${#ONLY_DISTDIR[@]} -ne 0 ]]; then
        echo "$0: ERROR: found only in distdir: ${ONLY_DISTDIR[*]}" 1>&2
    fi
    EXIT_CODE=13
    echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
fi

# firewall - verify that the "make distlist" files exist
#
make distlist >/dev/null 2>&1
status="$?"
if [[ $status -ne 0 ]]; then
    echo "$0: ERROR: make distlist exit code: $status" 1>&2
    EXIT_CODE=14
    echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
fi

# collect make distlist file list
#
declare -a DISTLIST
mapfile -t DISTLIST < <(make -s distlist | grep -v '^make\[[0-9]*\]: ' | sort -u)
if [[ ${#DISTLIST[@]} -le 0 ]]; then
    echo "$0: ERROR: distlist is empty" 1>&2
    EXIT_CODE=15
    echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
fi
declare -A DISTLIST_A
for i in "${DISTLIST[@]}"; do
    DISTLIST_A["$i"]="$i"
done

# case: under git control
#
if [[ -d .git ]]; then

    # obtain the list of files under git
    #
    # We ignore .git and GitHub related files because they are not part of the calc source tarball.
    #
    declare -a GITLS
    mapfile -t GITLS < <(git ls-tree -r --name-only HEAD |
        grep -v -E '^\.github/|^\.gitignore$|^CODE_OF_CONDUCT\.md$|^CONTRIBUTING\.md$|^SECURITY\.md$' |
        sort -u)
    if [[ ${#GITLS[@]} -le 0 ]]; then
        echo "$0: ERROR: ls-tree -r --name-only HEAD is empty" 1>&2
        EXIT_CODE=16
        echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
    fi

    # compare DISTLIST and GITLS
    #
    declare -a DIFF_DISTLIST_GITLS
    mapfile -t DIFF_DISTLIST_GITLS < <(printf '%s\n' "${DISTLIST[@]}" "${GITLS[@]}" |
                                       tr ' ' '\n' | sort | uniq -u)
    if [[ ${#DIFF_DISTLIST_GITLS[@]} -ne 0 ]]; then

        # report that DISTLIST and GITLS differ
        #
        echo "$0: ERROR: distlist and git ls-tree -r --name-only HEAD differ for critical files" 1>&2
        declare -a ONLY_GITLS
        mapfile -t ONLY_GITLS < <(printf '%s\n' "${DISTLIST[@]}" "${DISTLIST[@]}" "${GITLS[@]}" |
                                  tr ' ' '\n' | sort | uniq -u)
        if [[ ${#ONLY_GITLS[@]} -ne 0 ]]; then
            echo "$0: ERROR: found only in git ls-tree -r --name-only HEAD: ${ONLY_GITLS[*]}" 1>&2
        fi
        declare -a ONLY_DISTLIST
        mapfile -t ONLY_DISTLIST < <(printf '%s\n' "${GITLS[@]}" "${GITLS[@]}" "${DISTLIST[@]}" |
                                     tr ' ' '\n' | sort | uniq -u)
        if [[ ${#ONLY_DISTLIST[@]} -ne 0 ]]; then
            echo "$0: ERROR: found only in distlist: ${ONLY_DISTLIST[*]}" 1>&2
        fi
        EXIT_CODE=17
        echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
    fi

    # verify that there are no staged uncommitted changes under git
    #
    GIT_STATUS=$(git --no-pager status --untracked-files=no --porcelain 2>&1)
    if [[ -n $GIT_STATUS ]] || ! git --no-pager diff --cached --quiet --exit-code; then
        echo "$0: ERROR: there are staged uncommitted changes" 1>&2
        git --no-pager status --short
        EXIT_CODE=18
        echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
    fi

    # verify that there are no uncommitted changes under git
    #
    if ! git diff --quiet --exit-code; then
        echo "$0: ERROR: there are unstaged changes" 1>&2
        git --no-pager status --untracked-files=no --porcelain --short
        EXIT_CODE=19
        echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
    fi

    # show branch information
    #
    GIT_BRANCH=$(git branch --show-current 2<&1)
    if [[ -z $GIT_BRANCH ]]; then
        echo "$0: ERROR: cannot determine current branch" 1>&2
        git --no-pager branch --list
        EXIT_CODE=20
        echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
    fi
    GIT_BRANCH=${GIT_BRANCH:="master"}
    if [[ -z $M_FLAG && $GIT_BRANCH != master ]]; then
        echo "$0: ERROR: not on master branch and -m was not used" 1>&2
        EXIT_CODE=21
        echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
    fi

    # output branch info if -m
    #
    if [[ -n $M_FLAG ]]; then
        echo "$0: on branch: $GIT_BRANCH" 1>&2
        echo "$0: git branch starts below" 1>&2
        git --no-pager branch
        echo "$0: git branch ends above" 1>&2
    fi

    # verify that current branch is not ahead of 'origin/branch'
    #
    GIT_MASTER=$(git rev-list --count "$GIT_BRANCH")
    GIT_ORIGIN_MASTER=$(git rev-list --count origin/"$GIT_BRANCH")
    if [[ $GIT_MASTER -gt $GIT_ORIGIN_MASTER ]]; then
        echo "$0: ERROR: master branch is behind of origin/$GIT_BRANCH" 1>&2
        git --no-pager status master
        EXIT_CODE=23
        echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
    elif [[ $GIT_MASTER -lt $GIT_ORIGIN_MASTER ]]; then
        echo "$0: ERROR: master branch is ahead of origin/$GIT_BRANCH" 1>&2
        git --no-pager status master
        EXIT_CODE=23
        echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
    fi

    # output status if -m
    #
    if [[ -n $M_FLAG ]]; then
        echo "$0: git status starts below" 1>&2
        git --no-pager status
        echo "$0: git status ends above" 1>&2
    fi
fi

# collect make buildlist file list
#
declare -a BUILDLIST
mapfile -t BUILDLIST < <(make -s buildlist | grep -v '^make\[[0-9]*\]: ' | sort -u)
if [[ ${#BUILDLIST[@]} -le 0 ]]; then
    echo "$0: ERROR: buildlist is empty" 1>&2
    EXIT_CODE=24
    echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
fi
declare -A BUILDLIST_A
for i in "${BUILDLIST[@]}"; do
    BUILDLIST_A["$i"]="$i"
done

# look for something in DISTLIST that is also in BUILDLIST
#
# The BUILDLIST are a set of files that are result of building calc
# and thus should NOT be part of the DISTLIST (list of files used
# to form the calc source distribtion).
#
declare -a DISTLIST_ALSO_IN_BUILDLIST
mapfile -t DISTLIST_ALSO_IN_BUILDLIST < <(
    for i in "${DISTLIST_A[@]}"; do
        if [[ -n "${BUILDLIST_A[$i]}" ]]; then
            echo "$i";
        fi
    done
)
if [[ ${#DISTLIST_ALSO_IN_BUILDLIST[@]} -ne 0 ]]; then

    # report that something in DISTLIST was found in BUILDLIST
    #
    echo "$0: ERROR: distlist files found in buildlist" 1>&2
    echo "$0: ERROR: distlist files found in buildlist: ${DISTLIST_ALSO_IN_BUILDLIST[*]}" 1>&2
    EXIT_CODE=25
    echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
fi

# collect list of files
#
# We ignore NOTES because it contains notes that are not part of calc source.
# We ignore .git and GitHub because they are not part of the calc source tarball.
# We ignore .*.swp files as they are a result of vim sessions.
# We ignore *.out files as they are used to collect information.
# We ignore single letter files as they are used for temporary files.
# We ignore files containing the any of the following in any letter case:
#
#       foo bar baz curds whey rmme
#
declare -a FINDFILE
mapfile -t FINDFILE < <(find . -type f \
        ! -path './NOTES/*' ! -name NOTES \
        ! -path './.git/*' ! -name .git \
        ! -path './.github/*' ! -name .github \
        ! -name 'CODE_OF_CONDUCT.md' ! -name 'CONTRIBUTING.md' ! -name '.gitignore' ! -name 'SECURITY.md'  \
        ! -name '*.swp' ! -name '?' ! -iname '*.out*' \
        ! -iname '*foo*' ! -iname '*bar*' ! -iname '*baz*' \
        ! -name 'picky' ! -path './picky/*' \
        ! -iname '*curds*' ! -iname '*whey*' ! -iname '*rmmee' -print |
        sed -e 's/^\.\///' | sort -u)
if [[ ${#FINDFILE[@]} -le 0 ]]; then
    echo "$0: ERROR: find file is empty" 1>&2
    EXIT_CODE=26
    echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
fi

# look for something in FINDFILE that in neither DISTLIST nor BUILDLIST
#
# We look for a file that is neither a file that neither a result of building calc,
# nor part of the calc distribution list in a file list.
#
# NOTE that the file list has already excluded certain files (see previous section).
#
# We flag any file in the list that is neither a result of building calc,
# nor part of the calc distribution list in a file list.
#
declare -a UNKNOWN_FILE
mapfile -t UNKNOWN_FILE < <(
    for i in "${FINDFILE[@]}"; do

        # skip if this is a file that is result of building calc
        #
        if [[ -n ${BUILDLIST_A["$i"]} ]]; then
            continue
        fi

        # skip if this is a file that is calc distribution list
        #
        if [[ -n ${DISTLIST_A["$i"]} ]]; then
            continue
        fi

        # print the file of unknown status
        #
        echo "$i"
    done
)
if [[ ${#UNKNOWN_FILE[@]} -ne 0 ]]; then

    # report that something in DISTLIST was found in BUILDLIST
    #
    echo "$0: ERROR: files that are neither built nor distlist are found" 1>&2
    echo "$0: ERROR: distlist files found in buildlist: ${UNKNOWN_FILE[*]}" 1>&2
    EXIT_CODE=27
    echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
fi

# Look for a find in distlist that would otherwise be ignored
#
# We ignore for trailblank and FINDFILE, filenames with a single letter, and files
# that end in .out, and files  containing the any of the following
# in any letter case:
#
#       foo bar baz curds whey rmme
#
# So we will object is any distlist file is one of these ignored filenames.
#
INVALID_DISTLIST=$(printf '%s\n' "${DISTLIST[@]}" | \
                   tr '[:upper:]' '[:lower:]' | \
                   sed -n -e '/^.$/p' -e '/.*foo.*/p' -e '/.*bar.*/p' -e '/.*baz.*/p' \
                          -e '/.*curds.*/p' -e '/.*whey.*/p' -e '/.*rmme.*/p' |
                   tr '\n' ' ')
if [[ -n $INVALID_DISTLIST ]]; then
    echo "$0: ERROR: distlist contains invalid filename(s): $INVALID_DISTLIST" 1>&2
    EXIT_CODE=28
    echo "$0: Warning: set EXIT_CODE: $EXIT_CODE" 1>&2
fi

# All Done!!! -- Jessica Noll, Age 2
#
if [[ $EXIT_CODE -ne 0 ]]; then
    echo "$0: Warning: about to exit $EXIT_CODE" 1>&2
fi
exit "$EXIT_CODE"
