# Copyright 2005-2020 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

from portage import os
from portage.exception import PortageException
from portage.cache.mappings import ProtectedDict
from portage.localization import _
from portage.util import writemsg


class InvalidModuleName(PortageException):
    """An invalid or unknown module name."""


class ModuleVersionError(PortageException):
    """An incompatible module version"""


class Module:
    """Class to define and hold our plug-in module

    @type name: string
    @param name: the module name
    @type path: the path to the new module
    """

    def __init__(self, name, namepath):
        """Some variables initialization"""
        self.name = name
        self._namepath = namepath
        self.kids_names = []
        self.kids = {}
        self.initialized = self._initialize()

    def _initialize(self):
        """Initialize the plug-in module

        @rtype: boolean
        """
        self.valid = False
        try:
            mod_name = ".".join([self._namepath, self.name])
            self._module = __import__(mod_name, [], [], ["not empty"])
            self.valid = True
        except ImportError as e:
            print("MODULE; failed import", mod_name, "  error was:", e)
            return False
        self.module_spec = self._module.module_spec
        for submodule in self.module_spec["provides"]:
            kid = self.module_spec["provides"][submodule]
            kidname = kid["name"]
            try:
                kid["module_name"] = ".".join([mod_name, kid["sourcefile"]])
            except KeyError:
                kid["module_name"] = ".".join([mod_name, self.name])
                writemsg(
                    _(
                        f"{self.name} module's module_spec is old, missing attribute: "
                        "'sourcefile'.  Backward compatibility may be "
                        f"removed in the future.\nFile: {self._module.__file__}\n"
                    )
                )
            kid["is_imported"] = False
            self.kids[kidname] = kid
            self.kids_names.append(kidname)
        return True

    def get_class(self, name):
        if not name or name not in self.kids_names:
            raise InvalidModuleName(
                f"Module name '{name}' is invalid or not"
                f"part of the module '{self.name}'"
            )
        kid = self.kids[name]
        if kid["is_imported"]:
            module = kid["instance"]
        else:
            try:
                module = __import__(kid["module_name"], [], [], ["not empty"])
                kid["instance"] = module
                kid["is_imported"] = True
            except ImportError:
                raise
        mod_class = getattr(module, kid["class"])
        return mod_class


class Modules:
    """Dynamic modules system for loading and retrieving any of the
    installed emaint modules and/or provided class's

    @param path: Path to the "modules" directory
    @param namepath: Python import path to the "modules" directory
    """

    def __init__(self, path, namepath, compat_versions=None):
        self._module_path = path
        self._namepath = namepath
        self.compat_versions = compat_versions
        self.parents = []
        self._modules = self._get_all_modules()
        self.modules = ProtectedDict(self._modules)
        self.module_names = sorted(self._modules)

    def _get_all_modules(self):
        """scans the _module_path dir for loadable modules

        @rtype: dictionary of module_plugins
        """
        module_dir = self._module_path
        names = os.listdir(module_dir)

        def _a_real_module(entry):
            try:
                # test for statinfo to ensure it should a real module
                # it will bail if it errors
                os.lstat(os.path.join(module_dir, entry, "__init__.py"))
            except OSError:
                return False
            return True

        # The importables list cannot be a generator.
        # If it was a generator, it would be consumed by self.parents.extend()
        # and the following for loop wouldn't have anything to iterate with.
        importables = [
            entry
            for entry in names
            if not entry.startswith("__") and _a_real_module(entry)
        ]
        self.parents.extend(importables)

        kids = {}
        for entry in importables:
            new_module = Module(entry, self._namepath)
            self._check_compat(new_module)
            for module_name in new_module.kids:
                kid = new_module.kids[module_name]
                kid["parent"] = new_module
                kids[kid["name"]] = kid
        return kids

    def get_module_names(self):
        """Convenience function to return the list of installed modules
        available

        @rtype: list
        @return: the installed module names available
        """
        return self.module_names

    def get_class(self, modname):
        """Retrieves a module class desired

        @type modname: string
        @param modname: the module class name
        """
        if modname and modname in self.module_names:
            mod = self._modules[modname]["parent"].get_class(modname)
        else:
            raise InvalidModuleName(f"Module name '{modname}' is invalid or not found")
        return mod

    def get_description(self, modname):
        """Retrieves the module class decription

        @type modname: string
        @param modname: the module class name
        @type string
        @return: the modules class decription
        """
        if modname and modname in self.module_names:
            mod = self._modules[modname]["description"]
        else:
            raise InvalidModuleName(f"Module name '{modname}' is invalid or not found")
        return mod

    def get_functions(self, modname):
        """Retrieves the module class  exported function names

        @type modname: string
        @param modname: the module class name
        @type list
        @return: the modules class exported function names
        """
        if modname and modname in self.module_names:
            mod = self._modules[modname]["functions"]
        else:
            raise InvalidModuleName(f"Module name '{modname}' is invalid or not found")
        return mod

    def get_func_descriptions(self, modname):
        """Retrieves the module class  exported functions descriptions

        @type modname: string
        @param modname: the module class name
        @type dictionary
        @return: the modules class exported functions descriptions
        """
        if modname and modname in self.module_names:
            desc = self._modules[modname]["func_desc"]
        else:
            raise InvalidModuleName(f"Module name '{modname}' is invalid or not found")
        return desc

    def get_opt_descriptions(self, modname):
        """Retrieves the module class exported options descriptions

        @type modname: string
        @param modname: the module class name
        @type dictionary
        @return: the modules class exported options descriptions
        """
        if modname and modname in self.module_names:
            desc = self._modules[modname].get("opt_desc")
        else:
            raise InvalidModuleName(f"Module name '{modname}' is invalid or not found")
        return desc

    def get_spec(self, modname, var):
        """Retrieves the module class exported spec variable

        @type modname: string
        @param modname: the module class name
        @type var: string
        @param var: the base level variable to return
        @type dictionary
        @return: the modules class exported options descriptions
        """
        if modname and modname in self.module_names:
            value = self._modules[modname].get(var, None)
        else:
            raise InvalidModuleName(f"Module name '{modname}' is invalid or not found")
        return value

    def _check_compat(self, module):
        if self.compat_versions:
            if not module.module_spec["version"] in self.compat_versions:
                raise ModuleVersionError(
                    f"Error loading '{self._namepath}' plugin module: {module.module_spec['name']}, version: {module.module_spec['version']}\n"
                    "Module is not compatible with the current application version\n"
                    f"Compatible module API versions are: {self.compat_versions}"
                )
