#     Copyright 2014, Kay Hayen, mailto:kay.hayen@gmail.com
#
#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
#
""" Nodes related to importing modules or names.

Normally imports are mostly relatively static, but Nuitka also attempts to
cover the uses of "__import__" builtin and other import techniques, that
allow dynamic values.

If other optimizations make it possible to predict these, the compiler can go
deeper that what it normally could. The import expression node can recurse. An
"__import__" builtin may be converted to it, once the module name becomes a
compile time constant.
"""

from .NodeBases import (
    ExpressionChildrenHavingBase,
    StatementChildrenHavingBase,
    ExpressionMixin,
    NodeBase
)

from .ConstantRefNodes import ExpressionConstantRef

from nuitka import Importing, Utils

from logging import warning

class ExpressionImportModule(NodeBase, ExpressionMixin):
    kind = "EXPRESSION_IMPORT_MODULE"

    # Set of modules, that we failed to import, and gave warning to the user
    # about it.
    _warned_about = set()

    def __init__(self, module_name, import_list, level, source_ref):
        NodeBase.__init__(
            self,
            source_ref = source_ref
        )

        self.module_name = module_name
        self.import_list = import_list
        self.level = level

        self.module = None

        self.attempted_recurse = False
        self.found_modules = ()

    def getDetails(self):
        return {
            "module_name" : self.module_name,
            "level"       : self.level
        }

    def getModuleName(self):
        return self.module_name

    def getImportList(self):
        return self.import_list

    def getLevel(self):
        if self.level == 0:
            if self.source_ref.getFutureSpec().isAbsoluteImport():
                return 0
            else:
                return -1
        else:
            return self.level

    def getModule(self):
        return self.module

    def setModule(self, module):
        # Modules have no parent.
        assert module.parent is None

        self.module = module

    def _consider(self, constraint_collection, module_filename, module_package):
        assert module_package is None or \
              (type(module_package) is str and module_package != "")

        module_filename = Utils.normpath(module_filename)

        if Utils.isDir(module_filename):
            module_name = Utils.basename(module_filename)
            module_kind = "py"
        elif module_filename.endswith(".py"):
            module_name = Utils.basename(module_filename)[:-3]
            module_kind = "py"
        elif module_filename.endswith(".so"):
            module_kind = "shlib"
            module_name = Utils.basename(module_filename)[:-3]
        elif module_filename.endswith(".pyd"):
            module_kind = "shlib"
            module_name = Utils.basename(module_filename)[:-4]
        else:
            module_kind = None
            module_name = None

        if module_kind is not None:
            from nuitka.tree import Recursion

            decision, reason = Recursion.decideRecursion(
                module_filename = module_filename,
                module_name     = module_name,
                module_package  = module_package,
                module_kind     = module_kind
            )

            if decision:
                module_relpath = Utils.relpath(module_filename)

                imported_module, added_flag = Recursion.recurseTo(
                    module_package  = module_package,
                    module_filename = module_filename,
                    module_relpath  = module_relpath,
                    module_kind     = module_kind,
                    reason          = reason
                )

                if added_flag:
                    constraint_collection.signalChange(
                        "new_code",
                        imported_module.getSourceReference(),
                        "Recursed to module."
                    )

                return imported_module
            elif decision is None and module_kind == "py":
                if module_package is None:
                    module_fullpath = module_name
                else:
                    module_fullpath = module_package + "." + module_name

                if module_filename not in self._warned_about:
                    self._warned_about.add( module_filename )

                    warning(
                        """\
Not recursing to '%(full_path)s' (%(filename)s), please specify \
--recurse-none (do not warn), \
--recurse-all (recurse to all), \
--recurse-not-to=%(full_path)s (ignore it), \
--recurse-to=%(full_path)s (recurse to it) to change.""" % {
                            "full_path" : module_fullpath,
                            "filename"  : module_filename
                        }
                    )

    def _attemptRecursion(self, constraint_collection):
        assert self.getModule() is None

        parent_module = self.getParentModule()

        if parent_module.isPythonPackage():
            parent_package = parent_module.getFullName()
        else:
            parent_package = self.getParentModule().getPackage()

        module_package, _module_name, module_filename = Importing.findModule(
            source_ref     = self.source_ref,
            module_name    = self.getModuleName(),
            parent_package = parent_package,
            level          = self.getLevel(),
            warn           = True
        )

        # That would be an illegal package name, catch it.
        assert module_package != ""

        if module_filename is not None:
            imported_module = self._consider(
                constraint_collection = constraint_collection,
                module_filename       = module_filename,
                module_package        = module_package
            )

            if imported_module is not None:
                self.setModule(imported_module)
                self.found_modules = []

                import_list = self.getImportList()

                if import_list and imported_module.isPythonPackage():
                    for import_item in import_list:

                        module_package, _module_name, module_filename = \
                          Importing.findModule(
                            source_ref     = self.source_ref,
                            module_name    = import_item,
                            parent_package = imported_module.getFullName(),
                            level          = -1,
                            warn           = False
                        )

                        if module_filename is not None:
                            sub_imported_module = self._consider(
                                constraint_collection = constraint_collection,
                                module_filename       = module_filename,
                                module_package        = module_package
                            )

                            if sub_imported_module is not None:
                                self.found_modules.append(sub_imported_module)


    def computeExpression(self, constraint_collection):
        # Attempt to recurse if not already done.
        if not self.attempted_recurse:
            self._attemptRecursion(
                constraint_collection = constraint_collection
            )

            self.attempted_recurse = True

        if self.getModule() is not None:
            from nuitka.ModuleRegistry import addUsedModule
            addUsedModule(self.getModule())

            for found_module in self.found_modules:
                addUsedModule(found_module)

        # TODO: May return a module reference of some sort in the future with
        # embedded modules.
        return self, None, None


class ExpressionBuiltinImport(ExpressionChildrenHavingBase):
    kind = "EXPRESSION_BUILTIN_IMPORT"

    named_children = (
        "import_name", "globals", "locals", "fromlist", "level"
    )

    def __init__(self, name, import_globals, import_locals, fromlist, level,
                source_ref):
        if fromlist is None:
            fromlist = ExpressionConstantRef(
                constant   = [],
                source_ref = source_ref
            )

        if level is None:
            level = 0 if source_ref.getFutureSpec().isAbsoluteImport() else -1

            level = ExpressionConstantRef(
                constant   = level,
                source_ref = source_ref
            )

        ExpressionChildrenHavingBase.__init__(
            self,
            values     = {
                "import_name" : name,
                "globals"     : import_globals,
                "locals"      : import_locals,
                "fromlist"    : fromlist,
                "level"       : level
            },
            source_ref = source_ref
        )

    getImportName = ExpressionChildrenHavingBase.childGetter("import_name")
    getFromList = ExpressionChildrenHavingBase.childGetter("fromlist")
    getGlobals = ExpressionChildrenHavingBase.childGetter("globals")
    getLocals = ExpressionChildrenHavingBase.childGetter("locals")
    getLevel = ExpressionChildrenHavingBase.childGetter("level")

    def computeExpression(self, constraint_collection):
        module_name = self.getImportName()
        fromlist = self.getFromList()
        level = self.getLevel()

        # TODO: In fact, if the module is not a package, we don't have to insist
        # on the fromlist that much, but normally it's not used for anything but
        # packages, so it will be rare.

        if module_name.isExpressionConstantRef() and \
           fromlist.isExpressionConstantRef() and \
           level.isExpressionConstantRef():
            new_node = ExpressionImportModule(
                module_name = module_name.getConstant(),
                import_list = fromlist.getConstant(),
                level       = level.getConstant(),
                source_ref  = self.getSourceReference()
            )

            return (
                new_node,
                "new_import",
                "Replaced __import__ call with module import expression."
            )

        # TODO: May return a module or module variable reference of some sort in
        # the future with embedded modules.
        return self, None, None


class StatementImportStar(StatementChildrenHavingBase):
    kind = "STATEMENT_IMPORT_STAR"

    named_children = ( "module", )

    def __init__(self, module_import, source_ref):
        StatementChildrenHavingBase.__init__(
            self,
            values     = {
                "module" : module_import
            },
            source_ref = source_ref
        )

    getModule = StatementChildrenHavingBase.childGetter( "module" )

    def computeStatement(self, constraint_collection):
        constraint_collection.onExpression( self.getModule() )

        # Need to invalidate everything, and everything could be assigned to
        # something else now.
        constraint_collection.removeAllKnowledge()

        return self, None, None


class ExpressionImportName(ExpressionChildrenHavingBase):
    kind = "EXPRESSION_IMPORT_NAME"

    named_children = ( "module", )

    def __init__(self, module, import_name, source_ref):
        ExpressionChildrenHavingBase.__init__(
            self,
            values     = {
                "module" : module
            },
            source_ref = source_ref
        )

        self.import_name = import_name

    def getImportName(self):
        return self.import_name

    def getDetails(self):
        return { "import_name" : self.getImportName() }

    def getDetail(self):
        return "import %s from %s" % ( self.getImportName(), self.getModule() )

    getModule = ExpressionChildrenHavingBase.childGetter( "module" )

    def computeExpression(self, constraint_collection):
        # TODO: May return a module or module variable reference of some sort in
        # the future with embedded modules.
        return self, None, None
