#!/usr/bin/env python3

"""Checks for missing or incorrect header guards."""

__copyright__ = """
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""

from collections.abc import Iterable
from pathlib import Path
import re
import sys
from typing import NamedTuple, Optional


class Guard(NamedTuple):
    """A guard line in a file."""

    line: int
    guard: str


def find_guard(
    lines: list[str], pattern: str, from_end: bool
) -> Optional[Guard]:
    """Searches the lines for something matching the pattern."""
    lines_range: Iterable[str] = lines
    if from_end:
        lines_range = reversed(lines)
    for index, line in enumerate(lines_range):
        m = re.match(pattern, line)
        if m:
            if from_end:
                index = len(lines) - index - 1
            return Guard(index, m[1])
    return None


def maybe_replace(
    lines: list[str], old_guard: Guard, guard_prefix: str, guard: str
) -> None:
    """Replaces a header guard in the file if needed."""
    if guard != old_guard.guard:
        line = lines[old_guard.line].rstrip("\n")
        print(f"- Replacing line {old_guard.line}: `{line}`", file=sys.stderr)
        lines[old_guard.line] = f"{guard_prefix} {guard}\n"


def check_path(path: Path) -> bool:
    """Checks the path for header guard issues."""
    if path.suffix != ".h":
        print(f"Not a header: {path}", file=sys.stderr)
        return True

    with path.open() as f:
        lines = f.readlines()

    guard_path = str(path).upper().replace("/", "_").replace(".", "_")
    guard = f"CARBON_{guard_path}_"
    ifndef = find_guard(lines, "#ifndef ([A-Z0-9_]+_H_)", False)
    define = find_guard(lines, "#define ([A-Z0-9_]+_H_)", False)
    endif = find_guard(lines, "#endif(?:  // ([A-Z0-9_]+_H_))?", True)
    if ifndef is None or define is None or endif is None:
        print(f"Incomplete header guard in {path}:", file=sys.stderr)
        if ifndef is None:
            print(f"- Missing `#ifndef {guard}`", file=sys.stderr)
        if define is None:
            print(f"- Missing `#define {guard}`", file=sys.stderr)
        if endif is None:
            print(f"- Missing `#endif  // {guard}`", file=sys.stderr)
        return True

    if ifndef.line + 1 != define.line:
        print(
            f"Non-consecutive header guard in {path}: "
            f"#ifndef on line {ifndef.line + 1}, "
            f"#define on line {define.line + 1}.",
            file=sys.stderr,
        )
        return True

    if endif.line != len(lines) - 1:
        print(
            f"Misordered header guard in {path}: #endif on line {endif.line}, "
            f"should be on last line ({len(lines) - 1}).",
            file=sys.stderr,
        )
        return True

    if guard != ifndef.guard or guard != define.guard or guard != endif.guard:
        print(f"Fixing header guard in {path} to {guard}:", file=sys.stderr)
        maybe_replace(lines, ifndef, "#ifndef", guard)
        maybe_replace(lines, define, "#define", guard)
        maybe_replace(lines, endif, "#endif  //", guard)
        with path.open("w") as f:
            f.writelines(lines)
        return True

    return False


def main() -> None:
    has_errors = False
    for arg in sys.argv[1:]:
        if check_path(Path(arg)):
            has_errors = True
    if has_errors:
        exit(1)


if __name__ == "__main__":
    main()
