#! /bin/sh
#
# Copyright (c) 2020-2023 by the Zeek Project. See LICENSE for details.

usage() {
    echo "Usage: $(basename $0) [--fixit] [-j <proc>] [--clang-tidy-path <clang-tidy>] [--clang-tidy-arg <addl-arg>] <build-directory> [<files>]"
    exit 1
}

error() {
    echo "$@" >&2
}

abspath() {
    printf "%s/%s\n" "$(cd $(dirname $1) && pwd)" "$(basename $1)"
}

cleanup() {
    rm -rf "${tmpdir} ${error}"
}

fix=""
clang_tidy_args=""
files=""
parallel=1

if [ -n "${CLANG_TIDY}" ]; then
    clang_tidy_path=${CLANG_TIDY}
else
    clang_tidy_path=$(which clang-tidy 2>/dev/null)
fi

while true; do
    case "$1" in
        --fixit)           fix=1; shift;;
        --clang-tidy-path) clang_tidy_path="$2"; shift; shift;;
        --clang-tidy-arg)  clang_tidy_args="${clang_tidy_args} $2"; shift; shift;;
        --ignore)          ignores="${ignores:+${ignores}|}$2"; shift; shift;;
        -j)                parallel="$2"; shift; shift;;
        -h | --help)       usage;;
        --)                shift; break;;
        --*)               usage;;
        *)                 break;;
    esac
done

if [ $# -lt 1 ]; then
    usage
fi

build=$(cd $1 && pwd)
root=$(cd ${build}/.. && pwd)
shift

for f in $@; do
    files=$(printf "%s %s\n" ${files} $(abspath ${f}))
done

cd ${build}

cmake_cache=CMakeCache.txt
compile_json=compile_commands.json
clang_tidy_ignore=${root}/.clang-tidy.ignore

if ! which jq >/dev/null 2>&1; then
    error "Need jq in PATH, aborting." && exit 1
fi

for i in ${cmake_cache} ${compile_json} bin/hiltic; do
    if [ ! -f ${i} ]; then
        error "${i} not found, did you configure and build?" && exit 1
    fi
done

if [ -z "${clang_tidy_path}" ]; then
    clang_tidy_path=$(cat ${cmake_cache} | grep CMAKE_CXX_COMPILER:PATH | cut -d = -f 2 | sed 's/clang+\{0,2\}/clang-tidy/')
fi

if [ ! -x "${clang_tidy_path}" ]; then
    error "cannot find clang-tidy" && exit 1
fi

if [ "${fix}" = 1 -a -n "$(git status --porcelain | grep -E -v '^(M|\?\?) ')" ]; then
    echo >&2
    echo "error: uncommitted changes in working directory, won't apply changes" >&2
    echo >&2
    git status -sb >&2
    exit 1
fi

clang_apply_replacements_path=$(echo ${clang_tidy_path} | sed 's/clang-tidy/clang-apply-replacements/')

if [ ! -x "${clang_apply_replacements_path}" ]; then
    error "cannot find clang-apply-replacements" && exit 1
fi

if [ -f "${clang_tidy_ignore}" ]; then
    x=$(cat ${clang_tidy_ignore} | grep -E -v '^ *(#.*)?$' | awk '{printf "%s|", $1}' | sed 's/|$//g')
    ignores="${ignores:+${ignores}|}${x}"
fi

if [ -z "${ignores}" ]; then
    ignores="__NEVER_MATCHES__"
fi

if [ -z "${files}" ]; then
    files=$(cat ${compile_json} | jq -r '.[].file' | grep -E '\.(cc|c|h)$' | grep -v autogen/__ | grep -v '\.bif\.' | grep -E -v "${root}/(${ignores})")
fi

cmd="${clang_tidy_path} -quiet -p=${build} ${clang_tidy_args}"

error=/tmp/$(basename $0).$$.error.tmp
tmpdir=/tmp/$(basename $0).$$.tmp
rm -rf "${tmpdir}"
mkdir -p "${tmpdir}"
trap cleanup EXIT

base=$(cd ${build}/.. && pwd)
rm -f ${error}

echo "${files}" | awk -v "cmd=${cmd}" -v "tmp=${tmpdir}" '{t=$1; gsub("[/]", "_", t); printf("%s -export-fixes=%s/%s.yaml %s\n", cmd, tmp, t, $1);}' \
    | tr '\n' '\0' | (xargs -0 -n 1 -P ${parallel} sh -c 2>&1 || touch ${error}) | grep -v 'warnings\? generated\.$' | sed "s#^../\(.*:\)#${base}/\1#g"

if [ -e "${error}" ]; then
    rc=1
else
    rc=0
fi

if [ ${rc} != 0 -a -n "${fix}" ]; then
    # It looks like clang-apply-replacements can merge & unique changes from
    # multiple files. In case that turns out not to work, we could borrow
    # from LLVM's run-clang-tidy.py script). That has Python code to merge
    # replacements ahead of time.
    ${clang_apply_replacements_path} ${tmpdir}
fi

exit ${rc}
