#! /usr/bin/env bash
#
# Copyright (c) 2020-2023 by the Zeek Project. See LICENSE for details.
#
# Runs selected stages of the CI pipeline. Each stage assumes the
# preceesing one has been successfully executed already.
#
# We use bash here since that's useful (for pipefail in particular) and
# users are unlikely to run this script.

set -o pipefail

name=$(basename $0)
root=$(cd $(dirname $0)/.. && pwd)
build=
install=

color_red=$'\e[1;31m'
color_green=$'\e[1;32m'
color_yellow=$'\e[1;33m'
color_blue=$'\e[1;34m'
color_magenta=$'\e[1;35m'
color_cyan=$'\e[1;36m'
color_normal=$'\e[0m'

function usage {
cat <<EOF
Usage: ${mame} [<global options>] <stage> [<command options>]

Stages

    configure [release|debug] [<configure options>]   Configure the build for compiling a release/debug version
    build                                             Compile the code
    install                                           Install the built version
    test-code                                         Run code & formatting checks
    test-build                                        Run the test suite on the built version
    test-install                                      Run the test suite on the installed version
    cleanup                                           Delete all build artifacts.

Global options:

    -r <dir>   Base directory of repository checkout (default: ${root})
    -b <dir>   Build directory; will be deleted at completion (default: \${root}/build-ci)

Configure options:

    --build-toolchain={yes,no}     Build the Spicy compiler toolchain [default: yes]
    --clang-format <path>          Path to clang-format to use (default: found in PATH)
    --clang-tidy <path>            Path to clang-tidy to use   (default: found in PATH)
    --cxx-compiler <path>          Path to C++ compiler to use (default: found by cmake)
    --disable-precompiled-headers  Disable use of precompiled headers for developer tests

EOF

exit 1
}

function log_colored {
    color=$1
    shift
    printf "%s" "${color}"
    printf "%s" "$@"
    printf "%s\n" "${color_normal}"
}

function log_stage {
    echo
    log_colored "${color_magenta}" "### $@"
}

function log_warning {
    log_colored "${color_yellow}" "### $@"
}

function log_error {
    log_colored "${color_red}" "### $@"
}

function log_diag {
    log_colored "${color_yellow}" "--- $@"
}

function error {
    echo "### Error: $@"
    exit 1
}

function run_configure {
    build_type="$1"
    shift

    mkdir -p ${install}
    configure="./configure --builddir=${build} --prefix=${install} --generator=Ninja --enable-werror --enable-ccache --with-hilti-compiler-launcher=ccache"

    clang_format=$(which clang-format 2>/dev/null)
    clang_tidy=$(which clang-tidy 2>/dev/null)

    if [ "${build_type}" == "release" ]; then
        :
    elif [ "${build_type}" == "debug" ]; then
        configure="${configure} --enable-debug --enable-sanitizer"
    else
        usage
    fi

    while [ $# -ne 0 ]; do
        case "$1" in
            --cxx-compiler)
                test $# -gt 0 || usage
                configure="${configure} --with-cxx-compiler=$2"
                shift 2;
                ;;

            --clang-format)
                test $# -gt 0 || usage
                clang_format="$2"
                test -x ${clang_format} || error "clang-format not found in $2"
                shift 2;
                ;;

            --clang-tidy)
                test $# -gt 0 || usage
                clang_tidy="$2"
                test -x ${clang_tidy} || error "clang-tidy not found in $2"
                shift 2;
                ;;

            --build-toolchain)
                configure="${configure} --build-toolchain=$2"
                shift 2;
                ;;

            --disable-precompiled-headers)
                configure="${configure} --disable-precompiled-headers"
                shift 1;
                ;;

            --enable-werror)
                configure="${configure} --enable-werror"
                shift 1;
                ;;

            --build-static-libs)
                configure="${configure} --build-static-libs"
                shift 1;
                ;;

            *)  usage;;
        esac
    done

    if [ -e ${build} ]; then
        error "Build directory ${build} already exists, delete first"
    fi

    test -z "${clang_format}" && log_stage "Warning: No clang-format found, will skip any related tests"
    test -z "${clang_tidy}" && log_stage "Warning: No clang-tidy found, will skip any related tests"

    # Looks like Cirrus CI doesn't fetch tags.
    git fetch --tags

    log_stage "Running configure ... (${configure})"

    ${configure} || exit 1
    mkdir -p ${artifacts}

    echo "${clang_format}" >${build}/.clang_format
    echo "${clang_tidy}" >${build}/.clang_tidy
}

function run_build {
    log_stage "Building code ..."

    # The level of parallelism chosen here is tuned for what's configured
    # in `.cirrus.yml` so that (1) we align with number of CPUs, and (2) we
    # do not trigger OOM kills in the Docker environments.
    (cd ${build} && ninja -j4 all) || exit 1

    log_stage "Building docs ..."
    (cd ${root}/doc && make BUILDDIR=${build} DESTDIR=${artifacts}/doc doxygen html) || exit 1
}

function run_install {
    log_stage "Installing code ..."
    (cd ${build} && ninja install) || exit 1
}

function execute_btest {
    name="$1"
    cwd="$2"
    alternative="$3"

    if [ -n "${alternative}" ]; then
        alternative="-a ${alternative}"
    else
        alternative=""
    fi

    if [ -n "${SPICY_BTEST_GROUPS}" ]; then
        group="-g ${SPICY_BTEST_GROUPS}"
    else
        group=""
    fi


    log_stage "Running ${name} tests ... (${btest_alternative} ${btest_group})"
    pushd ${cwd} >/dev/null

    eval ${preload} \
        btest -j 5 \
              -f ${artifacts}/diag.log \
              -x ${artifacts}/diag.xml \
              -z 3 \
              ${alternative} \
              ${group}

    rc=$?

    if [ ${rc} != 0 ]; then
        cp -a .tmp ${artifacts}/btest-tmp
        log_diag "Begin diagnostics"
        cat ${artifacts}/diag.log
        log_diag "End diagnostics (complete test output in 'btest-tmp')"
        log_error "Tests have failed"
    fi

    popd >/dev/null
    return ${rc}
}

function run_test_btest {
    alternative="$1"
    execute_btest "Spicy" "tests" "${alternative}"
}

function run_test_clang_tidy {
    clang_tidy=$(cat ${build}/.clang_tidy 2>/dev/null)

    if [ ! -x ${clang_tidy} ]; then
        log_warning "clang-tidy not available, skipping"
        return 0
    fi

    log_stage "Running clang-tidy ..."

    export CLANG_TIDY=${clang_tidy}

    if ${root}/scripts/run-clang-tidy --fixit -j 3 ${build}; then
        echo "clang-tidy run passed"
        return 0
    else
        git diff | tee ${artifacts}/clang-tidy.diff
        log_diag "Patch to what can be fixed automatically in 'clang-tidy.diff'"
        log_error "clang-tidy has failed"
        return 1
    fi
}

function run_test_code {
    rc=0

    pre-commit run -a --show-diff-on-failure || rc=1

    pushd $(pwd) >/dev/null
    run_test_clang_tidy || rc=1
    popd >/dev/null

    return ${rc}
}

function run_test_common {
    btest_alternative="$1"
    rc=0

    run_test_btest "${btest_alternative}" || rc=1

    log_stage "Checking docs ..."
    # We currently ignore this check since lint checks can fail spuriously if
    # the remote is temporarily unavailable.
    (cd "${root}"/doc && make BUILDDIR="${build}" DESTDIR="${artifacts}"/doc check) || true

    return ${rc}
}

function run_test_build {
    export SPICY_BUILD_DIRECTORY=${build}
    run_test_common

    return ${rc}
}

function run_test_install {
    export SPICY_INSTALLATION_DIRECTORY=${install}
    run_test_common installation
}

function run_cleanup {
    log_stage "Cleaning up ..."
    (cd ${build} && ninja clean) || exit 1
    (cd doc && make clean) || exit 1
}

### Main

while getopts "r:b:" opt; do
    case "${opt}" in
        h)
            usage
            ;;
        r)
            root=${OPTARG}
            ;;
        b)
            build=${OPTARG}
            ;;
        *)
            echo "unknown option -${opt}"
            exit 1
            ;;
    esac
done
shift $((OPTIND-1))

if [ -z "${build}" ]; then
    build=${root}/build-ci
fi

install=/tmp/ci-install-$(basename ${build})
root=$(realpath ${root})
build=$(realpath ${build})
artifacts=${build}/ci

cmd=$1
shift

test -n "${cmd}" || usage

cd ${root}
test -e CMakeLists.txt || error "${root} is not the project's root git repository"

case "${cmd}" in
    configure)           (run_configure $@);;
    build)               (run_build $@);;
    install)             (run_install $@);;
    test-build)          (run_test_build $@);;
    test-install)        (run_test_install $@);;
    test-code)           (run_test_code $@);;
    cleanup)             (run_cleanup $@);;
    *)       usage;;
esac
