from typing import List, Callable, Optional, Any

from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot
from UM.Application import Application
from UM.Scene.Selection import Selection

from cura.Scene.CuraSceneNode import CuraSceneNode


class PrintOrderManager(QObject):
    """Allows to order the object list to set the print sequence manually"""

    def __init__(self, get_nodes: Callable[[], List[CuraSceneNode]]) -> None:
        super().__init__()
        self._get_nodes = get_nodes
        self._configureEvents()

    _settingsChanged = pyqtSignal()
    _uiActionsOutdated = pyqtSignal()
    printOrderChanged = pyqtSignal()

    @pyqtSlot()
    def swapSelectedAndPreviousNodes(self) -> None:
        selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
        self._swapPrintOrders(selected_node, previous_node)

    @pyqtSlot()
    def swapSelectedAndNextNodes(self) -> None:
        selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
        self._swapPrintOrders(selected_node, next_node)

    @pyqtProperty(str, notify=_uiActionsOutdated)
    def previousNodeName(self) -> str:
        selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
        return self._getNodeName(previous_node)

    @pyqtProperty(str, notify=_uiActionsOutdated)
    def nextNodeName(self) -> str:
        selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
        return self._getNodeName(next_node)

    @pyqtProperty(bool, notify=_uiActionsOutdated)
    def shouldEnablePrintBeforeAction(self) -> bool:
        selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
        can_swap_with_previous_node = selected_node is not None and previous_node is not None
        return can_swap_with_previous_node

    @pyqtProperty(bool, notify=_uiActionsOutdated)
    def shouldEnablePrintAfterAction(self) -> bool:
        selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
        can_swap_with_next_node = selected_node is not None and next_node is not None
        return can_swap_with_next_node

    @pyqtProperty(bool, notify=_settingsChanged)
    def shouldShowEditPrintOrderActions(self) -> bool:
        return PrintOrderManager.isUserDefinedPrintOrderEnabled()

    @staticmethod
    def isUserDefinedPrintOrderEnabled() -> bool:
        stack = Application.getInstance().getGlobalContainerStack()
        is_enabled = stack and \
                     stack.getProperty("print_sequence", "value") == "one_at_a_time" and \
                     stack.getProperty("user_defined_print_order_enabled", "value")
        return bool(is_enabled)

    @staticmethod
    def initializePrintOrders(nodes: List[CuraSceneNode]) -> None:
        """Just created (loaded from file) nodes have print order 0.

         This method initializes print orders with max value to put nodes at the end of object list"""
        max_print_order = max(map(lambda n: n.printOrder, nodes), default=0)
        for node in nodes:
            if node.printOrder == 0:
                max_print_order += 1
                node.printOrder = max_print_order

    @staticmethod
    def updatePrintOrdersAfterGroupOperation(
            all_nodes: List[CuraSceneNode],
            group_node: CuraSceneNode,
            grouped_nodes: List[CuraSceneNode]
    ) -> None:
        group_node.printOrder = min(map(lambda n: n.printOrder, grouped_nodes))

        all_nodes.append(group_node)
        for node in grouped_nodes:
            all_nodes.remove(node)

        # reassign print orders so there won't be gaps like 1 2 5 6 7
        sorted_nodes = sorted(all_nodes, key=lambda n: n.printOrder)
        for i, node in enumerate(sorted_nodes):
            node.printOrder = i + 1

    @staticmethod
    def updatePrintOrdersAfterUngroupOperation(
            all_nodes: List[CuraSceneNode],
            group_node: CuraSceneNode,
            ungrouped_nodes: List[CuraSceneNode]
    ) -> None:
        all_nodes.remove(group_node)
        nodes_to_update_print_order = filter(lambda n: n.printOrder > group_node.printOrder, all_nodes)
        for node in nodes_to_update_print_order:
            node.printOrder += len(ungrouped_nodes) - 1

        for i, child in enumerate(ungrouped_nodes):
            child.printOrder = group_node.printOrder + i
            all_nodes.append(child)

    def _swapPrintOrders(self, node1: CuraSceneNode, node2: CuraSceneNode) -> None:
        if node1 and node2:
            node1.printOrder, node2.printOrder = node2.printOrder, node1.printOrder  # swap print orders
            self.printOrderChanged.emit()  # update object list first
            self._uiActionsOutdated.emit()  # then update UI actions

    def _getSelectedAndNeighborNodes(self
                                     ) -> (Optional[CuraSceneNode], Optional[CuraSceneNode], Optional[CuraSceneNode]):
        nodes = self._get_nodes()
        ordered_nodes = sorted(nodes, key=lambda n: n.printOrder)
        for i, node in enumerate(ordered_nodes, 1):
            node.printOrder = i

        selected_node = PrintOrderManager._getSingleSelectedNode()
        if selected_node and selected_node in ordered_nodes:
            selected_node_index = ordered_nodes.index(selected_node)
        else:
            selected_node_index = None

        if selected_node_index is not None and selected_node_index - 1 >= 0:
            previous_node = ordered_nodes[selected_node_index - 1]
        else:
            previous_node = None

        if selected_node_index is not None and selected_node_index + 1 < len(ordered_nodes):
            next_node = ordered_nodes[selected_node_index + 1]
        else:
            next_node = None

        return selected_node, previous_node, next_node

    @staticmethod
    def _getNodeName(node: CuraSceneNode, max_length: int = 30) -> str:
        node_name = node.getName() if node else ""
        truncated_node_name = node_name[:max_length]
        return truncated_node_name

    @staticmethod
    def _getSingleSelectedNode() -> Optional[CuraSceneNode]:
        if len(Selection.getAllSelectedObjects()) == 1:
            selected_node = Selection.getSelectedObject(0)
            return selected_node
        return None

    def _configureEvents(self) -> None:
        Selection.selectionChanged.connect(self._onSelectionChanged)
        self._global_stack = None
        Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
        self._onGlobalStackChanged()

    def _onGlobalStackChanged(self) -> None:
        if self._global_stack:
            self._global_stack.propertyChanged.disconnect(self._onSettingsChanged)
            self._global_stack.containersChanged.disconnect(self._onSettingsChanged)

        self._global_stack = Application.getInstance().getGlobalContainerStack()

        if self._global_stack:
            self._global_stack.propertyChanged.connect(self._onSettingsChanged)
            self._global_stack.containersChanged.connect(self._onSettingsChanged)

    def _onSettingsChanged(self, *args: Any) -> None:
        self._settingsChanged.emit()

    def _onSelectionChanged(self) -> None:
        self._uiActionsOutdated.emit()