# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Parser driver.

This provides a high-level interface to parse a file into a syntax tree.

"""

# Python imports
import os
import token
import logging
import tokenize

# Fuel compiler imports
from sigma.fuelc import parse
from sigma.fuelc import astnode
from sigma.fuelc import grammar

# Global objects
logger = logging.getLogger("fuelc.driver")

# Interface to load and cache the grammar

_grammar = None

def get_grammar():
    """Get the grammar, cached or freshly loaded."""
    global _grammar
    if _grammar is None:
        _grammar = load_grammar()
    return _grammar

here = os.path.dirname(__file__)
grammar_txt = os.path.join(here, "Grammar.txt")
grammar_pickle = os.path.join(here, "Grammar.pickle")

def _pickle_token_numbers_match(g):
    """Check that a loaded grammar's token numbers match the running Python.

    The grammar pickle stores token type numbers (from the ``token``
    module) as keys in ``g.tokens`` and as the first element of each
    entry in ``g.labels``.  If the pickle was generated under a
    different Python version whose ``token`` constants differ (e.g.
    Python 2 vs 3), the DFA tables will silently mis-classify operator
    tokens and produce spurious ``ParseError("bad input")`` failures.

    We verify consistency by checking that each token number stored in
    the pickle's ``tokens`` dict points to a label whose first element
    equals that same token number.  A mismatch indicates the pickle was
    built with a different Python version's token constants.

    Return *True* if the numbers look consistent, *False* otherwise.
    """
    for tok_num, label_idx in g.tokens.items():
        if tok_num >= 256:
            continue  # symbol, not a token
        if label_idx >= len(g.labels):
            logger.info("Pickle token %d references out-of-range label %d",
                        tok_num, label_idx)
            return False
        label_tok, label_val = g.labels[label_idx]
        if label_tok != tok_num:
            logger.info("Pickle token %d -> label (%d, %r): mismatch",
                        tok_num, label_tok, label_val)
            return False
    # Also verify the QUESTIONMARK token matches the current Python.
    if grammar.QUESTIONMARK not in g.tokens:
        logger.info("Pickle is missing the QUESTIONMARK token (%d); "
                     "may have been built with a different Python version",
                     grammar.QUESTIONMARK)
        return False
    return True


def load_grammar(gt=grammar_txt, gp=grammar_pickle, save=True, force=False):
    """Load the grammar (maybe from a pickle)."""
    if force or not newer(gp, gt):
        logger.info("Generating grammar tables from %s", gt)
        from sigma.fuelc import pgen
        g = pgen.generate_grammar(gt)
        if save:
            logger.info("Writing grammar tables to %s", gp)
            g.dump(gp)
    else:
        g = grammar.Grammar()
        g.load(gp)
        if not _pickle_token_numbers_match(g):
            logger.info("Grammar pickle has stale token numbers; "
                        "regenerating from %s", gt)
            from sigma.fuelc import pgen
            g = pgen.generate_grammar(gt)
            if save:
                logger.info("Writing grammar tables to %s", gp)
                g.dump(gp)
    return g

def newer(a, b):
    """Inquire whether file a was written since file b."""
    if not os.path.exists(a):
        return False
    if not os.path.exists(b):
        return True
    return os.path.getmtime(a) >= os.path.getmtime(b)


# Top-level parsing interface

def parse_stream_raw(stream, convert=None, debug=False):
    """Parse a stream and return the concrete syntax tree."""
    g = get_grammar()
    p = parse.Parser(g, convert)
    p.setup()
    t = v = x = None
    try:
        for t, v, x, y, z in tokenize.generate_tokens(stream.readline):
            if t in (tokenize.COMMENT, tokenize.NL, tokenize.ENCODING):
                continue
            if t == token.OP:
                t = grammar.opmap[v]
            elif t == token.ERRORTOKEN:
                # This is a hack for a limitation of tokenize.py: it
                # doesn't recognize the '?' token, and somehow if one is
                # present in the source, it returns each whitespace
                # character before that as well as the '?' itself as a
                # separate error token.  Ignore the spaces and return the
                # token for the question mark
                if v.isspace():
                    continue
                t = grammar.opmap.get(v, t)
            if debug:
                logger.debug("%s %r", token.tok_name[t], v)
            if p.addtoken(t, v, x):
                if debug:
                    logger.debug("Stop.")
                break
        else:
            # We never broke out -- EOF is too soon (how can this happen???)
            raise parse.ParseError("incomplete input", t, v, x)
    except IndentationError as err:
        # Catch an exception expected in the next() call to the generator
        msg = str(err) + " (this actually applies to the following line)"
        raise parse.ParseError(msg, t, v, x)
    return p.rootnode

def parse_stream(stream, debug=False):
    """Parse a stream and return the abstract syntax tree."""
    return parse_stream_raw(stream, astnode.convert, debug)

def parse_file(filename, debug=False):
    """Parse a file and return the abstract syntax tree."""
    stream = open(filename, encoding="utf-8")
    try:
        return parse_stream(stream, debug)
    finally:
        stream.close()

def parse_string(text, debug=False):
    """Parse a string and return the abstract syntax tree."""
    from io import StringIO
    stream = StringIO(text)
    return parse_stream(stream, debug)
