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

"""
Breakpoint widget.
"""

# pylint: disable=C0103
# pylint: disable=R0903
# pylint: disable=R0911
# pylint: disable=R0201

# Standard library imports
import sys

# Third party imports
from qtpy import PYQT5
from qtpy.compat import to_qvariant
from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal
from qtpy.QtWidgets import QItemDelegate, QTableView, QVBoxLayout

# Local imports
from spyder.api.translations import _
from spyder.api.widgets.main_widget import (PluginMainWidgetMenus,
                                            PluginMainWidget)
from spyder.api.widgets.mixins import SpyderWidgetMixin
from spyder.utils.sourcecode import disambiguate_fname


# --- Constants
# ----------------------------------------------------------------------------
COLUMN_COUNT = 4
EXTRA_COLUMNS = 1
COL_FILE, COL_LINE, COL_CONDITION, COL_BLANK, COL_FULL = list(
    range(COLUMN_COUNT + EXTRA_COLUMNS))
COLUMN_HEADERS = (_("File"), _("Line"), _("Condition"), (""))


class BreakpointTableViewActions:
    # Triggers
    ClearAllBreakpoints = 'clear_all_breakpoints_action'
    ClearBreakpoint = 'clear_breakpoint_action'
    EditBreakpoint = 'edit_breakpoint_action'


# --- Widgets
# ----------------------------------------------------------------------------
class BreakpointTableModel(QAbstractTableModel):
    """
    Table model for breakpoints dictionary.
    """

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

        self._data = {} if data is None else data
        self.breakpoints = None

        self.set_data(self._data)

    def set_data(self, data):
        """
        Set model data.

        Parameters
        ----------
        data: dict
            Breakpoint data to use.
        """
        self._data = data
        self.breakpoints = []
        files = []
        # Generate list of filenames with active breakpoints
        for key in data:
            if data[key] and key not in files:
                files.append(key)

        # Insert items
        for key in files:
            for item in data[key]:
                # Store full file name in last position, which is not shown
                self.breakpoints.append((disambiguate_fname(files, key),
                                         item[0], item[1], "", key))
        self.reset()

    def rowCount(self, qindex=QModelIndex()):
        """
        Array row number.
        """
        return len(self.breakpoints)

    def columnCount(self, qindex=QModelIndex()):
        """
        Array column count.
        """
        return COLUMN_COUNT

    def sort(self, column, order=Qt.DescendingOrder):
        """
        Overriding sort method.
        """
        if column == COL_FILE:
            self.breakpoints.sort(key=lambda breakp: int(breakp[COL_LINE]))
            self.breakpoints.sort(key=lambda breakp: breakp[COL_FILE])
        elif column == COL_LINE:
            pass
        elif column == COL_CONDITION:
            pass
        elif column == COL_BLANK:
            pass

        self.reset()

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        """
        Overriding method headerData.
        """
        if role != Qt.DisplayRole:
            return to_qvariant()

        i_column = int(section)
        if orientation == Qt.Horizontal:
            return to_qvariant(COLUMN_HEADERS[i_column])
        else:
            return to_qvariant()

    def get_value(self, index):
        """
        Return current value.
        """
        return self.breakpoints[index.row()][index.column()]

    def data(self, index, role=Qt.DisplayRole):
        """
        Return data at table index.
        """
        if not index.isValid():
            return to_qvariant()

        if role == Qt.DisplayRole:
            value = self.get_value(index)
            return to_qvariant(value)
        elif role == Qt.TextAlignmentRole:
            if index.column() == COL_LINE:
                # Align line number right
                return to_qvariant(int(Qt.AlignRight | Qt.AlignVCenter))
            else:
                return to_qvariant(int(Qt.AlignLeft | Qt.AlignVCenter))
        elif role == Qt.ToolTipRole:
            if index.column() == COL_FILE:
                # Return full file name (in last position)
                value = self.breakpoints[index.row()][COL_FULL]
                return to_qvariant(value)
            else:
                return to_qvariant()

    def reset(self):
        self.beginResetModel()
        self.endResetModel()


class BreakpointDelegate(QItemDelegate):

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


class BreakpointTableView(QTableView, SpyderWidgetMixin):
    """
    Table to display code breakpoints.
    """

    # Signals
    sig_clear_all_breakpoints_requested = Signal()
    sig_clear_breakpoint_requested = Signal(str, int)
    sig_edit_goto_requested = Signal(str, int, str)
    sig_conditional_breakpoint_requested = Signal()

    def __init__(self, parent, data):
        if PYQT5:
            super().__init__(parent, class_parent=parent)
        else:
            QTableView.__init__(self, parent)
            SpyderWidgetMixin.__init__(self, class_parent=parent)

        # Widgets
        self.model = BreakpointTableModel(self, data)
        self.delegate = BreakpointDelegate(self)

        # Setup
        self.setSortingEnabled(False)
        self.setSelectionBehavior(self.SelectRows)
        self.setSelectionMode(self.SingleSelection)
        self.setModel(self.model)
        self.setItemDelegate(self.delegate)
        self.adjust_columns()
        self.columnAt(0)
        self.horizontalHeader().setStretchLastSection(True)

    # --- SpyderWidgetMixin API
    # ------------------------------------------------------------------------
    def setup(self):
        clear_all_action = self.create_action(
            BreakpointTableViewActions.ClearAllBreakpoints,
            _("Clear breakpoints in all files"),
            triggered=self.sig_clear_all_breakpoints_requested,
        )
        clear_action = self.create_action(
            BreakpointTableViewActions.ClearBreakpoint,
            _("Clear selected breakpoint"),
            triggered=self.clear_breakpoints,
        )
        edit_action = self.create_action(
            BreakpointTableViewActions.EditBreakpoint,
            _("Edit selected breakpoint"),
            triggered=self.edit_breakpoints,
        )

        self.popup_menu = self.create_menu(PluginMainWidgetMenus.Context)
        for item in [clear_all_action, clear_action, edit_action]:
            self.add_item_to_menu(item, menu=self.popup_menu)

    # --- Qt overrides
    # ------------------------------------------------------------------------
    def contextMenuEvent(self, event):
        """
        Override Qt method.
        """
        c_row = self.indexAt(event.pos()).row()
        enabled = bool(self.model.breakpoints) and c_row is not None
        clear_action = self.get_action(
            BreakpointTableViewActions.ClearBreakpoint)
        edit_action = self.get_action(
            BreakpointTableViewActions.EditBreakpoint)
        clear_action.setEnabled(enabled)
        edit_action.setEnabled(enabled)

        self.popup_menu.popup(event.globalPos())
        event.accept()

    def mouseDoubleClickEvent(self, event):
        """
        Override Qt method.
        """
        index_clicked = self.indexAt(event.pos())
        if self.model.breakpoints:
            c_row = index_clicked.row()
            filename = self.model.breakpoints[c_row][COL_FULL]
            line_number_str = self.model.breakpoints[c_row][COL_LINE]

            self.sig_edit_goto_requested.emit(
                filename, int(line_number_str), '')

        if index_clicked.column() == COL_CONDITION:
            self.sig_conditional_breakpoint_requested.emit()

    # --- API
    # ------------------------------------------------------------------------
    def set_data(self, data):
        """
        Set the model breakpoint data dictionary.

        Parameters
        ----------
        data: dict
            Breakpoint data to use.
        """
        self.model.set_data(data)
        self.adjust_columns()
        self.sortByColumn(COL_FILE, Qt.DescendingOrder)

    def adjust_columns(self):
        """
        Resize three first columns to contents.
        """
        for col in range(COLUMN_COUNT - 1):
            self.resizeColumnToContents(col)

    def clear_breakpoints(self):
        """
        Clear selected row breakpoint.
        """
        rows = self.selectionModel().selectedRows()
        if rows and self.model.breakpoints:
            c_row = rows[0].row()
            filename = self.model.breakpoints[c_row][COL_FULL]
            lineno = int(self.model.breakpoints[c_row][COL_LINE])

            self.sig_clear_breakpoint_requested.emit(filename, lineno)

    def edit_breakpoints(self):
        """
        Edit selected row breakpoint condition.
        """
        rows = self.selectionModel().selectedRows()
        if rows and self.model.breakpoints:
            c_row = rows[0].row()
            filename = self.model.breakpoints[c_row][COL_FULL]
            lineno = int(self.model.breakpoints[c_row][COL_LINE])

            self.sig_edit_goto_requested.emit(filename, lineno, '')
            self.sig_conditional_breakpoint_requested.emit()


class BreakpointWidget(PluginMainWidget):
    """
    Breakpoints widget.
    """

    # --- Signals
    # ------------------------------------------------------------------------
    sig_clear_all_breakpoints_requested = Signal()
    """
    This signal is emitted to send a request to clear all assigned
    breakpoints.
    """

    sig_clear_breakpoint_requested = Signal(str, int)
    """
    This signal is emitted to send a request to clear a single breakpoint.

    Parameters
    ----------
    filename: str
        The path to filename cotaining the breakpoint.
    line_number: int
        The line number of the breakpoint.
    """

    sig_edit_goto_requested = Signal(str, int, str)
    """
    Send a request to open a file in the editor at a given row and word.

    Parameters
    ----------
    filename: str
        The path to the filename containing the breakpoint.
    line_number: int
        The line number of the breakpoint.
    word: str
        Text `word` to select on given `line_number`.
    """

    sig_conditional_breakpoint_requested = Signal()
    """
    Send a request to set/edit a condition on a single selected breakpoint.
    """

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

        # Widgets
        self.breakpoints_table = BreakpointTableView(self, {})

        # Layout
        layout = QVBoxLayout()
        layout.addWidget(self.breakpoints_table)
        self.setLayout(layout)

        # Signals
        bpt = self.breakpoints_table
        bpt.sig_clear_all_breakpoints_requested.connect(
            self.sig_clear_all_breakpoints_requested)
        bpt.sig_clear_breakpoint_requested.connect(
            self.sig_clear_breakpoint_requested)
        bpt.sig_edit_goto_requested.connect(self.sig_edit_goto_requested)
        bpt.sig_conditional_breakpoint_requested.connect(
            self.sig_conditional_breakpoint_requested)

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

    def get_focus_widget(self):
        return self.breakpoints_table

    def setup(self):
        self.breakpoints_table.setup()

    def update_actions(self):
        rows = self.breakpoints_table.selectionModel().selectedRows()
        c_row = rows[0] if rows else None

        enabled = (bool(self.breakpoints_table.model.breakpoints)
                   and c_row is not None)
        clear_action = self.get_action(
            BreakpointTableViewActions.ClearBreakpoint)
        edit_action = self.get_action(
            BreakpointTableViewActions.EditBreakpoint)
        clear_action.setEnabled(enabled)
        edit_action.setEnabled(enabled)

    # --- Public API
    # ------------------------------------------------------------------------
    def set_data(self, data):
        """
        Set breakpoint data on widget.

        Parameters
        ----------
        data: dict
            Breakpoint data to use.
        """
        self.breakpoints_table.set_data(data)


# =============================================================================
# Tests
# =============================================================================
def test():
    """Run breakpoint widget test."""
    from spyder.utils.qthelpers import qapplication

    app = qapplication()
    widget = BreakpointWidget()
    widget.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    test()
