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

# Third party imports
from intervaltree import IntervalTree
from qtpy.QtCore import QSize, Qt, Slot
from qtpy.QtWidgets import QHBoxLayout

# Local imports
from spyder.config.base import _
from spyder.api.widgets.comboboxes import SpyderComboBoxWithIcons
from spyder.plugins.completion.api import SymbolKind
from spyder.plugins.editor.api.panel import Panel
from spyder.utils.icon_manager import ima
from spyder.utils.stylesheet import AppStyle


class ClassFunctionDropdown(Panel):
    """
    Class and Function/Method Dropdowns Widget.

    Parameters
    ----------
    editor : :class:`spyder.plugins.editor.widgets.codeeditor.CodeEditor`
        The editor to act on.
    """

    def __init__(self):
        super().__init__()

        # Internal data
        self._tree = IntervalTree()
        self._data = None
        self.classes = []
        self.funcs = []

        # Widgets
        self.class_cb = SpyderComboBoxWithIcons(self)
        self.method_cb = SpyderComboBoxWithIcons(self)

        # Widget setup
        self.class_cb.addItem(_('<None>'), 0)
        self.method_cb.addItem(_('<None>'), 0)

        # The layout
        hbox = QHBoxLayout()
        hbox.addWidget(self.class_cb)
        hbox.addWidget(self.method_cb)
        hbox.setSpacing(2 * AppStyle.MarginSize)
        hbox.setContentsMargins(*((AppStyle.MarginSize,) * 4))
        self.setLayout(hbox)

        # Signals
        self.class_cb.activated.connect(self.combobox_activated)
        self.method_cb.activated.connect(self.combobox_activated)

    def on_install(self, editor):
        """Manages install setup of the pane."""
        super().on_install(editor)
        # Define the editor
        self._editor = editor

        # Connect signals to the editor
        self._editor.sig_cursor_position_changed.connect(
            self._handle_cursor_position_change_event
        )

    def _getVerticalSize(self):
        """Get the default height of a QComboBox."""
        return self.class_cb.height() + 2 * AppStyle.MarginSize

    @Slot(int, int)
    def _handle_cursor_position_change_event(self, linenum, column):
        self.update_selected(linenum)

    def sizeHint(self):
        """Override Qt method."""
        return QSize(0, self._getVerticalSize())

    def showEvent(self, event):
        """
        Update contents in case there is available data and the widget hasn't
        been updated.
        """
        if self._data is not None and self.classes == [] and self.funcs == []:
            self.update_data(self._data, force=True)
        super().showEvent(event)

    def combobox_activated(self):
        """Move the cursor to the selected definition."""
        sender = self.sender()
        item = sender.itemData(sender.currentIndex())

        if item:
            line = item['location']['range']['start']['line'] + 1
            self.editor.go_to_line(line)

        if sender == self.class_cb:
            self.method_cb.setCurrentIndex(0)

    def update_selected(self, linenum):
        """Updates the dropdowns to reflect the current class and function."""
        possible_parents = list(sorted(self._tree[linenum]))
        for iv in possible_parents:
            item = iv.data
            kind = item.get('kind')

            if kind in [SymbolKind.CLASS]:
                # Update class combobox
                for idx in range(self.class_cb.count()):
                    if self.class_cb.itemData(idx) == item:
                        self.class_cb.setCurrentIndex(idx)
                        break
                else:
                    self.class_cb.setCurrentIndex(0)
            elif kind in [SymbolKind.FUNCTION, SymbolKind.METHOD]:
                # Update func combobox
                for idx in range(self.method_cb.count()):
                    if self.method_cb.itemData(idx) == item:
                        self.method_cb.setCurrentIndex(idx)
                        break
                else:
                    self.method_cb.setCurrentIndex(0)
            else:
                continue

        if len(possible_parents) == 0:
            self.class_cb.setCurrentIndex(0)
            self.method_cb.setCurrentIndex(0)

    def populate(self, combobox, data, add_parents=False):
        """
        Populate the given ``combobox`` with the class or function names.

        Parameters
        ----------
        combobox : :class:`spyder.api.widgets.comboboxes.SpyderComboBox`
            The combobox to populate
        data : list of :class:`dict`
            The data to populate with. There should be one list element per
            class or function definition in the file.
        add_parents : bool
            Add parents to name to create a fully qualified name.

        Returns
        -------
        None
        """
        combobox.clear()
        combobox.addItem(_('<None>'), 0)
        model = combobox.model()
        item = model.item(0)
        item.setFlags(Qt.NoItemFlags)

        cb_data = []
        for item in data:
            fqn = item['name']

            # Create a list of fully-qualified names if requested
            if add_parents:
                begin = item['location']['range']['start']['line']
                end = item['location']['range']['end']['line']
                possible_parents = sorted(self._tree.overlap(begin, end),
                                          reverse=True)
                for iv in possible_parents:
                    if iv.begin == begin and iv.end == end:
                        continue

                    # Check if it is a real parent
                    p_item = iv.data
                    p_begin = p_item['location']['range']['start']['line']
                    p_end = p_item['location']['range']['end']['line']
                    if p_begin <= begin and p_end >= end:
                        fqn = p_item['name'] + "." + fqn

            cb_data.append((fqn, item))

        for fqn, item in cb_data:
            # Set the icon (See: editortools.py)
            icon = None
            name = item['name']
            if item['kind'] in [SymbolKind.CLASS]:
                icon = ima.icon('class')
            else:
                if name.startswith('__'):
                    icon = ima.icon('private2')
                elif name.startswith('_'):
                    icon = ima.icon('private1')
                else:
                    icon = ima.icon('method')

            # Add the combobox item
            if icon is not None:
                combobox.addItem(icon, fqn, item)
            else:
                combobox.addItem(fqn, item)

        line, __ = self._editor.get_cursor_line_column()
        self.update_selected(line)

    def set_data(self, data):
        """Set data in internal attribute to use it when necessary."""
        self._data = data

    def update_data(self, data, force=False):
        """Update and process symbol data."""
        if not force and data == self._data:
            return

        self._data = data
        self._tree.clear()
        self.classes = []
        self.funcs = []

        for item in data:
            line_start = item['location']['range']['start']['line']
            line_end = item['location']['range']['end']['line']
            kind = item.get('kind')

            block = self._editor.document().findBlockByLineNumber(line_start)
            line_text = line_text = block.text() if block else ''

            # The symbol finder returns classes in import statements as well
            # so we filter them out
            if line_start != line_end and ' import ' not in line_text:
                self._tree[line_start:line_end] = item

                if kind in [SymbolKind.CLASS]:
                    self.classes.append(item)
                elif kind in [SymbolKind.FUNCTION, SymbolKind.METHOD]:
                    self.funcs.append(item)

        self.class_cb.clear()
        self.method_cb.clear()
        self.populate(self.class_cb, self.classes, add_parents=False)
        self.populate(self.method_cb, self.funcs, add_parents=True)
