# -*- coding: utf-8 -*-
#    Molecule Disc Image builder for Sabayon Linux
#    Copyright (C) 2009 Fabio Erculiani
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
import sys
import os
import inspect

class PluginFactory:

    """
    Generic Molecule Components Plugin Factory (loader).
    """

    _PLUGIN_SUFFIX = "_plugin"
    _PYTHON_EXTENSION = ".py"

    def __init__(self, base_plugin_class, plugin_package_module,
        default_plugin_name = None, fallback_plugin_name = None,
        egg_entry_point_group = None):
        """
        Molecule Generic Plugin Factory constructor.
        MANDATORY: every plugin module/package(name) must end with _plugin
        suffix.

        Base plugin classes must have the following class attributes set:

            - BASE_PLUGIN_API_VERSION: integer describing API revision in use
              in class

        Subclasses of Base plugin class must have the following class
        attributes set:

            - PLUGIN_API_VERSION: integer describing the currently implemented
              plugin API revision, must match with BASE_PLUGIN_API_VERSION
              above otherwise plugin won't be loaded and a warning will be
              printed.

        Moreover, plugin classes must be "Python new-style classes", otherwise
        parser won't be able to determine if classes have subclasses and thus
        pick the proper object (one with no subclasses!!).
        See: http://www.python.org/doc/newstyle -- in other words, you have
        to inherit the built-in "object" class (yeah, it's called object).
        So, even if using normal classes could work, if you start doing nasty
        things (nested inherittance of plugin classes), behaviour cannot
        be guaranteed.
        If it's not clear, let me repeat once again, valid plugin classes
        must not have subclasses around! Think about it, it's an obvious thing.

        If plugin class features a "PLUGIN_DISABLED" class attribute with
        a boolean value of True, such plugin will be ignored.

        If egg_entry_point_group is specified, Python Egg support is enabled
        and classes are loaded via this infrastructure.
        NOTE: if egg_entry_point_group is set, you NEED the setuptools package.

        @param base_plugin_class: Base class that valid plugin classes must
            inherit from.
        @type base_plugin_class: class
        @param plugin_package_module: every plugin repository must work as
            Python package, the value of this argument must be a valid
            Python package module that can be scanned looking for valid
            Molecule Plugin classes.
        @type plugin_package_module: Python module
        @keyword default_plugin_name: identifier of the default plugin to load
        @type default_plugin_name: string
        @keyword fallback_plugin_name: identifier of the fallback plugin to load
            if default is not available
        @type fallback_plugin_name: string
        @keyword egg_entry_point_group: valid Python Egg entry point group, in
            this case, Python Egg support is used
        @type egg_entry_point_group: string
        @raise AttributeError: when passed plugin_package_module is not a
            valid Python package module
        """
        self.__modfile = plugin_package_module.__file__
        self.__base_class = base_plugin_class
        self.__plugin_package_module = plugin_package_module
        self.__default_plugin_name = default_plugin_name
        self.__fallback_plugin_name = fallback_plugin_name
        self.__egg_entry_group = egg_entry_point_group
        self.__cache = None
        self.__inspect_cache = None

    def clear_cache(self):
        """
        Clear available plugins cache. When calling get_available_plugins()
        module object is parsed again.
        """
        self.__cache = None
        self.__inspect_cache = None

    def _inspect_object(self, obj):
        """
        This method verifies if given object is a valid plugin.

        @return: True, if valid
        @rtype: bool
        """

        if self.__inspect_cache is None:
            self.__inspect_cache = {}

        # use memory position
        obj_memory_pos = id(obj)
        # To avoid infinite recursion and improve
        # objects inspection, check if obj has been already
        # analyzed
        inspected_rc = self.__inspect_cache.get(obj_memory_pos)
        if inspected_rc is not None:
            # avoid infinite recursion
            return inspected_rc

        base_api = self.__base_class.BASE_PLUGIN_API_VERSION

        if not inspect.isclass(obj):
            self.__inspect_cache[obj_memory_pos] = False
            return False

        if not issubclass(obj, self.__base_class):
            self.__inspect_cache[obj_memory_pos] = False
            return False

        if hasattr(obj, '__subclasses__'):
            # new style class
            if obj.__subclasses__(): # only lower classes taken
                self.__inspect_cache[obj_memory_pos] = False
                return False
        else:
            sys.stderr.write("!!! Molecule Plugin warning: " \
                "%s is not a new style class !!!\n" % (obj,))

        if obj is self.__base_class:
            # in this case, obj is our base class,
            # so we are very sure that obj is not valid
            self.__inspect_cache[obj_memory_pos] = False
            return False

        if not hasattr(obj, "PLUGIN_API_VERSION"):
            sys.stderr.write("!!! Molecule Plugin warning: " \
                "no PLUGIN_API_VERSION in %s !!!\n" % (obj,))
            self.__inspect_cache[obj_memory_pos] = False
            return False

        if obj.PLUGIN_API_VERSION != base_api:
            sys.stderr.write("!!! Molecule Plugin warning: " \
                "PLUGIN_API_VERSION mismatch in %s !!!\n" % (obj,))
            self.__inspect_cache[obj_memory_pos] = False
            return False

        if hasattr(obj, 'PLUGIN_DISABLED'):
            if obj.PLUGIN_DISABLED:
                # this plugin has been disabled
                self.__inspect_cache[obj_memory_pos] = False
                return False

        self.__inspect_cache[obj_memory_pos] = True
        return True

    def _scan_dir(self):
        """
        Scan modules in given directory looking for a valid plugin class.
        Directory is os.path.dirname(self.__modfile).

        @return: module dictionary composed by module name as key and plugin
            class as value
        @rtype: dict
        """
        available = {}
        pkg_modname = self.__plugin_package_module.__name__
        mod_dir = os.path.dirname(self.__modfile)

        for modname in os.listdir(mod_dir):

            if modname.startswith("__"):
                continue # python stuff
            if not (modname.endswith(PluginFactory._PYTHON_EXTENSION) \
                or "." not in modname):
                continue # not something we want to load

            if modname.endswith(PluginFactory._PYTHON_EXTENSION):
                modname = modname[:-len(PluginFactory._PYTHON_EXTENSION)]

            if not modname.endswith(PluginFactory._PLUGIN_SUFFIX):
                continue

            # remove suffix
            modname_clean = modname[:-len(PluginFactory._PLUGIN_SUFFIX)]

            modpath = "%s.%s" % (pkg_modname, modname,)

            try:
                __import__(modpath)
            except ImportError as err:
                sys.stderr.write("!!! Molecule Plugin warning, cannot " \
                    "load module: %s | %s !!!\n" % (modpath, err,))
                continue

            for obj in list(sys.modules[modpath].__dict__.values()):

                valid = self._inspect_object(obj)
                if not valid:
                    continue

                available[modname_clean] = obj

        return available

    def _scan_egg_group(self):
        """
        Scan classes in given Python Egg group name looking for a valid plugin.

        @return: module dictionary composed by module name as key and plugin
            class as value
        @rtype: dict
        """
        # needs setuptools
        import pkg_resources
        available = {}

        for entry in pkg_resources.iter_entry_points(self.__egg_entry_group):

            obj = entry.load()
            valid = self._inspect_object(obj)
            if not valid:
                continue
            available[entry.name] = obj

        return available


    def get_available_plugins(self):
        """
        Return currently available plugin classes.
        Note: Molecule plugins can either be Python packages or modules and
        their name MUST end with PluginFactory._PLUGIN_SUFFIX ("_plugin").

        @return: dictionary composed by Molecule plugin id as key and Molecule
            Python module as value
        @rtype: dict
        """
        if self.__cache is not None:
            return self.__cache.copy()

        if self.__egg_entry_group:
            available = self._scan_egg_group()
        else:
            available = self._scan_dir()

        self.__cache = available.copy()
        return available

    def get_default_plugin(self):
        """
        Return currently configured Molecule Plugin class.

        @return: Molecule plugin class
        @rtype: default plugin class object
        @raise KeyError: if default plugin is not set or not found
        """
        available = self.get_available_plugins()
        plugin = self.__default_plugin_name
        fallback = self.__fallback_plugin_name
        klass = available.get(plugin)

        if klass is None:
            import warnings
            warnings.warn("%s: %s" % (
                "selected Plugin not available, using fallback", plugin,))
            klass = available.get(fallback)

        if klass is None:
            raise KeyError

        return klass

    _SPEC_FACTORY = None

    @staticmethod
    def _get_spec():
        if PluginFactory._SPEC_FACTORY is None:
            from molecule.specs.skel import GenericSpec
            try:
                from .. import plugins as plugs
            except ImportError:
                from . import plugins as plugs
            PluginFactory._SPEC_FACTORY = PluginFactory(GenericSpec, plugs)
        return PluginFactory._SPEC_FACTORY

    @staticmethod
    def get_spec_plugins():
        factory = PluginFactory._get_spec()
        plugins = factory.get_available_plugins()
        spec_plugins = dict((y.execution_strategy(), y) for x, y \
            in list(plugins.items()))
        return spec_plugins
