#!/usr/bin/env python3

# Copyright Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT

import argparse
import shutil
import stat
import subprocess
import sys
from io import StringIO
from pathlib import Path

import pylint.lint
import pylint.reporters.text

try:
    from tqdm.auto import tqdm

    def write(*args, **kwargs):
        tqdm.write(*args, **kwargs)

except ImportError:
    tqdm = None

    def write(*args, **kwargs):
        print(*args, **kwargs)


repo_dir = Path(__file__).resolve().parent.parent
sys.path.append(str(repo_dir / "scripts" / "lib"))

from rrperf import git  # noqa: E402


def format_and_patch(command: str, path: Path):
    p = subprocess.run(command, shell=True, capture_output=True, text=True)

    output = p.stdout

    if p.returncode == 0:
        assert output.strip() == ""
        return

    output += "\n\n"

    p2 = subprocess.run(
        ["patch", str(path)],
        check=True,
        input=output,
        encoding="UTF-8",
        stdout=subprocess.PIPE,
    )
    write(p2.stdout)


def is_tool_available(tool: str) -> bool:
    if shutil.which(tool) is None:
        return False

    try:
        subprocess.run(
            [tool, "--version"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            check=True,
        )
        return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        return False


def run_clang_format(path: Path):
    format_and_patch(f"clang-format '{path}' | diff {path} -", path)


def run_black(path: Path):
    format_and_patch(f"black --quiet --check --diff {path}", path)


def run_isort(path: Path):
    format_and_patch(f"isort --profile black --quiet --check --diff {path}", path)


def get_cpp_files(dir: Path) -> list[Path]:
    cpp_suffixes = {".h", ".hpp", ".cpp", ".h.in", ".hpp.in", ".cpp.in"}
    return [p for x in git.ls_tree(dir) if (p := Path(x)).suffix in cpp_suffixes]


def get_py_files(dir: Path) -> list[Path]:
    paths = []
    for x in git.ls_tree(dir):
        p = Path(x)
        if p.suffix in {".py"}:
            paths.append(p)
        elif p.is_file() and (stat.S_IXUSR & p.stat()[stat.ST_MODE]):
            if "python" in p.read_text().splitlines()[0]:
                paths.append(p)
    return paths


def fix_cpp(dir: Path):
    if not is_tool_available("clang-format"):
        print("clang-format is not available, skipping C++ formatting.")
        return

    paths = get_cpp_files(dir)
    write(f"Fixing {len(paths)} C++ files:")

    if tqdm is not None:
        paths = tqdm(paths, unit=" files")

    for path in paths:
        run_clang_format(path)
    write("Done.")


def lint_python(dir: Path):
    pylint_output = StringIO()
    reporter = pylint.reporters.text.TextReporter(pylint_output)
    pylint.lint.Run(
        [str(x) for x in get_py_files(dir)] + ["--disable=C"],
        reporter=reporter,
        exit=False,
    )

    # For now, just print out results
    print(pylint_output.getvalue())


def check_python(dir: Path):
    command = ["flake8"]
    write(" ".join(command))

    subprocess.run(command, cwd=dir, check=False)

    lint_python(dir)


def fix_python(dir: Path):
    isort_avail = is_tool_available("isort")
    black_avail = is_tool_available("black")
    if not isort_avail and not black_avail:
        print("isort and black are not available, skipping Python formatting.")
        return

    if not isort_avail:
        print("isort is not available, skipping.")
    if not black_avail:
        print("black is not available, skipping.")

    paths = get_py_files(dir)
    write(f"Fixing {len(paths)} Python files:")

    if tqdm is not None:
        paths = tqdm(paths, unit=" files")

    for path in paths:
        if isort_avail:
            run_isort(path)
        if black_avail:
            run_black(path)
    write("Done.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Fix C++ formatting and check Python formatting."
    )

    parser.add_argument(
        "--skip-cpp",
        dest="cpp",
        default=True,
        action="store_false",
        help="Don't format C++ files.",
    )
    parser.add_argument(
        "--skip-python",
        dest="python",
        default=True,
        action="store_false",
        help="Don't check Python files.",
    )
    parser.add_argument("dir", type=Path, default=Path.cwd(), nargs="?")

    args = parser.parse_args()

    if args.cpp:
        fix_cpp(args.dir)

    if args.python:
        check_python(args.dir)
        fix_python(args.dir)
