# ===============================================================================
# NAME: SerialCppVisitor.py
#
# DESCRIPTION: A visitor responsible for the generation of each serializable
#              class source code file.
#
# AUTHOR: reder
# EMAIL:  reder@jpl.nasa.gov
# DATE CREATED  : June 4, 2013
#
# Copyright 2013, California Institute of Technology.
# ALL RIGHTS RESERVED. U.S. Government Sponsorship acknowledged.
# ===============================================================================
#
# Python standard modules
#
import logging
import sys

from fprime_ac.generators import formatters

# from fprime_ac.utils import DiffAndRename
from fprime_ac.generators.visitors import AbstractVisitor

#
# Python extension modules and custom interfaces
#
# from Cheetah import Template
# from fprime_ac.utils import version
from fprime_ac.utils import ConfigManager

#
# Import precompiled templates here
#
try:
    from fprime_ac.generators.templates.serialize import includes1SerialCpp
    from fprime_ac.generators.templates.serialize import namespaceSerialCpp
    from fprime_ac.generators.templates.serialize import publicSerialCpp

    # from fprime_ac.generators.templates import privateSerialCpp
    from fprime_ac.generators.templates.serialize import finishSerialCpp
except ImportError:
    print("ERROR: must generate python templates first.")
    sys.exit(-1)

#
# Universal globals used within module go here.
# (DO NOT USE MANY!)
#
# Global logger init. below.
PRINT = logging.getLogger("output")
DEBUG = logging.getLogger("debug")

typelist = ["U8", "I8", "U16", "I16", "U32", "I32", "U64", "I64", "F32", "F64", "bool"]

#
# Module class or classes go here.
class SerialCppVisitor(AbstractVisitor.AbstractVisitor):
    """
    A visitor class responsible for generation of component header
    classes in C++.
    """

    __instance = None
    __config = None
    __fp = None
    __form = None
    __form_comment = None

    def __init__(self):
        """
        Constructor.
        """
        super().__init__()
        self.__config = ConfigManager.ConfigManager.getInstance()
        self.__form = formatters.Formatters()
        self.__form_comment = formatters.CommentFormatters()
        DEBUG.info("SerialCppVisitor: Instanced.")
        self.bodytext = ""
        self.prototypetext = ""

    def _get_args_proto_string(self, obj):
        """
        Return a string of (type, name) args, comma separated
        for use in templates that generate prototypes.
        """
        arg_str = ""
        for (name, mtype, size, format, comment) in obj.get_members():
            if isinstance(mtype, tuple):
                arg_str += "{} {}, ".format(mtype[0][1], name)
            elif mtype == "string":
                arg_str += "const {}::{}String& {}, ".format(obj.get_name(), name, name)
            elif mtype not in typelist:
                arg_str += "const {}& {}, ".format(mtype, name)
            elif size is not None:
                arg_str += "const {}* {}, ".format(mtype, name)
                arg_str += "NATIVE_INT_TYPE %sSize, " % (name)
            else:
                arg_str += "{} {}".format(mtype, name)
                arg_str += ", "

        arg_str = arg_str.strip(", ")
        return arg_str

    def _get_args_string(self, obj, prefix=""):
        """
        Return a string of name args, comma separated
        for use in templates that generate method or function calls.
        """
        args = obj.get_members()
        arg_str = ""
        for arg in args:
            if arg[2] is not None and arg[1] != "string":
                arg_str += prefix + "%s" % arg[0]
                arg_str += ", "
                arg_str += "%s" % arg[2]
                arg_str += ", "
            else:
                arg_str += prefix + "%s" % arg[0]
                arg_str += ", "

        arg_str = arg_str.strip(", ")
        return arg_str

    def _get_conv_mem_list(self, obj):
        """
        Return a list of struct member tuples
        """
        arg_list = list()

        for (name, mtype, size, format, comment) in obj.get_members():
            typeinfo = None
            if isinstance(mtype, tuple):
                mtype = mtype[0][1]
                typeinfo = "enum"
            elif mtype == "string":
                mtype = "{}::{}String".format(obj.get_name(), name)
                typeinfo = "string"
            elif mtype not in typelist:
                typeinfo = "extern"

            arg_list.append((name, mtype, size, format, comment, typeinfo))

        return arg_list

    def _writeTmpl(self, c, visit_str):
        """
        Wrapper to write tmpl to files desc.
        """
        DEBUG.debug("SerialCppVisitor:%s" % visit_str)
        DEBUG.debug("===================================")
        DEBUG.debug(c)
        self.__fp.writelines(c.__str__())
        DEBUG.debug("===================================")

    def initFilesVisit(self, obj):
        """
        Defined to generate files for generated code products.
        @param args: the instance of the concrete element to operation on.
        """
        # Build filename here...
        if self.__config.get("serialize", "XMLDefaultFileName") == "True":
            namespace = "".join(obj.get_namespace().split("::"))
            filename = (
                namespace
                + obj.get_name()
                + self.__config.get("serialize", "SerializableCpp")
            )
            PRINT.info(
                "Generating code filename: %s, using XML namespace and name attributes..."
                % filename
            )
        else:
            xml_file = obj.get_xml_filename()
            x = xml_file.split(".")
            s = self.__config.get("serialize", "SerializableXML").split(".")
            l = len(s[0])
            #
            if (x[0][-l:] == s[0]) & (x[1] == s[1]):
                filename = x[0].split(s[0])[0] + self.__config.get(
                    "serialize", "SerializableCpp"
                )
                PRINT.info(
                    "Generating code filename: %s, using default XML filename prefix..."
                    % filename
                )
            else:
                msg = (
                    "XML file naming format not allowed (must be XXXSerializableAi.xml), Filename: %s"
                    % xml_file
                )
                PRINT.info(msg)
                sys.exit(-1)

        # Open file for writing here...
        DEBUG.info("Open file: %s" % filename)
        self.__fp = open(filename, "w")
        if self.__fp is None:
            raise Exception("Could not open %s file.") % filename
        DEBUG.info("Completed")

    def startSourceFilesVisit(self, obj):
        """
        Defined to generate starting static code within files.
        """

    def includes1Visit(self, obj):
        """
        Defined to generate includes within a file.
        Usually used for the base classes but also for Serial types
        @param args: the instance of the concrete element to operation on.
        """
        relative_path = self.relativePath()
        #
        DEBUG.debug("Relative path: %s", relative_path)
        #
        c = includes1SerialCpp.includes1SerialCpp()
        c.name = obj.get_name()
        #
        # Hack to fix the include file so it is consistent...
        if self.__config.get("serialize", "XMLDefaultFileName") == "False":
            c.name = obj.get_xml_filename().split("SerializableAi.xml")[0]
            c.namespace = ""
        #
        # Added configurable override for includes for testing
        #
        if self.__config.get("includes", "serial_include_path") == "None":
            if relative_path is not None:
                c.include_path = relative_path
            else:
                c.include_path = obj.get_namespace()
        else:
            c.include_path = self.__config.get("includes", "serial_include_path")
        self._writeTmpl(c, "includes1Visit")

    def includes2Visit(self, obj):
        """
        Defined to generate internal includes within a file.
        Usually used for data type includes and system includes.
        @param args: the instance of the concrete element to operation on.
        """

    def namespaceVisit(self, obj):
        """
        Defined to generate namespace code within a file.
        Also any pre-condition code is generated.
        @param args: the instance of the concrete element to operation on.
        """
        c = namespaceSerialCpp.namespaceSerialCpp()
        if obj.get_namespace() is None:
            c.namespace_list = None
        else:
            c.namespace_list = obj.get_namespace().split("::")
        c.name = obj.get_name()
        c.mem_list = obj.get_members()

        self._writeTmpl(c, "namespaceVisit")

    def publicVisit(self, obj):
        """
        Defined to generate public stuff within a class.
        @param args: the instance of the concrete element to operation on.
        """
        c = publicSerialCpp.publicSerialCpp()
        c.name = obj.get_name()
        c.args_proto_string = self._get_args_proto_string(obj)
        c.args_string = self._get_args_string(obj)
        c.args_mstring = self._get_args_string(obj, "src.m_")
        c.args_mstring_ptr = self._get_args_string(obj, "src->m_")
        c.members = self._get_conv_mem_list(obj)
        self._writeTmpl(c, "publicVisit")

    def protectedVisit(self, obj):
        """
        Defined to generate protected stuff within a class.
        @param args: the instance of the concrete element to operation on.
        """

    def privateVisit(self, obj):
        """
        Defined to generate private stuff within a class.
        @param args: the instance of the concrete element to operation on.
        """

    def finishSourceFilesVisit(self, obj):
        """
        Defined to generate ending static code within files.
        """
        c = finishSerialCpp.finishSerialCpp()
        if obj.get_namespace() is None:
            c.namespace_list = None
        else:
            c.namespace_list = obj.get_namespace().split("::")
        self._writeTmpl(c, "finishSourceFilesVisit")
        self.__fp.close()
