#!/usr/bin/env python3.8

""" Convert a grammar into a dot-file suitable for use with GraphViz

    For example:
        Generate the GraphViz file:
        # scripts/grammar_grapher.py data/python.gram > python.gv

        Then generate the graph...

        # twopi python.gv -Tpng > python_twopi.png

        or

        # dot python.gv -Tpng > python_dot.png

        NOTE: The _dot_ and _twopi_ tools seem to produce the most useful results.
              The _circo_ tool is the worst of the bunch. Don't even bother.
"""

import argparse
import sys

from typing import Any, List

sys.path.insert(0, ".")

from pegen.build import build_parser
from pegen.grammar import (
    Alt,
    Cut,
    Forced,
    Group,
    Leaf,
    Lookahead,
    Rule,
    NameLeaf,
    NamedItem,
    Opt,
    Repeat,
    Rhs,
)

argparser = argparse.ArgumentParser(
    prog="graph_grammar",
    description="Graph a grammar tree",
)
argparser.add_argument(
    "-s",
    "--start",
    choices=["exec", "eval", "single"],
    default="exec",
    help="Choose the grammar's start rule (exec, eval or single)",
)
argparser.add_argument("grammar_file", help="The grammar file to graph")


def references_for_item(item: Any) -> List[Any]:
    if isinstance(item, Alt):
        return [_ref for _item in item.items for _ref in references_for_item(_item)]
    elif isinstance(item, Cut):
        return []
    elif isinstance(item, Forced):
        return references_for_item(item.node)
    elif isinstance(item, Group):
        return references_for_item(item.rhs)
    elif isinstance(item, Lookahead):
        return references_for_item(item.node)
    elif isinstance(item, NamedItem):
        return references_for_item(item.item)

    # NOTE NameLeaf must be before Leaf
    elif isinstance(item, NameLeaf):
        if item.value == "ENDMARKER":
            return []
        return [item.value]
    elif isinstance(item, Leaf):
        return []

    elif isinstance(item, Opt):
        return references_for_item(item.node)
    elif isinstance(item, Repeat):
        return references_for_item(item.node)
    elif isinstance(item, Rhs):
        return [_ref for alt in item.alts for _ref in references_for_item(alt)]
    elif isinstance(item, Rule):
        return references_for_item(item.rhs)
    else:
        raise RuntimeError(f"Unknown item: {type(item)}")


def main() -> None:
    args = argparser.parse_args()

    try:
        grammar, parser, tokenizer = build_parser(args.grammar_file)
    except Exception as err:
        print("ERROR: Failed to parse grammar file", file=sys.stderr)
        sys.exit(1)

    references = {}
    for name, rule in grammar.rules.items():
        references[name] = set(references_for_item(rule))

    # Flatten the start node if has only a single reference
    root_node = {"exec": "file", "eval": "eval", "single": "interactive"}[args.start]

    print("digraph g1 {")
    print('\toverlap="scale";')  # Force twopi to scale the graph to avoid overlaps
    print(f'\troot="{root_node}";')
    print(f"\t{root_node} [color=green, shape=circle];")
    for name, refs in references.items():
        for ref in refs:
            print(f"\t{name} -> {ref};")
    print("}")


if __name__ == "__main__":
    main()
