# Copyright (c) 2011-2014, B.I.Stepanov Institute of Physics, National Academy
# of Sciences of Belarus.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

from PyQt4.QtCore import *
from PyQt4.QtGui import *

__all__ = ['DataTableWidget', 'TableWidgetColumn']

# *****************************************************************************
class TableWidgetColumn:
    """Description of a data table widget column.

    Attributes:
      - 'dataAttributeName': attribute of a data record class instance that
        will correspond to the table widget column and hold the data for it, or
        'None' if the column has no data attribute associated with it;
      - 'columnLabel': text displayed in the table widget's column header;
      - 'formatString': string to format the data with '%' operator or either
        'date' or 'time' for date and time values, or 'None' if the column has
        no data attribute associated with it. Note: date and time values must
        have 'year', 'month', 'day' and 'hour', 'minute', 'second' attributes
        respectively to work properly;
      - 'toolTip': string holding the tooltip for the table header.
      - 'icon': 'QIcon' for the table widget's column header or 'None' if
        there's no need for an icon."""

    def __init__(self, dataAttributeName, columnLabel, formatString,
        toolTip = '', icon = QIcon()):

        self.dataAttributeName = dataAttributeName
        self.columnLabel = columnLabel
        self.formatString = formatString
        self.toolTip = toolTip
        self.icon = icon

# *****************************************************************************
class DataTableWidget(QTableWidget):
    """Table widget intended for display of list of uniform data records.

    Call 'setColumns' to define the structure of the table's header; call
    'populateTable' and 'updateTableRow' to modify the table's data or
    'clearTable' to clear the data.

    By default, table items are not editable, table rows are selected in their
    entirety, and multiple selection is disabled. To modify that, call the
    required 'QTableWidget' methods manually.

    If the widget is manually configured to contain editable items, then the
    following signals will be provided:
      - 'itemEditingStarted': an editor is opened in one of the widget's items.
      - 'itemEditingFinished': the item editor is closed.

    Warning: if several widget items are edited in a row, only the last one
    will trigger the 'itemEditingFinished' signal."""

    # ---- Signals ------------------------------------------------------------
    itemEditingStarted = pyqtSignal()
    itemEditingFinished = pyqtSignal()

    # ---- Public methods -----------------------------------------------------
    def __init__(self, parent = None):
        QTableWidget.__init__(self, parent)

        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        # By default, multiple row selection is disabled.
        self.setSelectionMode(QAbstractItemView.SingleSelection)
        self.setTabKeyNavigation(False)
        self.setCornerButtonEnabled(False)
        self.horizontalHeader().setHighlightSections(False)
        self.horizontalHeader().setResizeMode(QHeaderView.Fixed)
        self.verticalHeader().setResizeMode(QHeaderView.Fixed)
        self.verticalHeader().hide()

        # This will be a list of 'TableWidgetColumn' instances.
        self.columnList = []

    def setColumns(self, columnList):
        """Define the table structure by means of the given list of
        'TableWidgetColumn' instances and set the table widget's header."""

        self.columnList = columnList

        # Set the header text labels.
        columnLabels = [column.columnLabel for column in self.columnList]

        self.setColumnCount(len(self.columnList))
        self.setHorizontalHeaderLabels(columnLabels)

        # Set the header tooltips and icons.
        for i in range(len(self.columnList)):

            headerItem = self.horizontalHeaderItem(i)

            headerItem.setToolTip(self.columnList[i].toolTip)
            headerItem.setIcon(self.columnList[i].icon)

            self.setHorizontalHeaderItem(i, headerItem)

        self.resizeColumnsToContents()

    def getColumnIndexByAttributeName(self, dataAttributeName):
        """Return index of the column that corresponds to the given data record
        attribute."""

        for i in range(len(self.columnList)):
            if self.columnList[i].dataAttributeName == dataAttributeName:
                return i

    def getColumnByAttributeName(self, dataAttributeName):
        """Return 'TableWidgetColumn' instance that corresponds to the given
        data record attribute."""

        columnIndex = self.getColumnIndexByAttributeName(dataAttributeName)
        return self.columnList[columnIndex]

    def populateTable(self, dataRecordList):
        """Set table contents in accordance with its structure defined by
        'setColumns' and the given list of uniform data records.

        All the table widget items are enabled and selectable, but not
        editable by default. Different options are supposed to be set via
        manual manipulation of the items after calling this function."""

        self.setRowCount(len(dataRecordList))

        for rowIndex in range(len(dataRecordList)):

            dataRecord = dataRecordList[rowIndex]

            # Fill in the table widget row.
            for colIndex in range(len(self.columnList)):

                valueStr = self._getItemValueStr(dataRecord, colIndex)

                # Skip columns that have no associated data attributes.
                if valueStr is None:
                    continue

                tableItem = QTableWidgetItem(valueStr)
                tableItem.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
                tableItem.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)

                tableColumn = self.columnList[colIndex]
                value = getattr(dataRecord, tableColumn.dataAttributeName)

                # Align text columns to the left.
                if isinstance(value, unicode) or isinstance(value, str):
                    tableItem.setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter)

                self.setItem(rowIndex, colIndex, tableItem)

        # When the table is populated after having been cleared, it seems to
        # retain its current item index and scrollbar positions from the
        # previous layout. Thus we have to manually scroll to the table's top
        # left corner, unless the table is empty.
        firstItem = self.item(0, 0)
        if firstItem is not None:
            self.scrollToItem(firstItem)

        # 'resizeColumnsToContents' method only takes into account table rows
        # that are visible to the user at the moment. Thus, in order to apply
        # this method to the entire table we have to make all the table rows
        # visible at once. The only guaranteed way to do that is to set heights
        # of all the rows to zero. Fortunately, zero-height rows seem to still
        # be taken into account by 'resizeColumnsToContents'.
        for row in range(self.rowCount()):
            self.setRowHeight(row, 0)
        self.resizeColumnsToContents()
        # All the rows in the table should have the same height, thus this
        # operation always works as expected, unlike the previous one.
        self.resizeRowsToContents()

    def updateTableRow(self, rowIndex, dataRecord):
        """Set concents of an existing table row in accordance with the given
        data record, without modifying any icons, flags etc. associated with
        the table widget items."""

        # Indices of columns whose contents have been modified.
        modifiedColIndices = []

        # Replace the data in the table widget row.
        for colIndex in range(len(self.columnList)):

            valueStr = self._getItemValueStr(dataRecord, colIndex)

            # Skip columns that have no associated data attributes.
            if valueStr is None:
                continue

            # Use existing widget item to preserve all the custom settings.
            tableItem = self.item(rowIndex, colIndex)

            if tableItem.text() != valueStr:
                modifiedColIndices.append(colIndex)

            tableItem.setText(valueStr)

        # Do the same trick as in 'populateTable' in order to make the
        # 'resizeColumnToContents' method work as expected. It seems that such
        # a manipulation doesn't disrupt the table's scrollbar positions.
        for row in range(self.rowCount()):
            self.setRowHeight(row, 0)

        # Resize modified columns only, to speed up a bit this time-consuming
        # operation.
        for colIndex in modifiedColIndices:
            self.resizeColumnToContents(colIndex)

        self.resizeRowsToContents()

    def clearTable(self):
        """Remove all the data, retaining the sole header."""

        self.clearContents()
        self.setRowCount(0)
        self.resizeColumnsToContents()

    def disableRow(self, rowIndex):
        """Disable all the table cells in the given row."""
        for colIndex in range(self.columnCount()):
            self.item(rowIndex, colIndex).setFlags(Qt.NoItemFlags)

    def getSelectedRows(self):
        """Return a sorted list of indices of currently selected rows."""

        selectedRows = sorted(set(item.row() for item in self.selectedItems()))

        # Assure that the rows are selected in their entirety.
        for rowIndex in selectedRows:
            for colIndex in range(self.columnCount()):
                assert self.item(rowIndex, colIndex).isSelected()

        return selectedRows

    def getSelectedRow(self):
        """Return index of the currently selected row or 'None' if no row is
        selected. Fail if there are multiple rows selected."""

        selectedRows = self.getSelectedRows()
        assert len(selectedRows) <= 1

        if len(selectedRows) == 0:
            return None
        else:
            return selectedRows[0]

    # ---- Private overridden methods -----------------------------------------
    def edit(self, modelIndex, editTrigger, event):
        """Redefinition of a virtual function implementing the
        'itemEditingStarted' signal."""

        editingStarted = QTableWidget.edit(
            self, modelIndex, editTrigger, event)

        # Use 'self.state()' instead of 'QTableWidget.edit's return value,
        # since the latter function may return 'True' even if editing has not
        # actually started (e.g. in case of check box toggling).
        if self.state() == QAbstractItemView.EditingState:
            self.itemEditingStarted.emit()

        return editingStarted

    def closeEditor(self, editorWidget, endEditHint):
        """Redefinition of a virtual function implementing the
        'itemEditingFinished' signal."""

        QTableWidget.closeEditor(self, editorWidget, endEditHint)

        # The current Qt implementation is such that if several widget items
        # are edited in a row, the 'edit' function for the new item is called
        # prior to 'closeEditor' for the previous one. In such a case, the new
        # editing has already started (and reported) by now, making it too late
        # to fire 'itemEditingFinished' for the previous one. Thus it shall be
        # fired only once for several items in a row.

        if self.state() != QAbstractItemView.EditingState:
            self.itemEditingFinished.emit()

    # ---- Private methods ----------------------------------------------------
    def _getItemValueStr(self, dataRecord, colIndex):
        """Return the textual representation of a 'dataRecord's attribute
        associated with the given table column or 'None' if the column has no
        data attribute associated with it."""

        tableColumn = self.columnList[colIndex]

        # Check if the column has a data attribute mapped to it.
        if tableColumn.dataAttributeName is None:
            return None

        value = getattr(dataRecord, tableColumn.dataAttributeName)

        # Allow some data attributes to contain no valid data.
        if value is None:
            return ''

        # Process date and time values separately.
        elif tableColumn.formatString == 'date':
            return value.strftime('%Y-%m-%d')

        elif tableColumn.formatString == 'time':
            return value.strftime('%H:%M')

        else:
            # Use format string for ordinary values.
            return tableColumn.formatString % value
