#!/usr/bin/env python3
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

# Most of the code are inspired by https://github.com/apache/kudu/blob/856fa3404b00ee02bd3bc1d77d414ede2b2cd02e/build-support/clang_tidy_gerrit.py

import argparse
import collections
import json
import multiprocessing
from multiprocessing.pool import ThreadPool
import os
import re
import subprocess
import sys
import tempfile

ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))

BUILD_PATH = os.path.join(ROOT, "build", "latest")

def run_tidy(sha="HEAD", is_rev_range=False):
    diff_cmdline = ["git", "diff" if is_rev_range else "show", sha]

    # Figure out which paths changed in the given diff.
    changed_paths = subprocess.check_output(diff_cmdline + ["--name-only", "--pretty=format:"]).splitlines()
    changed_paths = [p for p in changed_paths if p]

    # Produce a separate diff for each file and run clang-tidy-diff on it
    # in parallel.
    #
    # Note: this will incorporate any configuration from .clang-tidy.
    def tidy_on_path(path):
        patch_file = tempfile.NamedTemporaryFile()
        cmd = diff_cmdline + [
            "--src-prefix=%s/" % ROOT,
            "--dst-prefix=%s/" % ROOT,
            "--",
            path]
        subprocess.check_call(cmd, stdout=patch_file, cwd=ROOT)
        # TODO(yingchun): some checks could be disabled before we fix them.
        #  "-checks=-llvm-include-order,-modernize-concat-nested-namespaces,-cppcoreguidelines-macro-usage,-cppcoreguidelines-special-member-functions,-hicpp-special-member-functions,-bugprone-easily-swappable-parameters,-google-readability-avoid-underscore-in-googletest-name,-cppcoreguidelines-avoid-c-arrays,-hicpp-avoid-c-arrays,-modernize-avoid-c-arrays,-llvm-header-guard,-cppcoreguidelines-pro-bounds-pointer-arithmetic",
        cmdline = ["clang-tidy-diff",
                   "-clang-tidy-binary",
                   "clang-tidy",
                   "-p0",
                   "-path", BUILD_PATH,
                   # Disable some checks that are not useful for us now.
                   # They are sorted by names, and should be consistent to .clang-tidy.
                   "-checks=-bugprone-easily-swappable-parameters,"
                           "-bugprone-lambda-function-name,"
                           "-bugprone-macro-parentheses,"
                           "-bugprone-sizeof-expression,"
                           "-cert-err58-cpp,"
                           "-clang-analyzer-cplusplus.NewDelete,"
                           "-concurrency-mt-unsafe,"
                           "-cppcoreguidelines-avoid-c-arrays,"
                           "-cppcoreguidelines-avoid-magic-numbers,"
                           "-cppcoreguidelines-avoid-non-const-global-variables,"
                           "-cppcoreguidelines-macro-usage,"
                           "-cppcoreguidelines-non-private-member-variables-in-classes,"
                           "-cppcoreguidelines-owning-memory,"
                           "-cppcoreguidelines-pro-bounds-array-to-pointer-decay,"
                           "-cppcoreguidelines-pro-bounds-pointer-arithmetic,"
                           "-cppcoreguidelines-pro-type-const-cast,"
                           "-cppcoreguidelines-pro-type-reinterpret-cast,"
                           "-cppcoreguidelines-pro-type-union-access,"
                           "-fuchsia-default-arguments-calls,"
                           "-fuchsia-multiple-inheritance,"
                           "-fuchsia-overloaded-operator,"
                           "-fuchsia-statically-constructed-objects,"
                           "-google-readability-avoid-underscore-in-googletest-name,"
                           "-hicpp-avoid-c-arrays,"
                           "-hicpp-named-parameter,"
                           "-hicpp-no-array-decay,"
                           "-llvm-header-guard,"
                           "-llvm-include-order,"
                           "-misc-definitions-in-headers,"
                           "-misc-non-private-member-variables-in-classes,"
                           "-misc-unused-parameters,"
                           "-modernize-avoid-bind,"
                           "-modernize-avoid-c-arrays,"
                           "-modernize-replace-disallow-copy-and-assign-macro,"
                           "-modernize-use-trailing-return-type,"
                           "-performance-unnecessary-value-param,"
                           "-readability-function-cognitive-complexity,"
                           "-readability-identifier-length,"
                           "-readability-magic-numbers,"
                           "-readability-named-parameter,"
                           "-readability-suspicious-call-argument",
                   "-extra-arg=-language=c++",
                   "-extra-arg=-std=c++17",
                   "-extra-arg=-Ithirdparty/output/include"]
        return subprocess.check_output(
            cmdline,
            stdin=open(patch_file.name),
            cwd=ROOT).decode()
    pool = ThreadPool(multiprocessing.cpu_count())
    try:
        return "".join(pool.imap(tidy_on_path, changed_paths))
    except KeyboardInterrupt as ki:
        sys.exit(1)
    finally:
        pool.terminate()
        pool.join()


if __name__ == "__main__":
    # Basic setup and argument parsing.
    parser = argparse.ArgumentParser(description="Run clang-tidy on a patch")
    parser.add_argument("--rev-range", action="store_true",
                        default=False,
                        help="Whether the revision specifies the 'rev..' range")
    parser.add_argument('rev', help="The git revision (or range of revisions) to process")
    args = parser.parse_args()

    # Run clang-tidy and parse the output.
    clang_output = run_tidy(args.rev, args.rev_range)
    parsed = re.match(r'.+(warning|error): .+', clang_output, re.MULTILINE | re.DOTALL)
    print(clang_output, file=sys.stderr)
    if not parsed:
        print("No warnings", file=sys.stderr)
        sys.exit(0)
    sys.exit(1)

