import sys
from functools import wraps
from queue import Queue
from types import FunctionType

from PyQt5.QtCore import QCoreApplication, QObject, QSemaphore, QThread, QTimer


def thread_command_execute(self):
    while not self.__thread_command_queue.empty():  # only one consumer so no lock required
        q = self.__thread_command_queue.get()
        q["r"] = q["f"](*q["a"], **q["k"])
        q["s"].release(1)  # mark as consumed
    self.__thread_command_timer.start()


def create_thread_command_executor(self):
    self.__thread_command_id = int(QThread.currentThreadId())
    self.__thread_command_queue = Queue()
    self.__thread_command_timer = QTimer()
    # self.__thread_command_timer.setTimerType(Qt.PreciseTimer)
    self.__thread_command_timer.setInterval(10)
    self.__thread_command_timer.setSingleShot(True)
    self.__thread_command_timer.timeout.connect(self._thread_command_execute)
    self.__thread_command_timer.start()


def run_or_enqueue(self, function, *args, **kwargs):
    if self.__thread_command_id is None or int(QThread.currentThreadId()) == self.__thread_command_id:
        return function(*args, **kwargs)
    else:
        s = {"f": function, "a": args, "k": kwargs, "s": QSemaphore(0), "r": None}
        self.__thread_command_queue.put(s)
        s["s"].acquire(max(s["s"].available(), 1))  # wait untill consumed
        s["s"].release()
        return s["r"]


def wrap(function, name):
    if name == "__init__":
        if function is not None:
            @wraps(function)
            def wrapper(self, *args, **kwargs):
                self.__thread_command_id = None
                return function(self, *args, **kwargs)
        else:
            def wrapper(self, *args, **kwargs):
                self.__thread_command_id = None
                return super(type(self), self).__init__(*args, **kwargs)
    elif function is None:
        return None
    elif issubclass(type(function), FunctionType):
        @wraps(function)
        def wrapper(self, *args, **kwargs):
            return run_or_enqueue(self, function, self, *args, **kwargs)
    elif issubclass(type(function), classmethod):
        @wraps(function)
        def wrapper(self, *args, **kwargs):
            return run_or_enqueue(self, function.__func__, self.__class__, *args, **kwargs)
    elif issubclass(type(function), staticmethod):
        @wraps(function)
        def wrapper(self, *args, **kwargs):
            return run_or_enqueue(self, function.__func__, *args, **kwargs)
    elif issubclass(type(function), property):
        wrapper = property(
            fget=wrap(function.fget, function.fget.__name__) if function.fget is not None else None,
            fset=wrap(function.fset, function.fset.__name__) if function.fset is not None else None,
            fdel=wrap(function.fdel, function.fdel.__name__) if function.fdel is not None else None,
            doc=function.__doc__,
        )
    else:
        raise NotImplementedError(f"wrap for type {type(function)} not implemented")
    return wrapper


class SynchronizingWrapper(type(QObject), type):
    def __new__(meta, classname, bases, old__dict__):
        new__dict__ = {}
        for name, attr in old__dict__.items():
            if name != "__init__":
                if issubclass(type(attr), FunctionType) or issubclass(type(attr), classmethod) or issubclass(type(attr), staticmethod) or issubclass(type(attr), property):
                    attr = wrap(attr, name)
            new__dict__[name] = attr
        new__dict__["__init__"] = wrap(new__dict__.get("__init__", None), "__init__")
        new__dict__["_create_thread_command_executor"] = create_thread_command_executor
        new__dict__["_thread_command_execute"] = thread_command_execute
        # return super(SynchronizingWrapper, meta).__new__(meta, classname, bases, new__dict__)
        return type.__new__(meta, classname, bases, new__dict__)


class SynchronizingBase(QObject, metaclass=SynchronizingWrapper):
    pass


if __name__ == "__main__":

    class RObject(QObject):
        def __init__(self, *args, **kwargs):
            print("RObject.__init__", self, args, kwargs, "thread:", int(QThread.currentThreadId()))
            super().__init__()

    class Runner(RObject, metaclass=SynchronizingWrapper):
        def __init__(self, *args, **kwargs):
            print("Runner.__init__", self, args, kwargs, "thread:", int(QThread.currentThreadId()))
            super().__init__(*args, **kwargs)

        def start(self):
            self._create_thread_command_executor()
            print("Runner.start", self, "thread:", int(QThread.currentThreadId()))
            self.timer = QTimer()
            self.timer.setInterval(1000)
            self.timer.timeout.connect(self.run)
            self.timer.timeout.connect(self.run_classmethod)
            self.timer.timeout.connect(self.run_staticmethod)
            self.timer.timeout.connect(lambda: self.run_property)
            self.timer.start()

        def run(self):
            print(f"run thread: {int(QThread.currentThreadId())}")
            return True

        @classmetho
        def run_classmethod(cls):
            print(f"run_classmethod thread: {int(QThread.currentThreadId())}")
            return True

        @staticmeth
        def run_staticmethod():
            print(f"run_staticmethod thread: {int(QThread.currentThreadId())}")
            return True

        @property
        def run_property(self):
            print(f"run_property thread: {int(QThread.currentThreadId())}")
            return True

    app = QCoreApplication(sys.argv)

    runner = Runner()
    # runner._create_thread_command_executor()

    thread = QThread()
    thread.setTerminationEnabled(True)
    runner.moveToThread(thread)
    thread.started.connect(runner.start)
    thread.start()

    timer = QTimer()
    timer.setInterval(1000)
    timer.timeout.connect(runner.run)
    timer.timeout.connect(runner.run_classmethod)
    timer.timeout.connect(runner.run_staticmethod)
    timer.timeout.connect(lambda: runner.run_property)
    timer.start()

    sys.exit(app.exec_())
