#!/bin/bash

#
# This file is distributed under the MIT License. See LICENSE.md for details.
#

set -e

function print_usage() {
    cat > /dev/stderr << END_USAGE_TEXT
SYNOPSIS
   $0 [<options>] [FILES...]


DESCRIPTION
   Checks that the files specified in FILES... respect the rev.ng coding
   conventions, and prints all the violations.

   If the FILES... arguments are missing, checks that all the C++ source files in
   the project that have already been added to the git repository project
   respect the rev.ng coding conventions.


OPTIONS
   --help
       Prints this help and exit

   --force-format
       In addition to checking the rev.ng coding conventions on the specified
       files, uses clang-format to try to enforce the coding conventions
       automatically.
       If this fails, exit with failure, otherwise exit with success.
       WARNING: using this option will overwrite your files, make sure to backup
       important stuff.

    --use-local-clang-format-file
      By default, clang-format is executed with a hard-coded configuration that
      should apply to all the projects under the rev.ng umbrella.
      When this option is passed, the hard-coded configuration is ignored, and
      clang-format looks for configuration in a .clang-format file in the
      current directory or in its parent directories.

      This allows to use different clang-format configuration on per-project
      basis, in cases where the hard-coded configuration does not fit (e.g Qt
      projects where Qt coding conventions are a better fit).

      See clang-format documentation for more details.

    --print-clang-format-config
      Print the clang-format configuration to stdout, ignoring all the other
      arguments except for --use-local-clang-format-file.
      The conventions are not checked.

    FILES
      List of filenames of the files for which you want to check the rev.ng
      coding conventions.

RETURN VALUES
   On success exit code is 0.
   On failure, i.e. if there is at least one file that is not respecting the
   coding conventions, exit code is 1.
END_USAGE_TEXT
}

SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

FORCE_FORMAT="0"

USE_CLANG_FORMAT_FILE="0"

PRINT_CLANG_FORMAT_CONFIG="0"

while [[ $# > 0 ]]; do
    key="$1"
    case $key in
        --print-clang-format-config)
            PRINT_CLANG_FORMAT_CONFIG="1"
            shift # past argument
            ;;
        --force-format)
            FORCE_FORMAT="1"
            shift # past argument
            ;;
        --use-local-clang-format-file)
            USE_CLANG_FORMAT_FILE="1"
            shift # past argument
            ;;
        --help)
            print_usage
            exit 0
            ;;
        -*)
            echo "Error: unrecognized option $key" > /dev/stderr
            print_usage
            exit 1
            ;;
        *)
            break
            ;;
    esac
done

CLANG_FORMAT_STYLE_FILE="$SCRIPT_PATH/clang-format-style-file"
if ! test -e "$CLANG_FORMAT_STYLE_FILE"; then
    CLANG_FORMAT_STYLE_FILE="$SCRIPT_PATH/../share/revng/clang-format-style-file"
    if ! test -e "$CLANG_FORMAT_STYLE_FILE"; then
        echo "Can't find clang-format-style-file" > /dev/stderr
        exit 1
    fi
fi

CLANG_FORMAT_STYLE=$(cat "$CLANG_FORMAT_STYLE_FILE")

if test "$USE_CLANG_FORMAT_FILE" -gt 0; then
    CLANG_FORMAT_STYLE=file
fi

# If the user passed the --print-clang-format-config, dump the config and exit
# with success.
if test "$PRINT_CLANG_FORMAT_CONFIG" -gt 0; then
    clang-format --dry-run -style="$CLANG_FORMAT_STYLE" --dump-config
    exit 0
fi


if [[ $# -eq 0 ]]; then
    FILES="$(git ls-files | grep -E '\.(c|cc|cpp|h|hpp)$')"
else
    FILES="$@"
fi

GREP="git grep -n --color=always"

# Run clang-format on FILES
function run_clang_format() {
    if test "$FORCE_FORMAT" -gt 0; then
        clang-format -style="$CLANG_FORMAT_STYLE" -i $FILES
    else
        clang-format --dry-run -style="$CLANG_FORMAT_STYLE" -i $FILES
    fi
}

# Run revng-specific checks on files
function run_revng_checks() {
    # Check for lines longer than 80 columns
    $GREP -E '^.{81,}$' $FILES | cat

    # Things should never match
    for REGEXP in '\(--> 0\)' ';;' '^\s*->.*;$' 'Twine [^&]'; do
        $GREP "$REGEXP" $FILES | cat
    done

    # Things should never match (except in support.c)
    FILTERED_FILES="$(echo $FILES | tr ' ' '\n' | grep -v \
        -e '^runtime/support\.c$' \
        -e '^lib/Support/Assert\.cpp$' \
        )"

    for REGEXP in '\babort(' '\bassert(' 'assert(false' 'llvm_unreachable'; do
        $GREP "$REGEXP" $FILTERED_FILES | cat
    done

    # Things should never be at the end of a line
    for REGEXP in '::' '<' 'RegisterPass.*>' '} else' '\bopt\b.*>'; do
        $GREP "$REGEXP\$" $FILES | cat
    done

    # Includes should never use <..> except for C++ standard includes
    $GREP "^\s*#include <.*\.hpp>" $FILES | cat
    FILES_WITHOUT_C_STDLIB_INCLUDES="$(echo $FILES | tr ' ' '\n' | grep -v \
        -e '^runtime/support\.h$' \
        -e '^runtime/support\.c$' \
        -e '^runtime/early-linked\.c$' \
        -e '^include/revng/Support/Assert\.h$' \
        -e '^include/revng/Support/ClassSentinel\.h$' \
        -e '^share/revngc/revng-c-include\.c$' \
        )"
    $GREP "^\s*#include <.*\.h>" $FILES_WITHOUT_C_STDLIB_INCLUDES | cat

    # Parenthesis at the end of line (except for raw strings)
    $GREP "(\$" $FILES | grep -v 'R"LLVM.*(' | cat

    # Things should never be at the beginning of a line
    for REGEXP in '\.[^\.]' '\*>' '/[^/\*]' ':[^:\(]*)' '==' '\!=' '<[^<]' '>' '>=' '<=' '//\s*WIP' '#if\s*[01]'; do
        $GREP "^\s*$REGEXP" $FILES | cat
    done

    # Check there are no static functions in header files
    for FILE in $FILES; do
        if [[ $FILE == *h ]]; then
            $GREP -H '^static\b[^=]*$' "$FILE" | cat
            head -n1 "$FILE" |  grep -E '^#pragma once$' > /dev/null || echo "$FILE: header does not start with #pragma once"
        fi
    done

}

# First, run the checks and output warnings in a human readable format.
run_clang_format
run_revng_checks | sort -u

# Second, re-run the check commands, and fail if any of them prints anything.
# This ensures that that user sees all the errors on the command line, while at
# the same time setting a proper exit value that can be checked by other scripts
# or by git hooks.
if [[ $( run_clang_format 2>&1 | head -c 1 | wc -c ) -ne 0 ]]; then
    exit 1
fi
if [[ $( run_revng_checks 2>&1 | head -c 1 | wc -c ) -ne 0 ]]; then
    exit 1
fi
