#!/usr/bin/env python3
# montauk-syscalls.py - Keep the TCC C API header's syscall numbers in sync
# with the canonical syscall ABI.
#
# Source of truth : programs/include/Api/Syscall.hpp  (montauk::abi::SYS_*)
# Generated block : programs/include/libc/montauk.h   (#define MTK_SYS_*)
#
# The libc/montauk.h header is a hand-maintained, TCC-compatible C mirror of the
# syscall surface. Its typed wrapper functions stay hand-written (their argument
# types are not machine-derivable from the enum), but the raw "#define MTK_SYS_*"
# number table is mechanical and lives between the @SYSCALLS-BEGIN/END markers,
# which this script regenerates.
#
# Usage:
#   montauk-syscalls.py check    Verify every SYS_* has a matching MTK_SYS_* with
#                                the same number. Exits non-zero on any drift.
#                                Run from the build so stale headers fail loudly.
#   montauk-syscalls.py gen      Rewrite the MTK_SYS_* block in montauk.h from the
#                                canonical enum (numbers only; wrappers untouched).

import os
import re
import sys

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.dirname(SCRIPT_DIR)
ABI_HDR = os.path.join(REPO_ROOT, "programs/include/Api/Syscall.hpp")
C_HDR = os.path.join(REPO_ROOT, "programs/include/libc/montauk.h")

BEGIN_MARKER = "/* @SYSCALLS-BEGIN"
END_MARKER = "/* @SYSCALLS-END */"

# Canonical SYS_* names intentionally NOT exposed in the C API. Empty by default;
# add a name here (without the SYS_ prefix) to suppress a kernel-internal syscall.
EXCLUDE = set()

# static constexpr uint64_t SYS_FOO = 42;
ABI_RE = re.compile(r"constexpr\s+\w+\s+SYS_([A-Z0-9_]+)\s*=\s*(\d+)\s*;")
# #define MTK_SYS_FOO 42
DEF_RE = re.compile(r"#define\s+MTK_SYS_([A-Z0-9_]+)\s+(\d+)\b")


def read(path):
    with open(path, "r") as f:
        return f.read()


def parse_abi():
    """Return {NAME: number} for every canonical syscall, minus EXCLUDE."""
    out = {}
    for name, num in ABI_RE.findall(read(ABI_HDR)):
        if name in EXCLUDE:
            continue
        out[name] = int(num)
    if not out:
        sys.exit("montauk-syscalls: no SYS_* entries found in %s" % ABI_HDR)
    return out


def parse_defines(text):
    """Return {NAME: number} for every MTK_SYS_* define in the C header."""
    return {name: int(num) for name, num in DEF_RE.findall(text)}


def diff(abi, defs):
    """Return (missing, mismatched, extra) describing drift of defs vs abi."""
    missing = sorted(n for n in abi if n not in defs)
    extra = sorted(n for n in defs if n not in abi)
    mismatched = sorted(
        (n, abi[n], defs[n]) for n in abi if n in defs and abi[n] != defs[n]
    )
    return missing, mismatched, extra


def cmd_check():
    abi = parse_abi()
    defs = parse_defines(read(C_HDR))
    missing, mismatched, extra = diff(abi, defs)
    if not (missing or mismatched or extra):
        print("montauk-syscalls: OK (%d syscalls in sync)" % len(abi))
        return 0

    print("montauk-syscalls: DRIFT between Api/Syscall.hpp and libc/montauk.h",
          file=sys.stderr)
    if missing:
        print("  %d missing from montauk.h:" % len(missing), file=sys.stderr)
        for n in missing:
            print("      SYS_%-22s = %d" % (n, abi[n]), file=sys.stderr)
    if mismatched:
        print("  %d number mismatch(es):" % len(mismatched), file=sys.stderr)
        for n, a, d in mismatched:
            print("      SYS_%-22s abi=%d  montauk.h=%d" % (n, a, d),
                  file=sys.stderr)
    if extra:
        print("  %d stale (not in ABI):" % len(extra), file=sys.stderr)
        for n in extra:
            print("      MTK_SYS_%s" % n, file=sys.stderr)
    print("\n  Run 'make gen-syscalls' to regenerate the MTK_SYS_* number block,",
          file=sys.stderr)
    print("  then add C wrapper functions for any new syscalls by hand.",
          file=sys.stderr)
    return 1


def render_block(abi):
    width = max(len("MTK_SYS_" + n) for n in abi) + 1
    lines = [BEGIN_MARKER,
             "   Generated from Api/Syscall.hpp by scripts/montauk-syscalls.py.",
             "   Do not edit by hand; run 'make gen-syscalls' to refresh.",
             "   Wrapper functions below are hand-written. */"]
    for name in sorted(abi, key=lambda n: abi[n]):
        macro = "MTK_SYS_" + name
        lines.append("#define %-*s%d" % (width, macro, abi[name]))
    lines.append(END_MARKER)
    return "\n".join(lines)


def cmd_gen():
    abi = parse_abi()
    text = read(C_HDR)
    block = render_block(abi)

    if BEGIN_MARKER in text and END_MARKER in text:
        start = text.index(BEGIN_MARKER)
        end = text.index(END_MARKER) + len(END_MARKER)
        new = text[:start] + block + text[end:]
    else:
        sys.exit("montauk-syscalls: markers not found in %s; add a "
                 "%s ... %s block first" % (C_HDR, BEGIN_MARKER, END_MARKER))

    if new == text:
        print("montauk-syscalls: montauk.h already up to date (%d syscalls)"
              % len(abi))
        return 0
    with open(C_HDR, "w") as f:
        f.write(new)
    print("montauk-syscalls: regenerated MTK_SYS_* block (%d syscalls)" % len(abi))
    missing_wrappers = [
        n for n in sorted(abi)
        if not re.search(r"\bMTK_SYS_%s\b" % n, new[new.index(END_MARKER):])
    ]
    if missing_wrappers:
        print("  note: %d syscall(s) have a number but no wrapper yet: %s"
              % (len(missing_wrappers),
                 ", ".join("SYS_" + n for n in missing_wrappers)))
    return 0


def main():
    if len(sys.argv) != 2 or sys.argv[1] not in ("check", "gen"):
        sys.exit("usage: montauk-syscalls.py {check|gen}")
    return cmd_check() if sys.argv[1] == "check" else cmd_gen()


if __name__ == "__main__":
    sys.exit(main())
