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

"""
Base plugin class
"""

# Standard library imports
import configparser
import inspect
import os
import sys

# Third party imports
from qtpy.QtCore import QByteArray, Qt, Slot
from qtpy.QtGui import QCursor, QKeySequence
from qtpy.QtWidgets import (QApplication, QMainWindow, QMenu, QMessageBox,
                            QShortcut, QToolButton)

# Local imports
from spyder.config.base import _
from spyder.config.gui import get_color_scheme, get_font
from spyder.config.manager import CONF
from spyder.config.user import NoDefault
from spyder.py3compat import is_text_string, qbytearray_to_str
from spyder.utils.icon_manager import ima
from spyder.utils.qthelpers import (
    add_actions, create_action, create_toolbutton, MENU_SEPARATOR,
    toggle_actions, set_menu_icons)
from spyder.utils.stylesheet import APP_STYLESHEET
from spyder.widgets.dock import DockTitleBar, SpyderDockWidget


class BasePluginMixin(object):
    """Implementation of the basic functionality for Spyder plugins."""

    # Define configuration name map for plugin to split configuration
    # among several files. See spyder/config/main.py
    # Status: Hidden
    _CONF_NAME_MAP = None

    def __init__(self, parent=None):
        super(BasePluginMixin, self).__init__()

        # Check compatibility
        check_compatibility, message = self.check_compatibility()

        self._register_plugin()

        self._is_compatible = True
        if not check_compatibility:
            self._is_compatible = False
            self._show_compatibility_message(message)

    def _register_plugin(self):
        """Register plugin configuration."""
        CONF.register_plugin(self)

    def _set_option(self, option, value, section=None,
                    recursive_notification=True):
        """Set option in spyder.ini"""
        section = self.CONF_SECTION if section is None else section
        CONF.set(section, str(option), value,
                 recursive_notification=recursive_notification)

    def _get_option(self, option, default=NoDefault, section=None):
        """Get option from spyder.ini."""
        section = self.CONF_SECTION if section is None else section
        return CONF.get(section, option, default)

    def _remove_option(self, option, section=None):
        """Remove option from spyder.ini."""
        section = self.CONF_SECTION if section is None else section
        CONF.remove_option(section, option)

    def _show_status_message(self, message, timeout=0):
        """Show message in main window's status bar."""
        self.main.statusBar().showMessage(message, timeout)

    def _show_compatibility_message(self, message):
        """Show a compatibility message."""
        messageBox = QMessageBox(self)
        messageBox.setWindowModality(Qt.NonModal)
        messageBox.setAttribute(Qt.WA_DeleteOnClose)
        messageBox.setWindowTitle('Compatibility Check')
        messageBox.setText(message)
        messageBox.setStandardButtons(QMessageBox.Ok)
        messageBox.show()

    def _starting_long_process(self, message):
        """
        Show message in main window's status bar and change cursor to
        Qt.WaitCursor
        """
        self._show_status_message(message)
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        QApplication.processEvents()

    def _ending_long_process(self, message=""):
        """
        Clear main window's status bar and restore mouse cursor.
        """
        QApplication.restoreOverrideCursor()
        self._show_status_message(message, timeout=2000)
        QApplication.processEvents()

    def _get_plugin_path(self):
        """Return filesystem path to the root directory of the plugin."""
        return os.path.dirname(inspect.getfile(self.__class__))

    def _create_configwidget(self, dlg, main_window):
        """Create configuration dialog box page widget"""
        if self.CONFIGWIDGET_CLASS is not None:
            parent = self
            main = dlg
            if not hasattr(self, 'dockwidget'):
                # Prevent QWidget assignment to a plugin that does not have
                # a graphical widget.
                parent = dlg
                main = main_window
            configwidget = self.CONFIGWIDGET_CLASS(parent, main)
            configwidget.initialize()
            return configwidget


class PluginWindow(QMainWindow):
    """MainWindow subclass that contains a Spyder Plugin."""

    def __init__(self, plugin):
        QMainWindow.__init__(self)
        self.plugin = plugin

        # Setting interface theme
        self.setStyleSheet(str(APP_STYLESHEET))

    def closeEvent(self, event):
        """Reimplement Qt method."""
        self.plugin.set_ancestor(self.plugin.main)
        self.plugin.dockwidget.setWidget(self.plugin)
        self.plugin.dockwidget.setVisible(True)
        self.plugin.switch_to_plugin()

        # Save window geometry to restore it when undocking the plugin
        # again.
        geometry = self.saveGeometry()
        self.plugin.set_option('window_geometry', qbytearray_to_str(geometry))

        # Close window
        QMainWindow.closeEvent(self, event)

        # Qt might want to do something with this soon,
        # So it should not be deleted by python yet.
        # Fixes spyder-ide/spyder#10704
        self.plugin.__unsafe__window = self
        self.plugin._undocked_window = None


class BasePluginWidgetMixin(object):
    """
    Implementation of the basic functionality for Spyder plugin widgets.
    """

    def __init__(self, parent=None):
        super(BasePluginWidgetMixin, self).__init__()

        # Actions to add to the Options menu
        self._plugin_actions = None

        # Attribute to keep track if the plugin is undocked in a
        # separate window
        self._undocked_window = None

        self._ismaximized = False
        self._default_margins = None
        self._isvisible = False

        self.shortcut = None

        # Options buttons
        self.options_button = create_toolbutton(self, text=_('Options'),
                                                icon=ima.icon('tooloptions'))
        self.options_button.setPopupMode(QToolButton.InstantPopup)

        # Options menu
        self._options_menu = QMenu(self)

        # We decided to create our own toggle action instead of using
        # the one that comes with dockwidget because it's not possible
        # to raise and focus the plugin with it.
        self._toggle_view_action = None

        # Default actions for Options menu
        self._dock_action = create_action(
            self,
            _("Dock"),
            icon=ima.icon('dock'),
            tip=_("Dock the pane"),
            triggered=self._close_window)

        self._lock_unlock_action = create_action(
            self,
            text=_("Move"),
            tip=_("Unlock to move pane to another position"),
            icon=ima.icon('drag_dock_widget'),
            triggered=self._lock_unlock_position,
        )

        self._undock_action = create_action(
            self,
            _("Undock"),
            icon=ima.icon('undock'),
            tip=_("Undock the pane"),
            triggered=self._create_window)

        self._close_plugin_action = create_action(
            self,
            _("Close"),
            icon=ima.icon('close_pane'),
            tip=_("Close the pane"),
            triggered=self._plugin_closed)

    def _initialize_plugin_in_mainwindow_layout(self):
        """
        If this is the first time the plugin is shown, perform actions to
        initialize plugin position in Spyder's window layout.

        Use on_first_registration to define the actions to be run
        by your plugin
        """
        if self.get_option('first_time', True):
            try:
                self.on_first_registration()
            except NotImplementedError:
                return
            self.set_option('first_time', False)

    def _update_margins(self):
        """Update plugin margins"""
        layout = self.layout()
        if self._default_margins is None:
            self._default_margins = layout.getContentsMargins()
        if CONF.get('main', 'use_custom_margin'):
            margin = CONF.get('main', 'custom_margin')
            layout.setContentsMargins(*[margin]*4)
        else:
            layout.setContentsMargins(*self._default_margins)

    def _update_plugin_title(self):
        """Update plugin title, i.e. dockwidget or window title"""
        if self.dockwidget is not None:
            win = self.dockwidget
        elif self._undocked_window is not None:
            win = self._undocked_window
        else:
            return
        win.setWindowTitle(self.get_plugin_title())

    def _create_dockwidget(self):
        """Add to parent QMainWindow as a dock widget"""
        # Creating dock widget
        dock = SpyderDockWidget(self.get_plugin_title(), self.main)

        # Set properties
        dock.setObjectName(self.__class__.__name__+"_dw")
        dock.setAllowedAreas(dock.ALLOWED_AREAS)
        dock.setFeatures(dock.FEATURES)
        dock.setWidget(self)
        self._update_margins()
        dock.visibilityChanged.connect(self._visibility_changed)
        dock.topLevelChanged.connect(self._on_top_level_changed)
        dock.sig_plugin_closed.connect(self._plugin_closed)
        dock.sig_title_bar_shown.connect(self._on_title_bar_shown)
        self.dockwidget = dock

        # NOTE: Don't use the default option of CONF.get to assign a
        # None shortcut to plugins that don't have one. That will mess
        # the creation of our Keyboard Shortcuts prefs page
        try:
            context = '_'
            name = 'switch to {}'.format(self.CONF_SECTION)
            self.shortcut = CONF.get_shortcut(context, name,
                                              plugin_name=self.CONF_SECTION)
        except (configparser.NoSectionError, configparser.NoOptionError):
            pass

        if self.shortcut is not None and self.main is not None:
            sc = QShortcut(QKeySequence(self.shortcut), self.main,
                           self.switch_to_plugin)
            self.register_shortcut(sc, "_", "Switch to {}".format(
                self.CONF_SECTION))

        return (dock, dock.LOCATION)

    def _switch_to_plugin(self):
        """Switch to plugin."""
        self.main.switch_to_plugin(self)

    @Slot()
    def _plugin_closed(self):
        """DockWidget was closed."""
        if self._toggle_view_action:
            self._toggle_view_action.setChecked(False)

    def _get_font(self, rich_text=False):
        """Return plugin font."""
        if rich_text:
            option = 'rich_font'
            font_size_delta = self.RICH_FONT_SIZE_DELTA
        else:
            option = 'font'
            font_size_delta = self.FONT_SIZE_DELTA

        return get_font(option=option, font_size_delta=font_size_delta)

    def set_plugin_font(self):
        """
        Set plugin font option.

        Note: All plugins in Spyder use a global font. To define a different
        size, the plugin must define a 'FONT_SIZE_DELTA' class variable.
        """
        raise Exception("Plugins font is based on the general settings, "
                        "and cannot be set directly on the plugin."
                        "This method is deprecated.")

    def _create_toggle_view_action(self):
        """Associate a toggle view action with each plugin"""
        title = self.get_plugin_title()
        if self.CONF_SECTION == 'editor':
            title = _('Editor')
        if self.shortcut is not None:
            action = create_action(self, title,
                             toggled=lambda checked: self.toggle_view(checked),
                             shortcut=QKeySequence(self.shortcut),
                             context=Qt.WidgetShortcut)
        else:
            action = create_action(self, title, toggled=lambda checked:
                                                self.toggle_view(checked))
        self._toggle_view_action = action

    @Slot()
    def _close_window(self):
        """Close QMainWindow instance that contains this plugin."""
        if self._undocked_window is not None:
            # Save window geometry to restore it when undocking the plugin
            # again.
            geometry = self._undocked_window.saveGeometry()
            self.set_option('window_geometry', qbytearray_to_str(geometry))

            self._undocked_window.close()
            self._undocked_window = None

            # Oddly, these actions can appear disabled after the Dock
            # action is pressed
            self._undock_action.setDisabled(False)
            self._close_plugin_action.setDisabled(False)

    @Slot()
    def _create_window(self):
        """Create a QMainWindow instance containing this plugin."""
        self._undocked_window = window = PluginWindow(self)
        window.setAttribute(Qt.WA_DeleteOnClose)
        icon = self.get_plugin_icon()
        if is_text_string(icon):
            icon = self.get_icon(icon)

        window.setWindowIcon(icon)
        window.setWindowTitle(self.get_plugin_title())
        window.setCentralWidget(self)
        window.resize(self.size())

        # Restore window geometry
        geometry = self.get_option('window_geometry', default='')
        if geometry:
            try:
                window.restoreGeometry(
                    QByteArray().fromHex(str(geometry).encode('utf-8'))
                )
            except Exception:
                pass

        self.refresh_plugin()
        self.set_ancestor(window)
        self.dockwidget.setFloating(False)
        self.dockwidget.setVisible(False)

        window.show()

    @Slot(bool)
    def _on_top_level_changed(self, top_level):
        """Actions to perform when a plugin is undocked to be moved."""
        if top_level:
            self._undock_action.setDisabled(True)
        else:
            self._undock_action.setDisabled(False)

    def _visibility_changed(self, enable):
        """Dock widget visibility has changed."""
        if self.dockwidget is None:
            return
        if enable:
            self.dockwidget.raise_()
            widget = self.get_focus_widget()
            if widget is not None and self._undocked_window is not None:
                widget.setFocus()
        visible = self.dockwidget.isVisible() or self._ismaximized
        if self.DISABLE_ACTIONS_WHEN_HIDDEN:
            toggle_actions(self._plugin_actions, visible)
        self._isvisible = enable and visible
        if self._isvisible:
            self.refresh_plugin()

    def _refresh_actions(self):
        """Refresh Options menu."""
        self._options_menu.clear()

        # Decide what additional actions to show
        if self._undocked_window is None:
            additional_actions = [MENU_SEPARATOR,
                                  self._lock_unlock_action,
                                  self._undock_action,
                                  self._close_plugin_action]
        else:
            additional_actions = [MENU_SEPARATOR,
                                  self._dock_action]

        # Create actions list
        self._plugin_actions = self.get_plugin_actions() + additional_actions
        add_actions(self._options_menu, self._plugin_actions)

        if sys.platform == 'darwin':
            set_menu_icons(self._options_menu, True)

    def _setup(self):
        """
        Setup Options menu, create toggle action and connect signals.
        """
        # Creat toggle view action
        self._create_toggle_view_action()

        # Create Options menu
        self._plugin_actions = (
            self.get_plugin_actions() +
            [MENU_SEPARATOR, self._lock_unlock_action, self._undock_action]
        )
        add_actions(self._options_menu, self._plugin_actions)
        self.options_button.setMenu(self._options_menu)
        self._options_menu.aboutToShow.connect(self._refresh_actions)

        # Show icons in Mac plugin menus
        if sys.platform == 'darwin':
            self._options_menu.aboutToHide.connect(
                lambda menu=self._options_menu:
                set_menu_icons(menu, False))

        # Update title
        self.sig_update_plugin_title.connect(self._update_plugin_title)
        self.setWindowTitle(self.get_plugin_title())

    def _get_color_scheme(self):
        """Get the current color scheme."""
        return get_color_scheme(CONF.get('appearance', 'selected'))

    def _add_dockwidget(self):
        """Add dockwidget to the main window and set it up."""
        self.main.add_dockwidget(self)

        # This is not necessary for the Editor because it calls
        # _setup directly on init.
        if self.CONF_SECTION != 'editor':
            self._setup()

    def _tabify(self, core_plugin):
        """Tabify plugin next to a core plugin."""
        self.main.layouts.tabify_plugins(core_plugin, self)

    def _lock_unlock_position(self):
        """Show/hide title bar to move/lock position."""
        if isinstance(self.dockwidget.titleBarWidget(), DockTitleBar):
            self.dockwidget.remove_title_bar()
        else:
            self.dockwidget.set_title_bar()

    @Slot(bool)
    def _on_title_bar_shown(self, visible):
        """Actions to perform when the title bar is shown/hidden."""
        if visible:
            self._lock_unlock_action.setText(_('Lock'))
            self._lock_unlock_action.setIcon(ima.icon('lock_open'))
            for method_name in ['setToolTip', 'setStatusTip']:
                method = getattr(self._lock_unlock_action, method_name)
                method(_("Lock pane to the current position"))
        else:
            self._lock_unlock_action.setText(_('Move'))
            self._lock_unlock_action.setIcon(ima.icon('drag_dock_widget'))
            for method_name in ['setToolTip', 'setStatusTip']:
                method = getattr(self._lock_unlock_action, method_name)
                method(_("Unlock to move pane to another position"))
