# -*- coding: utf-8 -*-
"""
This file contains the qudi log widget class.

Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this
distribution and on <https://github.com/Ulm-IQO/qudi-core/>

This file is part of qudi.

Qudi is free software: you can redistribute it and/or modify it under the terms of
the GNU Lesser General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.

Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with qudi.
If not, see <https://www.gnu.org/licenses/>.
"""

from PySide2 import QtCore, QtGui, QtWidgets
from qudi.core.logger import get_record_table_model


class LogFilterProxy(QtCore.QSortFilterProxyModel):
    """
    A subclass of QProxyFilterModel that determines which log entries contained in the log model
    are shown in the view.
    """

    def __init__(self, parent=None):
        """
        Create the LogFilterProxy.

        Parameters
        ----------
        parent : QObject
            Parent object of the filter.
        """
        super().__init__(parent)
        self._show_levels = frozenset({'debug', 'info', 'warning', 'error', 'critical'})

    def filterAcceptsRow(self, source_row, source_parent):
        """
        Determine whether a row (log entry) should be shown.

        Parameters
        ----------
        source_row : QModelIndex
            The row in the source model that needs to be filtered.
        source_parent : QModelIndex
            The parent model index.

        Returns
        -------
        bool
            `True` if the row (log entry) should be shown, `False` otherwise.
        """
        model = self.sourceModel()
        level = model.data(model.index(source_row, 1), QtCore.Qt.DisplayRole)
        if level is None:
            return False
        return level in self._show_levels

    def set_levels(self, levels):
        """
        Set which types of messages are shown through the filter.

        Parameters
        ----------
        levels : set of str
            Set of all message levels that should be shown.
        """
        self._show_levels = frozenset(levels)
        self.invalidateFilter()


class SelectableTextDelegate(QtWidgets.QStyledItemDelegate):
    """A subclass of QStyledItemDelegate to display a text editor for copying text fragments.
    """
    def createEditor(self, parent, option, index):
        """
        Overwrite method from base class QStyledItemDelegate to show a read-only QLabel widget.
        This is necessary to disable editing by the user but still allow text to be marked and copied.

        Parameters
        ----------
        parent : QObject
            The parent object for the editor to be created.
        option : QStyleOptionViewItem
            Display options for the editor widget.
        index : QModelIndex
            Data model index.

        Returns
        -------
        QLabel
            Instance of QLabel.
        """
        editor = QtWidgets.QLabel(parent)
        editor.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
        editor.setAlignment(option.displayAlignment)
        return editor

    def setEditorData(self, editor, index):
        """
        Overwrite method from the base class QStyledItemDelegate to fill the QLineEdit widget with data.

        Parameters
        ----------
        editor : QLineEdit
            Editor widget to be populated with data.
        index : QModelIndex
            Data model index.
        """
        data = index.data(QtCore.Qt.EditRole)
        editor.setText(f' {data}')


class LogTableWidget(QtWidgets.QTableView):
    """Customized QTableView including the model for display of logging entries.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        record_model = get_record_table_model()
        if record_model is None:
            raise RuntimeError('No record table model set up in qudi.core.logger')
        self.filter_model = LogFilterProxy()
        self.filter_model.setSourceModel(record_model)
        self.setModel(self.filter_model)

        self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        self.setEditTriggers(QtWidgets.QTableView.DoubleClicked)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QtWidgets.QTableView.NoSelection)
        self.setHorizontalScrollMode(QtWidgets.QTableView.ScrollPerPixel)
        self.setVerticalScrollMode(QtWidgets.QTableView.ScrollPerPixel)
        self.setShowGrid(False)
        self.setCornerButtonEnabled(False)
        self.horizontalHeader().setCascadingSectionResizes(True)
        self.horizontalHeader().setStretchLastSection(True)
        self.verticalHeader().hide()
        self.setItemDelegate(SelectableTextDelegate())
        self.horizontalHeader().setMinimumSectionSize(50)
        self.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
        self.horizontalHeader().setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
        # Set fixed with for "Time" and "Level" columns since they contain fixed width strings
        metrics = QtGui.QFontMetrics(self.font())
        self.setColumnWidth(0, metrics.horizontalAdvance(' 5555-55-55 55:55:55 '))
        self.setColumnWidth(1, metrics.horizontalAdvance(' warning '))
        # Estimate starting width of "Source" column
        self.setColumnWidth(2, metrics.horizontalAdvance(__name__ * 2))

        self.filter_model.rowsInserted.connect(self._entry_added)

    @QtCore.Slot(QtCore.QModelIndex, int, int)
    def _entry_added(self, parent, first, last):
        while first <= last:
            self.resizeRowToContents(first)
            first += 1
        self.scrollToBottom()

    def set_level_filter(self, show_levels):
        self.filter_model.set_levels(show_levels)
        self.scrollToBottom()


class LogWidget(QtWidgets.QSplitter):
    """A widget to show log entries and filter them.
    """

    def __init__(self, parent=None, debug_mode=False, **kwargs):
        """
        Creates the log widget.

        Parameters
        ----------
        parent : QObject
            Qt parent object for the log widget.
        manager : Manager
            Manager instance this widget belongs to.
        """
        super().__init__(QtCore.Qt.Horizontal, parent, **kwargs)

        # Build GUI elements
        # Set up QTableView to display log entries
        self.log_tablewidget = LogTableWidget()
        self.log_tablewidget.setObjectName('log_table_widget')

        # Set up QTreeWidget for log filter ui
        self.filter_treewidget = QtWidgets.QTreeWidget()
        self.filter_treewidget.setObjectName('filter_treewidget')
        self.filter_treewidget.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
                                             QtWidgets.QSizePolicy.Preferred)
        self.filter_treewidget.setMinimumSize(210, 0)
        self.filter_treewidget.setEditTriggers(QtWidgets.QTreeWidget.NoEditTriggers)
        self.filter_treewidget.setDropIndicatorShown(False)
        self.filter_treewidget.setDragEnabled(False)
        self.filter_treewidget.setSelectionMode(QtWidgets.QTreeWidget.NoSelection)
        self.filter_treewidget.setSelectionBehavior(QtWidgets.QTreeWidget.SelectItems)
        self.filter_treewidget.setColumnCount(1)
        self.filter_treewidget.setHeaderLabels(('Display:',))
        item = QtWidgets.QTreeWidgetItem()
        item.setText(0, 'All message types:')
        item.setCheckState(0, QtCore.Qt.Checked)
        log_levels = ('debug', 'info', 'warning', 'error', 'critical')[int(not debug_mode):]
        for text in log_levels:
            child_item = QtWidgets.QTreeWidgetItem()
            child_item.setText(0, text)
            child_item.setCheckState(0, QtCore.Qt.Checked)
            item.addChild(child_item)
        self.filter_treewidget.addTopLevelItem(item)
        self.filter_treewidget.expandItem(item)
        self.log_tablewidget.set_level_filter(log_levels)

        # embed log view and filter tree into QSplitter widget
        self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        self.addWidget(self.log_tablewidget)
        self.addWidget(self.filter_treewidget)
        self.setStretchFactor(0, 1)

        # connect signals
        self.filter_treewidget.itemChanged.connect(self.update_filter_state)

    @QtCore.Slot(object, int)
    def update_filter_state(self, item, column):
        """
        Update the log view based on the filter widget check states and synchronize check box states.

        Parameters
        ----------
        item : int
            Item number.
        column : int
            Column number.
        """
        # check all / uncheck all state
        show_all_item = self.filter_treewidget.topLevelItem(0)
        level_items = [show_all_item.child(ii) for ii in range(show_all_item.childCount())]
        if item is show_all_item:
            self.filter_treewidget.expandItem(item)
            if show_all_item.checkState(0):
                self.filter_treewidget.blockSignals(True)
                for it in level_items:
                    it.setCheckState(0, QtCore.Qt.Checked)
                self.filter_treewidget.blockSignals(False)
            else:
                # Prevent user from unchecking "show all"
                self.filter_treewidget.blockSignals(True)
                show_all_item.setCheckState(0, QtCore.Qt.Checked)
                self.filter_treewidget.blockSignals(False)
        else:
            show_all = all(it.checkState(0) for it in level_items)
            self.filter_treewidget.blockSignals(True)
            show_all_item.setCheckState(0, QtCore.Qt.Checked if show_all else QtCore.Qt.Unchecked)
            self.filter_treewidget.blockSignals(False)

        # set level filters
        level_filter = {str(it.text(0)) for it in level_items if it.checkState(0)}
        self.log_tablewidget.set_level_filter(level_filter)
