# -*- encoding: utf-8 -*-

"""
    sleekxmpp.plugins.base
    ~~~~~~~~~~~~~~~~~~~~~~

    This module provides XMPP functionality that
    is specific to client connections.

    Part of SleekXMPP: The Sleek XMPP Library

    :copyright: (c) 2012 Nathanael C. Fritz
    :license: MIT, see LICENSE for more details
"""

import threading
import logging


log = logging.getLogger(__name__)


#: Associate short string names of plugins with implementations. The
#: plugin names are based on the spec used by the plugin, such as
#: `'xep_0030'` for a plugin that implements XEP-0030.
PLUGIN_REGISTRY = {}

#: In order to do cascading plugin disabling, reverse dependencies
#: must be tracked. 
PLUGIN_DEPENDENTS = {}

#: Only allow one thread to manipulate the plugin registry at a time. 
REGISTRY_LOCK = threading.RLock()


def register_plugin(impl, name=None):
    """Add a new plugin implementation to the registry.

    :param class impl: The plugin class.

    The implementation class must provide a :attr:`~BasePlugin.name`
    value that will be used as a short name for enabling and disabling
    the plugin. The name should be based on the specification used by
    the plugin. For example, a plugin implementing XEP-0030 would be
    named `'xep_0030'`.
    """
    if name is None:
        name = impl.name
    with REGISTRY_LOCK:
        PLUGIN_REGISTRY[name] = impl
        if name not in PLUGIN_DEPENDENTS:
            PLUGIN_DEPENDENTS[name] = set()
        for dep in impl.dependencies:
            if dep not in PLUGIN_DEPENDENTS:
                PLUGIN_DEPENDENTS[dep] = set()
            PLUGIN_DEPENDENTS[dep].add(name)


class PluginNotFound(Exception):
    """Raised if an unknown plugin is accessed."""


class PluginManager(object): 
    def __init__(self, xmpp, config=None):
        #: We will track all enabled plugins in a set so that we
        #: can enable plugins in batches and pull in dependencies
        #: without problems.
        self._enabled = set()

        #: Maintain references to active plugins.
        self._plugins = {}

        self._plugin_lock = threading.RLock()

        #: Globally set default plugin configuration. This will
        #: be used for plugins that are auto-enabled through
        #: dependency loading.
        self.config = config if config else {}

        self.xmpp = xmpp

    def register(self, plugin, enable=True):
        """Register a new plugin, and optionally enable it.

        :param class plugin: The implementation class of the plugin
                             to register.
        :param bool enable: If ``True``, immediately enable the
                            plugin after registration.
        """
        register_plugin(plugin)
        if enable:
            self.enable(plugin.name)

    def enable(self, name, config=None, enabled=None):
        """Enable a plugin, including any dependencies.

        :param string name: The short name of the plugin.
        :param dict config: Optional settings dictionary for
                            configuring plugin behaviour.
        """
        if enabled is None:
            enabled = set()

        with self._plugin_lock:
            if name not in self._enabled:
                enabled.add(name)
                self._enabled.add(name)
                plugin_class = PLUGIN_REGISTRY.get(name, None)
                if not plugin_class:
                    raise PluginNotFound(name)

                if config is None:
                    config = self.config.get(name, None)

                plugin = plugin_class(self.xmpp, config)
                self._plugins[name] = plugin
                for dep in plugin.dependencies:
                    self.enable(dep, enabled=enabled)
                plugin.plugin_init()
                log.debug("Loaded Plugin: %s", plugin.description)

    def enable_all(self, names=None, config=None):
        """Enable all registered plugins.
        
        :param list names: A list of plugin names to enable. If
                           none are provided, all registered plugins
                           will be enabled.
        :param dict config: A dictionary mapping plugin names to
                            configuration dictionaries, as used by
                            :meth:`~PluginManager.enable`.
        """
        names = names if names else PLUGIN_REGISTRY.keys()
        if config is None:
            config = {}
        for name in names:
            self.enable(name, config.get(name, {}))

    def enabled(self, name):
        """Check if a plugin has been enabled.

        :param string name: The name of the plugin to check.
        :return: boolean
        """
        return name in self._enabled

    def registered(self, name):
        """Check if a plugin has been registered.

        :param string name: The name of the plugin to check.
        :return: boolean
        """
        return name in PLUGIN_REGISTRY

    def disable(self, name, _disabled=None):
        """Disable a plugin, including any dependent upon it.

        :param string name: The name of the plugin to disable.
        :param set _disabled: Private set used to track the
                              disabled status of plugins during
                              the cascading process.
        """
        if _disabled is None:
            _disabled = set()
        with self._plugin_lock:
            if name not in _disabled and name in self._enabled:
                _disabled.add(name)
                plugin = self._plugins.get(name, None)
                if plugin is None:
                    raise PluginNotFound(name)
                for dep in PLUGIN_DEPENDENTS[name]:
                    self.disable(dep, _disabled)
                plugin.plugin_end()
                if name in self._enabled:
                    self._enabled.remove(name)
                del self._plugins[name]

    def __keys__(self):
        """Return the set of enabled plugins."""
        return self._plugins.keys()

    def __getitem__(self, name):
        """
        Allow plugins to be accessed through the manager as if
        it were a dictionary.
        """
        plugin = self._plugins.get(name, None)
        if plugin is None:
            raise PluginNotFound(name)
        return plugin

    def __iter__(self):
        """Return an iterator over the set of enabled plugins."""
        return self._plugins.__iter__()

    def __len__(self):
        """Return the number of enabled plugins."""
        return len(self._plugins)


class BasePlugin(object):

    #: A short name for the plugin based on the implemented specification.
    #: For example, a plugin for XEP-0030 would use `'xep_0030'`.
    name = ''

    #: A longer name for the plugin, describing its purpose. For example,
    #: a plugin for XEP-0030 would use `'Service Discovery'` as its
    #: description value.
    description = ''

    #: Some plugins may depend on others in order to function properly.
    #: Any plugin names included in :attr:`~BasePlugin.dependencies` will
    #: be initialized as needed if this plugin is enabled.
    dependencies = set()

    def __init__(self, xmpp, config=None):
        self.xmpp = xmpp

        #: A plugin's behaviour may be configurable, in which case those
        #: configuration settings will be provided as a dictionary.
        self.config = config if config is not None else {}

    def plugin_init(self):
        """Initialize plugin state, such as registering event handlers."""
        pass

    def plugin_end(self):
        """Cleanup plugin state, and prepare for plugin removal."""
        pass

    def post_init(self):
        """Initialize any cross-plugin state.
       
        Only needed if the plugin has circular dependencies.
        """
        pass


base_plugin = BasePlugin