# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""
Layout Plugin API.
"""

# Standard libray imports
import copy

# Third party imports
from qtpy.QtCore import QRectF, Qt, QTimer
from qtpy.QtWidgets import QGridLayout, QPlainTextEdit, QWidget

# Local imports
from spyder.api.exceptions import SpyderAPIError
from spyder.api.plugin_registration.registry import PLUGIN_REGISTRY


class BaseGridLayoutType:
    """
    A base layout type to create custom layouts for Spyder panes.

    The API for this plugin is a subset of a QGridLayout, so the same
    concepts, like row, column, spans and stretches apply.

    Notes
    -----
    See: https://doc.qt.io/qt-5/qgridlayout.html
    """

    ID = None
    """Unique string identifier for the layout."""

    def __init__(self, parent_plugin):
        self.plugin = parent_plugin
        self._areas = []
        self._area_rects = []
        self._column_stretchs = {}
        self._row_stretchs = {}
        self._default_added = False
        self._default_area = None
        self._visible_areas = []
        self._rows = 0
        self._cols = 0
        self._plugin_ids = []

    # --- Private API
    # ------------------------------------------------------------------------
    def _check_layout_validity(self):
        """
        Check the current layout is a valid one.
        """
        self._visible_areas = []
        # Check ID
        if self.ID is None:
            raise SpyderAPIError("A Layout must define an `ID` class "
                                 "attribute!")

        # Check name
        self.get_name()

        # All layouts need to add at least 1 area
        if not self._areas:
            raise SpyderAPIError("A Layout must define add least one area!")

        default_areas = []
        area_zero_zero = False

        for area in self._areas:
            default_areas.append(area["default"])
            if area["default"]:
                self._default_area = area

            self._visible_areas.append(area["visible"])

            if area_zero_zero and area["row"] == 0 and area["column"] == 0:
                raise SpyderAPIError(
                    "Multiple areas defined their row and column as 0!")

            if area["row"] == 0 and area["column"] == 0:
                area_zero_zero = True

            if not set(area["hidden_plugin_ids"]) <= set(area["plugin_ids"]):
                raise SpyderAPIError(
                    "At least 1 hidden plugin id is not being specified "
                    "in the area plugin ids list!\n SpyderLayout: {}\n "
                    "hidden_plugin_ids: {}\n"
                    "plugin_ids: {}".format(self.get_name(),
                                            area["hidden_plugin_ids"],
                                            area["plugin_ids"]))

        # Check that there is at least 1 visible!
        if not any(self._visible_areas):
            raise SpyderAPIError("At least 1 area must be `visible`")

        # Check that there is a `default` area!
        if not any(default_areas):
            raise SpyderAPIError("No area is the `default`!")

        # Check that there is 1 `default` area!
        if default_areas.count(True) != 1:
            raise SpyderAPIError("Only 1 area can be the `default`!")

        # Check one area has row zero and column zero
        if not area_zero_zero:
            raise SpyderAPIError(
                "1 area needs to be specified with row 0 and column 0!")

        # Check Area
        self._check_area()

    def _check_area(self):
        """
        Check if the current layout added areas cover the entire rectangle.

        Rectangle given by the extreme points for the added areas.
        """
        self._area_rects = []
        height = self._rows + 1
        area_float_rects = []
        delta = 0.0001
        for index, area in enumerate(self._areas):
            # These areas are used with a small delta to ensure if they are
            # next to each other they will not overlap.
            rectf = QRectF()
            rectf.setLeft(area["column"] + delta)
            rectf.setRight(area["column"] + area["col_span"] - delta)
            rectf.setTop(height - area["row"] - delta)
            rectf.setBottom(height - area["row"] - area["row_span"] + delta)
            rectf.index = index
            rectf.plugin_ids = area["plugin_ids"]
            area_float_rects.append(rectf)

            # These areas are used to calculate the actual total area
            rect = QRectF()
            rect.setLeft(area["column"])
            rect.setRight(area["column"] + area["col_span"])
            rect.setTop(height - area["row"])
            rect.setBottom(height - area["row"] - area["row_span"])
            rect.index = index
            rect.plugin_ids = area["plugin_ids"]

            self._area_rects.append(rect)

        # Check if areas are overlapping!
        for rect_1 in area_float_rects:
            for rect_2 in area_float_rects:
                if rect_1.index != rect_2.index:
                    if rect_1.intersects(rect_2):
                        raise SpyderAPIError(
                            "Area with plugins {0} is overlapping area "
                            "with plugins {1}".format(rect_1.plugin_ids,
                                                      rect_2.plugin_ids))

        # Check the total area (using corner points) versus the sum of areas
        total_area = 0
        tops = []
        rights = []
        for index, rect in enumerate(self._area_rects):
            tops.append(rect.top())
            rights.append(rect.right())
            area = abs(rect.width() * rect.height())
            total_area += area
            self._areas[index]["area"] = area

        if total_area != max(rights)*max(tops):
            raise SpyderAPIError(
                "Areas are not covering the entire section!\n"
                "Either an area is missing or col_span/row_span are "
                "not correctly set!"
            )

    # --- SpyderGridLayout API
    # ------------------------------------------------------------------------
    def get_name(self):
        """
        Return the layout localized name.

        Returns
        -------
        str
            Localized name of the layout.

        Notes
        -----
        This is a method to be able to update localization without a restart.
        """
        raise NotImplementedError("A layout must define a `get_name` method!")

    # --- Public API
    # ------------------------------------------------------------------------
    def add_area(
        self,
        plugin_ids,
        row,
        column,
        row_span=1,
        col_span=1,
        default=False,
        visible=True,
        hidden_plugin_ids=[],
    ):
        """
        Add a new area and `plugin_ids` that will populate it to the layout.

        The area will start at row, column spanning row_pan rows and
        column_span columns.

        Parameters
        ----------
        plugin_ids: list
            List of plugin ids that will be in the area
        row: int
            Initial row where the area starts
        column: int
            Initial column where the area starts
        row_span: int, optional
            Number of rows that the area covers
        col_span: int, optional
            Number of columns the area covers
        default: bool, optiona
            Defines an area as the default one, i.e all other plugins that where
            not passed in the `plugins_ids` will be added to the default area.
            By default is False.
        visible: bool, optional
            Defines if the area is visible when setting up the layout.
            Default is True.

        Notes
        -----
        See: https://doc.qt.io/qt-5/qgridlayout.html
        """
        if self._default_added and default:
            raise SpyderAPIError("A default location has already been "
                                 "defined!")

        self._plugin_ids += plugin_ids
        self._rows = max(row, self._rows)
        self._cols = max(column, self._cols)
        self._default_added = default
        self._column_stretchs[column] = 1
        self._row_stretchs[row] = 1
        self._areas.append(
            dict(
                plugin_ids=plugin_ids,
                row=row,
                column=column,
                row_span=row_span,
                col_span=col_span,
                default=default,
                visible=visible,
                hidden_plugin_ids=hidden_plugin_ids,
            )
        )

    def set_column_stretch(self, column, stretch):
        """
        Set the factor of column to stretch.

        The stretch factor is relative to the other columns in this grid.
        Columns with a higher stretch factor take more of the available space.

        Parameters
        ----------
        column: int
            The column number. The first column is number 0.
        stretch: int
            Column stretch factor.

        Notes
        -----
        See: https://doc.qt.io/qt-5/qgridlayout.html
        """
        self._column_stretchs[column] = stretch

    def set_row_stretch(self, row, stretch):
        """
        Set the factor of row to stretch.

        The stretch factor is relative to the other rows in this grid.
        Rows with a higher stretch factor take more of the available space.

        Parameters
        ----------
        row: int
            The row number. The first row is number 0.
        stretch: int
            Row stretch factor.

        Notes
        -----
        See: https://doc.qt.io/qt-5/qgridlayout.html
        """
        self._row_stretchs[row] = stretch

    def preview_layout(self, show_hidden_areas=False):
        """
        Show the layout with placeholder texts using a QWidget.
        """
        from spyder.utils.qthelpers import qapplication

        app = qapplication()
        widget = QWidget()
        layout = QGridLayout()
        for area in self._areas:
            label = QPlainTextEdit()
            label.setReadOnly(True)
            label.setPlainText("\n".join(area["plugin_ids"]))
            if area["visible"] or show_hidden_areas:
                layout.addWidget(
                    label,
                    area["row"],
                    area["column"],
                    area["row_span"],
                    area["col_span"],
                )

            # label.setVisible(area["visible"])
            if area["default"]:
                label.setStyleSheet(
                    "QPlainTextEdit {background-color: #ff0000;}")

            if not area["visible"]:
                label.setStyleSheet(
                    "QPlainTextEdit {background-color: #eeeeee;}")

        for row, stretch in self._row_stretchs.items():
            layout.setRowStretch(row, stretch)

        for col, stretch in self._column_stretchs.items():
            layout.setColumnStretch(col, stretch)

        widget.setLayout(layout)
        widget.showMaximized()
        app.exec_()

    def set_main_window_layout(self, main_window, dockable_plugins):
        """
        Set the given mainwindow layout.

        First validate the current layout definition, then clear the mainwindow
        current layout and finally calculate and set the new layout.
        """
        # Define plugins assigned to areas, all the available plugins and
        # initial docks for each area
        all_plugin_ids = []

        # External dockable plugins to show after the layout is applied
        external_plugins_to_show = []

        # Base plugins (i.e. first ones in the area) to hide after the layout
        # is applied
        base_plugins_to_hide = []

        # Actions to be performed before applying the layout
        for plugin in dockable_plugins:
            all_plugin_ids.append(plugin.NAME)

            # Save currently displayed external plugins
            if (
                plugin.NAME in PLUGIN_REGISTRY.external_plugins
                and plugin.dockwidget.isVisible()
            ):
                external_plugins_to_show.append(plugin.NAME)

            # Restore undocked_before_hiding state so that the layout is
            # applied as expected, i.e. all plugins are shown in their expected
            # positions. It also avoids an error that leaves the main window in
            # an inconsistent state.
            # Fixes spyder-ide/spyder#22494
            plugin.set_conf('window_was_undocked_before_hiding', False)

            # Dock all plugins that are undocked before applying the layout to
            # avoid odd RuntimeError's.
            # Fixes spyder-ide/spyder#22584
            if plugin.get_widget().windowwidget is not None:
                plugin.close_window()

            # Hide all plugins
            plugin.toggle_view(False)

        # Add plugins without an area assigned to the default area and made
        # them hidden. Deep copy needed since test can run multiple times with
        # the same Mainwindow instance when using the 'main_window' fixture
        patched_default_area = copy.deepcopy(self._default_area)
        unassgined_plugin_ids = list(
            set(self._plugin_ids) ^ set(all_plugin_ids))
        patched_default_area["plugin_ids"] += unassgined_plugin_ids
        patched_default_area["hidden_plugin_ids"] += unassgined_plugin_ids

        patched_areas = [
            patched_default_area if area["default"] else area
            for area in self._areas]

        # Define base plugins for each area
        docks = {}
        for area in patched_areas:
            current_area = area

            # Iterate over plugins in current_area until we find one that is
            # available
            base_plugin = None
            for base_plugin_id in current_area["plugin_ids"]:
                base_plugin = main_window.get_plugin(base_plugin_id, error=False)
                if base_plugin is not None:
                    break

            if base_plugin:
                dock = base_plugin.dockwidget

                docks[(current_area["row"], current_area["column"])] = dock
                dock.area = area["area"]
                dock.col_span = area["col_span"]
                dock.row_span = area["row_span"]

                # These plugins need to be visible at this point so that the
                # call to resizeDocks works as expected
                base_plugin.toggle_view(True)
                if not area["visible"]:
                    base_plugins_to_hide.append(base_plugin)

        # Define base layout (distribution of dockwidgets
        # following defined areas)
        layout_data = []

        # Find dock splits in the horizontal direction
        direction = Qt.Horizontal
        for row in range(0, self._rows + 1):
            dock = None
            for col in range(0, self._cols + 1):
                key = (row, col)
                if key in docks:
                    if dock is None:
                        dock = docks[key]
                    else:
                        layout_data.append(
                            (1/docks[key].area,
                             key,
                             dock,
                             docks[key],
                             direction))
                        dock = docks[key]

                    main_window.addDockWidget(
                        Qt.LeftDockWidgetArea, dock, direction)

        # Find dock splits in the vertical direction
        direction = Qt.Vertical
        for col in range(0, self._cols + 1):
            dock = None
            for row in range(0, self._rows + 1):
                key = (row, col)
                if key in docks:
                    if dock is None:
                        dock = docks[key]
                    else:
                        layout_data.append(
                            (1/docks[key].area,
                             key,
                             dock,
                             docks[key],
                             direction))
                        dock = docks[key]

        # We sort based on the inverse of the area, then the row and then
        # the column. This allows to make the dock splits in the right order.
        sorted_data = sorted(layout_data, key=lambda x: (x[0], x[1]))
        for area, key, first, second, direction in sorted_data:
            main_window.splitDockWidget(first, second, direction)

        plugins_to_tabify = []
        for area in patched_areas:
            area_visible = area["visible"]

            # Iterate over plugins in area until we find one that is available
            base_plugin = None
            for i, plugin_id in enumerate(area["plugin_ids"]):
                base_plugin = main_window.get_plugin(
                    area["plugin_ids"][i], error=False
                )
                if base_plugin is not None:
                    break

            if base_plugin:
                plugin_ids = area["plugin_ids"][i + 1 :]
                hidden_plugin_ids = area["hidden_plugin_ids"]
                for plugin_id in plugin_ids:
                    current_plugin = main_window.get_plugin(
                        plugin_id, error=False)
                    if current_plugin:
                        if (plugin_id in unassgined_plugin_ids and
                                hasattr(current_plugin, 'TABIFY')):
                            plugins_to_tabify.append(
                                (current_plugin, base_plugin))
                        else:
                            self.plugin.tabify_plugins(
                                base_plugin, current_plugin)
                            if plugin_id not in hidden_plugin_ids:
                                current_plugin.toggle_view(area_visible)
                            else:
                                current_plugin.toggle_view(False)

                # Raise front widget per area
                if area["visible"]:
                    base_plugin.dockwidget.show()
                    base_plugin.dockwidget.raise_()

        # try to use the TABIFY attribute to add the plugin to the layout.
        # Otherwise use the default area base plugin
        for plugin, base_plugin in plugins_to_tabify:
            if not self.plugin.tabify_plugin(plugin):
                self.plugin.tabify_plugins(base_plugin, plugin)
            if current_plugin:
                current_plugin.toggle_view(False)

        column_docks = []
        column_stretches = []
        for key, dock in docks.items():
            for col, stretch in self._column_stretchs.items():
                if key[1] == col and dock.col_span == 1:
                    column_docks.append(dock)
                    column_stretches.append(stretch)

        row_docks = []
        row_stretches = []
        for key, dock in docks.items():
            for row, stretch in self._row_stretchs.items():
                if key[0] == row and dock.row_span == 1:
                    row_docks.append(dock)
                    row_stretches.append(stretch)

        main_window.showMaximized()
        main_window.resizeDocks(column_docks, column_stretches, Qt.Horizontal)
        main_window.resizeDocks(row_docks, row_stretches, Qt.Vertical)

        # Hide base plugins in not visible areas
        def hide_not_visible_base_plugins():
            for plugin in base_plugins_to_hide:
                plugin.toggle_view(False)

        # We need to wait for a little bit before hiding the not visible base
        # plugins to correctly set the proportions declared in the layout for
        # rows and columns. Otherwise the calls to resizeDocks above won't have
        # the expected effect.
        QTimer.singleShot(50, hide_not_visible_base_plugins)

        # Restore displayed external plugins
        for plugin_id in external_plugins_to_show:
            plugin = main_window.get_plugin(plugin_id, error=False)
            if plugin:
                # Show plugin
                plugin.blockSignals(True)
                plugin.dockwidget.show()
                plugin.toggle_view_action.setChecked(True)
                plugin.blockSignals(False)

                # Make visible the first plugin in the dockwidget's tab bar
                # because it should be an internal one.
                dock_tabbar = plugin.dockwidget.dock_tabbar
                if dock_tabbar:
                    dock_tabbar.setCurrentIndex(0)
