#!/usr/bin/env python3
# genToolbox.py — generate IIgs toolbox wrappers from NList.Data.
#
# Reads Dave Lyons' NiftyList database (`tools/nlist/NList.Data.txt`),
# which catalogues every IIgs toolbox call and GS/OS call with tool
# number, parameter sizes, and return-value size.  Emits two outputs:
#   - C header with `static inline` wrappers using clang inline-asm
#   - .s file with extern wrapper bodies for multi-arg routines that
#     can't fit in inline asm (our backend's constraints don't take
#     memory operands).
#
# NList line format (see tools/nlist/README.md):
#   TTTT [NS:]FuncName([N:]arg[/sz], ...)[:ret[/sz]]
#
# Tool number convention: TTTT loaded into X; low byte = tool-set,
# high byte = function code.  Dispatcher: JSL $E10000 for normal
# toolbox; JSL $E100A8 for GS/OS (entries prefixed `GS/OS:`).
#
# Calling convention: C ABI passes arg0 (i16) in A, arg0 (i32) in A:X,
# arg1+ on stack RTL.  Each generated wrapper re-pushes args in toolbox
# (Pascal-style L-to-R) order, with result space first (if any), then
# JSL the dispatcher, then pops result.
#
# Output files are written to the runtime tree.

import re
import sys
from pathlib import Path

NLIST_PATH = Path("/home/scott/claude/llvm816/tools/nlist/NList.Data.txt")
OUT_HEADER = Path("/home/scott/claude/llvm816/runtime/include/iigs/toolbox.h")
OUT_ASM = Path("/home/scott/claude/llvm816/runtime/src/iigsToolbox.s")

# Type table: (size in bytes, c-type)
TYPE_MAP = {
    "void":       (0, "void"),
    "Word":       (2, "unsigned short"),
    "Boolean":    (2, "unsigned short"),
    "Integer":    (2, "short"),
    "Char":       (2, "char"),     # widened on stack
    "Byte":       (2, "unsigned char"),
    "LongWord":   (4, "unsigned long"),
    "Long":       (4, "long"),
    "Handle":     (4, "void *"),    # 4-byte handle
    "Pointer":    (4, "void *"),    # 4-byte pointer (toolbox semantics)
    "Ref":        (4, "void *"),
    "Ptr":        (4, "void *"),
    "ResType":    (4, "unsigned long"),
    "Real":       (4, "float"),
    "Double":     (8, "double"),
    "Comp":       (8, "long long"),
    "Extended":   (10, "long double"),
    "GrafPortPtr":(4, "void *"),
    "WindowPtr":  (4, "void *"),
    "MenuHandle": (4, "void *"),
    "CtlRecHndl": (4, "void *"),
    "DialogPtr":  (4, "void *"),
    "RgnHandle":  (4, "void *"),
    "PrPort":     (4, "void *"),
    "PrRecHndl":  (4, "void *"),
    "PicHandle":  (4, "void *"),
    "WindRecHndl":(4, "void *"),
}

# Tool number → tool-set name mapping (low byte of toolNumber)
TOOLSET_NAME = {
    0x01: "ToolLocator",
    0x02: "MemoryManager",
    0x03: "MiscTools",
    0x04: "QuickDraw",
    0x05: "DeskManager",
    0x06: "EventManager",
    0x07: "Scheduler",
    0x08: "SoundManager",
    0x09: "AppleDeskBus",
    0x0A: "SANE",
    0x0B: "IntegerMath",
    0x0C: "TextTools",
    0x0E: "WindowManager",
    0x0F: "MenuManager",
    0x10: "ControlManager",
    0x11: "Loader",
    0x12: "QDAuxiliary",
    0x13: "PrintManager",
    0x14: "LineEdit",
    0x15: "DialogManager",
    0x16: "ScrapManager",
    0x17: "StandardFile",
    0x18: "DiskUtil",
    0x19: "NoteSynth",
    0x1A: "NoteSequencer",
    0x1B: "FontManager",
    0x1C: "ListManager",
    0x1D: "ACETools",
    0x1E: "ResourceManager",
    0x1F: "MIDITools",
    0x20: "VideoOverlay",
    0x21: "Teletext",
    0x22: "TextEdit",
    0x23: "MediaControl",
    0x32: "MediaControl2",
}


# NList default sizes.  Pointer (`@name`) is 4 bytes; an explicit `/N`
# suffix overrides.  Also: a handle abbreviation (bare `H`, or any
# trailing capital `H` on a short name like `StateH`, `MsgH`) is 4
# bytes — that's NList's convention for `:H`-style return values
# (NewHandle, GetMsgHandle, SaveTextState all use it).  Everything
# else defaults to 2 bytes (Word).
_HANDLE_SHORTHAND_RE = re.compile(r"^[A-Z][a-zA-Z]*H$")


def parseSize(rawName):
    """Strip a trailing /N size suffix and return (bareName, size, isPtr).
    isPtr is True when the source spelling implied a pointer/handle (the
    `@` prefix or an `H`-shorthand for handle); the generator then picks
    `void *` rather than `unsigned long` as the C type.
    """
    rawName = rawName.strip()
    if not rawName:
        return ("", 0, False)
    sz = None
    m = re.search(r"/(\d+)$", rawName)
    if m:
        sz = int(m.group(1))
        rawName = rawName[: m.start()].strip()
    isPtr = rawName.startswith("@")
    if isPtr:
        rawName = rawName[1:]
    isHandle = rawName == "H" or bool(_HANDLE_SHORTHAND_RE.match(rawName))
    if sz is None:
        if isPtr:
            sz = 4
        elif isHandle:
            sz = 4
        else:
            sz = 2
    return (rawName, sz, isPtr or (sz == 4 and isHandle))


_ENTRY_RE = re.compile(
    r"^([0-9A-Fa-f]{4})\s+"
    r"(?:(GS/OS|P8|P16|Shell|\w+:[\w/]+):)?"   # optional namespace prefix(es)
    r"(\w+)\s*"                                # function name
    r"\(([^)]*)\)"                             # arg list
    r"(?::([^,\s]+(?:,[^,\s]+)*))?\s*$"        # optional :ret(,ret2,...)
)


def parseLine(line):
    """Parse an NList.Data line into a toolbox decl dict.
    Returns None for comments, section dividers, unfilled stubs, or
    non-toolbox namespaces (P8 / P16 / Shell).
    """
    line = line.rstrip()
    if not line:
        return None
    # Comment forms: `* ...`, `fff9 | ...`, `TTTT === name ===`.
    head4 = line[:4]
    rest = line[4:].lstrip() if len(line) > 4 else ""
    if not re.fullmatch(r"[0-9A-Fa-f]{4}", head4):
        return None
    if rest.startswith("|") or rest.startswith("==="):
        return None
    if rest.startswith("(...)") or "(...)" in rest:
        return None

    # Filter by namespace.  Standard toolbox calls have no prefix.
    # GS/OS calls have `GS/OS:` and dispatch via $E100A8.  Skip P8/P16/
    # Shell — they use other dispatchers we don't currently expose.
    dispatcher = "dispatcher"           # → JSL $E10000
    nsMatch = re.match(r"^([A-Za-z][\w/]*):", rest)
    if nsMatch:
        ns = nsMatch.group(1)
        if ns == "GS/OS":
            dispatcher = "_CallBackVector"  # → JSL $E100A8
            rest = rest[nsMatch.end():]
        elif ns in ("P8", "P16", "Shell"):
            return None
        else:
            # Sub-prefix like `P8:ATLK:...` — already filtered above by
            # the outer P8; anything else unknown gets skipped.
            return None

    m = re.match(r"^(\w+)\s*\(([^)]*)\)(.*)$", rest)
    if not m:
        return None
    name, argsRaw, tail = m.group(1, 2, 3)
    toolNum = int(head4, 16)

    # Return type lives after `:`.  Strip optional leading whitespace.
    # Multi-value returns (e.g. `:Created,Type`) sum sizes; if any
    # element looked pointer-ish, the overall return type is `void *`
    # (handle-style returns are the dominant 4-byte case).
    retSize = 0
    retIsPtr = False
    tail = tail.strip()
    if tail.startswith(":"):
        retSpec = tail[1:].strip()
        for r in retSpec.split(","):
            _, sz, isPtr = parseSize(r)
            retSize += sz
            retIsPtr = retIsPtr or isPtr

    # Arg list: strip optional leading arg-count `N:` (e.g. `1-7:` or
    # `4or84:`) and split on commas.
    argsRaw = argsRaw.strip()
    if ":" in argsRaw:
        head, sep, body = argsRaw.partition(":")
        if re.fullmatch(r"[\d\-or ]+", head.strip()):
            argsRaw = body.strip()

    argSpec = []
    if argsRaw and argsRaw != "void":
        for a in argsRaw.split(","):
            _, sz, isPtr = parseSize(a)
            if sz == 0:
                return None
            argSpec.append((sz, isPtr))

    return {
        "name": name,
        "argSpec": argSpec,         # list of (size, isPtr)
        "retSize": retSize,
        "retIsPtr": retIsPtr,
        "tool": toolNum,
        "dispatcher": dispatcher,
    }


def typeInfo(t):
    """Return (size_bytes, c_type) for ORCA type, or None if unsupported."""
    if t in TYPE_MAP:
        return TYPE_MAP[t]
    # Default: assume 4 bytes / void* (pointer-like)
    return (4, "void *")


# Map an (NList byte size, isPtr) tuple to a (stack-size, c-type) tuple.
# NList only carries sizes — no semantic type names — so the generator
# picks a generic C type.  isPtr=True (NList `@name` prefix or `H`
# shorthand) selects `void *` for 4-byte slots; otherwise 4-byte
# non-pointer args become `unsigned long` so the common
# `NewHandle(0x900UL, ...)` pattern compiles without a pointer cast.
def sizeInfo(sz, isPtr=False):
    """Return (effective-size, c-type) for an NList byte size."""
    if sz == 0:
        return (0, "void")
    if sz == 1:
        return (2, "unsigned char")        # widened on stack
    if sz == 2:
        return (2, "unsigned short")
    if sz == 3:
        return (4, "unsigned long")        # rare; widened to 4
    if sz == 4:
        return (4, "void *" if isPtr else "unsigned long")
    if sz == 8:
        return (8, "long long")
    if sz == 10:
        return (10, "long double")
    return (4, "void *")


def emit(decls):
    """Generate C header and .s file from parsed decls."""

    cLines = [
        "// AUTOGENERATED by scripts/genToolbox.py from NList.Data.",
        "// DO NOT EDIT by hand — regenerate to update.",
        "// Source: tools/nlist/NList.Data.txt (Dave Lyons' NiftyList database).",
        "//",
        "// Complete IIgs toolbox + GS/OS surface (~2000 routines).",
        "// Names match Apple's IIgs Toolbox Reference (TLStartUp,",
        "// MMStartUp, NewWindow, SysBeep, etc.).  Multi-arg wrappers",
        "// (those whose stub body uses memory operands) live in",
        "// runtime/src/iigsToolbox.s; zero-arg / single-arg simple",
        "// ones are inlined here.",
        "",
        "#ifndef IIGS_TOOLBOX_H",
        "#define IIGS_TOOLBOX_H",
        "",
        "#ifdef __cplusplus",
        'extern "C" {',
        "#endif",
        "",
        "// IigsCursorT - opaque handle for the QD CursorRecord layout.",
        "// Apple/ORCA `Cursor` is variable-length (cursorData[] and",
        "// cursorMask[] sized by cursorHeight/cursorWidth), so we expose",
        "// it as an opaque blob.  Use iigs/cursor.h helpers to push/pop",
        "// stock ROM shapes (arrow, busy) without poking the fields by",
        "// hand.  Pointer-sized; pass to SetCursor() / GetCursorAdr().",
        "typedef struct IigsCursorT IigsCursorT;",
        "",
    ]

    sLines = [
        "; AUTOGENERATED by scripts/genToolbox.py from NList.Data.",
        "; DO NOT EDIT by hand — regenerate to update.",
        "; Source: tools/nlist/NList.Data.txt (Dave Lyons' NiftyList database).",
        ";",
        "; IIgs toolbox multi-arg wrappers.",
        ";",
        "; C ABI: arg0 (i16) in A, arg0 (i32) in A:X, arg1+ on stack (4,S etc.).",
        "; Each wrapper re-pushes args in toolbox (Pascal-style L-to-R) order,",
        "; preceded by result space if non-void return, then JSL $E10000",
        "; (or $E100A8 for GS/OS).  Pops result if non-void.",
        ";",
        "; Tool number: high byte = function, low byte = tool set.",
        "",
        "\t.text",
        "",
    ]

    seenNames = set()
    inlineCount = 0
    asmCount = 0
    skipped = []

    for d in decls:
        name = d["name"]
        if name in seenNames:
            continue  # duplicate (e.g. P16 / GS/OS overload of same name).
        seenNames.add(name)

        tool = d["tool"]
        dispatcher = d["dispatcher"]

        # Translate NList byte sizes into (effective-size, c-type) tuples
        # the emit logic expects.  argTypes is rebuilt for the diagnostic
        # comment only.  Pointer detection follows NList's signal: an
        # `@name` prefix or `H`/`*H`-shorthand → `void *`; an explicit
        # `/4` without those markers → `unsigned long`.  That matches
        # how the NList catalog distinguishes integer Long/LongWord
        # values (TickCount → :Ticks/4) from pointers/handles
        # (NewHandle → :H, GetCursorAdr → :@Curs).
        retSize, retC = sizeInfo(d["retSize"], d["retIsPtr"])
        argInfo = [sizeInfo(sz, isPtr) for (sz, isPtr) in d["argSpec"]]
        argTypes = [
            "Pointer" if ai[1] == "void *" else
            "LongWord" if ai[0] == 4 else
            "Word" if ai[0] == 2 else
            f"{ai[0]}B"
            for ai in argInfo
        ]
        retType = (
            "Pointer" if retC == "void *" else
            "LongWord" if retSize == 4 else
            "Word" if retSize == 2 else
            "void" if retSize == 0 else
            f"{retSize}B"
        )

        # Build C-style arg list.
        cArgs = ", ".join(f"{ai[1]} a{i}" for i, ai in enumerate(argInfo))
        if not cArgs:
            cArgs = "void"
        cDecl = f"{retC} {name}({cArgs});"

        # Decide inline vs asm.
        # Simple cases that can be inlined: no args (with or without 16-bit
        # return), or single 16-bit arg with void return / 16-bit return.
        canInline = False
        if not argInfo and retSize in (0, 2):
            canInline = True
        elif (
            len(argInfo) == 1
            and argInfo[0][0] == 2
            and retSize in (0, 2)
        ):
            canInline = True

        dispAddr = "0xe10000" if dispatcher == "dispatcher" else "0xe100a8"

        if canInline:
            # Generate inline asm body.
            if not argInfo:
                if retSize == 0:
                    body = (
                        f'    __asm__ volatile (\n'
                        f'        "ldx #0x{tool:04X}\\n"\n'
                        f'        "jsl {dispAddr}\\n"\n'
                        f'        :\n'
                        f'        :\n'
                        f'        : "a", "x", "y", "memory"\n'
                        f'    );\n'
                    )
                else:  # 16-bit return
                    body = (
                        f'    {retC} _r;\n'
                        f'    __asm__ volatile (\n'
                        f'        "pha\\n"               // result space\n'
                        f'        "ldx #0x{tool:04X}\\n"\n'
                        f'        "jsl {dispAddr}\\n"\n'
                        f'        "pla\\n"\n'
                        f'        : "=a"(_r)\n'
                        f'        :\n'
                        f'        : "x", "y", "memory"\n'
                        f'    );\n'
                        f'    return _r;\n'
                    )
            else:  # 1-arg
                if retSize == 0:
                    body = (
                        f'    __asm__ volatile (\n'
                        f'        "pha\\n"               // arg0\n'
                        f'        "ldx #0x{tool:04X}\\n"\n'
                        f'        "jsl {dispAddr}\\n"\n'
                        f'        :\n'
                        f'        : "a"(a0)\n'
                        f'        : "x", "y", "memory"\n'
                        f'    );\n'
                    )
                else:
                    body = (
                        f'    {retC} _r;\n'
                        f'    __asm__ volatile (\n'
                        f'        "pha\\n"               // result space\n'
                        f'        "pha\\n"               // arg0\n'
                        f'        "ldx #0x{tool:04X}\\n"\n'
                        f'        "jsl {dispAddr}\\n"\n'
                        f'        "pla\\n"\n'
                        f'        : "=a"(_r)\n'
                        f'        : "a"(a0)\n'
                        f'        : "x", "y", "memory"\n'
                        f'    );\n'
                        f'    return _r;\n'
                    )

            cLines.append(f"// tool 0x{tool:04X}  set 0x{tool & 0xFF:02X}  ({TOOLSET_NAME.get(tool & 0xFF, '?')})")
            cLines.append(f"static inline {retC} {name}({cArgs}) {{")
            cLines.append(body.rstrip())
            cLines.append("}")
            cLines.append("")
            inlineCount += 1
        else:
            # Extern decl in header, asm body in .s file.
            cLines.append(f"extern {retC} {name}({cArgs});  // 0x{tool:04X}")

            # Generate asm body.
            sLines.append(f"; {name}({', '.join(argTypes) or 'void'}) -> {retType}")
            sLines.append(f"; tool 0x{tool:04X}, set 0x{tool & 0xFF:02X} ({TOOLSET_NAME.get(tool & 0xFF, '?')})")
            # One section per wrapper so link816's --gc-sections can
            # drop the ~880 wrappers the user's demo doesn't reference.
            # Without this the entire 45 KB toolbox.s gets linked into
            # every binary.
            sLines.append(f"\t.section .text.{name},\"ax\"")
            sLines.append(f"\t.globl {name}")
            sLines.append(f"{name}:")

            # Compute total stack arg bytes (excluding arg0 which is in regs).
            # Determine where each arg starts on the caller's stack.
            # arg0 is in A (or A:X for i32-first-arg).
            firstArgIs32 = argInfo and argInfo[0][0] == 4
            stackArgStart = 4  # offset to first stack-passed arg after JSL retaddr

            # Stash arg0 if any args exist.  i16: 'sta scratch'.  i32: 'sta
            # scratch; stx scratch+2'.  void-arg functions skip this entirely
            # — emitting a phantom `pha` for arg0 corrupts the dispatcher's
            # stack frame (caught when GetTick crashed under Loader).
            #
            # No PBR-bank workaround here anymore: the backend now emits a
            # proper R_W65816_BANK16 relocation on the high half of &symbol
            # so the OMF Loader patches it with our actual placement bank
            # at load time.  See feedback_pointer_constant_bank_zero.md.
            scratchDP = 0xE0  # libcall scratch zone
            if argInfo:
                sLines.append(f"\t; --- stash arg0 (in A{'/X' if firstArgIs32 else ''}) ---")
                sLines.append(f"\tsta 0x{scratchDP:02X}")
                if firstArgIs32:
                    sLines.append(f"\tstx 0x{scratchDP + 2:02X}")

            # Push result space (toolbox order: result is highest on stack).
            if retSize > 0:
                sLines.append(f"\t; --- result space ({retSize} bytes) ---")
                for _ in range((retSize + 1) // 2):
                    sLines.append(f"\tpea 0")

            # Push args in Pascal order (L-to-R).  For multi-byte values
            # the toolbox expects HIGH word first then LOW word (matches
            # ORCA-C's PushLong macro: `pea ^addr ; pea addr`).  After
            # the two pushes, memory at (S+1..S+4) is in little-endian
            # long-value order (byte 0 low at lowest address) so a
            # `pull long` / 32-bit indirect read sees the value
            # correctly.  Earlier "lo first then hi" emission left the
            # high word at the wrong stack offset and the toolbox saw
            # garbage (NewHandle returned NULL for a $900-byte request).
            # Tracker: how many bytes have we pushed beyond the original
            # caller-stack so all stack-arg loads need to add (pushed) to
            # their original offset.
            pushedBytes = (retSize + 1) // 2 * 2  # result space rounded up to word
            # arg0 first (if any args).
            if argInfo:
                sLines.append(f"\t; --- arg0 ---")
                if firstArgIs32:
                    # HIGH word first (Long convention).
                    sLines.append(f"\tlda 0x{scratchDP + 2:02X}")
                    sLines.append(f"\tpha")
                    pushedBytes += 2
                sLines.append(f"\tlda 0x{scratchDP:02X}")
                sLines.append(f"\tpha")
                pushedBytes += 2

            # arg1, arg2, ... — each loaded from caller stack at original
            # offset + pushedBytes.
            stackArgOffset = stackArgStart  # original offset of next arg
            for i, ai in enumerate(argInfo[1:], start=1):
                size = ai[0]
                sLines.append(f"\t; --- arg{i} ({argTypes[i]}, {size}B) ---")
                # i16 / 16-bit-on-stack args: 1 word, push lo
                # i32 / 32-bit-on-stack: 2 words, push lo then hi
                # We're loading from caller's pre-push stack.  Original
                # offsets: arg1 at 4, arg2 at 4+size(arg1), ...
                # But each load from `(orig+pushed),s` accounts for pushes.
                if size <= 2:
                    sLines.append(f"\tlda {stackArgOffset + pushedBytes}, s")
                    sLines.append(f"\tpha")
                    pushedBytes += 2
                    stackArgOffset += 2
                elif size == 4:
                    # Long: push HIGH word first, then LOW word (ORCA
                    # PushLong convention; same fix as the arg0 Long path).
                    # HI is at caller offset (stackArgOffset + 2); LO at
                    # stackArgOffset.  After the HI PHA, both halves'
                    # current stack-rel offsets shift +2.
                    sLines.append(f"\tlda {stackArgOffset + pushedBytes + 2}, s")
                    sLines.append(f"\tpha")
                    pushedBytes += 2
                    # LO at original offset stackArgOffset; current = +pushedBytes.
                    sLines.append(f"\tlda {stackArgOffset + pushedBytes}, s")
                    sLines.append(f"\tpha")
                    pushedBytes += 2
                    stackArgOffset += 4
                else:
                    # Bigger types (8-byte Comp, 10-byte Extended) — push word by word.
                    nWords = (size + 1) // 2
                    for _ in range(nWords):
                        sLines.append(f"\tlda {stackArgOffset + pushedBytes}, s")
                        sLines.append(f"\tpha")
                        pushedBytes += 2
                    stackArgOffset += size

            # Dispatch.
            sLines.append(f"\tldx #0x{tool:04X}")
            sLines.append(f"\tjsl {dispAddr}")

            # Pop result.
            if retSize == 2:
                sLines.append(f"\tpla              ; result -> A")
            elif retSize == 4:
                sLines.append(f"\tpla              ; result lo -> A")
                sLines.append(f"\tplx              ; result hi -> X")
            elif retSize > 4:
                # Larger results: pop into scratch then load A/X for return.
                # Treat as "best effort" — caller should not expect a real
                # return value beyond what fits in A:X.
                nWords = (retSize + 1) // 2
                for _ in range(nWords):
                    sLines.append(f"\tpla")

            sLines.append(f"\trtl")
            sLines.append("")

            asmCount += 1

    cLines.append("")
    cLines.append("#ifdef __cplusplus")
    cLines.append("}")
    cLines.append("#endif")
    cLines.append("")
    cLines.append("#endif  // IIGS_TOOLBOX_H")

    OUT_HEADER.write_text("\n".join(cLines))
    OUT_ASM.write_text("\n".join(sLines))

    print(f"wrote {OUT_HEADER}: {inlineCount} inline + {asmCount} extern decls")
    print(f"wrote {OUT_ASM}: {asmCount} bodies")
    if skipped:
        print(f"skipped {len(skipped)} routines (unhandled types):")
        for n, why in skipped[:5]:
            print(f"  {n}: {why}")


def main():
    decls = []
    if not NLIST_PATH.exists():
        print(f"error: {NLIST_PATH} not found", file=sys.stderr)
        sys.exit(1)
    for line in NLIST_PATH.read_text().splitlines():
        d = parseLine(line)
        if d:
            decls.append(d)
    print(f"parsed {len(decls)} declarations from {NLIST_PATH}")
    emit(decls)


if __name__ == "__main__":
    main()
