import gevent
import time
from gevent.event import AsyncResult

from . import ThreadPool


class Noparallel:  # Only allow function running once in same time

    def __init__(self, blocking=True, ignore_args=False, ignore_class=False, queue=False):
        self.threads = {}
        self.blocking = blocking  # Blocking: Acts like normal function else thread returned
        self.queue = queue  # Execute again when blocking is done
        self.queued = False
        self.ignore_args = ignore_args  # Block does not depend on function call arguments
        self.ignore_class = ignore_class  # Block does not depeds on class instance

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            if not ThreadPool.isMainThread():
                return ThreadPool.main_loop.call(wrapper, *args, **kwargs)

            if self.ignore_class:
                key = func  # Unique key only by function and class object
            elif self.ignore_args:
                key = (func, args[0])  # Unique key only by function and class object
            else:
                key = (func, tuple(args), str(kwargs))  # Unique key for function including parameters
            if key in self.threads:  # Thread already running (if using blocking mode)
                if self.queue:
                    self.queued = True
                thread = self.threads[key]
                if self.blocking:
                    if self.queued:
                        res = thread.get()  # Blocking until its finished
                        if key in self.threads:
                            return self.threads[key].get()  # Queue finished since started running
                        self.queued = False
                        return wrapper(*args, **kwargs)  # Run again after the end
                    else:
                        return thread.get()  # Return the value

                else:  # No blocking
                    if thread.ready():  # Its finished, create a new
                        thread = gevent.spawn(func, *args, **kwargs)
                        self.threads[key] = thread
                        return thread
                    else:  # Still running
                        return thread
            else:  # Thread not running
                if self.blocking:  # Wait for finish
                    asyncres = AsyncResult()
                    self.threads[key] = asyncres
                    try:
                        res = func(*args, **kwargs)
                        asyncres.set(res)
                        self.cleanup(key, asyncres)
                        return res
                    except Exception as err:
                        asyncres.set_exception(err)
                        self.cleanup(key, asyncres)
                        raise(err)
                else:  # No blocking just return the thread
                    thread = gevent.spawn(func, *args, **kwargs)  # Spawning new thread
                    thread.link(lambda thread: self.cleanup(key, thread))
                    self.threads[key] = thread
                    return thread
        wrapper.__name__ = func.__name__

        return wrapper

    # Cleanup finished threads
    def cleanup(self, key, thread):
        if key in self.threads:
            del(self.threads[key])


if __name__ == "__main__":


    class Test():

        @Noparallel()
        def count(self, num=5):
            for i in range(num):
                print(self, i)
                time.sleep(1)
            return "%s return:%s" % (self, i)

    class TestNoblock():

        @Noparallel(blocking=False)
        def count(self, num=5):
            for i in range(num):
                print(self, i)
                time.sleep(1)
            return "%s return:%s" % (self, i)

    def testBlocking():
        test = Test()
        test2 = Test()
        print("Counting...")
        print("Creating class1/thread1")
        thread1 = gevent.spawn(test.count)
        print("Creating class1/thread2 (ignored)")
        thread2 = gevent.spawn(test.count)
        print("Creating class2/thread3")
        thread3 = gevent.spawn(test2.count)

        print("Joining class1/thread1")
        thread1.join()
        print("Joining class1/thread2")
        thread2.join()
        print("Joining class2/thread3")
        thread3.join()

        print("Creating class1/thread4 (its finished, allowed again)")
        thread4 = gevent.spawn(test.count)
        print("Joining thread4")
        thread4.join()

        print(thread1.value, thread2.value, thread3.value, thread4.value)
        print("Done.")

    def testNoblocking():
        test = TestNoblock()
        test2 = TestNoblock()
        print("Creating class1/thread1")
        thread1 = test.count()
        print("Creating class1/thread2 (ignored)")
        thread2 = test.count()
        print("Creating class2/thread3")
        thread3 = test2.count()
        print("Joining class1/thread1")
        thread1.join()
        print("Joining class1/thread2")
        thread2.join()
        print("Joining class2/thread3")
        thread3.join()

        print("Creating class1/thread4 (its finished, allowed again)")
        thread4 = test.count()
        print("Joining thread4")
        thread4.join()

        print(thread1.value, thread2.value, thread3.value, thread4.value)
        print("Done.")

    def testBenchmark():
        import time

        def printThreadNum():
            import gc
            from greenlet import greenlet
            objs = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)]
            print("Greenlets: %s" % len(objs))

        printThreadNum()
        test = TestNoblock()
        s = time.time()
        for i in range(3):
            gevent.spawn(test.count, i + 1)
        print("Created in %.3fs" % (time.time() - s))
        printThreadNum()
        time.sleep(5)

    def testException():
        import time
        @Noparallel(blocking=True, queue=True)
        def count(self, num=5):
            s = time.time()
            # raise Exception("err")
            for i in range(num):
                print(self, i)
                time.sleep(1)
            return "%s return:%s" % (s, i)
        def caller():
            try:
                print("Ret:", count(5))
            except Exception as err:
                print("Raised:", repr(err))

        gevent.joinall([
            gevent.spawn(caller),
            gevent.spawn(caller),
            gevent.spawn(caller),
            gevent.spawn(caller)
        ])


    from gevent import monkey
    monkey.patch_all()

    testException()

    """
    testBenchmark()
    print("Testing blocking mode...")
    testBlocking()
    print("Testing noblocking mode...")
    testNoblocking()
    """
