#!/usr/bin/env python3

import functools
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from enum import Enum
from builtins import object
from PySide6.QtWidgets import QWidget, QApplication
from PySide6.QtGui import QAction
from PySide6.QtCore import QEvent, QLibraryInfo, qVersion, QCoreApplication


_testIterationCount = 20
_testCodeWindow = None


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


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.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 test_iteration(test_type):
    print(f"\n\n=== Testing {test_type.value} ===", file=sys.stderr)

    iterations = _testIterationCount
    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)


if __name__ == "__main__":
    print('Python {}.{}.{}'.format(sys.version_info[0], sys.version_info[1],
                                   sys.version_info[2]), file=sys.stderr)
    print(QLibraryInfo.build(), file=sys.stderr)
    app = QApplication(sys.argv)

    all_test_types = [TestType.TestWithDecorator, TestType.TestWithDecoratorAndDisconnect,
                      TestType.TestWithFuncTools]
    test_types = all_test_types

    argument_parser = ArgumentParser(description="PYSIDE 3148",
                                     formatter_class=RawTextHelpFormatter)
    argument_parser.add_argument("--iterations", "-i", help="iterations", action="store", type=int)
    argument_parser.add_argument("--test", "-t", help="test type index", action="store", type=int)

    options = argument_parser.parse_args()
    if options.iterations:
        _testIterationCount = options.iterations
    if options.test is not None:
        t = all_test_types[options.test]
        test_types = [t]
        print("Testing only ", t, file=sys.stderr)

    print(f"Starting UI cycle test ({len(test_types)} types(s))", file=sys.stderr)

    for test_type in test_types:
        test_iteration(test_type)

    print(f"UI cycle test completed for ({len(test_types)} types(s))", file=sys.stderr)
