# Copyright 2014-2014 the openage authors. See copying.md for legal info.

from string import Template
import os.path

from .content_snippet import ContentSnippet
from .header_snippet import HeaderSnippet
from openage.log import dbg

class GeneratedFile:
    """
    represents a writable file that was generated automatically.

    it's filled by many ContentSnippets before its contents are generated.
    """

    namespace = "gamedata"

    @classmethod
    def namespacify(cls, var_type):
        return "%s::%s" % (cls.namespace, var_type)

    #inserted warning message for generated files
    dontedit = """
//do not edit this file, it's autogenerated. all changes will be undone!
//make changes in the convert script and regenerate the files."""

    #default preferences for output modes
    default_preferences = {
        "folder":         "",
        "file_suffix":    "",
        "content_prefix": Template(""),
        "content_suffix": Template(""),
    }


    #override the default preferences with the
    #configuration for all the output formats
    output_preferences = {
        "csv": {
            "folder":      "",
            "file_suffix": ".docx",
        },
        "struct": {
            "file_suffix": ".gen.h",
            "content_prefix": Template("""#ifndef OPENAGE_${header_guard}_GEN_H_
#define OPENAGE_${header_guard}_GEN_H_

${headers}
%s

namespace ${namespace} {

""" % dontedit),
            "content_suffix": Template("""
} //namespace ${namespace}

#endif
"""),
        },
        "structimpl": {
            "file_suffix":    ".gen.cpp",
            "content_prefix": Template("""
${headers}
%s

namespace ${namespace} {

""" % dontedit),
            "content_suffix": Template("} //namespace ${namespace}\n"),
        }
    }


    def __init__(self, file_name, format):
        self.snippets  = set()
        self.typedefs  = set()
        self.typerefs  = set()
        self.file_name = file_name
        self.format    = format
        self.included_typedefs = set()

    def add_snippet(self, snippet, inherit_typedefs=True):
        if not isinstance(snippet, ContentSnippet):
            raise Exception("only ContentSnippets can be added to generated files, tried %s" % type(snippet))

        if not snippet.file_name == self.file_name and snippet.file_name != True:
            raise Exception("only snippets with the same target file_name can be put into the same generated file.")

        if snippet not in (self.snippets):
            self.snippets.add(snippet)

            if inherit_typedefs:
                self.typedefs |= snippet.typedefs
                self.typerefs |= snippet.typerefs
            else:
                self.included_typedefs |= snippet.typedefs

            dbg(lazymsg=lambda: "adding snippet to %s:" % (repr(self)), lvl=2)
            dbg(lazymsg=lambda: " %s"                   % repr(snippet), lvl=2)
            dbg(lazymsg=lambda: " `- typedefs:  %s"     % snippet.typedefs, lvl=3)
            dbg(lazymsg=lambda: " `- typerefs:  %s"     % snippet.typerefs, lvl=3)
            dbg(lazymsg=lambda: " `- includes:  %s {"   % repr(snippet.includes), push="snippet_add", lvl=3)

            #add all included snippets, namely HeaderSnippets for #include lol.h
            for s in snippet.includes:
                self.add_snippet(s, inherit_typedefs=False)

            dbg(pop="snippet_add", lazymsg=lambda: "}", lvl=3)
        else:
            dbg(lazymsg=lambda: "skipping already present snippet %s" % (repr(snippet)), lvl=2)

    def get_include_snippet(self, file_name=True):
        """
        return a snippet with a header entry for this file to be able to include it.
        """

        ret = HeaderSnippet(
            self.file_name + self.output_preferences[self.format]["file_suffix"],
            file_name=file_name,
            is_global=False,
        )

        ret.typedefs |= self.typedefs
        return ret

    def create_xref_headers(self, file_pool):
        """
        discover and add needed header snippets for type references accross files.
        """

        dbg("%s typerefs %s" % (repr(self), repr(self.typerefs)), lvl=3)
        dbg("%s typedefs %s" % (repr(self), repr(self.typedefs)), lvl=3)

        new_resolves = set()
        for include_candidate in file_pool:
            candidate_resolves = include_candidate.typedefs & (self.typerefs - self.typedefs)

            if len(candidate_resolves) > 0:
                new_header = include_candidate.get_include_snippet()

                dbg(lazymsg=lambda: "%s: to resolve %s" % (repr(self), candidate_resolves), push="add_header", lvl=3)
                self.add_snippet(new_header, inherit_typedefs=False)
                dbg(pop="add_header")

                new_resolves |= candidate_resolves

        still_missing = ((self.typerefs - self.typedefs) - self.included_typedefs) - new_resolves
        if len(still_missing) > 0:
            raise Exception("still missing types for %s:\n%s" % (self, still_missing))

    def create_forward_declarations(self, file_pool):
        """
        create forward declarations for this generated file.

        a forward declatation is needed when a referenced type is defined
        in an included header, that includes a header that includes the first one.
        """

        pass

    def generate(self):
        """
        actually generate the content for this file.
        """

        #TODO: create new snippets for resolving cyclic dependencies (forward declarations)

        dbg(push="generation", lvl=2)

        dbg(lazymsg=lambda: "".join((
            "\n=========== generating %s\n" % (repr(self)),
            "content snippets stored to be inserted:\n",
            pprint.pformat(self.snippets),
            "\n-----------",
        )), lvl=3)

        #apply preference overrides
        prefs = self.default_preferences.copy()
        prefs.update(self.output_preferences[self.format])

        snippets_header = {s for s in self.snippets if s.section == ContentSnippet.section_header}
        snippets_body   = self.snippets - snippets_header

        if len(snippets_body) == 0:
            raise Exception("generated file %s has no body snippets!" % (repr(self)))

        #type references in this file that could not be resolved
        missing_types = set()

        #put snippets into list in correct order
        #snippets will be written according to this [(snippet, prio), ...] list
        snippets_priorized = list()

        #determine each snippet's priority by number of type references and definitions
        #smaller prio means written earlier in the file.
        #also, find snippet dependencies
        dbg("assigning snippet priorities:", push="snippetprio", lvl=4)
        for s in sorted(snippets_body):
            snippet_prio = len(s.typerefs) - len(s.typedefs)
            snippets_priorized.append((s, snippet_prio))
            dbg(lazymsg=lambda: "prio %3.d => %s" % (snippet_prio, repr(s)), lvl=4)

            #let each snippet find others as dependencies
            missing_types |= s.add_required_snippets(self.snippets)

        dbg(pop="snippetprio")

        if len(missing_types) > 0:
            raise Exception("missing types for %s:\n%s" % (repr(self), pprint.pformat(missing_types)))

        #sort snippets according to their priority determined above
        snippets_priorized_sorted = sorted(snippets_priorized, key=lambda s: s[1])

        #create list of snippets to be put in the generated file.
        #[(snippet, prio)]
        snippets_body_sorted = list()
        snippets_body_set = set()

        #fetch list of all required snippets for all snippets to put in the file
        for snippet, prio in snippets_priorized_sorted:
            snippet_candidates = snippet.get_required_snippets()

            dbg(lazymsg=lambda: "required dependency snippet candidates: %s" % (pprint.pformat(snippet_candidates)), lvl=3)
            for s in snippet_candidates:
                if s.section == ContentSnippet.section_header:
                    if s not in snippets_header:
                        dbg(lazymsg=lambda: " `-> ADD  header snippet %s" % (repr(s)), lvl=4)
                        snippets_header.add(s)
                        continue

                elif s.section == ContentSnippet.section_body:
                    if s not in snippets_body_set:
                        snippets_body_sorted.append(s)
                        snippets_body_set.add(s)
                        dbg(lazymsg=lambda: " `-> ADD  body snippet %s" % (repr(s)), lvl=4)
                        continue

                dbg(lazymsg=lambda: " `-> SKIP snippet %s" % (repr(s)), lvl=4)


        #these snippets will be written outside the namespace
        #in the #include section
        snippets_header_sorted = sorted(snippets_header, key=lambda h: (not h.is_global, h.name))

        dbg(lazymsg=lambda: "".join((
            "\n-----------\n",
            "snippets after ordering for %s:\n" % (repr(self)),
            pprint.pformat(snippets_header_sorted + snippets_body_sorted),
            "\n===========",
        )), lvl=3)

        #merge file contents
        header_data = "".join(header.get_data() for header in snippets_header_sorted)
        file_data   = "\n".join(snippet.get_data() for snippet in snippets_body_sorted)

        namespace    = self.namespace
        header_guard = "".join((namespace.upper(), "_", self.file_name.replace("/", "_").upper()))

        #fill file header and footer with the generated file_name
        content_prefix = prefs["content_prefix"].substitute(header_guard=header_guard, namespace=namespace, headers=header_data)
        content_suffix = prefs["content_suffix"].substitute(header_guard=header_guard, namespace=namespace)

        #this is the final file content
        file_data = "".join((content_prefix, file_data, content_suffix))

        #determine output file name
        output_file_name_parts = [
            prefs["folder"],
            "%s%s" % (self.file_name, prefs["file_suffix"])
        ]

        dbg(pop="generation")

        #whee, return the (file_name, content)
        return (os.path.join(*output_file_name_parts), file_data)

    def __repr__(self):
        return "GeneratedFile<%s>(file_name=%s)" % (self.format, self.file_name)
