# -----------------------------------------------------------------
# pycparser: cdecl.py
#
# Example of the CDECL tool using pycparser. CDECL "explains" C type
# declarations in plain English.
#
# The AST generated by pycparser from the given declaration is traversed
# recursively to build the explanation. Note that the declaration must be a
# valid external declaration in C. As shown below, typedef can be optionally
# expanded.
#
# For example:
#
#   c_decl = 'typedef int Node; const Node* (*ar)[10];'
#
#   explain_c_declaration(c_decl)
#   => ar is a pointer to array[10] of pointer to const Node
#
# struct and typedef can be optionally expanded:
#
#   explain_c_declaration(c_decl, expand_typedef=True)
#   => ar is a pointer to array[10] of pointer to const int
#
#   c_decl = 'struct P {int x; int y;} p;'
#
#   explain_c_declaration(c_decl)
#   => p is a struct P
#
#   explain_c_declaration(c_decl, expand_struct=True)
#   => p is a struct P containing {x is a int, y is a int}
#
# Eli Bendersky [https://eli.thegreenplace.net/]
# License: BSD
# -----------------------------------------------------------------
import copy
import sys
from typing import Optional

# This is not required if you've installed pycparser into
# your site-packages/ with setup.py
sys.path.extend([".", ".."])

from pycparser import c_parser, c_ast


def explain_c_declaration(
    c_decl: str, expand_struct: bool = False, expand_typedef: bool = False
) -> str:
    """Parses the declaration in c_decl and returns a text
    explanation as a string.

    The last external node of the string is used, to allow earlier typedefs
    for used types.

    expand_struct=True will spell out struct definitions recursively.
    expand_typedef=True will expand typedef'd types.
    """
    parser = c_parser.CParser()

    try:
        node = parser.parse(c_decl, filename="<stdin>")
    except c_parser.ParseError:
        e = sys.exc_info()[1]
        return "Parse error:" + str(e)

    if not isinstance(node, c_ast.FileAST) or not isinstance(node.ext[-1], c_ast.Decl):
        return "Not a valid declaration"

    try:
        expanded = expand_struct_typedef(
            node.ext[-1],
            node,
            expand_struct=expand_struct,
            expand_typedef=expand_typedef,
        )
    except Exception as e:
        return "Not a valid declaration: " + str(e)

    return _explain_decl_node(expanded)


def _explain_decl_node(decl_node: c_ast.Decl) -> str:
    """Receives a c_ast.Decl note and returns its explanation in
    English.
    """
    storage = " ".join(decl_node.storage) + " " if decl_node.storage else ""

    return decl_node.name + " is a " + storage + _explain_type(decl_node.type)


def _explain_type(decl: c_ast.Node) -> str:
    """Recursively explains a type decl node"""
    match decl:
        case c_ast.TypeDecl():
            quals = " ".join(decl.quals) + " " if decl.quals else ""
            return quals + _explain_type(decl.type)
        case c_ast.Typename() | c_ast.Decl():
            return _explain_type(decl.type)
        case c_ast.IdentifierType():
            return " ".join(decl.names)
        case c_ast.PtrDecl():
            quals = " ".join(decl.quals) + " " if decl.quals else ""
            return quals + "pointer to " + _explain_type(decl.type)
        case c_ast.ArrayDecl():
            arr = "array"
            if decl.dim is not None:
                arr += f"[{decl.dim.value}]"
            return arr + " of " + _explain_type(decl.type)
        case c_ast.FuncDecl():
            if decl.args is not None:
                params = [_explain_type(param) for param in decl.args.params]
                args = ", ".join(params)
            else:
                args = ""
            return f"function({args}) returning " + _explain_type(decl.type)
        case c_ast.Struct():
            decls = [_explain_decl_node(mem_decl) for mem_decl in decl.decls]
            members = ", ".join(decls)
            struct_name = f" {decl.name}" if decl.name else ""
            contents = f"containing {{{members}}}" if members else ""
            return f"struct{struct_name} " + contents
        case _:
            return ""


def expand_struct_typedef(
    cdecl: c_ast.Decl,
    file_ast: c_ast.FileAST,
    expand_struct: bool = False,
    expand_typedef: bool = False,
) -> c_ast.Decl:
    """Expand struct & typedef and return a new expanded node."""
    decl_copy = copy.deepcopy(cdecl)
    _expand_in_place(decl_copy, file_ast, expand_struct, expand_typedef)
    return decl_copy


def _expand_in_place(
    decl: c_ast.Node,
    file_ast: c_ast.FileAST,
    expand_struct: bool = False,
    expand_typedef: bool = False,
) -> c_ast.Node:
    """Recursively expand struct & typedef in place, throw RuntimeError if
    undeclared struct or typedef are used
    """
    match decl:
        case c_ast.Decl() | c_ast.TypeDecl() | c_ast.PtrDecl() | c_ast.ArrayDecl():
            decl.type = _expand_in_place(
                decl.type, file_ast, expand_struct, expand_typedef
            )
        case c_ast.Struct():
            if not decl.decls:
                struct = _find_struct(decl.name, file_ast)
                if struct is None:
                    raise RuntimeError(f"using undeclared struct {decl.name}")
                decl.decls = struct.decls

            for i, mem_decl in enumerate(decl.decls):
                decl.decls[i] = _expand_in_place(
                    mem_decl, file_ast, expand_struct, expand_typedef
                )
            if not expand_struct:
                decl.decls = []
        case c_ast.IdentifierType() if decl.names[0] not in ("int", "char"):
            typedef = _find_typedef(decl.names[0], file_ast)
            if typedef is None:
                raise RuntimeError(f"using undeclared type {decl.names[0]}")
            if expand_typedef:
                return typedef.type
        case _:
            pass

    return decl


def _find_struct(name: str, file_ast: c_ast.FileAST) -> Optional[c_ast.Struct]:
    """Receives a struct name and return declared struct object in file_ast"""
    for node in file_ast.ext:
        if isinstance(node, c_ast.Decl) and isinstance(node.type, c_ast.Struct):
            if node.type.name == name:
                return node.type
    return None


def _find_typedef(name: str, file_ast: c_ast.FileAST) -> Optional[c_ast.Typedef]:
    """Receives a type name and return typedef object in file_ast"""
    for node in file_ast.ext:
        if isinstance(node, c_ast.Typedef) and node.name == name:
            return node
    return None


if __name__ == "__main__":
    if len(sys.argv) > 1:
        c_decl = sys.argv[1]
    else:
        c_decl = "char *(*(**foo[][8])())[];"

    print("Explaining the declaration: " + c_decl + "\n")
    print(explain_c_declaration(c_decl) + "\n")
