'''
Tools to scan for, find, register, and setup plugins.
'''

from __future__ import with_statement
import traceback
import logging
import config

import util.primitives
import util.data_importer as importers
from contextlib import closing
from util.data_importer import zipopen
from babel.messages.pofile import read_po
from babel.messages.mofile import write_mo
from StringIO import StringIO
import peak.util.plugins as Plugins

from path import path

log = logging.getLogger('plugin_registry')

METAINFO_FILENAME = 'info.yaml'

type_handlers = {}

def register_plugin_default(name, metainfo):
    '''
    Default handler for a discovered plugin.
    '''
    log.error('Not registering type %r because of unknown "type" key (%r). its metainfo is: %r', name, metainfo.get('type', None), metainfo)

def register_type_handler(typename, handler):
    '''
    Sets 'handler' as the callable for plugins with type == typename. 'handler' should be a callable
    that takes two arguments, (str name, mapping metainfo)
    '''
    old_handler = type_handlers.get(typename, None)
    if old_handler is not None:
        log.warning('Overwriting handler for type %r: was %r, now %r', typename, old_handler, handler)

    type_handlers[typename] = handler

def get_type_handler(typename):
    '''
    Returns the handler for 'typename', or the default handler if none is registerd.
    '''
    try:
        return type_handlers[typename]
    except KeyError:
        return register_plugin_default

def type_handler(typename):
    '''
    Decorator to mark a function as the type handler for 'typename'
    '''
    def register_handler(f):
        register_type_handler(typename, f)
        return f
    return register_handler

def register_type(name, metainfo):
    '''
    str name:         name of the plugin, ex: 'oscar', 'nowplaying'
    mapping metainfo: information about the plugin, ex: {'type' : 'im', 'shortname' : 'aim'}
    '''
    # TODO: move protocolmeta.protocols to this module and get rid of protolmeta?
    # use this as a central hub for all protocol meta info.
    # XXX: what should we do with functions like is_compatible and things like that from
    # protocolmeta?

    a_type = metainfo.get('type', None)
    if a_type is None:
        log.error('Not registering type %r because it did not have a "type" key. its metainfo is: %r', name, metainfo)
        return
    else:
        platforms = metainfo.get('platforms', None)
        if platforms is None or config.platformName in platforms:
            PluginType = get_type_handler(a_type)

            # TODO: store these things somewhere?
            plugin = PluginType(metainfo.__file__.parent, metainfo)
            plugin.init()

            return plugin

class PluginLoader(object):
    def __init__(self, dirpath, metainfo):
        self.path = dirpath
        self.shortname = metainfo.get('shortname', self.path.name)
        self.info = metainfo


    def init(self):
        self.name = self.info.name

        self.init_skin()
        self.init_notifications()
        self.init_actions()

        log.info('registered plugin type: %r', self.name)

    def init_skin(self):
        # self.skin contains transformed data with paths made relative to the
        # plugin's directory, while
        # self.info.skin contains the "raw info" from the plugin's metadata.
        #
        # when looking up skin values, you should really use self.skin
        self.skin = self.resolve_paths(self.get_component('skin'))

    def resolve_paths(self, d):
        '''recursively transform any string leafs in a mapping into paths
        relative to this plugin's directory, if those paths exist.'''

        if not isinstance(d, dict):
            return d
        ret = {}
        for k,v in d.items():
            if isinstance(v, basestring):
                pth = self.path / v
                if pth.isfile() or pth.isdir():
                    v = pth
            elif isinstance(v, dict):
                v = self.resolve_paths(v)
            ret[k] = v
        return ret

    def init_actions(self):
        actions = self.get_component('actions')
        if actions is None:
            return

        import common.actions
        common.actions.add_actions(actions)

    def init_notifications(self):
        nots = self.get_component('notifications')

        if nots is None:
            return

        import common.notifications as n
        #import gui.skin.skintransform as gst
        #nots.notifications = map(lambda x: gst.transform(x, False), nots.notifications)

        defaults = {}
        for d in nots:
            for k in d:
                not_ = d[k]
                default = not_.get('default', {})
                default_reactions = [{'reaction' : reaction} for reaction in default.get('reaction', [])]
                defaults[k.replace('_','.')] = default_reactions

        n.add_notifications(nots)
        n.add_default_notifications(defaults)
        self.set_component('notifications', nots)

    def get_component(self, attr, yamlname = None):
        attempts = [
                    lambda: getattr(self, attr, None),
                    lambda: getattr(self.info, attr, None),
                    lambda: getattr(self.yaml_load(yamlname or attr), '__content__', None),
                    ]

        for attempt in attempts:
            thing = attempt()
            if thing is not None:
                break
        else:
            return None

        self.set_component(attr, thing)
        return thing

    def set_component(self, attr, thing):
        setattr(self, attr, thing)
        setattr(self.info, attr, thing)

    def yaml_load(self, yamlname):
        try:
            return importers.yaml_import(yamlname, loadpath = [self.path])
        except ImportError:
            return None

    def load_entry_points(self):
        """Connects plugin entry points defined in info.yaml"""

        import pkg_resources
        from .plugin_resources import PluginDistribution
        import config as digsbyconfig
        platforms = self.get_component('platforms')
        if platforms is None or digsbyconfig.platformName in platforms:
            pkg_resources.working_set.add(PluginDistribution(location = str(self.path.parent),
                                                             project_name = self.info.name,
                                                             ep_yaml = self.get_component('entry_points')))

    def __repr__(self):
        return "<%s %r>" % (type(self).__name__, self.name)

class ProtocolPluginLoader(PluginLoader):
    def init(self, dictnames = None):
        PluginLoader.init(self)
        self.init_info(dictnames)

    def init_info(self, dictnames = None):
        import common.protocolmeta as pm

        plugin_info = util.primitives.mapping.Storage(self.info)

        if dictnames is not None:
            for dictname in dictnames:
                d = getattr(pm, dictname, None)
                if d is not None:
                    d[self.shortname] = plugin_info

        pm.protocols[self.shortname] = plugin_info

@type_handler('im')
class IMProtocolPluginLoader(ProtocolPluginLoader):
    def init(self):
        return ProtocolPluginLoader.init(self, ['improtocols'])

@type_handler('email')
class EmailProtocolPluginLoader(ProtocolPluginLoader):
    def init(self):
        return ProtocolPluginLoader.init(self, ['emailprotocols'])

@type_handler('social')
class SocialProtocolPluginLoader(ProtocolPluginLoader):
    def init(self):
        result = ProtocolPluginLoader.init(self, ['socialprotocols'])
        self.load_entry_points()
        return result

@type_handler('meta')
class MetaProtocolPluginLoader(ProtocolPluginLoader):
    def init(self):
        return ProtocolPluginLoader.init(self)

@type_handler('platform')
@type_handler('pure')
class PurePluginLoader(PluginLoader):
    def init(self):
        self.load_entry_points()
        super(PurePluginLoader, self).init()

@type_handler('multi')
class MultiPluginLoader(PurePluginLoader):
    def init(self):
        self.plugins = []
        plugin_dicts = self.info['plugins']
        for name, plugin in plugin_dicts.items():
            plugin_obj = register_type(name, plugin)
            if plugin_obj is not None:
                self.plugins.append(plugin_obj)
        super(MultiPluginLoader, self).init()


@type_handler("service_provider")
class ServiceProviderPlugin(PurePluginLoader):
    def init(self):
        res = super(ServiceProviderPlugin, self).init()
        self.type = self.info.type
        self.name = self.info.name
        self.provider_id = self.info.provider_id

        return res

@type_handler("service_component")
class ServiceComponentPlugin(ProtocolPluginLoader):
    def init(self):
        pm_key = {
                  'social' : 'socialprotocols',
                  'email'  : 'emailprotocols',
                  'im'     : 'improtocols',
                  }.get(self.info.component_type, None)

        dictnames = []
        if pm_key is not None:
            dictnames.append(pm_key)
        result = ProtocolPluginLoader.init(self, dictnames)
        self.load_entry_points()
        self.service_provider = self.provider_id = self.info.service_provider
        self.component_type = self.info.component_type
        return result

@type_handler('lang')
class LangPluginLoader(PurePluginLoader):
    def init(self):
        super(LangPluginLoader, self).init()
#
    def get_catalog(self):
        if self.info.get('catalog_format') == 'po':
            return self.get_po_catalog()
        return self.get_mo_catalog()

    @property
    def file_base(self):
        return self.path / (self.info['domain'] + '-' + self.info['language'])

    def get_mo_catalog(self):
        with closing(zipopen(self.file_base + '.mo')) as f:
            return StringIO(f.read())

    def get_po_catalog(self):
        cat = StringIO()
        with closing(zipopen(self.file_base + '.po')) as f:
            po = read_po(f)
            write_mo(cat, po)
            cat.seek(0)
            return cat

pkg_dirs = set()

def scan(dirname):
    '''
    Search 'dirname' for .zip, .egg, or directories with an info.yaml.
    for each found, loads the plugin info and calls register_type with
    the discovered name and information.
    '''
    root = path(dirname)
    plugins = []
    for pkg_dir in root.dirs() + root.files('*.zip') + root.files('*.egg'):
        name = pkg_dir.namebase
        if exclude_dir(name):
            continue

#        sys.path.append(pkg_dir)
        try:
            plugin = _load_plugin_info_from_item(pkg_dir)

            if plugin is None:
    #            sys.path.remove(pkg_dir)
                log.info("No protocol info found in %r", pkg_dir)
            else:
                pkg_dirs.add(pkg_dir.abspath())
                ret = register_type(name, plugin)
                if ret is None:
                    continue

                plugins.append(ret)
                if hasattr(ret, 'plugins'):
                    plugins.extend(ret.plugins)
        except Exception:
            traceback.print_exc()

    return plugins

def exclude_dir(dirname):
    return dirname.startswith('.')

def _load_plugin_info_from_item(pkgdir):
    '''
    Tries really hard to load the plugin info from the specified item.
    '''

    try:
        return importers.yaml_import('info', loadpath = [pkgdir])
    except ImportError:
        try:
            return importers.yaml_load(pkgdir / 'info.yaml')
        except Exception:
            return None

def plugins_skintrees():
    'All info.yamls can define a "skin:" section that gets merged with the skin tree.'

    import wx
    plugins = wx.GetApp().plugins

    trees = []
    for plugin in plugins:
        skin = plugin.get_component('skin') or None
        if skin is not None:
            trees.append(skin)

    return trees

def plugins_skinpaths():
    'Returns paths images for plugins can be loaded from.'

    return list(pkg_dirs) # todo: limit skin lookups for plugins to their own res/ directory

Plugins.Hook('digsby.skin.load.trees', 'plugins_skin').register(plugins_skintrees)
Plugins.Hook('digsby.skin.load.skinpaths', 'plugins_skin').register(plugins_skinpaths)

if __name__ == '__main__':
    import wx
    a = wx.App()
    logging.basicConfig()
    log.setLevel(1)
    scan('c:\\workspace\\digsby\\src\\plugins')

