import copy
import sys
import traceback
from functools import wraps
from types import FunctionType

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


def save_init_state(self, *args, **kwargs):
    if "__init_args__" not in self.__dict__:
        self.__init_args__ = (copy.deepcopy(args), copy.deepcopy(kwargs))
    if "__attributes__" not in self.__dict__:
        self.__attributes__ = set()  # to include __attributes__ in itself
        self.__attributes__.update(dir(self))


def reinit(self):
    # for attr_name in set(dir(self)) - self.__attributes__:
    #     delattr(self, attr_name)
    self.__init__(*self.__init_args__[0], **self.__init_args__[1])


def try_run(self, function, *args, **kwargs):
    try:
        return function(*args, **kwargs)
    except AssertionError as exception:
        traceback.print_exc()
        print(f"{repr(self)} threw {repr(exception)} on {repr(function)}(args={args}, kwargs={kwargs})")
        print(f"reinitializing {repr(self)}.")
        reinit(self)
        return exception


def wrap(function, name):
    if name == "__init__":
        if function is not None:
            @wraps(function)
            def wrapper(self, *args, **kwargs):
                save_init_state(self, *args, **kwargs)
                return function(self, *args, **kwargs)
        else:
            def wrapper(self, *args, **kwargs):
                save_init_state(self, *args, **kwargs)
                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 try_run(self, function, self, *args, **kwargs)
    elif issubclass(type(function), classmethod):
        @wraps(function)
        def wrapper(self, *args, **kwargs):
            return try_run(self, function.__func__, self.__class__, *args, **kwargs)
    elif issubclass(type(function), staticmethod):
        @wraps(function)
        def wrapper(self, *args, **kwargs):
            return try_run(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 ReloadingWrapper(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__")
        # return super(ReloadingWrapper, meta).__new__(meta, classname, bases, new__dict__)
        return type.__new__(meta, classname, bases, new__dict__)


class ReloadingBase(QObject, metaclass=ReloadingWrapper):
    pass


if __name__ == "__main__":

    class TObject(QObject):
        def __init__(self, *args, **kwargs):
            print("TObject.__init__", self, args, kwargs)
            super().__init__()

    class Thrower(TObject, metaclass=ReloadingWrapper):
        def __init__(self, *args, **kwargs):
            print("Thrower.__init__", self, args, kwargs)
            super().__init__(*args, **kwargs)

        def start(self):
            self.timer = QTimer()
            self.timer.setInterval(1000)
            self.timer.timeout.connect(self.throw)
            self.timer.timeout.connect(self.throw_classmethod)
            self.timer.timeout.connect(self.throw_staticmethod)
            self.timer.timeout.connect(lambda: self.throw_property)
            self.timer.start()

        def throw(self):
            raise AssertionError("error")

        @classmethod
        def throw_classmethod(cls):
            raise AssertionError("error")

        @staticmethod
        def throw_staticmethod():
            raise AssertionError("error")

        @property
        def throw_property(self):
            raise AssertionError("error")

    app = QCoreApplication(sys.argv)

    thrower = Thrower("test_arg", test_kwarg="test_kwarg")

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

    timer = QTimer()
    timer.setInterval(1000)
    timer.timeout.connect(thrower.throw)
    timer.timeout.connect(thrower.throw_classmethod)
    timer.timeout.connect(thrower.throw_staticmethod)
    timer.timeout.connect(lambda: thrower.throw_property)
    timer.start()

    sys.exit(app.exec_())
