#!/usr/bin/env python3

"""
DataCollectingParser subclasses ctypesparser.CtypesParser and builds Description
objects from the CtypesType objects and other information from CtypesParser.
After parsing is complete, a DescriptionCollection object can be retrieved by
calling DataCollectingParser.data().
"""
from __future__ import print_function


import os
from tempfile import mkstemp

from . import ctypesparser
from ctypesgencore.ctypedescs import *
from ctypesgencore.descriptions import *
from ctypesgencore.expressions import *
from ctypesgencore.messages import *


class DataCollectingParser(ctypesparser.CtypesParser,
                           ctypesparser.CtypesTypeVisitor):
    """Main class for the Parser component. Steps for use:
    p=DataCollectingParser(names_of_header_files,options)
    p.parse()
    data=p.data() #A dictionary of constants, enums, structs, functions, etc.
    """

    def __init__(self, headers, options):
        ctypesparser.CtypesParser.__init__(self, options)
        self.headers = headers
        self.options = options

        self.constants = []
        self.typedefs = []
        self.structs = []
        self.enums = []
        self.functions = []
        self.variables = []
        self.macros = []

        self.all = []
        self.output_order = []

        # NULL is a useful macro to have defined
        null = ConstantExpressionNode(None)
        nullmacro = ConstantDescription("NULL", null, ("<built-in>", 1))
        self.constants.append(nullmacro)
        self.all.append(nullmacro)
        self.output_order.append(("constant", nullmacro))

        # A list of tuples describing macros; saved to be processed after
        # everything else has been parsed
        self.saved_macros = []
        # A set of structs that are already known
        self.already_seen_structs = set()
        # A dict of structs that have only been seen in opaque form
        self.already_seen_opaque_structs = {}
        # A set of enums that are already known
        self.already_seen_enums = set()
        # A dict of enums that have only been seen in opaque form
        self.already_seen_opaque_enums = {}

    def parse(self):
        fd, fname = mkstemp(suffix=".h")
        f = os.fdopen(fd, 'w')
        for header in self.options.other_headers:
            print('#include <%s>' % header, file=f)
        for header in self.headers:
            print('#include "%s"' % os.path.abspath(header), file=f)
        f.flush()
        f.close()
        ctypesparser.CtypesParser.parse(self, fname, False)
        os.remove(fname)

        for name, params, expr, (filename, lineno) in self.saved_macros:
            self.handle_macro(name, params, expr, filename, lineno)

    def handle_define_constant(self, name, expr, filename, lineno):
        # Called by CParser
        # Save to handle later
        self.saved_macros.append((name, None, expr, (filename, lineno)))

    def handle_define_unparseable(self, name, params, value, filename, lineno):
        # Called by CParser
        if params:
            original_string = "#define %s(%s) %s" % \
                (name, ",".join(params), " ".join(value))
        else:
            original_string = "#define %s %s" % \
                (name, " ".join(value))
        macro = MacroDescription(name, params, None,
                                 src=(filename, lineno))
        macro.error("Could not parse macro \"%s\"" % original_string,
                    cls='macro')
        macro.original_string = original_string
        self.macros.append(macro)
        self.all.append(macro)
        self.output_order.append(('macro', macro))

    def handle_define_macro(self, name, params, expr, filename, lineno):
        # Called by CParser
        # Save to handle later
        self.saved_macros.append((name, params, expr, (filename, lineno)))

    def handle_ctypes_typedef(self, name, ctype, filename, lineno):
        # Called by CtypesParser
        ctype.visit(self)

        typedef = TypedefDescription(name,
                                     ctype,
                                     src=(filename, repr(lineno)))

        self.typedefs.append(typedef)
        self.all.append(typedef)
        self.output_order.append(('typedef', typedef))

    def handle_ctypes_new_type(self, ctype, filename, lineno):
        # Called by CtypesParser
        if isinstance(ctype, ctypesparser.CtypesEnum):
            self.handle_enum(ctype, filename, lineno)
        else:
            self.handle_struct(ctype, filename, lineno)

    def handle_ctypes_function(self, name, restype, argtypes, errcheck,
                               variadic, filename, lineno):
        # Called by CtypesParser
        restype.visit(self)
        for argtype in argtypes:
            argtype.visit(self)

        function = FunctionDescription(name,
                                       restype,
                                       argtypes,
                                       errcheck,
                                       variadic=variadic,
                                       src=(filename, repr(lineno)))

        self.functions.append(function)
        self.all.append(function)
        self.output_order.append(('function', function))

    def handle_ctypes_variable(self, name, ctype, filename, lineno):
        # Called by CtypesParser
        ctype.visit(self)

        variable = VariableDescription(name,
                                       ctype,
                                       src=(filename, repr(lineno)))

        self.variables.append(variable)
        self.all.append(variable)
        self.output_order.append(('variable', variable))

    def handle_struct(self, ctypestruct, filename, lineno):
        # Called from within DataCollectingParser

        # When we find an opaque struct, we make a StructDescription for it
        # and record it in self.already_seen_opaque_structs. If we later
        # find a transparent struct with the same tag, we fill in the
        # opaque struct with the information from the transparent struct and
        # move the opaque struct to the end of the struct list.

        name = "%s %s" % (ctypestruct.variety, ctypestruct.tag)

        if name in self.already_seen_structs:
            return

        if ctypestruct.opaque:
            if name not in self.already_seen_opaque_structs:
                struct = StructDescription(ctypestruct.tag,
                                           ctypestruct.packed,
                                           ctypestruct.variety,
                                           None,  # No members
                                           True,  # Opaque
                                           ctypestruct,
                                           src=(filename, str(lineno)))

                self.already_seen_opaque_structs[name] = struct
                self.structs.append(struct)
                self.all.append(struct)
                self.output_order.append(('struct', struct))

        else:
            for (membername, ctype) in ctypestruct.members:
                ctype.visit(self)

            if name in self.already_seen_opaque_structs:
                # Fill in older version
                struct = self.already_seen_opaque_structs[name]
                struct.opaque = False
                struct.members = ctypestruct.members
                struct.ctype = ctypestruct
                struct.src = ctypestruct.src

                self.output_order.append(('struct-body', struct))

                del self.already_seen_opaque_structs[name]

            else:
                struct = StructDescription(ctypestruct.tag,
                                           ctypestruct.packed,
                                           ctypestruct.variety,
                                           ctypestruct.members,
                                           False,  # Not opaque
                                           src=(filename, str(lineno)),
                                           ctype=ctypestruct)
                self.structs.append(struct)
                self.all.append(struct)
                self.output_order.append(('struct', struct))
                self.output_order.append(('struct-body', struct))

            self.already_seen_structs.add(name)

    def handle_enum(self, ctypeenum, filename, lineno):
        # Called from within DataCollectingParser.

        # Process for handling opaque enums is the same as process for opaque
        # structs. See handle_struct() for more details.

        tag = ctypeenum.tag
        if tag in self.already_seen_enums:
            return

        if ctypeenum.opaque:
            if tag not in self.already_seen_opaque_enums:
                enum = EnumDescription(ctypeenum.tag,
                                       None,
                                       ctypeenum,
                                       src=(filename, str(lineno)))
                enum.opaque = True

                self.already_seen_opaque_enums[tag] = enum
                self.enums.append(enum)
                self.all.append(enum)
                self.output_order.append(('enum', enum))

        else:
            if tag in self.already_seen_opaque_enums:
                # Fill in older opaque version
                enum = self.already_seen_opaque_enums[tag]
                enum.opaque = False
                enum.ctype = ctypeenum
                enum.src = ctypeenum.src
                enum.members = ctypeenum.enumerators

                del self.already_seen_opaque_enums[tag]

            else:
                enum = EnumDescription(ctypeenum.tag,
                                       ctypeenum.enumerators,
                                       src=(filename, str(lineno)),
                                       ctype=ctypeenum)
                enum.opaque = False

                self.enums.append(enum)
                self.all.append(enum)
                self.output_order.append(('enum', enum))

            self.already_seen_enums.add(tag)

            for (enumname, expr) in ctypeenum.enumerators:
                constant = ConstantDescription(enumname, expr,
                                               src=(filename, lineno))

                self.constants.append(constant)
                self.all.append(constant)
                self.output_order.append(('constant', constant))

    def handle_macro(self, name, params, expr, filename, lineno):
        # Called from within DataCollectingParser
        src = (filename, lineno)

        if expr is None:
            expr = ConstantExpressionNode(True)
            constant = ConstantDescription(name, expr, src)
            self.constants.append(constant)
            self.all.append(constant)
            return

        expr.visit(self)

        if isinstance(expr, CtypesType):
            if params:
                macro = MacroDescription(name, "", src)
                macro.error("%s has parameters but evaluates to a type. "
                            "Ctypesgen does not support it." % macro.casual_name(),
                            cls='macro')
                self.macros.append(macro)
                self.all.append(macro)
                self.output_order.append(('macro', macro))

            else:
                typedef = TypedefDescription(name, expr, src)
                self.typedefs.append(typedef)
                self.all.append(typedef)
                self.output_order.append(('typedef', typedef))

        else:
            macro = MacroDescription(name, params, expr, src)
            self.macros.append(macro)
            self.all.append(macro)
            self.output_order.append(('macro', macro))

        # Macros could possibly contain things like __FILE__, __LINE__, etc...
        # This could be supported, but it would be a lot of work. It would
        # probably also bloat the Preamble considerably.

    def handle_error(self, message, filename, lineno):
        # Called by CParser
        error_message("%s:%d: %s" % (filename, lineno, message), cls='cparser')

    def handle_pp_error(self, message):
        # Called by PreprocessorParser
        error_message("%s: %s" % (self.options.cpp, message), cls='cparser')

    def handle_status(self, message):
        # Called by CParser
        status_message(message)

    def visit_struct(self, struct):
        self.handle_struct(struct, struct.src[0], struct.src[1])

    def visit_enum(self, enum):
        self.handle_enum(enum, enum.src[0], enum.src[1])

    def data(self):
        return DescriptionCollection(self.constants,
                                     self.typedefs,
                                     self.structs,
                                     self.enums,
                                     self.functions,
                                     self.variables,
                                     self.macros,
                                     self.all,
                                     self.output_order)
