#!/usr/bin/env python3.12
"""
Pre-commit hook to check that all CPython APIs used
by pyobjc-core are in its 'python-api-used.h' header
(used with clang's static analyzer)
"""
import pathlib
import re
import sys
import typing

PY_NAME = re.compile(r"[^_A-Za-z0-9](Py[A-Za-z0-9_]*)[()]")

IGNORE_NAMES = {
    "PyCFunction",  # Not a callable
    "PyCriticalSection2_End",  # Internal
    "PyCriticalSection_End",  # Internal
    "PyErr_Warn",  # Macro
    "PyHeapTypeObject",  # Not a callable
    "PyMemberDef",  # Not a callable
    "PyMutex_Lock",  # Inline
    "PyMutex_Unlock",  # Inline
    "PyObject",  # Not a function
    "PyObject_GC_New",  # Macro
    "PyObject_New",  # Macro
    "PyObject_NewVar",  # Macro
    "PyObject_TypeCheck",  # Macro
    "Py_False",  # Not a callable
    "Py_NewRef",  # Macro
    "Py_None",  # Not a callable
    "Py_True",  # Not a callable
    "Py_XNewRef",  # Macro
    "Py_hash_t",  # Not a callable
    "Py_tp_doc",  # Not a callable
    "Python",  # Not a callable
    "PyObjectPtr_Convert",  # XXX: PyObjC function
    "PyObjectPtr_New",  # XXX: PyObjC function
}


def _process_file(stream: typing.TextIO, symbols: set[str]) -> None:
    for line in stream:
        for nm in PY_NAME.findall(line):
            if nm.partition("_")[2].isupper():
                # Macro
                continue
            elif nm.startswith("PyObjC"):
                # PyObjC internal API
                continue

            elif nm.startswith("PyExc_"):
                # Python exception
                continue

            elif nm.startswith("PyInit"):
                # Module init function
                continue

            elif nm.endswith("_Check") or nm.endswith("_CheckExact"):
                # Type check macros
                continue

            elif nm.endswith("_Type"):
                # Type check macros
                continue

            elif nm in IGNORE_NAMES:
                # not function names
                continue

            symbols.add(nm)


def _get_py_symbols(source_root: pathlib.Path) -> tuple[set[str], set[str]]:
    api_used : set[str] = set()
    symbols : set[str] = set()

    for dirpath, dirs, files in source_root.walk():
        if "test" in dirs:
            dirs.remove("test")
        for fn in files:
            if fn == "python-api-used.h":
                with (dirpath / fn).open() as stream:
                    _process_file(stream, api_used)
            elif fn == "ctests.m":
                continue
            else:
                if any(fn.endswith(sfx) for sfx in (".h", ".c", ".m")):
                    with (dirpath / fn).open() as stream:
                        _process_file(stream, symbols)
    return symbols, api_used


def test_api_used() -> None:
    symbols, api_used = _get_py_symbols(
        pathlib.Path(__file__).resolve().parent.parent / "pyobjc-core" / "Modules"
    )

    ok = True
    for nm in sorted(symbols):
        if nm not in api_used:
            print(f"{nm}: not in python-api-used.h")
            ok = False

    raise SystemExit(not ok)


if __name__ == "__main__":
    for nm in sys.argv[1:]:
        if nm.startswith("pyobjc-core"):
            test_api_used()
            break
