# (C) Copyright 2005-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

from itertools import combinations
import logging


from pyface.qt import QtCore, QtGui, is_qt4


from traits.api import Any, HasTraits


from pyface.tasks.task_layout import (
    LayoutContainer,
    PaneItem,
    Tabbed,
    Splitter,
    HSplitter,
    VSplitter,
)
from .dock_pane import AREA_MAP

# Contants.
ORIENTATION_MAP = {
    "horizontal": QtCore.Qt.Orientation.Horizontal,
    "vertical": QtCore.Qt.Orientation.Vertical,
}

# Logging.
logger = logging.getLogger(__name__)


class MainWindowLayout(HasTraits):
    """ A class for applying declarative layouts to a QMainWindow.
    """

    # 'MainWindowLayout' interface -----------------------------------------

    # The QMainWindow control to lay out.
    control = Any()

    # ------------------------------------------------------------------------
    # 'MainWindowLayout' interface.
    # ------------------------------------------------------------------------

    def get_layout(self, layout, include_sizes=True):
        """ Get the layout by adding sublayouts to the specified DockLayout.
        """
        for name, q_dock_area in AREA_MAP.items():
            sublayout = self.get_layout_for_area(q_dock_area, include_sizes)
            setattr(layout, name, sublayout)

    def get_layout_for_area(self, q_dock_area, include_sizes=True):
        """ Gets a LayoutItem for the specified dock area.
        """
        # Build the initial set of leaf-level items.
        items = set()
        rects = {}
        for child in self.control.children():
            # Iterate through *visibile* dock widgets. (Inactive tabbed dock
            # widgets are "visible" but have invalid positions.)
            if (
                isinstance(child, QtGui.QDockWidget)
                and child.isVisible()
                and self.control.dockWidgetArea(child) == q_dock_area
                and child.x() >= 0
                and child.y() >= 0
            ):
                # Get the list of dock widgets in this tab group in order.
                geometry = child.geometry()
                tabs = [
                    tab
                    for tab in self.control.tabifiedDockWidgets(child)
                    if tab.isVisible()
                ]
                if tabs:
                    tab_bar = self._get_tab_bar(child)
                    tab_index = tab_bar.currentIndex()
                    tabs.insert(tab_index, child)
                    geometry = tab_bar.geometry().united(geometry)

                # Create the leaf-level item for the child.
                if tabs:
                    panes = [
                        self._prepare_pane(dock_widget, include_sizes)
                        for dock_widget in tabs
                    ]
                    item = Tabbed(*panes, active_tab=panes[tab_index].id)
                else:
                    item = self._prepare_pane(child, include_sizes)
                items.add(item)
                rects[item] = geometry

        # Build the layout tree bottom-up, in multiple passes.
        while len(items) > 1:
            add, remove = set(), set()

            for item1, item2 in combinations(items, 2):
                if item1 not in remove and item2 not in remove:
                    rect1, rect2 = rects[item1], rects[item2]
                    orient = self._get_division_orientation(rect1, rect2, True)
                    if orient == QtCore.Qt.Orientation.Horizontal:
                        if rect1.y() < rect2.y():
                            item = VSplitter(item1, item2)
                        else:
                            item = VSplitter(item2, item1)
                    elif orient == QtCore.Qt.Orientation.Vertical:
                        if rect1.x() < rect2.x():
                            item = HSplitter(item1, item2)
                        else:
                            item = HSplitter(item2, item1)
                    else:
                        continue
                    rects[item] = rect1.united(rect2)
                    add.add(item)
                    remove.update((item1, item2))

            if add or remove:
                items.update(add)
                items.difference_update(remove)
            else:
                # Raise an exception instead of falling into an infinite loop.
                raise RuntimeError(
                    "Unable to extract layout from QMainWindow."
                )

        if items:
            return items.pop()
        return None

    def set_layout(self, layout):
        """ Applies a DockLayout to the window.
        """
        # Remove all existing dock widgets.
        for child in self.control.children():
            if isinstance(child, QtGui.QDockWidget):
                child.hide()
                self.control.removeDockWidget(child)

        # Perform the layout. This will assign fixed sizes to the dock widgets
        # to enforce size constraints specified in the PaneItems.
        for name, q_dock_area in AREA_MAP.items():
            sublayout = getattr(layout, name)
            if sublayout:
                self.set_layout_for_area(
                    sublayout, q_dock_area, _toplevel_call=False
                )

        if is_qt4:
            # Remove the fixed sizes once Qt activates the layout.
            QtCore.QTimer.singleShot(0, self._reset_fixed_sizes)

    def set_layout_for_area(
        self, layout, q_dock_area, _toplevel_added=False, _toplevel_call=True
    ):
        """ Applies a LayoutItem to the specified dock area.
        """
        # If we try to do the layout bottom-up, Qt will become confused. In
        # order to do it top-down, we have know which dock widget is
        # "effectively" top level, requiring us to reach down to the leaves of
        # the layout. (This is really only an issue for Splitter layouts, since
        # Tabbed layouts are, for our purposes, leaves.)

        if isinstance(layout, PaneItem):
            if not _toplevel_added:
                widget = self._prepare_toplevel_for_item(layout)
                if widget:
                    self.control.addDockWidget(q_dock_area, widget)
                    widget.show()

        elif isinstance(layout, Tabbed):
            active_widget = first_widget = None
            for item in layout.items:
                widget = self._prepare_toplevel_for_item(item)
                if not widget:
                    continue
                if item.id == layout.active_tab:
                    active_widget = widget
                if first_widget:
                    self.control.tabifyDockWidget(first_widget, widget)
                else:
                    if not _toplevel_added:
                        self.control.addDockWidget(q_dock_area, widget)
                    first_widget = widget
                widget.show()

            # Activate the appropriate tab, if possible.
            if not active_widget:
                # By default, Qt will activate the last widget.
                active_widget = first_widget
            if active_widget:
                # It seems that the 'raise_' call only has an effect after the
                # QMainWindow has performed its internal layout.
                QtCore.QTimer.singleShot(0, active_widget.raise_)

        elif isinstance(layout, Splitter):
            # Perform top-level splitting as per above comment.
            orient = ORIENTATION_MAP[layout.orientation]
            prev_widget = None
            for item in layout.items:
                widget = self._prepare_toplevel_for_item(item)
                if not widget:
                    continue
                if prev_widget:
                    self.control.splitDockWidget(prev_widget, widget, orient)
                elif not _toplevel_added:
                    self.control.addDockWidget(q_dock_area, widget)
                prev_widget = widget
                widget.show()

            # Now we can recurse.
            for i, item in enumerate(layout.items):
                self.set_layout_for_area(
                    item,
                    q_dock_area,
                    _toplevel_added=True,
                    _toplevel_call=False,
                )

        else:
            raise MainWindowLayoutError("Unknown layout item %r" % layout)

        if is_qt4:
            # Remove the fixed sizes once Qt activates the layout.
            if _toplevel_call:
                QtCore.QTimer.singleShot(0, self._reset_fixed_sizes)

    # ------------------------------------------------------------------------
    # 'MainWindowLayout' abstract interface.
    # ------------------------------------------------------------------------

    def _get_dock_widget(self, pane):
        """ Returns the QDockWidget associated with a PaneItem.
        """
        raise NotImplementedError()

    def _get_pane(self, dock_widget):
        """ Returns a PaneItem for a QDockWidget.
        """
        raise NotImplementedError()

    # ------------------------------------------------------------------------
    # Private interface.
    # ------------------------------------------------------------------------

    def _get_division_orientation(self, one, two, splitter=False):
        """ Returns whether there is a division between two visible QWidgets.

        Divided in context means that the widgets are adjacent and aligned along
        the direction of the adjaceny.
        """
        united = one.united(two)
        if splitter:
            sep = self.control.style().pixelMetric(
                QtGui.QStyle.PixelMetric.PM_DockWidgetSeparatorExtent, None, self.control
            )
            united.adjust(0, 0, -sep, -sep)

        if (
            one.x() == two.x()
            and one.width() == two.width()
            and united.height() == one.height() + two.height()
        ):
            return QtCore.Qt.Orientation.Horizontal

        elif (
            one.y() == two.y()
            and one.height() == two.height()
            and united.width() == one.width() + two.width()
        ):
            return QtCore.Qt.Orientation.Vertical

        return 0

    def _get_tab_bar(self, dock_widget):
        """ Returns the tab bar associated with the given QDockWidget, or None
        if there is no tab bar.
        """
        dock_geometry = dock_widget.geometry()
        for child in self.control.children():
            if isinstance(child, QtGui.QTabBar) and child.isVisible():
                geometry = child.geometry()
                if self._get_division_orientation(dock_geometry, geometry):
                    return child
        return None

    def _prepare_pane(self, dock_widget, include_sizes=True):
        """ Returns a sized PaneItem for a QDockWidget.
        """
        pane = self._get_pane(dock_widget)
        if include_sizes:
            pane.width = dock_widget.widget().width()
            pane.height = dock_widget.widget().height()
        return pane

    def _prepare_toplevel_for_item(self, layout):
        """ Returns a sized toplevel QDockWidget for a LayoutItem.
        """
        if isinstance(layout, PaneItem):
            dock_widget = self._get_dock_widget(layout)
            if dock_widget is None:
                logger.warning(
                    "Cannot retrieve dock widget for pane %r" % layout.id
                )
            else:
                if is_qt4:
                    if layout.width > 0:
                        dock_widget.widget().setFixedWidth(layout.width)
                    if layout.height > 0:
                        dock_widget.widget().setFixedHeight(layout.height)
                else:
                    sizeHint = lambda: QtCore.QSize(layout.width, layout.height)
                    dock_widget.widget().sizeHint = sizeHint
            return dock_widget

        elif isinstance(layout, LayoutContainer):
            return self._prepare_toplevel_for_item(layout.items[0])

        else:
            raise MainWindowLayoutError("Leaves of layout must be PaneItems")

    def _reset_fixed_sizes(self):
        """ Clears any fixed sizes assined to QDockWidgets.
        """
        if self.control is None:
            return
        QWIDGETSIZE_MAX = (1 << 24) - 1  # Not exposed by Qt bindings.
        for child in self.control.children():
            if isinstance(child, QtGui.QDockWidget):
                child.widget().setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)
                child.widget().setMinimumSize(0, 0)
                # QDockWidget somehow manages to set its own
                # min/max sizes and hence that too needs to be reset.
                child.setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)
                child.setMinimumSize(0, 0)


class MainWindowLayoutError(ValueError):
    """ Exception raised when a malformed LayoutItem is passed to the
    MainWindowLayout.
    """

    pass
