# Generated by parsegen_code.py from parsegen JSON tables. Do not edit.
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional, Protocol

from runtime.token import Token, new_token
from runtime.ast import AST, ASTNode, new_ast_node, new_ast_node_terminal
from runtime.lexer import AbstractLexer
from runtime.token import TOKEN_TYPE_EOF, TOKEN_TYPE_ERROR


class ActionKind:
    SHIFT = 0
    REDUCE = 1
    ACCEPT = 2


@dataclass
class Action:
    kind: int
    target: int


@dataclass
class Production:
    lhs: str
    rhs_count: int
    has_hint: bool = False
    has_passthrough: bool = False
    has_parent_literal: bool = False
    has_with_appended_children: bool = False
    has_with_prepended_children: bool = False
    has_with_adopted_grandchildren: bool = False
    parent_index: int = 0
    passthrough_index: int = 0
    parent_literal: str = ""
    child_indices: list[int] = None
    with_appended_children: list[int] = None
    with_prepended_children: list[int] = None
    with_adopted_grandchildren: list[int] = None
    node_type: str = ""

    def __post_init__(self) -> None:
        if self.child_indices is None:
            self.child_indices = []
        if self.with_appended_children is None:
            self.with_appended_children = []
        if self.with_prepended_children is None:
            self.with_prepended_children = []
        if self.with_adopted_grandchildren is None:
            self.with_adopted_grandchildren = []


# No-AST sentinel for ast_mode "noast"
_{{ class_name }}_NO_AST_SENTINEL = ASTNode(token=None, type="", children=[])

ACTIONS: dict[int, dict[str, Action]] = {
{% for a in actions %}
    {{ a.state }}: {
{% for e in a.entries %}
        {{ e.terminal_literal }}: Action(kind={{ e.kind_literal }}, target={{ e.target }}),
{% endfor %}
    },
{% endfor %}
}

GOTOS: dict[int, dict[str, int]] = {
{% for g in gotos %}
    {{ g.state }}: {
{% for e in g.entries %}
        {{ e.nonterm_literal }}: {{ e.target }},
{% endfor %}
    },
{% endfor %}
}

PRODUCTIONS: list[Production] = [
{% for p in productions %}
    Production(
        lhs={{ p.lhs_literal }},
        rhs_count={{ p.rhs_count }},
        has_hint={{ p.has_hint }},
        has_passthrough={{ p.has_passthrough }},
        has_parent_literal={{ p.has_parent_literal }},
        has_with_appended_children={{ p.has_with_appended_children }},
        has_with_prepended_children={{ p.has_with_prepended_children }},
        has_with_adopted_grandchildren={{ p.has_with_adopted_grandchildren }},
        parent_index={{ p.parent_index }},
        passthrough_index={{ p.passthrough_index }},
        parent_literal={{ p.parent_literal|repr }},
        child_indices={{ p.child_indices }},
        with_appended_children={{ p.with_appended_children }},
        with_prepended_children={{ p.with_prepended_children }},
        with_adopted_grandchildren={{ p.with_adopted_grandchildren }},
        node_type={{ p.node_type|repr }},
    ),
{% endfor %}
]


class TraceHooks:
    on_token: Optional[callable] = None
    on_action: Optional[callable] = None
    on_stack: Optional[callable] = None


class {{ class_name }}:
    def __init__(self) -> None:
        self.trace: Optional[TraceHooks] = None

    def parse(
        self,
        lexer: AbstractLexer,
        ast_mode: str = "",
    ) -> Optional[AST]:
        if lexer is None:
            raise ValueError("parser: nil lexer")
        state_stack: list[int] = [0]
        node_stack: list[ASTNode] = []
        lookahead = lexer.scan()
        if self.trace and self.trace.on_token and lookahead:
            self.trace.on_token(lookahead)
        while True:
            if lookahead is None:
                raise ValueError("parser: lexer returned None token")
            if lookahead.type == TOKEN_TYPE_ERROR:
                raise ValueError(f"lexer error: {lookahead.lexeme}")
            state = state_stack[-1]
            state_actions = ACTIONS.get(state, {})
            action = state_actions.get(lookahead.type)
            if action is None:
                raise ValueError(
                    f"parse error: unexpected {lookahead.type!r} ({lookahead.lexeme!r})"
                )
            if self.trace and self.trace.on_action:
                self.trace.on_action(state, action, lookahead)
            if action.kind == ActionKind.SHIFT:
                if ast_mode == "noast":
                    node_stack.append(_{{ class_name }}_NO_AST_SENTINEL)
                else:
                    node_stack.append(
                        new_ast_node_terminal(lookahead, lookahead.type)
                    )
                state_stack.append(action.target)
                lookahead = lexer.scan()
                if self.trace and self.trace.on_token and lookahead:
                    self.trace.on_token(lookahead)
                if self.trace and self.trace.on_stack:
                    self.trace.on_stack(state_stack, node_stack)
            elif action.kind == ActionKind.REDUCE:
                prod = PRODUCTIONS[action.target]
                rhs_nodes: list[ASTNode] = [None] * prod.rhs_count  # type: ignore
                for i in range(prod.rhs_count - 1, -1, -1):
                    state_stack.pop()
                    rhs_nodes[i] = node_stack.pop()
                if ast_mode == "noast":
                    node_stack.append(_{{ class_name }}_NO_AST_SENTINEL)
                else:
                    node = _reduce_{{ class_name }}(prod, rhs_nodes, ast_mode)
                    node_stack.append(node)
                state = state_stack[-1]
                state_gotos = GOTOS.get(state, {})
                next_state = state_gotos.get(prod.lhs)
                if next_state is None:
                    raise ValueError(f"parse error: missing goto for {prod.lhs}")
                state_stack.append(next_state)
                if self.trace and self.trace.on_stack:
                    self.trace.on_stack(state_stack, node_stack)
            else:  # ACCEPT
                if len(node_stack) != 1:
                    raise ValueError(
                        f"parse error: unexpected parse stack size {len(node_stack)}"
                    )
                if self.trace and self.trace.on_stack:
                    self.trace.on_stack(state_stack, node_stack)
                if ast_mode == "noast":
                    return None
                return AST(root_node=node_stack[0])

    def attach_cli_trace(
        self,
        trace_tokens: bool = False,
        trace_states: bool = False,
        trace_stack: bool = False,
    ) -> None:
        if not (trace_tokens or trace_states or trace_stack):
            return
        import sys
        self.trace = TraceHooks()
        if trace_tokens:
            def on_token(tok: Optional[Token]) -> None:
                if tok:
                    loc = f" line={tok.location.line} col={tok.location.column}" if tok.location else ""
                    print(f"TOK type={tok.type} lexeme={tok.lexeme!r}{loc}", file=sys.stderr)
            self.trace.on_token = on_token
        if trace_states:
            def on_action(state: int, action: Action, lookahead: Optional[Token]) -> None:
                kind_str = "shift" if action.kind == ActionKind.SHIFT else ("reduce" if action.kind == ActionKind.REDUCE else "accept")
                target = f"({action.target})" if action.kind != ActionKind.ACCEPT else ""
                la_type = lookahead.type if lookahead else ""
                la_lex = lookahead.lexeme if lookahead else ""
                print(f"STATE {state} {kind_str}{target} on {la_type}({la_lex!r})", file=sys.stderr)
            self.trace.on_action = on_action
        if trace_stack:
            def on_stack(state_stack: list[int], node_stack: list[ASTNode]) -> None:
                states = "[" + " ".join(str(s) for s in state_stack) + "]"
                nodes = "[" + " ".join(n.type for n in node_stack) + "]"
                print(f"STACK states={states} nodes={nodes}", file=sys.stderr)
            self.trace.on_stack = on_stack


def _reduce_{{ class_name }}(prod: Production, rhs_nodes: list[ASTNode], ast_mode: str) -> ASTNode:
    use_full_tree = ast_mode == "fullast"
    if not use_full_tree and prod.has_passthrough:
        return rhs_nodes[prod.passthrough_index]
{% if hint_mode == "hints" %}
    if not use_full_tree and prod.has_with_appended_children:
        if prod.has_parent_literal:
            parent_token = new_token(prod.parent_literal, prod.parent_literal)
            parent_type = prod.parent_literal
            parent = None
        else:
            parent = rhs_nodes[prod.parent_index]
            parent_token = parent.token
            parent_type = parent.type
        node_type = prod.node_type or parent_type
        new_children: list[ASTNode] = []
        if parent is not None and parent.children:
            new_children.extend(parent.children)
        for ci in prod.with_appended_children:
            new_children.append(rhs_nodes[ci])
        return new_ast_node(parent_token, node_type, new_children)
    if not use_full_tree and prod.has_with_prepended_children:
        if prod.has_parent_literal:
            parent_token = new_token(prod.parent_literal, prod.parent_literal)
            parent_type = prod.parent_literal
            parent = None
        else:
            parent = rhs_nodes[prod.parent_index]
            parent_token = parent.token
            parent_type = parent.type
        node_type = prod.node_type or parent_type
        new_children = []
        for ci in prod.with_prepended_children:
            new_children.append(rhs_nodes[ci])
        if parent is not None and parent.children:
            new_children.extend(parent.children)
        return new_ast_node(parent_token, node_type, new_children)
    if not use_full_tree and prod.has_with_adopted_grandchildren:
        if prod.has_parent_literal:
            parent_token = new_token(prod.parent_literal, prod.parent_literal)
            parent_type = prod.parent_literal
            parent = None
        else:
            parent = rhs_nodes[prod.parent_index]
            parent_token = parent.token
            parent_type = parent.type
        node_type = prod.node_type or parent_type
        new_children = []
        for ci in prod.with_adopted_grandchildren:
            child_node = rhs_nodes[ci]
            if child_node is not None and child_node.children:
                new_children.extend(child_node.children)
        return new_ast_node(parent_token, node_type, new_children)
    if not use_full_tree and prod.has_hint:
        node_type = prod.node_type or prod.lhs
        if prod.has_parent_literal:
            parent_token = new_token(prod.parent_literal, prod.parent_literal)
        elif 0 <= prod.parent_index < len(rhs_nodes) and rhs_nodes[prod.parent_index].token:
            parent_token = rhs_nodes[prod.parent_index].token
        else:
            parent_token = None
        hint_children = [rhs_nodes[ci] for ci in prod.child_indices]
        return new_ast_node(parent_token, node_type, hint_children)
{% endif %}
    if prod.rhs_count == 1:
        return rhs_nodes[0]
    if prod.rhs_count == 0:
        return new_ast_node(None, prod.lhs, [])
    return new_ast_node(None, prod.lhs, rhs_nodes)
