import six
import threading
import inspect

if six.PY3:
    import asyncio
else:
    asyncio = None


class Updater(object):

    def __init__(self, element, updater):
        self.element = element
        self.updater = updater


class ElementUpdater(object):

    def __init__(self):
        self.threads = []
        self.started = False
        if six.PY3:
            self.async_thread = threading.Thread(target=self._asyncio_run)
            self.async_thread.daemon = True
            self.async_loop = asyncio.new_event_loop()
        else:
            self.async_thread = None
            self.async_loop = None

    def add(self, updater):
        assert isinstance(updater, Updater)
        fn = updater.updater
        if six.PY3 and inspect.isasyncgenfunction(fn):
            self.async_loop.call_soon_threadsafe(self._add_async_generator_updater, updater)
        elif six.PY3 and inspect.iscoroutinefunction(fn):
            self.async_loop.call_soon_threadsafe(self._add_async_updater, updater)
        elif inspect.isgeneratorfunction(fn):
            self._add_generator_updater(updater)
        elif callable(fn):
            self._add_callable_updater(updater)
        else:
            raise ValueError('Invalid updater: {}'.format(fn))

    def start(self):
        if self.started:
            return
        if self.async_thread:
            self.async_thread.start()
        for thread in self.threads:
            thread.start()
        self.started = True

    def _asyncio_run(self):
        loop = self.async_loop
        asyncio.set_event_loop(loop)
        try:
            loop.run_forever()
        finally:
            loop.run_until_complete(loop.shutdown_asyncgens())
            loop.close()

    def _add_async_updater(self, updater):
        self.async_loop.create_task(updater.updater(updater.element))

    def _add_async_generator_updater(self, updater):
        generator = updater.updater(updater.element)
        anext = generator.__anext__

        def callback(old_task):
            if old_task and old_task.exception():
                exception = old_task.exception()
                if not isinstance(exception, __builtins__.StopAsyncIteration):
                    raise exception
                return
            t = self.async_loop.create_task(anext())
            t.add_done_callback(callback)
        callback(None)

    def _add_generator_updater(self, updater):
        def updater_wrapper():
            for _ in updater.updater(updater.element):
                pass
        self._add_thread(updater_wrapper)

    def _add_callable_updater(self, updater):
        self._add_thread(lambda: updater.updater(updater.element))

    def _add_thread(self, fn):
        thread = threading.Thread(target=fn)
        thread.daemon = True
        self.threads.append(thread)
        if self.started:
            thread.start()