# -*- coding: utf-8 -*-
"""
This file contains a custom QAbstractTableModel object providing text data for all logged records.

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/>.
"""

__all__ = ('LogRecordsTableModel',)

import traceback
from datetime import datetime
from PySide2 import QtCore, QtGui
from qudi.util.mutex import Mutex


class LogRecordsTableModel(QtCore.QAbstractTableModel):
    """This is a Qt model that represents textual information about all logged records.
    Can be displayed with a QTableView for example.
    """

    _color_map = {'debug'   : QtGui.QColor('#77F'),
                  'info'    : QtGui.QColor('#1F1'),
                  'warning' : QtGui.QColor('#F90'),
                  'error'   : QtGui.QColor('#F11'),
                  'critical': QtGui.QColor('#FF00FF'),
                  }
    _fallback_color = QtGui.QColor('#FFF')
    _header = ('Time', 'Level', 'Source', 'Message')

    def __init__(self, *args, max_records=10000, **kwargs):
        super().__init__(*args, **kwargs)

        self._thread_lock = Mutex()
        self._max_records = max(int(max_records), 1)
        self._records = list()
        self._begin = 0
        self._end = 0
        self._fill_count = 0

    def rowCount(self, parent=None):
        """Returns the number of log records stored in the model.

        Returns
        -------
        int
            Number of log records stored.
        """
        return self._fill_count

    def columnCount(self, parent=None):
        """Returns the number of columns each log record has.

        Returns
        -------
        int
            Number of columns for each log record.
        """
        return len(self._header)

    def flags(self, index):
        """Determines what can be done with log record cells in the table view.

        Parameters
        ----------
        index : QModelIndex
            Cell for which the flags are requested.

        Returns
        -------
        Qt.ItemFlags
            Actions allowed for this cell.
        """
        return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable

    def data(self, index, role):
        """Get data from model for a given cell. Data can have a role that affects display.

        Parameters
        ----------
        index : QModelIndex
            Cell for which data is requested.
        role : ItemDataRole
            Role for which data is requested.

        Returns
        -------
        QVariant
            Data for given cell and role.
        """
        if index.isValid():
            record = self._records[(self._begin + index.row()) % self._max_records]
            if role == QtCore.Qt.TextColorRole:
                return self._color_map.get(record[1], self._fallback_color)
            if role in (QtCore.Qt.DisplayRole, QtCore.Qt.ToolTipRole, QtCore.Qt.EditRole):
                return record[index.column()]

    def headerData(self, section, orientation, role=None):
        """Data for the table view headers.

        Parameters
        ----------
        section : int
            Number of the column to get header data for.
        orientation : Qt.Orientation
            Orientation of header (horizontal or vertical).
        role : ItemDataRole
            Role for which to get data.

        Returns
        -------
        QVariant
            Header data for given column and role.
        """
        if (role is None or role == QtCore.Qt.DisplayRole) and orientation == QtCore.Qt.Horizontal:
            try:
                return self._header[section]
            except IndexError:
                pass
        return

    @QtCore.Slot(object)
    def add_record(self, data):
        """Add a single log entry to the end of the table model.

        Parameters
        ----------
        data : logging.LogRecord
            Log record as returned from logging module.

        Returns
        -------
        bool
            True if adding entry succeeded, False otherwise.
        """
        with self._thread_lock:
            if self._fill_count < self._max_records:
                self.beginInsertRows(QtCore.QModelIndex(), self._end, self._end)
                self._records.append(self._format_log_record(data))
                self._fill_count += 1
                self._end = (self._end + 1) % self._max_records
                self.endInsertRows()
            else:
                row = self._max_records - 1
                self.beginRemoveRows(QtCore.QModelIndex(), 0, 0)
                self._begin = (self._begin + 1) % self._max_records
                self._fill_count -= 1
                self.endRemoveRows()

                self.beginInsertRows(QtCore.QModelIndex(), row, row)
                self._records[self._end] = self._format_log_record(data)
                self._end = (self._end + 1) % self._max_records
                self._fill_count += 1
                self.endInsertRows()

    @QtCore.Slot()
    def clear(self):
        with self._thread_lock:
            self.beginResetModel()
            self._begin = 0
            self._end = 0
            self._fill_count = 0
            self._records = list()
            self.endResetModel()

    @property
    def max_size(self):
        return self._max_records

    @staticmethod
    def _format_log_record(record):
        # Compose message to display
        message = record.getMessage()  # message if hasattr(record, 'message') else record.msg
        if record.exc_info is not None:
            message += f'\n\n{traceback.format_exception(*record.exc_info)[-1][:-1]}'
            tb = '\n'.join(traceback.format_exception(*record.exc_info)[:-1])
            if tb:
                message += f'\n{tb}'

        # Create human-readable timestamp
        timestamp = datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S')

        # return 4 element tuple (timestamp, level, name, message)
        # Avoid problems with Qt by eliminating NULL bytes in strings.
        return timestamp, record.levelname, record.name, message.replace('\0', '\\x00')
