import threading
from unittest import mock

import pyautogui
from PySide6 import QtCore, QtTest, QtWidgets
from PySide6.QtCore import QCoreApplication, QPoint, QPointF, Qt
from PySide6.QtGui import QInputDevice, QPointingDevice, QWheelEvent
from PySide6.QtWidgets import QTreeWidget, QTreeWidgetItem, QWidget

from asammdf.gui.widgets.plot import Plot
from test.asammdf.gui.test_base import DragAndDrop
from test.asammdf.gui.widgets.test_BaseFileWidget import TestFileWidget


class QMenuWrap(QtWidgets.QMenu):
    return_action = None

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def exec(self, *args, **kwargs):
        if not self.return_action:
            return super().exec(*args, **kwargs)
        return self.return_action


class TestPlotWidget(TestFileWidget):
    plot: Plot

    class Column:
        NAME = 0
        RAWVALUE = 1
        VALUE = 2
        UNIT = 3
        COMMON_AXIS = 4
        INDIVIDUAL_AXIS = 5
        ORIGIN = 6

    def add_channel_to_plot(self, plot=None, channel_name=None, channel_index=None):
        if not plot and self.plot:
            plot = self.plot

        # Select channel
        channel_tree = self.widget.channels_tree
        channel_tree.clearSelection()

        selected_channel = self.find_channel(channel_tree, channel_name, channel_index)
        selected_channel.setSelected(True)
        if not channel_name:
            channel_name = selected_channel.text(self.Column.NAME)

        drag_position = channel_tree.visualItemRect(selected_channel).center()
        drop_position = plot.channel_selection.viewport().rect().center()

        # PreEvaluation
        DragAndDrop(
            src_widget=channel_tree,
            dst_widget=plot.channel_selection,
            src_pos=drag_position,
            dst_pos=drop_position,
        )
        self.processEvents(0.05)
        plot_channel = None
        iterator = QtWidgets.QTreeWidgetItemIterator(plot.channel_selection)
        while iterator.value():
            item = iterator.value()
            if item and item.text(0) == channel_name:
                plot_channel = item
            iterator += 1

        return plot_channel

    def context_menu(self, action_text, position=None):
        self.processEvents()
        with mock.patch("asammdf.gui.widgets.tree.QtWidgets.QMenu", wraps=QMenuWrap):
            mo_action = mock.MagicMock()
            mo_action.text.return_value = action_text
            QMenuWrap.return_action = mo_action

            if not position:
                QtTest.QTest.mouseClick(self.plot.channel_selection.viewport(), QtCore.Qt.MouseButton.RightButton)
            else:
                QtTest.QTest.mouseClick(
                    self.plot.channel_selection.viewport(),
                    QtCore.Qt.MouseButton.RightButton,
                    QtCore.Qt.KeyboardModifier.NoModifier,
                    position,
                )
            self.processEvents(0.01)

            while not mo_action.text.called:
                self.processEvents(0.02)

    def move_item_inside_channels_tree_widget(
        self, plot=None, src: QTreeWidgetItem = None, dst: QTreeWidgetItem | QTreeWidget = None
    ):
        if src is None or dst is None:
            raise Exception("src and dst is cannot be None")

        def get_global_center(_item: QTreeWidgetItem) -> QPoint:
            _tree_widget = _item.treeWidget()
            index = _tree_widget.indexFromItem(_item)
            rect = _tree_widget.visualRect(index)
            return _tree_widget.viewport().mapToGlobal(rect.center())

        # drag start and stop coordinates
        global_start = get_global_center(src)
        if isinstance(dst, QTreeWidgetItem):
            global_end = get_global_center(dst)
        else:
            global_end = dst.viewport().mapToGlobal(dst.viewport().rect().center())

        # create event to synchronize mouse mover thread and main thread
        done_event = threading.Event()

        def move_mouse():
            # jump to start position
            pyautogui.moveTo(global_start.x(), global_start.y())

            # start drag by pressing left mouse button and moving mouse slowly
            pyautogui.mouseDown()
            pyautogui.moveTo(global_start.x() + 20, global_start.y(), duration=0.2)

            # jump to target
            pyautogui.moveTo(global_end.x() + 20, global_end.y())

            # drop by moving mouse slowly and releasing mouse button
            pyautogui.moveTo(global_end.x(), global_end.y(), duration=0.2)
            pyautogui.mouseUp()
            done_event.set()

        mouse_mover_thread = threading.Thread(target=move_mouse)
        mouse_mover_thread.start()

        while not done_event.is_set():
            self.processEvents(0.1)

        mouse_mover_thread.join()
        self.processEvents(0.1)

    def wheel_action(self, w: QWidget, x: float, y: float, angle_delta: int):
        """
        Used to simulate mouse wheel event.

        Parameters
        ----------
        w: widget - widget object
        x: float - x position of cursor
        y: float - y position of cursor
        angle_delta: int - physical-wheel rotations units

        Returns
        -------

        """
        pos = QPointF(x, y)

        widget_x, widget_y = self.widget.geometry().x(), self.widget.geometry().y()
        widget_width, widget_height = self.widget.width(), self.widget.height()

        global_pos = QPointF(widget_width + widget_x - x, widget_height + widget_y - y)

        pixel_d = QPoint(0, 0)
        angle_d = QPoint(0, angle_delta * 120)
        buttons = Qt.MouseButton.NoButton
        modifiers = Qt.KeyboardModifier.NoModifier
        phase = Qt.ScrollPhase(0x0)
        inverted = False
        source = Qt.MouseEventSource(0x0)
        device = QPointingDevice(
            "core pointer",
            1,
            QInputDevice.DeviceType(0x1),
            QPointingDevice.PointerType(0x0),
            QInputDevice.Capability.All,
            1,
            3,
        )
        # Create event
        event = QWheelEvent(pos, global_pos, pixel_d, angle_d, buttons, modifiers, phase, inverted, source, device)
        # Post event
        QCoreApplication.postEvent(w, event)
        self.assertTrue(event.isAccepted())
        QCoreApplication.processEvents()
