"""
Orange Canvas Tool Dock widget

"""
import sys
import warnings
from typing import Optional, Any

from AnyQt.QtWidgets import (
    QWidget, QSplitter, QVBoxLayout, QAction, QSizePolicy, QApplication,
    QToolButton, QTreeView
)
from AnyQt.QtGui import (
    QPalette, QBrush, QDrag, QResizeEvent, QHideEvent, QPaintEvent
)
from AnyQt.QtCore import (
    Qt, QSize, QObject, QPropertyAnimation, QEvent, QRect, QPoint,
    QAbstractItemModel, QModelIndex, QPersistentModelIndex, QEventLoop,
    QMimeData
)
from AnyQt.QtCore import pyqtProperty as Property, pyqtSignal as Signal

from .. import styles
from ..gui.iconengine import StyledIconEngine
from ..gui.toolgrid import ToolGrid, ToolGridButton
from ..gui.toolbar import DynamicResizeToolBar
from ..gui.quickhelp import QuickHelp
from ..gui.framelesswindow import FramelessWindow
from ..gui.utils import create_css_gradient, available_screen_geometry
from ..document.quickmenu import MenuPage
from .widgettoolbox import WidgetToolBox, iter_index, item_text, item_icon, item_tooltip
from ..registry.qt import QtWidgetRegistry


class SplitterResizer(QObject):
    """
    An object able to control the size of a widget in a QSplitter instance.
    """
    def __init__(self, parent=None, **kwargs):
        # type: (Optional[QObject], Any) -> None
        super().__init__(parent, **kwargs)
        self.__splitter = None  # type: Optional[QSplitter]
        self.__widget = None    # type: Optional[QWidget]
        self.__updateOnShow = True  # Need __update on next show event
        self.__animationEnabled = True
        self.__size = -1
        self.__expanded = False
        self.__animation = QPropertyAnimation(
            self, b"size_", self, duration=200
        )
        self.__action = QAction("toggle-expanded", self, checkable=True)
        self.__action.triggered[bool].connect(self.setExpanded)

    def setSize(self, size):
        # type: (int) -> None
        """
        Set the size of the controlled widget (either width or height
        depending on the orientation).

        .. note::
            The controlled widget's size is only updated when it it is shown.
        """
        if self.__size != size:
            self.__size = size
            self.__update()

    def size(self):
        # type: () -> int
        """
        Return the size of the widget in the splitter (either height of
        width) depending on the splitter orientation.
        """
        if self.__splitter and self.__widget:
            index = self.__splitter.indexOf(self.__widget)
            sizes = self.__splitter.sizes()
            return sizes[index]
        else:
            return -1

    size_ = Property(int, fget=size, fset=setSize)

    def setAnimationEnabled(self, enable):
        # type: (bool) -> None
        """Enable/disable animation."""
        self.__animation.setDuration(0 if enable else 200)

    def animationEnabled(self):
        # type: () -> bool
        return self.__animation.duration() == 0

    def setSplitterAndWidget(self, splitter, widget):
        # type: (QSplitter, QWidget) -> None
        """Set the QSplitter and QWidget instance the resizer should control.

        .. note:: the widget must be in the splitter.
        """
        if splitter and widget and not splitter.indexOf(widget) > 0:
            raise ValueError("Widget must be in a splitter.")

        if self.__widget is not None:
            self.__widget.removeEventFilter(self)
        if self.__splitter is not None:
            self.__splitter.removeEventFilter(self)

        self.__splitter = splitter
        self.__widget = widget

        if widget is not None:
            widget.installEventFilter(self)
        if splitter is not None:
            splitter.installEventFilter(self)

        self.__update()

        size = self.size()
        if self.__expanded and size == 0:
            self.open()
        elif not self.__expanded and size > 0:
            self.close()

    def toggleExpandedAction(self):
        # type: () -> QAction
        """Return a QAction that can be used to toggle expanded state.
        """
        return self.__action

    def toogleExpandedAction(self):
        warnings.warn(
            "'toogleExpandedAction is deprecated, use 'toggleExpandedAction' "
            "instead.", DeprecationWarning, stacklevel=2
        )
        return self.toggleExpandedAction()

    def open(self):
        # type: () -> None
        """Open the controlled widget (expand it to sizeHint).
        """
        self.__expanded = True
        self.__action.setChecked(True)

        if self.__splitter is None or self.__widget is None:
            return

        hint = self.__widget.sizeHint()

        if self.__splitter.orientation() == Qt.Vertical:
            end = hint.height()
        else:
            end = hint.width()

        self.__animation.setStartValue(0)
        self.__animation.setEndValue(end)
        self.__animation.start()

    def close(self):
        # type: () -> None
        """Close the controlled widget (shrink to size 0).
        """
        self.__expanded = False
        self.__action.setChecked(False)

        if self.__splitter is None or self.__widget is None:
            return

        self.__animation.setStartValue(self.size())
        self.__animation.setEndValue(0)
        self.__animation.start()

    def setExpanded(self, expanded):
        # type: (bool) -> None
        """Set the expanded state."""
        if self.__expanded != expanded:
            if expanded:
                self.open()
            else:
                self.close()

    def expanded(self):
        # type: () -> bool
        """Return the expanded state."""
        return self.__expanded

    def __update(self):
        # type: () -> None
        """Update the splitter sizes."""
        if self.__splitter and self.__widget:
            if sum(self.__splitter.sizes()) == 0:
                # schedule update on next show event
                self.__updateOnShow = True
                return

            splitter = self.__splitter
            index = splitter.indexOf(self.__widget)
            sizes = splitter.sizes()
            current = sizes[index]
            diff = current - self.__size
            sizes[index] = self.__size
            sizes[index - 1] = sizes[index - 1] + diff
            self.__splitter.setSizes(sizes)

    def eventFilter(self, obj, event):
        # type: (QObject, QEvent) -> bool
        if event.type() == QEvent.Resize and obj is self.__widget and \
                self.__animation.state() == QPropertyAnimation.Stopped:
            # Update the expanded state when the user opens/closes the widget
            # by dragging the splitter handle.
            assert self.__splitter is not None
            assert isinstance(event, QResizeEvent)
            if self.__splitter.orientation() == Qt.Vertical:
                size = event.size().height()
            else:
                size = event.size().width()

            if self.__expanded and size == 0:
                self.__action.setChecked(False)
                self.__expanded = False
            elif not self.__expanded and size > 0:
                self.__action.setChecked(True)
                self.__expanded = True

        if event.type() == QEvent.Show and obj is self.__splitter and \
                self.__updateOnShow:
            # Update the splitter state after receiving valid geometry
            self.__updateOnShow = False
            self.__update()
        return super().eventFilter(obj, event)


class QuickHelpWidget(QuickHelp):
    def minimumSizeHint(self):
        # type: () -> QSize
        """Reimplemented to allow the Splitter to resize the widget
        with a continuous animation.
        """
        hint = super().minimumSizeHint()
        return QSize(hint.width(), 0)


class CanvasToolDock(QWidget):
    """Canvas dock widget with widget toolbox, quick help and
    canvas actions.
    """
    def __init__(self, parent=None, **kwargs):
        # type: (Optional[QWidget], Any) -> None
        super().__init__(parent, **kwargs)

        self.__setupUi()

    def __setupUi(self):
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self.toolbox = WidgetToolBox()

        self.help = QuickHelpWidget(objectName="quick-help")

        self.__splitter = QSplitter()
        self.__splitter.setOrientation(Qt.Vertical)

        self.__splitter.addWidget(self.toolbox)
        self.__splitter.addWidget(self.help)

        self.toolbar = DynamicResizeToolBar()
        self.toolbar.setMovable(False)
        self.toolbar.setFloatable(False)

        self.toolbar.setSizePolicy(QSizePolicy.Ignored,
                                   QSizePolicy.Preferred)

        layout.addWidget(self.__splitter, 10)
        layout.addWidget(self.toolbar)

        self.setLayout(layout)
        self.__splitterResizer = SplitterResizer(self)
        self.__splitterResizer.setSplitterAndWidget(self.__splitter, self.help)

    def setQuickHelpVisible(self, state):
        # type: (bool) -> None
        """Set the quick help box visibility status."""
        self.__splitterResizer.setExpanded(state)

    def quickHelpVisible(self):
        # type: () -> bool
        return self.__splitterResizer.expanded()

    def setQuickHelpAnimationEnabled(self, enabled):
        # type: (bool) -> None
        """Enable/disable the quick help animation."""
        self.__splitterResizer.setAnimationEnabled(enabled)

    def toggleQuickHelpAction(self):
        # type: () -> QAction
        """Return a checkable QAction for help show/hide."""
        return self.__splitterResizer.toggleExpandedAction()

    def toogleQuickHelpAction(self):
        warnings.warn(
            "'toogleQuickHelpAction' is deprecated, use "
            "'toggleQuickHelpAction' instead.", DeprecationWarning,
            stacklevel=2
        )
        return self.toggleQuickHelpAction()


class _ToolGridButton(ToolGridButton):
    def paintEvent(self, event: QPaintEvent) -> None:
        with StyledIconEngine.setOverridePalette(styles.breeze_light()):
            super().paintEvent(event)


class QuickCategoryToolbar(ToolGrid):
    """A toolbar with category buttons."""
    def __init__(self, parent=None, buttonSize=QSize(), iconSize=QSize(),
                 **kwargs):
        # type: (Optional[QWidget], QSize, QSize, Any) -> None
        super().__init__(parent, 1, buttonSize, iconSize,
                         Qt.ToolButtonIconOnly, **kwargs)
        self.__model = None  # type: Optional[QAbstractItemModel]

    def setColumnCount(self, count):
        raise Exception("Cannot set the column count on a Toolbar")

    def setModel(self, model):
        # type: (Optional[QAbstractItemModel]) -> None
        """
        Set the registry model.
        """
        if self.__model is not None:
            self.__model.dataChanged.disconnect(self.__on_dataChanged)
            self.__model.rowsInserted.disconnect(self.__on_rowsInserted)
            self.__model.rowsRemoved.disconnect(self.__on_rowsRemoved)
            self.clear()

        self.__model = model
        if model is not None:
            model.dataChanged.connect(self.__on_dataChanged)
            model.rowsInserted.connect(self.__on_rowsInserted)
            model.rowsRemoved.connect(self.__on_rowsRemoved)
            self.__initFromModel(model)

    def __initFromModel(self, model):
        # type: (QAbstractItemModel) -> None
        """
        Initialize the toolbar from the model.
        """
        for index in iter_index(model, QModelIndex()):
            action = self.createActionForItem(index)
            self.addAction(action)

    def createActionForItem(self, index):
        # type: (QModelIndex) -> QAction
        """
        Create the QAction instance for item at `index` (`QModelIndex`).
        """
        action = QAction(
            item_icon(index), item_text(index), self,
            toolTip=item_tooltip(index)
        )
        action.setData(QPersistentModelIndex(index))
        return action

    def createButtonForAction(self, action):
        # type: (QAction) -> QToolButton
        """
        Create a button for the action.
        """
        button = _ToolGridButton(
            self,
            toolButtonStyle=self.toolButtonStyle(),
            iconSize=self.iconSize(),
        )
        button.setDefaultAction(action)
        if self.buttonSize().isValid():
            button.setFixedSize(self.buttonSize())
        item = action.data()  # QPersistentModelIndex
        assert isinstance(item, QPersistentModelIndex)

        brush = item.data(Qt.BackgroundRole)
        if not isinstance(brush, QBrush):
            brush = item.data(QtWidgetRegistry.BACKGROUND_ROLE)
            if not isinstance(brush, QBrush):
                brush = self.palette().brush(QPalette.Button)

        palette = button.palette()
        palette.setColor(QPalette.Button, brush.color())
        palette.setColor(QPalette.Window, brush.color())
        button.setPalette(palette)
        button.setProperty("quick-category-toolbutton", True)

        style_sheet = ("QToolButton {\n"
                       "    background: %s;\n"
                       "    border: none;\n"
                       "    border-bottom: 1px solid palette(mid);\n"
                       "}")
        button.setStyleSheet(style_sheet % create_css_gradient(brush.color()))

        return button

    def __on_dataChanged(self, topLeft, bottomRight):
        # type: (QModelIndex, QModelIndex) -> None
        assert self.__model is not None
        parent = topLeft.parent()
        if not parent.isValid():
            for row in range(topLeft.row(), bottomRight.row() + 1):
                item = self.__model.index(row, 0)
                action = self.actions()[row]
                action.setText(item_text(item))
                action.setIcon(item_icon(item))
                action.setToolTip(item_tooltip(item))

    def __on_rowsInserted(self, parent, start, end):
        # type: (QModelIndex, int, int) -> None
        assert self.__model is not None
        if not parent.isValid():
            for row in range(start, end + 1):
                item = self.__model.index(row, 0)
                self.insertAction(row, self.createActionForItem(item))

    def __on_rowsRemoved(self, parent, start, end):
        # type: (QModelIndex, int, int) -> None
        assert self.__model is not None
        if not parent.isValid():
            for row in range(end, start - 1, -1):
                action = self.actions()[row]
                self.removeAction(action)


# This implements the (single category) node selection popup when the
# tooldock is not expanded.
class CategoryPopupMenu(FramelessWindow):
    """
    A menu popup from which nodes can be dragged or clicked/activated.
    """
    triggered = Signal(QAction)
    hovered = Signal(QAction)

    def __init__(self, parent=None, **kwargs):
        # type: (Optional[QWidget], Any) -> None
        super().__init__(parent, **kwargs)
        self.setWindowFlags(self.windowFlags() | Qt.Popup)

        layout = QVBoxLayout()
        layout.setContentsMargins(6, 6, 6, 6)

        self.__menu = MenuPage()
        self.__menu.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE)

        if sys.platform == "darwin":
            self.__menu.view().setAttribute(Qt.WA_MacShowFocusRect, False)

        self.__menu.triggered.connect(self.__onTriggered)
        self.__menu.hovered.connect(self.hovered)

        self.__dragListener = ItemViewDragStartEventListener(self)
        self.__dragListener.dragStarted.connect(self.__onDragStarted)

        self.__menu.view().viewport().installEventFilter(self.__dragListener)
        self.__menu.view().installEventFilter(self)

        layout.addWidget(self.__menu)

        self.setLayout(layout)

        self.__action = None  # type: Optional[QAction]
        self.__loop = None    # type: Optional[QEventLoop]

    def setCategoryItem(self, item):
        """
        Set the category root item (:class:`QStandardItem`).
        """
        warnings.warn(
            "setCategoryItem is deprecated. Use the more general 'setModel'"
            "and setRootIndex", DeprecationWarning, stacklevel=2
        )
        model = item.model()
        self.__menu.setModel(model)
        self.__menu.setRootIndex(item.index())

    def setModel(self, model):
        # type: (QAbstractItemModel) -> None
        """
        Set the model.

        Parameters
        ----------
        model : QAbstractItemModel
        """
        self.__menu.setModel(model)

    def setRootIndex(self, index):
        # type: (QModelIndex) -> None
        """
        Set the root index in `model`.

        Parameters
        ----------
        index : QModelIndex
        """
        self.__menu.setRootIndex(index)

    def setActionRole(self, role):
        # type: (Qt.ItemDataRole) -> None
        """
        Set the action role in model.

        This is an item role in `model` that returns a QAction for the item.

        Parameters
        ----------
        role : Qt.ItemDataRole
        """
        self.__menu.setActionRole(role)

    def popup(self, pos=None):
        # type: (Optional[QPoint]) -> None
        """
        Show the popup at `pos`.

        Parameters
        ----------
        pos : Optional[QPoint]
            The position in global screen coordinates
        """
        if pos is None:
            pos = self.pos()
        self.adjustSize()
        geom = widget_popup_geometry(pos, self)
        self.setGeometry(geom)
        self.show()
        self.__menu.view().setFocus()

    def exec(self, pos=None):
        # type: (Optional[QPoint]) -> Optional[QAction]
        self.popup(pos)
        self.__loop = QEventLoop()

        self.__action = None
        self.__loop.exec()
        self.__loop = None

        if self.__action is not None:
            action = self.__action
        else:
            action = None
        return action

    def exec_(self, *args, **kwargs):
        warnings.warn(
            "exec_ is deprecated, use exec", DeprecationWarning, stacklevel=2
        )
        return self.exec(*args, **kwargs)

    def hideEvent(self, event):
        # type: (QHideEvent) -> None
        if self.__loop is not None:
            self.__loop.exit(0)
        super().hideEvent(event)

    def __onTriggered(self, action):
        # type: (QAction) -> None
        self.__action = action
        self.triggered.emit(action)
        self.hide()

        if self.__loop:
            self.__loop.exit(0)

    def __onDragStarted(self, index):
        # type: (QModelIndex) -> None
        desc = index.data(QtWidgetRegistry.WIDGET_DESC_ROLE)
        icon = index.data(Qt.DecorationRole)

        drag_data = QMimeData()
        drag_data.setData(
            "application/vnd.orange-canvas.registry.qualified-name",
            desc.qualified_name.encode('utf-8')
        )
        drag = QDrag(self)
        drag.setPixmap(icon.pixmap(38))
        drag.setMimeData(drag_data)

        # TODO: Should animate (accept) hide.
        self.hide()

        # When a drag is started and the menu hidden the item's tool tip
        # can still show for a short time UNDER the cursor preventing a
        # drop.
        viewport = self.__menu.view().viewport()
        filter = ToolTipEventFilter()
        viewport.installEventFilter(filter)

        drag.exec(Qt.CopyAction)

        viewport.removeEventFilter(filter)

    def eventFilter(self, obj, event):
        if isinstance(obj, QTreeView) and event.type() == QEvent.KeyPress:
            key = event.key()
            if key in [Qt.Key_Return, Qt.Key_Enter]:
                curr = obj.currentIndex()
                if curr.isValid():
                    obj.activated.emit(curr)
                    return True
        return super().eventFilter(obj, event)


class ItemViewDragStartEventListener(QObject):
    dragStarted = Signal(QModelIndex)

    def __init__(self, parent=None, **kwargs):
        # type: (Optional[QObject], Any) -> None
        super().__init__(parent, **kwargs)
        self._pos = None    # type: Optional[QPoint]
        self._index = None  # type: Optional[QPersistentModelIndex]

    def eventFilter(self, viewport, event):
        # type: (QObject, QEvent) -> bool
        view = viewport.parent()

        if event.type() == QEvent.MouseButtonPress and \
                event.button() == Qt.LeftButton:

            index = view.indexAt(event.pos())

            if index is not None:
                self._pos = event.pos()
                self._index = QPersistentModelIndex(index)

        elif event.type() == QEvent.MouseMove and self._pos is not None and \
                ((self._pos - event.pos()).manhattanLength() >=
                 QApplication.startDragDistance()):
            assert self._index is not None
            if self._index.isValid():
                # Map to a QModelIndex in the model.
                index = QModelIndex(self._index)
                self._pos = None
                self._index = None

                self.dragStarted.emit(index)

        return super().eventFilter(view, event)


class ToolTipEventFilter(QObject):
    def eventFilter(self, receiver, event):
        # type: (QObject, QEvent) -> bool
        if event.type() == QEvent.ToolTip:
            return True

        return super().eventFilter(receiver, event)


def widget_popup_geometry(pos, widget):
    # type: (QPoint, QWidget) -> QRect
    widget.ensurePolished()

    if widget.testAttribute(Qt.WA_Resized):
        size = widget.size()
    else:
        size = widget.sizeHint()

    screen = QApplication.screenAt(pos)
    if screen is None:
        screen = QApplication.primaryScreen()
    screen_geom = screen.availableGeometry()
    size = size.boundedTo(screen_geom.size())
    geom = QRect(pos, size)

    if geom.top() < screen_geom.top():
        geom.moveTop(screen_geom.top())
    if geom.left() < screen_geom.left():
        geom.moveLeft(screen_geom.left())

    bottom_margin = screen_geom.bottom() - geom.bottom()
    right_margin = screen_geom.right() - geom.right()
    if bottom_margin < 0:
        # Falls over the bottom of the screen, move it up.
        geom.translate(0, bottom_margin)

    # TODO: right to left locale
    if right_margin < 0:
        # Falls over the right screen edge, move the menu to the
        # other side of pos.
        geom.translate(-size.width(), 0)

    return geom


def popup_position_from_source(popup, source, orientation=Qt.Vertical):
    # type: (QWidget, QWidget, Qt.Orientation) -> QPoint
    popup.ensurePolished()
    source.ensurePolished()

    if popup.testAttribute(Qt.WA_Resized):
        size = popup.size()
    else:
        size = popup.sizeHint()

    screen_geom = available_screen_geometry(source)
    source_rect = QRect(source.mapToGlobal(QPoint(0, 0)), source.size())

    if orientation == Qt.Vertical:
        if source_rect.right() + size.width() < screen_geom.right():
            x = source_rect.right()
        else:
            x = source_rect.left() - size.width()

        # bottom overflow
        dy = source_rect.top() + size.height() - screen_geom.bottom()
        if dy < 0:
            y = source_rect.top()
        else:
            y = max(screen_geom.top(), source_rect.top() - dy)
    else:
        # right overflow
        dx = source_rect.left() + size.width() - screen_geom.right()
        if dx < 0:
            x = source_rect.left()
        else:
            x = max(source_rect.left() - dx, screen_geom.left())

        if source_rect.bottom() + size.height() < screen_geom.bottom():
            y = source_rect.bottom()
        else:
            y = source_rect.top() - size.height()

    return QPoint(x, y)
