# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""
This module  define model and widget to display 1D slices from numpy
array using compound data types or hdf5 databases.
"""
from __future__ import division

import itertools
import numpy
from silx.gui import qt
import silx.io
from .TextFormatter import TextFormatter
from silx.gui.widgets.TableWidget import CopySelectedCellsAction

__authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "29/08/2018"


class _MultiLineItem(qt.QItemDelegate):
    """Draw a multiline text without hiding anything.

    The paint method display a cell without any wrap. And an editor is
    available to scroll into the selected cell.
    """

    def __init__(self, parent=None):
        """
        Constructor

        :param qt.QWidget parent: Parent of the widget
        """
        qt.QItemDelegate.__init__(self, parent)
        self.__textOptions = qt.QTextOption()
        self.__textOptions.setFlags(qt.QTextOption.IncludeTrailingSpaces |
                                    qt.QTextOption.ShowTabsAndSpaces)
        self.__textOptions.setWrapMode(qt.QTextOption.NoWrap)
        self.__textOptions.setAlignment(qt.Qt.AlignTop | qt.Qt.AlignLeft)

    def paint(self, painter, option, index):
        """
        Write multiline text without using any wrap or any alignment according
        to the cell size.

        :param qt.QPainter painter: Painter context used to displayed the cell
        :param qt.QStyleOptionViewItem option: Control how the editor is shown
        :param qt.QIndex index: Index of the data to display
        """
        painter.save()

        # set colors
        painter.setPen(qt.QPen(qt.Qt.NoPen))
        if option.state & qt.QStyle.State_Selected:
            brush = option.palette.highlight()
            painter.setBrush(brush)
        else:
            brush = index.data(qt.Qt.BackgroundRole)
            if brush is None:
                # default background color for a cell
                brush = qt.Qt.white
            painter.setBrush(brush)
        painter.drawRect(option.rect)

        if index.isValid():
            if option.state & qt.QStyle.State_Selected:
                brush = option.palette.highlightedText()
            else:
                brush = index.data(qt.Qt.ForegroundRole)
                if brush is None:
                    brush = option.palette.text()
            painter.setPen(qt.QPen(brush.color()))
            text = index.data(qt.Qt.DisplayRole)
            painter.drawText(qt.QRectF(option.rect), text, self.__textOptions)

        painter.restore()

    def createEditor(self, parent, option, index):
        """
        Returns the widget used to edit the item specified by index for editing.

        We use it not to edit the content but to show the content with a
        convenient scroll bar.

        :param qt.QWidget parent: Parent of the widget
        :param qt.QStyleOptionViewItem option: Control how the editor is shown
        :param qt.QIndex index: Index of the data to display
        """
        if not index.isValid():
            return super(_MultiLineItem, self).createEditor(parent, option, index)

        editor = qt.QTextEdit(parent)
        editor.setReadOnly(True)
        return editor

    def setEditorData(self, editor, index):
        """
        Read data from the model and feed the editor.

        :param qt.QWidget editor: Editor widget
        :param qt.QIndex index: Index of the data to display
        """
        text = index.model().data(index, qt.Qt.EditRole)
        editor.setText(text)

    def updateEditorGeometry(self, editor, option, index):
        """
        Update the geometry of the editor according to the changes of the view.

        :param qt.QWidget editor: Editor widget
        :param qt.QStyleOptionViewItem option: Control how the editor is shown
        :param qt.QIndex index: Index of the data to display
        """
        editor.setGeometry(option.rect)


class RecordTableModel(qt.QAbstractTableModel):
    """This data model provides access to 1D slices from numpy array using
    compound data types or hdf5 databases.

    Each entries are displayed in a single row, and each columns contain a
    specific field of the compound type.

    It also allows to display 1D arrays of simple data types.
    array.

    :param qt.QObject parent: Parent object
    :param numpy.ndarray data: A numpy array or a h5py dataset
    """
    def __init__(self, parent=None, data=None):
        qt.QAbstractTableModel.__init__(self, parent)

        self.__data = None
        self.__is_array = False
        self.__fields = None
        self.__formatter = None
        self.__editFormatter = None
        self.setFormatter(TextFormatter(self))

        # set _data
        self.setArrayData(data)

    # Methods to be implemented to subclass QAbstractTableModel
    def rowCount(self, parent_idx=None):
        """Returns number of rows to be displayed in table"""
        if self.__data is None:
            return 0
        elif not self.__is_array:
            return 1
        else:
            return len(self.__data)

    def columnCount(self, parent_idx=None):
        """Returns number of columns to be displayed in table"""
        if self.__fields is None:
            return 1
        else:
            return len(self.__fields)

    def data(self, index, role=qt.Qt.DisplayRole):
        """QAbstractTableModel method to access data values
        in the format ready to be displayed"""
        if not index.isValid():
            return None

        if self.__data is None:
            return None

        if self.__is_array:
            if index.row() >= len(self.__data):
                return None
            data = self.__data[index.row()]
        else:
            if index.row() > 0:
                return None
            data = self.__data

        if self.__fields is not None:
            if index.column() >= len(self.__fields):
                return None
            key = self.__fields[index.column()][1]
            data = data[key[0]]
            if len(key) > 1:
                data = data[key[1]]

        # no dtype in case of 1D array of unicode objects (#2093)
        dtype = getattr(data, "dtype", None)

        if role == qt.Qt.DisplayRole:
            return self.__formatter.toString(data, dtype=dtype)
        elif role == qt.Qt.EditRole:
            return self.__editFormatter.toString(data, dtype=dtype)
        return None

    def headerData(self, section, orientation, role=qt.Qt.DisplayRole):
        """Returns the 0-based row or column index, for display in the
        horizontal and vertical headers"""
        if section == -1:
            # PyQt4 send -1 when there is columns but no rows
            return None

        if role == qt.Qt.DisplayRole:
            if orientation == qt.Qt.Vertical:
                if not self.__is_array:
                    return "Scalar"
                else:
                    return str(section)
            if orientation == qt.Qt.Horizontal:
                if self.__fields is None:
                    if section == 0:
                        return "Data"
                    else:
                        return None
                else:
                    if section < len(self.__fields):
                        return self.__fields[section][0]
                    else:
                        return None
        return None

    def flags(self, index):
        """QAbstractTableModel method to inform the view whether data
        is editable or not.
        """
        return qt.QAbstractTableModel.flags(self, index)

    def setArrayData(self, data):
        """Set the data array and the viewing perspective.

        You can set ``copy=False`` if you need more performances, when dealing
        with a large numpy array. In this case, a simple reference to the data
        is used to access the data, rather than a copy of the array.

        .. warning::

            Any change to the data model will affect your original data
            array, when using a reference rather than a copy..

        :param data: 1D numpy array, or any object that can be
            converted to a numpy array using ``numpy.array(data)`` (e.g.
            a nested sequence).
        """
        if qt.qVersion() > "4.6":
            self.beginResetModel()

        self.__data = data
        if isinstance(data, numpy.ndarray):
            self.__is_array = True
        elif silx.io.is_dataset(data) and data.shape != tuple():
            self.__is_array = True
        else:
            self.__is_array = False

        self.__fields = []
        if data is not None:
            if data.dtype.fields is not None:
                fields = sorted(data.dtype.fields.items(), key=lambda e: e[1][1])
                for name, (dtype, _index) in fields:
                    if dtype.shape != tuple():
                        keys = itertools.product(*[range(x) for x in dtype.shape])
                        for key in keys:
                            label = "%s%s" % (name, list(key))
                            array_key = (name, key)
                            self.__fields.append((label, array_key))
                    else:
                        self.__fields.append((name, (name,)))
            else:
                self.__fields = None

        if qt.qVersion() > "4.6":
            self.endResetModel()
        else:
            self.reset()

    def arrayData(self):
        """Returns the internal data.

        :rtype: numpy.ndarray of h5py.Dataset
        """
        return self.__data

    def setFormatter(self, formatter):
        """Set the formatter object to be used to display data from the model

        :param TextFormatter formatter: Formatter to use
        """
        if formatter is self.__formatter:
            return

        if qt.qVersion() > "4.6":
            self.beginResetModel()

        if self.__formatter is not None:
            self.__formatter.formatChanged.disconnect(self.__formatChanged)

        self.__formatter = formatter
        self.__editFormatter = TextFormatter(formatter)
        self.__editFormatter.setUseQuoteForText(False)

        if self.__formatter is not None:
            self.__formatter.formatChanged.connect(self.__formatChanged)

        if qt.qVersion() > "4.6":
            self.endResetModel()
        else:
            self.reset()

    def getFormatter(self):
        """Returns the text formatter used.

        :rtype: TextFormatter
        """
        return self.__formatter

    def __formatChanged(self):
        """Called when the format changed.
        """
        self.__editFormatter = TextFormatter(self, self.getFormatter())
        self.__editFormatter.setUseQuoteForText(False)
        self.reset()


class _ShowEditorProxyModel(qt.QIdentityProxyModel):
    """
    Allow to custom the flag edit of the model
    """

    def __init__(self, parent=None):
        """
        Constructor

        :param qt.QObject arent: parent object
        """
        super(_ShowEditorProxyModel, self).__init__(parent)
        self.__forceEditable = False

    def flags(self, index):
        flag = qt.QIdentityProxyModel.flags(self, index)
        if self.__forceEditable:
            flag = flag | qt.Qt.ItemIsEditable
        return flag

    def forceCellEditor(self, show):
        """
        Enable the editable flag to allow to display cell editor.
        """
        if self.__forceEditable == show:
            return
        self.beginResetModel()
        self.__forceEditable = show
        self.endResetModel()


class RecordTableView(qt.QTableView):
    """TableView using DatabaseTableModel as default model.
    """
    def __init__(self, parent=None):
        """
        Constructor

        :param qt.QWidget parent: parent QWidget
        """
        qt.QTableView.__init__(self, parent)

        model = _ShowEditorProxyModel(self)
        self._model = RecordTableModel()
        model.setSourceModel(self._model)
        self.setModel(model)

        self.__multilineView = _MultiLineItem(self)
        self.setEditTriggers(qt.QAbstractItemView.AllEditTriggers)
        self._copyAction = CopySelectedCellsAction(self)
        self.addAction(self._copyAction)

    def copy(self):
        self._copyAction.trigger()

    def setArrayData(self, data):
        model = self.model()
        sourceModel = model.sourceModel()
        sourceModel.setArrayData(data)

        if data is not None:
            if issubclass(data.dtype.type, (numpy.string_, numpy.unicode_)):
                # TODO it would be nice to also fix fields
                # but using it only for string array is already very useful
                self.setItemDelegateForColumn(0, self.__multilineView)
                model.forceCellEditor(True)
            else:
                self.setItemDelegateForColumn(0, None)
                model.forceCellEditor(False)
