"""`#define` macro expansion.

Confirmed (reverse_engineering.md, batches 3 & 4): `#define`d constants
compile *identically* to writing the value directly, in every context
tested (plain assignment, function-call argument). So the simplest correct
implementation is textual substitution on the token stream before parsing:
delete the `#define` lines, and splice each macro's body tokens in wherever
its name is referenced.

This is a single, non-recursive substitution pass -- a macro's body isn't
itself re-expanded for other macro references. The manual's own examples
(`#define CommandID _GO + 5`) don't nest macros, and nothing in the
confirmed corpus does either, so this is left as a TODO rather than
speculatively implemented.
"""

from __future__ import annotations

from .tokens import Token, TokenType


class PreprocessorError(Exception):
    pass


def expand_macros(tokens: list[Token]) -> list[Token]:
    macros: dict[str, list[Token]] = {}
    out: list[Token] = []

    i = 0
    n = len(tokens)
    while i < n:
        tok = tokens[i]
        if tok.type == TokenType.KEYWORD and tok.value == "#define":
            if i + 1 >= n or tokens[i + 1].type != TokenType.IDENT:
                raise PreprocessorError(
                    f"#define must be followed by a macro name (line {tok.line})"
                )
            name = tokens[i + 1].value.lower()
            body: list[Token] = []
            j = i + 2
            while j < n and tokens[j].type not in (TokenType.NEWLINE, TokenType.EOF):
                body.append(tokens[j])
                j += 1
            macros[name] = body
            i = j  # skip the trailing NEWLINE/EOF check is handled by outer loop
            continue
        out.append(tok)
        i += 1

    if not macros:
        return out

    expanded: list[Token] = []
    for tok in out:
        if tok.type == TokenType.IDENT and tok.value.lower() in macros:
            expanded.extend(macros[tok.value.lower()])
        else:
            expanded.append(tok)
    return expanded
