#!/usr/bin/env python3

import functools
import sys
from enum import Enum
from builtins import object
from PyQt6.QtWidgets import QWidget, QApplication
from PyQt6.QtGui import QAction
from PyQt6.QtCore import QEvent, qVersion, QCoreApplication


_testIterationCount = 20
_testCodeWindow = None


class TestType(Enum):
    TestWithFuncTools = "TestWithFuncTools"
    TestWithDecorator = "TestWithDecorator"
    TestWithDecoratorAndDisconnect = "TestWithDecoratorAndDisconnect"


class TestContext(object):
    def __enter__(self):
        print(r"TestContext::__enter__", file=sys.stderr)
        pass

    def __exit__(self, type, value, traceback):
        print(r"TestContext::__exit__", file=sys.stderr)
        pass


# Requires explicit disconnect of actions in dispose, otherwise results in malloc corruption as seen below.
# Python(28690,0x2081a9f00) malloc: Corruption of free object 0x14e19aa30: msizes 2/0 disagree
# Python(28690,0x2081a9f00) malloc: *** set a breakpoint in malloc_error_break to debug
# Tested with TestWithDecoratorAndDisconnect, TestWithDecorator.
def testContextWithDecorator():
    def decorator(f):
        def wrapper(*args, **kwargs):
            with TestContext():
                return f(*args, **kwargs)
        return wrapper
    return decorator


# Doesn't require explicit disconnect of actions in dispose.
# Tested with TestWithFuncTools.
def testContextWithFuncToolsWraps(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        with TestContext():
            return f(*args, **kwargs)
    return wrapper


def renderSetupWindowClosed():
    global _testCodeWindow
    if not _testCodeWindow:
        return
    _testCodeWindow.dispose()
    _testCodeWindow = None


class TestCodeWindow(QWidget):
    def __init__(self, test_type=TestType.TestWithFuncTools):
        super(TestCodeWindow, self).__init__(parent=None)
        self.test_type = test_type
        self.staticActions = []
        self.setWindowTitle(str(test_type) + ' ' + qVersion())
        print('TestCodeWindow.__init__', test_type, file=sys.stderr)

        # Create actions based on test type
        if test_type == TestType.TestWithFuncTools:
            self.actionOne = QAction(r"ACTION_ONE_FUNCTOOLS", self)
            self.actionTwo = QAction(r"ACTION_TWO_FUNCTOOLS", self)
            self.actionOne.triggered.connect(self._actionOneWithFuncToolsWraps)
            self.actionTwo.triggered.connect(self._actionTwoWithFuncToolsWraps)

        elif test_type == TestType.TestWithDecorator:
            self.actionOne = QAction(r"ACTION_ONE_DECORATOR", self)
            self.actionTwo = QAction(r"ACTION_TWO_DECORATOR", self)
            print(">connect _actionOneWithDecorator", file=sys.stderr)
            self.actionOne.triggered.connect(self._actionOneWithDecorator)
            print("<connect _actionOneWithDecorator", file=sys.stderr)
            print(">connect _actionTwoWithDecorator", file=sys.stderr)
            self.actionTwo.triggered.connect(self._actionTwoWithDecorator)
            print("<connect _actionTwoWithDecorator", file=sys.stderr)

        elif test_type == TestType.TestWithDecoratorAndDisconnect:
            self.actionOne = QAction(r"ACTION_ONE_DECORATOR_DISCONNECT", self)
            self.actionTwo = QAction(r"ACTION_TWO_DECORATOR_DISCONNECT", self)
            self.actionOne.triggered.connect(self._actionOneWithDecorator)
            self.actionTwo.triggered.connect(self._actionTwoWithDecorator)
            self.staticActions = [self.actionOne, self.actionTwo]

        self.staticActions = [self.actionOne, self.actionTwo]

        for action in self.staticActions:
            action.trigger()

        self.setObjectName(r"TestCodeWindow")
        self.resize(300, 200)
        self.show()

    def dispose(self):
        print(f">TestCodeWindow dispose - {self.test_type.value}", file=sys.stderr)

        # Disconnect actions if TestWithDecoratorAndDisconnect
        # If this is not done i.e TestWithDecorator, it would result in malloc corruption.
        if self.test_type == TestType.TestWithDecoratorAndDisconnect:
            for action in self.staticActions:
                action.triggered.disconnect()
        print(f"<TestCodeWindow dispose - {self.test_type.value}", file=sys.stderr)

    def event(self, event):
        if event.type() == QEvent.Type.Close:
            print("close()", file=sys.stderr)
            renderSetupWindowClosed()
        return super(TestCodeWindow, self).event(event)

    @testContextWithFuncToolsWraps
    def _actionOneWithFuncToolsWraps(self):
        print("_actionOneWithFuncToolsWraps() Action One with FuncTools triggered", file=sys.stderr)

    @testContextWithFuncToolsWraps
    def _actionTwoWithFuncToolsWraps(self):
        print("_actionTwoWithFuncToolsWraps() Action Two with FuncTools triggered", file=sys.stderr)

    @testContextWithDecorator()
    def _actionOneWithDecorator(self):
        print("_actionOneWithDecorator() Action One with Decorator triggered", file=sys.stderr)

    @testContextWithDecorator()
    def _actionTwoWithDecorator(self):
        print("_actionTwoWithDecorator() Action Two with Decorator triggered", file=sys.stderr)


def createUI(test_type=TestType.TestWithFuncTools):
    global _testCodeWindow

    if _testCodeWindow is None:
        _testCodeWindow = TestCodeWindow(test_type)


def destroyUI():
    global _testCodeWindow
    if _testCodeWindow:
        _testCodeWindow.close()
        _testCodeWindow = None


def main(iterations=_testIterationCount):
    test_types = [TestType.TestWithDecorator, TestType.TestWithDecoratorAndDisconnect, TestType.TestWithFuncTools]

    print(f"Starting UI cycle test - {iterations} iterations for each test type", file=sys.stderr)

    for test_type in test_types:
        print(f"\n\n=== Testing {test_type.value} ===", file=sys.stderr)

        for i in range(iterations):
            print(f"Iteration {i + 1}/{iterations}", file=sys.stderr)

            print("Creating UI...", test_type, file=sys.stderr)
            createUI(test_type)

            QCoreApplication.processEvents()
            # time.sleep(0.1)

            print("Destroying UI...", test_type, file=sys.stderr)
            destroyUI()

            QCoreApplication.processEvents()
            # time.sleep(0.1)

    print("UI cycle test completed for all test types", file=sys.stderr)


if __name__ == "__main__":
    print('PyQt {}.{}.{}'.format(sys.version_info[0], sys.version_info[1],
                                 sys.version_info[2]))
    print(qVersion())
    app = QApplication(sys.argv)
    main()
