#!/usr/bin/python3

import argparse
import json
import re
import pathlib
import subprocess
import sys

CLANG_FMT_STYLE_CFG = {
    "BasedOnStyle": "llvm",
    "BreakBeforeBraces": "Attach",
    "IndentWidth": 4,
    "IndentPPDirectives": "None",
}

SRCFILE_EXT = ("c", "cc", "cpp", "cxx", "h", "hh", "hpp", "hxx", "cu", "cuh")
IGNORE_PATTERN = ["external"]

DEFAULT_CLANG_FORMAT_VERSION=12

BASE_ARGS = f"-style={json.dumps(CLANG_FMT_STYLE_CFG)}"

def parse_version(version_string):
    version_rgx = "version (\d+)"

    m = re.search(version_rgx, version_string)
    return int(m.group(1))

def check_bin(command):
    try:
        p = subprocess.run([command, "--version"], 
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
        version = parse_version(p.stdout)

        if version < DEFAULT_CLANG_FORMAT_VERSION:
            print(f"Using clang-format version {version}. \
                    As this is lower than the version used for the CI, \
                    the CI may fail even after formatting.")
    except FileNotFoundError as exc:
        raise FileNotFoundError(
            f"{command} is not installed or is not in PATH."
        ) from exc


def parse_args():
    parser = argparse.ArgumentParser(
        description="Opinionated C/C++ formatter. Based on clang-format"
    )
    parser.add_argument(
        "paths", nargs="+", metavar="DIR", help="paths to the root source directories"
    )
    parser.add_argument(
        "-c",
        "--check",
        action="store_true",
        help="don't write files, just return status. "
        "A non-zero return code indicates some files would be re-formatted",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="print detailed information about format violations",
    )
    parser.add_argument(
        "-f",
        "--cfversion",
        type=int,
        default=0,
        action="store",
        help="set a version number for clang-format",
    )
    return parser.parse_args()


def get_files(args):
    files = []

    for path in args.paths:
        for ext in SRCFILE_EXT:
            for file_path in pathlib.Path(path).rglob(f"*.{ext}"):
                files.append(str(file_path))

    return [f for f in files if f not in IGNORE_PATTERN]


def fmt(args, files, command) -> int:
    cmd = (command, BASE_ARGS, "-i", *files)

    paths = ", ".join(args.paths)
    sys.stderr.write(f"Formatting {len(files)} files in {paths}.\n")

    ret = subprocess.run(cmd, capture_output=True, universal_newlines=True)
    if ret.returncode != 0:
        sys.stderr.write(ret.stderr)
        return 1

    return 0


def check(args, files, command) -> int:
    cmd = (command, BASE_ARGS, "--dry-run", "-Werror")

    needs_reformatted_ct = 0

    for file in files:
        ret = subprocess.run(
            (*cmd, file), capture_output=True, universal_newlines=True
        )

        if ret.returncode != 0:
            sys.stderr.write(f"Error: {file} would be reformatted.\n")
            if args.verbose:
                sys.stderr.write(ret.stderr)

            needs_reformatted_ct += 1

    sys.stderr.write(f"{needs_reformatted_ct} files would be re-formatted.\n")
    sys.stderr.write(f"{len(files) - needs_reformatted_ct} would be left unchanged.\n")

    return needs_reformatted_ct


if __name__ == "__main__":
    args = parse_args()

    files = get_files(args)

    if len(files) == 0:
        print("No source files found! Nothing to do.")
        sys.exit(0)

    cf_version = args.cfversion
    cf_cmd = "clang-format"

    if cf_version:
        cf_cmd += f"-{cf_version}"

    check_bin(cf_cmd)

    if args.check:
        ret = check(args, files, cf_cmd)
    else:
        ret = fmt(args, files, cf_cmd)

    sys.exit(int(ret > 0))