from functools import partial as functools_partial
from gevent import sleep as gevent_sleep, spawn as gevent_spawn, spawn_later as gevent_spawn_later, getcurrent as gevent_getcurrent
from gevent.event import AsyncResult
from inspect import isclass as inspect_isclass, ismethod as inspect_ismethod, getmembers as inspect_getmembers
from types import MethodType
from weakref import WeakSet as WeakSet

from disco.util.emitter import Priority
from disco.util.logging import LoggingClass
from disco.bot.command import Command, CommandError


# Contains a list of classes which will be excluded when auto discovering plugins
#  to load. This allows anyone to create subclasses of Plugin that act as a base
#  plugin class within their project/bot.
_plugin_base_classes = set()


def register_plugin_base_class(cls):
    """
    This function registers the given class under an internal registry of plugin
    base classes. This will cause the class passed to behave exactly like the
    builtin `Plugin` class.

    This is particularly useful if you wish to subclass `Plugin` to create a new
    base class that other plugins in your project inherit from, but do not want
    the automatic plugin loading to consider the class for loading.
    """
    if not inspect_isclass(cls):
        raise TypeError('cls must be a class')

    _plugin_base_classes.add(cls)
    return cls


def find_loadable_plugins(mod):
    """
    Generator which produces a list of loadable plugins given a Python module. This
    function will exclude any plugins which are registered as a plugin base class
    via the `register_plugin_base_class` function.
    """
    module_attributes = (getattr(mod, attr) for attr in dir(mod))
    for modattr in module_attributes:
        if not inspect_isclass(modattr):
            continue

        if not issubclass(modattr, Plugin):
            continue

        if modattr in _plugin_base_classes:
            continue

        yield modattr


class BasePluginDeco:
    Prio = Priority

    # TODO: don't smash class methods
    @classmethod
    def add_meta_deco(cls, meta):
        def deco(f):
            if not hasattr(f, 'meta'):
                f.meta = []

            f.meta.append(meta)

            return f
        return deco

    @classmethod
    def with_config(cls, config_cls):
        """
        Sets the plugins config class to the specified config class.
        """
        def deco(plugin_cls):
            plugin_cls.config_cls = config_cls
            return plugin_cls
        return deco

    @classmethod
    def listen(cls, *args, **kwargs):
        """
        Binds the function to listen for a given event name.
        """
        return cls.add_meta_deco({
            'type': 'listener',
            'what': 'event',
            'args': args,
            'kwargs': kwargs,
        })

    @classmethod
    def listen_packet(cls, *args, **kwargs):
        """
        Binds the function to listen for a given gateway op code.
        """
        return cls.add_meta_deco({
            'type': 'listener',
            'what': 'packet',
            'args': args,
            'kwargs': kwargs,
        })

    @classmethod
    def command(cls, *args, **kwargs):
        """
        Creates a new command attached to the function.
        """

        return cls.add_meta_deco({
            'type': 'command',
            'args': args,
            'kwargs': kwargs,
        })

    @classmethod
    def pre_command(cls):
        """
        Runs a function before a command is triggered.
        """
        return cls.add_meta_deco({
            'type': 'pre_command',
        })

    @classmethod
    def post_command(cls):
        """
        Runs a function after a command is triggered.
        """
        return cls.add_meta_deco({
            'type': 'post_command',
        })

    @classmethod
    def pre_listener(cls):
        """
        Runs a function before a listener is triggered.
        """
        return cls.add_meta_deco({
            'type': 'pre_listener',
        })

    @classmethod
    def post_listener(cls):
        """
        Runs a function after a listener is triggered.
        """
        return cls.add_meta_deco({
            'type': 'post_listener',
        })

    @classmethod
    def schedule(cls, *args, **kwargs):
        """
        Runs a function repeatedly, waiting for a specified interval.
        """
        return cls.add_meta_deco({
            'type': 'schedule',
            'args': args,
            'kwargs': kwargs,
        })

    @classmethod
    def add_argument(cls, *args, **kwargs):
        """
        Adds an argument to the argument parser.
        """
        return cls.add_meta_deco({
            'type': 'parser.add_argument',
            'args': args,
            'kwargs': kwargs,
        })

    @classmethod
    def route(cls, *args, **kwargs):
        """
        Adds an HTTP route.
        """
        return cls.add_meta_deco({
            'type': 'http.add_route',
            'args': args,
            'kwargs': kwargs,
        })


class PluginDeco(BasePluginDeco):
    """
    A utility mixin which provides various function decorators that a plugin
    author can use to create bound event/command handlers.
    """
    parser = BasePluginDeco


@register_plugin_base_class
class Plugin(LoggingClass, PluginDeco):
    """
    A plugin is a set of listeners/commands which can be loaded/unloaded by a bot.

    Parameters
    ----------
    bot : :class:`disco.bot.Bot`
        The bot this plugin is a member of.
    config : any
        The configuration data for this plugin.

    Attributes
    ----------
    client : :class:`disco.client.Client`
        An alias to the client the bot is running with.
    state : :class:`disco.state.State`
        An alias to the state object for the client.
    listeners : list
        List of all bound listeners this plugin owns.
    commands : list(:class:`disco.bot.command.Command`)
        List of all commands this plugin owns.
    """
    def __init__(self, bot, config):
        super(Plugin, self).__init__()
        self.bot = bot
        self.client = bot.client
        self.state = bot.client.state
        self.ctx = bot.ctx
        self.storage = bot.storage
        self.config = config

        # General declarations
        self.listeners = []
        self.commands = []
        self.schedules = {}
        self.greenlets = WeakSet()
        self._pre = {}
        self._post = {}

        # This is an array of all meta functions we sniff at init
        self.meta_funcs = []

        for name, member in inspect_getmembers(self, predicate=inspect_ismethod):
            if hasattr(member, 'meta'):
                self.meta_funcs.append(member)

                # Unsmash local functions
                if hasattr(Plugin, name):
                    method = MethodType(getattr(Plugin, name), self, self.__class__)
                    setattr(self, name, method)

        self.bind_all()

    @property
    def name(self):
        return self.__class__.__name__

    def bind_all(self):
        self.listeners = []
        self.commands = []
        self.schedules = {}
        self.greenlets = set()

        self._pre = {'command': [], 'listener': []}
        self._post = {'command': [], 'listener': []}

        for member in self.meta_funcs:
            for meta in reversed(member.meta):
                self.bind_meta(member, meta)

    def bind_meta(self, member, meta):
        if meta['type'] == 'listener':
            self.register_listener(member, meta['what'], *meta['args'], **meta['kwargs'])
        elif meta['type'] == 'command':
            # meta['kwargs']['update'] = True
            self.register_command(member, *meta['args'], **meta['kwargs'])
        elif meta['type'] == 'schedule':
            self.register_schedule(member, *meta['args'], **meta['kwargs'])
        elif meta['type'].startswith('pre_') or meta['type'].startswith('post_'):
            when, typ = meta['type'].split('_', 1)
            self.register_trigger(typ, when, member)
        elif meta['type'].startswith('parser.'):
            for command in self.commands:
                if command.func == member:
                    getattr(command.parser, meta['type'].split('.', 1)[-1])(
                        *meta['args'],
                        **meta['kwargs'])
        elif meta['type'] == 'http.add_route':
            meta['kwargs']['view_func'] = member
            self.bot.http.add_url_rule(*meta['args'], **meta['kwargs'])
        else:
            raise Exception('unhandled meta type {}'.format(meta))

    def handle_exception(self, greenlet, event):
        self.log.error('[disco.bot.plugin - handle_exception] - {}\n{}'.format(greenlet, event.__dict__))
        pass

    def wait_for_event(self, event_name, conditional=None, **kwargs):
        result = AsyncResult()
        listener = None

        def _event_callback(event):
            for k, v in kwargs.items():
                obj = event
                for inst in k.split('__'):
                    obj = getattr(obj, inst)

                if obj != v:
                    break
            else:
                if conditional and not conditional(event):
                    return

                listener.remove()
                return result.set(event)

        listener = self.bot.client.events.on(event_name, _event_callback)

        return result

    def spawn_wrap(self, spawner, method, *args, **kwargs):
        def wrapped(*args, **kwargs):
            self.ctx['plugin'] = self
            try:
                res = method(*args, **kwargs)
                return res
            finally:
                self.ctx.drop()

        obj = spawner(wrapped, *args, **kwargs)
        self.greenlets.add(obj)
        return obj

    def spawn(self, *args, **kwargs):
        return self.spawn_wrap(gevent_spawn, *args, **kwargs)

    def spawn_later(self, delay, *args, **kwargs):
        return self.spawn_wrap(functools_partial(gevent_spawn_later, delay), *args, **kwargs)

    def execute(self, event):
        """
        Executes a CommandEvent this plugin owns.
        """
        if not event.command.oob:
            self.greenlets.add(gevent_getcurrent())
        try:
            return event.command.execute(event)
        except CommandError as e:
            event.reply(e.msg)
            return False
        finally:
            self.ctx.drop()

    def register_trigger(self, typ, when, func):
        """
        Registers a trigger.
        """
        getattr(self, '_' + when)[typ].append(func)

    def dispatch(self, typ, func, event, *args, **kwargs):
        # Link the greenlet with our exception handler
        gevent_getcurrent().link_exception(lambda g: self.handle_exception(g, event))

        # TODO: this is ugly
        if typ != 'command':
            self.greenlets.add(gevent_getcurrent())

        self.ctx['plugin'] = self

        if hasattr(event, 'guild'):
            self.ctx['guild'] = event.guild
        if hasattr(event, 'channel'):
            self.ctx['channel'] = event.channel
        if hasattr(event, 'author'):
            self.ctx['user'] = event.author

        for pre in self._pre[typ]:
            event = pre(func, event, args, kwargs)

        if event is None:
            return False

        result = func(event, *args, **kwargs)

        for post in self._post[typ]:
            post(func, event, args, kwargs, result)

        return True

    def register_listener(self, func, what, *args, **kwargs):
        """
        Registers a listener.

        Parameters
        ----------
        what : str
            What the listener is for (event, packet)
        func : function
            The function to be registered.
        desc
            The descriptor of the event/packet.
        """
        args = list(args) + [functools_partial(self.dispatch, 'listener', func)]

        if what == 'event':
            li = self.bot.client.events.on(*args, **kwargs)
        elif what == 'packet':
            li = self.bot.client.packets.on(*args, **kwargs)
        else:
            raise Exception('Invalid listener what: {}'.format(what))

        self.listeners.append(li)

    def register_command(self, func, *args, **kwargs):
        """
        Registers a command.

        Parameters
        ----------
        func : function
            The function to be registered.
        args
            Arguments to pass onto the :class:`disco.bot.command.Command` object.
        kwargs
            Keyword arguments to pass onto the :class:`disco.bot.command.Command`
            object.
        """
        self.commands.append(Command(self, func, *args, **kwargs))

    def register_schedule(self, func, interval, repeat=True, init=True, kwargs=None):
        """
        Registers a function to be called repeatedly, waiting for an interval
        duration.

        Parameters
        ----------
        func : function
            The function to be registered.
        interval : int
            Interval (in seconds) to repeat the function on.
        repeat : bool
            Whether this schedule is repeating (or one time).
        init : bool
            Whether to run this schedule once immediately, or wait for the first
            scheduled iteration.
        kwargs : dict
            kwargs which will be passed to executed `func`.
        """
        if kwargs is None:
            kwargs = {}

        def repeat_func():
            if init:
                func(**kwargs)

            while True:
                gevent_sleep(interval)
                func(**kwargs)
                if not repeat:
                    break

        self.schedules[func.__name__] = self.spawn(repeat_func)

    def load(self, ctx):
        """
        Called when the plugin is loaded.
        """
        pass

    def unload(self, ctx):
        """
        Called when the plugin is unloaded.
        """
        for greenlet in self.greenlets:
            greenlet.kill()

        for listener in self.listeners:
            listener.remove()

        for schedule in self.schedules.values():
            schedule.kill()

    def reload(self):
        self.bot.reload_plugin(self.__class__)
