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

"""
Variable Explorer Main Plugin Widget.
"""

# Third party imports
from qtpy.QtCore import QTimer, Signal, Slot
from qtpy.QtWidgets import (
    QAction, QHBoxLayout, QStackedWidget, QVBoxLayout, QWidget)

# Local imports
from spyder.api.config.decorators import on_conf_change
from spyder.api.translations import get_translation
from spyder.api.widgets.main_widget import PluginMainWidget
from spyder.plugins.variableexplorer.widgets.namespacebrowser import (
    NamespaceBrowser, NamespacesBrowserFinder, VALID_VARIABLE_CHARS)
from spyder.utils.programs import is_module_installed

# Localization
_ = get_translation('spyder')


# =============================================================================
# ---- Constants
# =============================================================================
class VariableExplorerWidgetActions:
    # Triggers
    ImportData = 'import_data_action'
    SaveData = 'save_data_action'
    SaveDataAs = 'save_data_as_action'
    ResetNamespace = 'reset_namespaces_action'
    Search = 'search'
    Refresh = 'refresh'

    # Toggles
    ToggleExcludePrivate = 'toggle_exclude_private_action'
    ToggleExcludeUpperCase = 'toggle_exclude_uppercase_action'
    ToggleExcludeCapitalized = 'toggle_exclude_capitalized_action'
    ToggleExcludeUnsupported = 'toggle_exclude_unsupported_action'
    ToggleExcludeCallablesAndModules = (
        'toggle_exclude_callables_and_modules_action')
    ToggleMinMax = 'toggle_minmax_action'


class VariableExplorerWidgetOptionsMenuSections:
    Display = 'excludes_section'
    Highlight = 'highlight_section'


class VariableExplorerWidgetMainToolBarSections:
    Main = 'main_section'


class VariableExplorerWidgetMenus:
    EmptyContextMenu = 'empty'
    PopulatedContextMenu = 'populated'


class VariableExplorerContextMenuActions:
    ResizeRowsAction = 'resize_rows_action'
    ResizeColumnsAction = 'resize_columns_action'
    PasteAction = 'paste_action'
    CopyAction = 'copy'
    EditAction = 'edit_action'
    PlotAction = 'plot_action'
    HistogramAction = 'histogram_action'
    ImshowAction = 'imshow_action'
    SaveArrayAction = 'save_array_action'
    InsertAction = 'insert_action'
    InsertActionAbove = 'insert_action_above'
    InsertActionBelow = 'insert_action_below'
    RemoveAction = 'remove_action'
    RenameAction = 'rename_action'
    DuplicateAction = 'duplicate_action'
    ViewAction = 'view_action'


class VariableExplorerContextMenuSections:
    Edit = 'edit_section'
    Rename = 'rename_section'
    Resize = 'resize_section'


# =============================================================================
# ---- Widgets
# =============================================================================
class NamespaceStackedWidget(QStackedWidget):
    # Signals
    sig_free_memory_requested = Signal()
    sig_start_spinner_requested = Signal()
    sig_stop_spinner_requested = Signal()
    sig_hide_finder_requested = Signal()

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

    def addWidget(self, widget):
        """
        Override Qt method.
        """
        if isinstance(widget, NamespaceBrowser):
            widget.sig_free_memory_requested.connect(
                self.sig_free_memory_requested)
            widget.sig_start_spinner_requested.connect(
                self.sig_start_spinner_requested)
            widget.sig_stop_spinner_requested.connect(
                self.sig_stop_spinner_requested)
            widget.sig_hide_finder_requested.connect(
                self.sig_hide_finder_requested)

        super().addWidget(widget)


class VariableExplorerWidget(PluginMainWidget):

    # PluginMainWidget class constants
    ENABLE_SPINNER = True

    # Other class constants
    INITIAL_FREE_MEMORY_TIME_TRIGGER = 60 * 1000  # ms
    SECONDARY_FREE_MEMORY_TIME_TRIGGER = 180 * 1000  # ms

    # Signals
    sig_free_memory_requested = Signal()

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

        # Widgets
        self._stack = NamespaceStackedWidget(self)
        self._shellwidgets = {}
        self.context_menu = None
        self.empty_context_menu = None

        # --- Finder
        self.finder = None

        # Layout
        layout = QVBoxLayout()
        layout.addWidget(self._stack)
        # Note: Later with the addition of the first NamespaceBrowser the
        # find/search widget is added. See 'set_current_widget'
        self.setLayout(layout)

        # Signals
        self._stack.sig_free_memory_requested.connect(self.free_memory)
        self._stack.sig_start_spinner_requested.connect(self.start_spinner)
        self._stack.sig_stop_spinner_requested.connect(self.stop_spinner)
        self._stack.sig_hide_finder_requested.connect(self.hide_finder)

    # ---- PluginMainWidget API
    # ------------------------------------------------------------------------
    def get_title(self):
        return _('Variable Explorer')

    def get_focus_widget(self):
        return self.current_widget()

    def setup(self):
        # ---- Options menu actions
        exclude_private_action = self.create_action(
            VariableExplorerWidgetActions.ToggleExcludePrivate,
            text=_("Exclude private variables"),
            tip=_("Exclude variables that start with an underscore"),
            toggled=True,
            option='exclude_private',
        )

        exclude_uppercase_action = self.create_action(
            VariableExplorerWidgetActions.ToggleExcludeUpperCase,
            text=_("Exclude all-uppercase variables"),
            tip=_("Exclude variables whose name is uppercase"),
            toggled=True,
            option='exclude_uppercase',
        )

        exclude_capitalized_action = self.create_action(
            VariableExplorerWidgetActions.ToggleExcludeCapitalized,
            text=_("Exclude capitalized variables"),
            tip=_("Exclude variables whose name starts with a capital "
                  "letter"),
            toggled=True,
            option='exclude_capitalized',
        )

        exclude_unsupported_action = self.create_action(
            VariableExplorerWidgetActions.ToggleExcludeUnsupported,
            text=_("Exclude unsupported data types"),
            tip=_("Exclude references to data types that don't have "
                  "an specialized viewer or can't be edited."),
            toggled=True,
            option='exclude_unsupported',
        )

        exclude_callables_and_modules_action = self.create_action(
            VariableExplorerWidgetActions.ToggleExcludeCallablesAndModules,
            text=_("Exclude callables and modules"),
            tip=_("Exclude references to functions, modules and "
                  "any other callable."),
            toggled=True,
            option='exclude_callables_and_modules'
        )

        self.show_minmax_action = self.create_action(
            VariableExplorerWidgetActions.ToggleMinMax,
            text=_("Show arrays min/max"),
            tip=_("Show minimum and maximum of arrays"),
            toggled=True,
            option='minmax'
        )

        # ---- Toolbar actions
        import_data_action = self.create_action(
            VariableExplorerWidgetActions.ImportData,
            text=_('Import data'),
            icon=self.create_icon('fileimport'),
            triggered=lambda x: self.import_data(),
        )

        save_action = self.create_action(
            VariableExplorerWidgetActions.SaveData,
            text=_("Save data"),
            icon=self.create_icon('filesave'),
            triggered=lambda x: self.save_data(),
        )

        save_as_action = self.create_action(
            VariableExplorerWidgetActions.SaveDataAs,
            text=_("Save data as..."),
            icon=self.create_icon('filesaveas'),
            triggered=lambda x: self.save_data(),
        )

        reset_namespace_action = self.create_action(
            VariableExplorerWidgetActions.ResetNamespace,
            text=_("Remove all variables"),
            icon=self.create_icon('editdelete'),
            triggered=lambda x: self.reset_namespace(),
        )

        search_action = self.create_action(
            VariableExplorerWidgetActions.Search,
            text=_("Search variable names and types"),
            icon=self.create_icon('find'),
            toggled=self.show_finder,
            register_shortcut=True
        )

        refresh_action = self.create_action(
            VariableExplorerWidgetActions.Refresh,
            text=_("Refresh variables"),
            icon=self.create_icon('refresh'),
            triggered=self.refresh_table,
            register_shortcut=True,
        )

        # ---- Context menu actions
        resize_rows_action = self.create_action(
            VariableExplorerContextMenuActions.ResizeRowsAction,
            text=_("Resize rows to contents"),
            triggered=self.resize_rows
        )

        resize_columns_action = self.create_action(
            VariableExplorerContextMenuActions.ResizeColumnsAction,
            _("Resize columns to contents"),
            triggered=self.resize_columns
        )

        self.paste_action = self.create_action(
            VariableExplorerContextMenuActions.PasteAction,
            _("Paste"),
            icon=self.create_icon('editpaste'),
            triggered=self.paste
        )

        self.copy_action = self.create_action(
            VariableExplorerContextMenuActions.CopyAction,
            _("Copy"),
            icon=self.create_icon('editcopy'),
            triggered=self.copy
        )

        self.edit_action = self.create_action(
            VariableExplorerContextMenuActions.EditAction,
            _("Edit"),
            icon=self.create_icon('edit'),
            triggered=self.edit_item
        )

        self.plot_action = self.create_action(
            VariableExplorerContextMenuActions.PlotAction,
            _("Plot"),
            icon=self.create_icon('plot'),
            triggered=self.plot_item
        )
        self.plot_action.setVisible(False)

        self.hist_action = self.create_action(
            VariableExplorerContextMenuActions.HistogramAction,
            _("Histogram"),
            icon=self.create_icon('hist'),
            triggered=self.histogram_item
        )
        self.hist_action.setVisible(False)

        self.imshow_action = self.create_action(
            VariableExplorerContextMenuActions.ImshowAction,
            _("Show image"),
            icon=self.create_icon('imshow'),
            triggered=self.imshow_item
        )
        self.imshow_action.setVisible(False)

        self.save_array_action = self.create_action(
            VariableExplorerContextMenuActions.SaveArrayAction,
            _("Save array"),
            icon=self.create_icon('filesave'),
            triggered=self.save_array
        )
        self.save_array_action.setVisible(False)

        self.insert_action = self.create_action(
            VariableExplorerContextMenuActions.InsertAction,
            _("Insert"),
            icon=self.create_icon('insert'),
            triggered=self.insert_item
        )

        self.remove_action = self.create_action(
            VariableExplorerContextMenuActions.RemoveAction,
            _("Remove"),
            icon=self.create_icon('editdelete'),
            triggered=self.remove_item
        )

        self.rename_action = self.create_action(
            VariableExplorerContextMenuActions.RenameAction,
            _("Rename"),
            icon=self.create_icon('rename'),
            triggered=self.rename_item
        )

        self.duplicate_action = self.create_action(
            VariableExplorerContextMenuActions.DuplicateAction,
            _("Duplicate"),
            icon=self.create_icon('edit_add'),
            triggered=self.duplicate_item
        )

        self.view_action = self.create_action(
            VariableExplorerContextMenuActions.ViewAction,
            _("View with the Object Explorer"),
            icon=self.create_icon('outline_explorer'),
            triggered=self.view_item
        )

        # Options menu
        options_menu = self.get_options_menu()
        for item in [exclude_private_action, exclude_uppercase_action,
                     exclude_capitalized_action, exclude_unsupported_action,
                     exclude_callables_and_modules_action,
                     self.show_minmax_action]:
            self.add_item_to_menu(
                item,
                menu=options_menu,
                section=VariableExplorerWidgetOptionsMenuSections.Display,
            )

        # Main toolbar
        main_toolbar = self.get_main_toolbar()
        for item in [import_data_action, save_action, save_as_action,
                     reset_namespace_action, search_action, refresh_action]:
            self.add_item_to_toolbar(
                item,
                toolbar=main_toolbar,
                section=VariableExplorerWidgetMainToolBarSections.Main,
            )
        save_action.setEnabled(False)

        # ---- Context menu to show when there are variables present
        self.context_menu = self.create_menu(
            VariableExplorerWidgetMenus.PopulatedContextMenu)
        for item in [self.edit_action, self.plot_action, self.hist_action,
                     self.imshow_action, self.save_array_action,
                     self.insert_action, self.remove_action, self.copy_action,
                     self.paste_action, self.view_action]:
            self.add_item_to_menu(
                item,
                menu=self.context_menu,
                section=VariableExplorerContextMenuSections.Edit,
            )

        for item in [self.rename_action, self.duplicate_action]:
            self.add_item_to_menu(
                item,
                menu=self.context_menu,
                section=VariableExplorerContextMenuSections.Rename,
            )

        for item in [resize_rows_action, resize_columns_action,
                     self.show_minmax_action]:
            self.add_item_to_menu(
                item,
                menu=self.context_menu,
                section=VariableExplorerContextMenuSections.Resize,
            )

        # ---- Context menu when the variable explorer is empty
        self.empty_context_menu = self.create_menu(
            VariableExplorerWidgetMenus.EmptyContextMenu)
        for item in [self.insert_action, self.paste_action]:
            self.add_item_to_menu(
                item,
                menu=self.empty_context_menu,
                section=VariableExplorerContextMenuSections.Edit,
            )

    def update_style(self):
        self._stack.setStyleSheet(
            "NamespaceStackedWidget {padding: 0px; border: 0px}")

    def update_actions(self):
        action = self.get_action(VariableExplorerWidgetActions.ToggleMinMax)
        action.setEnabled(is_module_installed('numpy'))
        nsb = self.current_widget()

        for __, action in self.get_actions().items():
            if action:
                # IMPORTANT: Since we are defining the main actions in here
                # and the context is WidgetWithChildrenShortcut we need to
                # assign the same actions to the children widgets in order
                # for shortcuts to work
                if nsb:
                    save_data_action = self.get_action(
                        VariableExplorerWidgetActions.SaveData)
                    save_data_action.setEnabled(nsb.filename is not None)

                    nsb_actions = nsb.actions()
                    if action not in nsb_actions:
                        nsb.addAction(action)

    @on_conf_change
    def on_section_conf_change(self, section):
        for index in range(self.count()):
            widget = self._stack.widget(index)
            if widget:
                widget.setup()

    # ---- Stack accesors
    # ------------------------------------------------------------------------
    def add_widget(self, nsb):
        self._stack.addWidget(nsb)

    def count(self):
        return self._stack.count()

    def current_widget(self):
        return self._stack.currentWidget()

    def remove_widget(self, nsb):
        self._stack.removeWidget(nsb)

    def update_finder(self, nsb, old_nsb):
        """Initialize or update finder widget."""
        if self.finder is None:
            # Initialize finder/search related widgets
            self.finder = QWidget(self)
            self.text_finder = NamespacesBrowserFinder(
                nsb.editor,
                callback=nsb.editor.set_regex,
                main=nsb,
                regex_base=VALID_VARIABLE_CHARS)
            self.finder.text_finder = self.text_finder
            self.finder_close_button = self.create_toolbutton(
                'close_finder',
                triggered=self.hide_finder,
                icon=self.create_icon('DialogCloseButton'),
            )

            finder_layout = QHBoxLayout()
            finder_layout.addWidget(self.finder_close_button)
            finder_layout.addWidget(self.text_finder)
            finder_layout.setContentsMargins(0, 0, 0, 0)
            self.finder.setLayout(finder_layout)

            layout = self.layout()
            layout.addSpacing(1)
            layout.addWidget(self.finder)
        else:
            # Just update references to the same text_finder (Custom QLineEdit)
            # widget to the new current NamespaceBrowser and save current
            # finder state in the previous NamespaceBrowser
            if old_nsb is not None:
                self.save_finder_state(old_nsb)
            self.text_finder.update_parent(
                nsb.editor,
                callback=nsb.editor.set_regex,
                main=nsb,
            )

    def set_current_widget(self, nsb, old_nsb):
        """
        Set the current NamespaceBrowser.

        This also setup the finder widget to work with the current
        NamespaceBrowser.
        """
        self.update_finder(nsb, old_nsb)
        finder_visible = nsb.set_text_finder(self.text_finder)
        self._stack.setCurrentWidget(nsb)
        self.finder.setVisible(finder_visible)
        search_action = self.get_action(VariableExplorerWidgetActions.Search)
        search_action.setChecked(finder_visible)

    # ---- Public API
    # ------------------------------------------------------------------------
    def add_shellwidget(self, shellwidget):
        """
        Register shell with variable explorer.

        This function creates a new NamespaceBrowser for browsing
        variables in the shell.
        """
        shellwidget_id = id(shellwidget)
        if shellwidget_id not in self._shellwidgets:
            old_nsb = self.current_widget()
            nsb = NamespaceBrowser(self)
            nsb.set_shellwidget(shellwidget)
            nsb.setup()
            self.add_widget(nsb)
            self._set_actions_and_menus(nsb)
            self._shellwidgets[shellwidget_id] = nsb
            self.set_current_widget(nsb, old_nsb)
            self.update_actions()
            return nsb

    def remove_shellwidget(self, shellwidget):
        shellwidget_id = id(shellwidget)
        if shellwidget_id in self._shellwidgets:
            nsb = self._shellwidgets.pop(shellwidget_id)
            self.remove_widget(nsb)
            nsb.close()

    def set_shellwidget(self, shellwidget):
        shellwidget_id = id(shellwidget)
        old_nsb = self.current_widget()
        if shellwidget_id in self._shellwidgets:
            nsb = self._shellwidgets[shellwidget_id]
            self.set_current_widget(nsb, old_nsb)

    def import_data(self, filenames=None):
        """
        Import data in current namespace.
        """
        if self.count():
            nsb = self.current_widget()
            nsb.refresh_table()
            nsb.import_data(filenames=filenames)

    def save_data(self):
        if self.count():
            nsb = self.current_widget()
            nsb.save_data()
            self.update_actions()

    def reset_namespace(self):
        if self.count():
            nsb = self.current_widget()
            nsb.reset_namespace()

    @Slot(bool)
    def show_finder(self, checked):
        if self.count():
            nsb = self.current_widget()
            if checked:
                self.finder.text_finder.setText(nsb.last_find)
            else:
                self.save_finder_state(nsb)
                self.finder.text_finder.setText('')
            self.finder.setVisible(checked)
            if self.finder.isVisible():
                self.finder.text_finder.setFocus()
            else:
                nsb.editor.setFocus()

    @Slot()
    def hide_finder(self):
        action = self.get_action(VariableExplorerWidgetActions.Search)
        action.setChecked(False)
        nsb = self.current_widget()
        self.save_finder_state(nsb)
        self.finder.text_finder.setText('')

    def save_finder_state(self, nsb):
        """
        Save finder state (last input text and visibility).

        The values are saved in the given NamespaceBrowser.
        """
        last_find = self.text_finder.text()
        finder_visibility = self.finder.isVisible()
        nsb.save_finder_state(last_find, finder_visibility)

    def refresh_table(self):
        if self.count():
            nsb = self.current_widget()
            nsb.refresh_table()

    @Slot()
    def free_memory(self):
        """
        Free memory signal.
        """
        self.sig_free_memory_requested.emit()
        QTimer.singleShot(self.INITIAL_FREE_MEMORY_TIME_TRIGGER,
                          self.sig_free_memory_requested.emit)
        QTimer.singleShot(self.SECONDARY_FREE_MEMORY_TIME_TRIGGER,
                          self.sig_free_memory_requested.emit)

    def resize_rows(self):
        self._current_editor.resizeRowsToContents()

    def resize_columns(self):
        self._current_editor.resize_column_contents()

    def paste(self):
        self._current_editor.paste()

    def copy(self):
        self._current_editor.copy()

    def edit_item(self):
        self._current_editor.edit_item()

    def plot_item(self):
        self._current_editor.plot_item('plot')

    def histogram_item(self):
        self._current_editor.plot_item('hist')

    def imshow_item(self):
        self._current_editor.imshow_item()

    def save_array(self):
        self._current_editor.save_array()

    def insert_item(self):
        self._current_editor.insert_item(below=False)

    def remove_item(self):
        self._current_editor.remove_item()

    def rename_item(self):
        self._current_editor.rename_item()

    def duplicate_item(self):
        self._current_editor.duplicate_item()

    def view_item(self):
        self._current_editor.view_item()

    # ---- Private API
    # ------------------------------------------------------------------------
    @property
    def _current_editor(self):
        editor = None
        if self.count():
            nsb = self.current_widget()
            editor = nsb.editor
        return editor

    def _set_actions_and_menus(self, nsb):
        """
        Set actions and menus created here and used by the namespace
        browser editor.

        Although this is not ideal, it's necessary to be able to use
        the CollectionsEditor widget separately from this plugin.
        """
        editor = nsb.editor

        # Actions
        editor.paste_action = self.paste_action
        editor.copy_action = self.copy_action
        editor.edit_action = self.edit_action
        editor.plot_action = self.plot_action
        editor.hist_action = self.hist_action
        editor.imshow_action = self.imshow_action
        editor.save_array_action = self.save_array_action
        editor.insert_action = self.insert_action
        editor.remove_action = self.remove_action
        editor.minmax_action = self.show_minmax_action
        editor.rename_action = self.rename_action
        editor.duplicate_action = self.duplicate_action
        editor.view_action = self.view_action

        # Menus
        editor.menu = self.context_menu
        editor.empty_ws_menu = self.empty_context_menu

        # These actions are not used for dictionaries (so we don't need them
        # for namespaces) but we have to create them so they can be used in
        # several places in CollectionsEditor.
        editor.insert_action_above = QAction()
        editor.insert_action_below = QAction()
