#!/usr/bin/env python

# ###########################################################################
#
# This file is part of Taurus
#
# http://taurus-scada.org
#
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
#
# Taurus 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.
#
# Taurus 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 Taurus.  If not, see <http://www.gnu.org/licenses/>.
#
# ###########################################################################

"""
itemsmodel Model and view for new CurveItem configuration
"""

# raise UnimplementedError('Under Construction!')

import copy

from taurus.external.qt import Qt
import taurus
from taurus.core.taurusbasetypes import TaurusElementType
from taurus.core.taurusexception import TaurusException
from taurus.qt.qtcore.mimetypes import TAURUS_MODEL_LIST_MIME_TYPE
from taurus.qt.qtcore.mimetypes import TAURUS_ATTR_MIME_TYPE
from taurus.qt.qtcore.mimetypes import TAURUS_MODEL_MIME_TYPE
from taurus.qt.qtgui.icon import getElementTypeIcon


# set some named constants
SRC_ROLE = Qt.Qt.UserRole + 1


class TaurusModelItem(object):
    """An item object for :class:`TaurusModelModel`. Exposes `display` `icon`
    and `ok` attributes which are calculated and kept in synch with the
    property `src`
    """

    def __init__(self, src=None, display=None):
        self.icon = Qt.QIcon()
        self.ok = True
        self._src = None
        self.setSrc(src)
        if display is not None:
            self.display = display

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        result.icon = Qt.QIcon(self.icon)
        result.ok = copy.deepcopy(self.ok, memo)
        result._src = copy.deepcopy(self._src, memo)
        result.display = copy.deepcopy(self.display, memo)
        return result

    def __repr__(self):
        ret = "TaurusModelItem('%s')" % (self.display)
        return ret

    def getSrc(self):
        """getter for src."""
        return self._src

    def setSrc(self, src):
        """processes the src and sets the values of _src, display, icon and ok
        attributes"""
        if src is None:
            self._src, self.display, self.icon, self.ok = (
                "",
                "(Empty)",
                Qt.QIcon(),
                True,
            )
            return
        src = str(src).strip()
        if src == self._src:
            return
        self._src = src
        # empty
        if src == "":
            self.display, self.icon, self.ok = "(Empty)", Qt.QIcon(), True
            return
        # TODO tango-centric. For tango devices
        try:
            dev = taurus.Device(src)
            if dev.getDeviceProxy() is not None:
                self.display, self.icon, self.ok = (
                    dev.getSimpleName(),
                    getElementTypeIcon(TaurusElementType.Device),
                    True,
                )
                return
            else:
                self.display, self.icon, self.ok = (
                    src,
                    Qt.QIcon.fromTheme("network-error"),
                    False,
                )
                return
        except Exception:
            # for tango attributes
            try:
                attr = taurus.Attribute(src)
                dev = attr.getParentObj()
            except TaurusException:
                self.display, self.icon, self.ok = (
                    src,
                    Qt.QIcon.fromTheme("dialog-warning"),
                    False,
                )
                return
            except Exception:
                # @todo: this catchall except is here as an emergency bugfix,
                #        but should probably be narrowed to PyTango DevFailed.
                self.display, self.icon, self.ok = (
                    src,
                    Qt.QIcon.fromTheme("network-error"),
                    False,
                )
                return
            if (
                taurus.getSchemeFromName(attr.getFullName()) == "tango"
                and dev.getDeviceProxy() is None
            ):
                self.display, self.icon, self.ok = (
                    src,
                    Qt.QIcon.fromTheme("network-error"),
                    False,
                )
                return

            self.display, self.icon, self.ok = (
                attr.getSimpleName(),
                getElementTypeIcon(TaurusElementType.Attribute),
                True,
            )

    # properties
    src = property(getSrc, setSrc)


class TaurusModelModel(Qt.QAbstractListModel):
    """A Qt data model for describing taurus models"""

    def __init__(self, items=None):
        if items is None:
            items = []
        super(TaurusModelModel, self).__init__()
        self.items = items

    def addItem(self, item):
        """appends an item to the internal list

        :param item:
        :type item: TaurusModelItem
        """
        self.items.append(item)

    def dumpData(self):
        """returns a deep copy of the internal item list representation"""
        return copy.deepcopy(self.items)

    def rowCount(self, index=Qt.QModelIndex()):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        return len(self.items)

    def data(self, index, role=Qt.Qt.DisplayRole):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if not index.isValid() or not (0 <= index.row() < self.rowCount()):
            return None
        row = index.row()
        # Display Role
        if role == Qt.Qt.DisplayRole:
            return str(self.items[row].display)
        elif role == Qt.Qt.DecorationRole:
            return self.items[row].icon
        elif role == Qt.Qt.TextColorRole:
            if not self.items[row].src:
                return Qt.QColor("gray")
            return Qt.QColor(self.items[row].ok and "green" or "red")
        elif role == SRC_ROLE:
            return str(self.items[row].src)
        elif role == Qt.Qt.ToolTipRole:
            return str(self.items[row].src)
        if role == Qt.Qt.EditRole:
            return str(self.items[row].src)
        return None

    def flags(self, index):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if not index.isValid():
            return Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsDropEnabled
        return Qt.Qt.ItemFlags(
            Qt.Qt.ItemIsEnabled
            | Qt.Qt.ItemIsEditable
            | Qt.Qt.ItemIsDragEnabled
            | Qt.Qt.ItemIsSelectable
        )

    def setData(self, index, value=None, role=Qt.Qt.EditRole):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if index.isValid() and (0 <= index.row() < self.rowCount()):
            row = index.row()
            item = self.items[row]
            if role == Qt.Qt.EditRole:
                item.src = value
            elif role == Qt.Qt.DisplayRole:
                item.display = value
            self.dataChanged.emit(index, index)
            return True
        return False

    def insertRows(self, position=None, rows=1, parentindex=None, items=None):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if position is None or position == -1:
            position = self.rowCount()
        if parentindex is None:
            parentindex = Qt.QModelIndex()
        if items is None:
            slice = [TaurusModelItem() for i in range(rows)]
        else:
            slice = list(items)
            # note that the rows parameter is ignored if items is passed
            rows = len(slice)
        self.beginInsertRows(parentindex, position, position + rows - 1)
        self.items = self.items[:position] + slice + self.items[position:]
        self.endInsertRows()
        return True

    def removeRows(self, position, rows=1, parentindex=None):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if parentindex is None:
            parentindex = Qt.QModelIndex()
        self.beginResetModel()
        self.beginRemoveRows(parentindex, position, position + rows - 1)
        self.items = self.items[:position] + self.items[position + rows :]
        self.endRemoveRows()
        self.endResetModel()
        return True

    def clearAll(self):
        """clears all rows"""
        self.removeRows(0, self.rowCount())

    def swapItems(self, index1, index2):
        """swap the items described by index1 and index2 in the list"""
        r1, r2 = index1.row(), index2.row()
        self.items[r1], self.items[r2] = self.items[r2], self.items[r1]
        self.dataChanged.emit(index1, index2)

    def mimeTypes(self):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        result = list(Qt.QAbstractItemModel.mimeTypes(self))
        result += [
            TAURUS_ATTR_MIME_TYPE,
            TAURUS_MODEL_MIME_TYPE,
            TAURUS_MODEL_LIST_MIME_TYPE,
            "text/plain",
        ]
        return result

    def dropMimeData(self, data, action, row, column, parent):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        if row == -1 and parent.isValid():
            row = parent.row()
        if data.hasFormat(TAURUS_ATTR_MIME_TYPE):
            items = [bytes(data.data(TAURUS_ATTR_MIME_TYPE)).decode("utf-8")]
        elif data.hasFormat(TAURUS_MODEL_MIME_TYPE):
            items = [bytes(data.data(TAURUS_MODEL_MIME_TYPE)).decode("utf-8")]
        elif data.hasFormat(TAURUS_MODEL_LIST_MIME_TYPE):
            items = (
                bytes(data.data(TAURUS_MODEL_LIST_MIME_TYPE))
                .decode("utf-8")
                .split()
            )
        elif data.hasText():
            items = [str(data.text())]
        else:
            return False
        self.insertItems(row, items)
        return True

    def insertItems(self, row, items):
        """convenience method to add new rows by passing a list of strings ()

        :param row: the row of the list at which the item insertion starts, if
            row==-1, items will be appended to the list
        :type row: int
        :param items: a sequence items to add to the list. The objects in the
            sequence can be either strings, :class:`TaurusModelItem` objects or
            tuples of valid arguments for initializing :class:`TaurusModelItem`
            objects
        :type items: seq
        """
        itemobjs = []
        for e in items:
            if isinstance(e, TaurusModelItem):
                itemobjs.append(e)
            elif isinstance(e, str):
                itemobjs.append(TaurusModelItem(src=e))
            else:
                # assuming it is a sequence of arguments that can be passed
                # to the constructor of TaurusModelItem
                itemobjs.append(TaurusModelItem(*e))
        self.insertRows(position=row, items=itemobjs)

    def mimeData(self, indexes):
        """reimplemented from :class:`Qt.QAbstractListModel`"""
        mimedata = Qt.QAbstractListModel.mimeData(self, indexes)
        if len(indexes) == 1:
            # mimedata.setData(TAURUS_ATTR_MIME_TYPE, self.data(indexes[0]))
            txt = self.data(indexes[0], role=SRC_ROLE)
            mimedata.setText(txt)
        return mimedata
        # mimedata.setData()


class TaurusModelList(Qt.QListView):
    """A list view widget to display and manage a list of models

    Tries to identify the type of model and show the state of the device/attr
    associated with it. It also allows drag and drop of models and sorting.
    """

    dataChangedSignal = Qt.pyqtSignal(list, name="dataChanged")

    def __init__(self, parent=None, items=None, designMode=False):
        super(TaurusModelList, self).__init__(parent)
        if items is None:
            items = []
        self._model = TaurusModelModel(items)
        self.setModel(self._model)
        self.setDragDropMode(self.DragDrop)
        # self.setAcceptDrops(True)
        self.setSelectionMode(self.ExtendedSelection)

        self._contextMenu = Qt.QMenu(self)
        self.addRowAction = self._contextMenu.addAction(
            Qt.QIcon.fromTheme("list-add"),
            "Add new row",
            self.newRow,
            Qt.QKeySequence.New,
        )
        self.removeSelectedAction = self._contextMenu.addAction(
            Qt.QIcon.fromTheme("list-remove"),
            "Remove Selected",
            self.removeSelected,
            Qt.QKeySequence.Delete,
        )
        self.removeAllAction = self._contextMenu.addAction(
            Qt.QIcon.fromTheme("edit-clear"),
            "Clear all",
            self.clear,
            Qt.QKeySequence("Ctrl+Del"),
        )
        self.moveUpAction = self._contextMenu.addAction(
            Qt.QIcon.fromTheme("go-up"),
            "Move up in the list",
            self._onMoveUpAction,
            Qt.QKeySequence("Alt+Up"),
        )
        self.moveDownAction = self._contextMenu.addAction(
            Qt.QIcon.fromTheme("go-down"),
            "Move down in the list",
            self._onMoveDownAction,
            Qt.QKeySequence("Alt+Down"),
        )
        self.editDisplayAction = self._contextMenu.addAction(
            Qt.QIcon("actions:format-text-italic.svg"),
            "Edit the display (leave the source)",
            self._onEditDisplay,
            Qt.QKeySequence("Alt+D"),
        )

        self.addActions(
            [
                self.addRowAction,
                self.removeSelectedAction,
                self.removeAllAction,
                self.moveUpAction,
                self.moveDownAction,
                self.editDisplayAction,
            ]
        )

        # signal connections
        selectionmodel = self.selectionModel()
        selectionmodel.selectionChanged.connect(self._onSelectionChanged)
        self._model.dataChanged.connect(self._onDataChanged)
        self._model.rowsInserted.connect(self._onDataChanged)
        self._model.rowsRemoved.connect(self._onDataChanged)
        self._onSelectionChanged(Qt.QItemSelection(), Qt.QItemSelection())

    def clear(self):
        """removes all items from the list"""
        self._model.clearAll()

    def _onEditDisplay(self):
        selected = self.selectionModel().selectedIndexes()
        if len(selected) == 1:
            idx = selected[0]
        else:
            return
        value = self._model.data(idx, role=Qt.Qt.DisplayRole)
        src = self._model.data(idx, role=SRC_ROLE)
        value, ok = Qt.QInputDialog.getText(
            self,
            "Display Value",
            "Display value for %s?" % src,
            Qt.QLineEdit.Normal,
            value,
        )
        if not ok:
            return
        self._model.setData(idx, value, role=Qt.Qt.DisplayRole)

    def _onSelectionChanged(self, selected, deselected):
        """updates the status of the actions that depend on the selection"""
        selectedIndexes = self.selectionModel().selectedRows()
        self.removeSelectedAction.setEnabled(len(selectedIndexes) > 0)
        self.moveUpAction.setEnabled(
            len(selectedIndexes) == 1 and selectedIndexes[0].row() > 0
        )
        self.moveDownAction.setEnabled(
            len(selectedIndexes) == 1
            and (0 <= selectedIndexes[0].row() < self._model.rowCount() - 1)
        )
        self.editDisplayAction.setEnabled(len(selectedIndexes) > 0)

    def _onDataChanged(self, *args):
        """emits a signal containing the current data as a list of strings"""
        self.dataChangedSignal.emit(self.getModelItems())

    def contextMenuEvent(self, event):
        """see :meth:`QWidget.contextMenuEvent`"""
        self._contextMenu.exec_(event.globalPos())
        event.accept()

    def _onMoveUpAction(self):
        """slot for move up action"""
        selected = self.selectionModel().selectedIndexes()
        if len(selected) != 1:
            return
        i1 = selected[0]
        i2 = self._model.index(i1.row() - 1)
        self._model.swapItems(i1, i2)
        self.selectionModel().select(i2, Qt.QItemSelectionModel.ClearAndSelect)

    def _onMoveDownAction(self):
        """slot for move down action"""
        selected = self.selectionModel().selectedIndexes()
        if len(selected) != 1:
            return
        i1 = selected[0]
        i2 = self._model.index(i1.row() + 1)
        self._model.swapItems(i1, i2)
        self.selectionModel().select(i2, Qt.QItemSelectionModel.ClearAndSelect)

    def newRow(self, position=None):
        """adds an empty row *before* the given position

        :param position: position at which the new row will be added. If None
            passed, it will be added at the end.
        :type position: int or None
        """
        if position is None:
            selected = self.selectionModel().selectedIndexes()
            if len(selected) == 0:
                position = -1
            elif len(selected) == 1:
                position = selected[0].row()
            else:
                return
        self._model.insertItems(position, [""])

    def removeSelected(self):
        """removes selected items from the list"""
        selected = sorted(
            [idx.row() for idx in self.selectionModel().selectedRows()],
            reverse=True,
        )
        for row in selected:  # we remove rows starting from the last one
            self._model.removeRows(row)

    def addModels(self, models):
        """adds models to the list

        :param models: sequence of model names to be added
        :type models: list<str>
        """
        self._model.insertItems(-1, models)

    def getModelItems(self):
        """returns the model item objects

        :return:
        :rtype: list<TaurusModelItem>


        .. seealso:: :meth:`getModelList`
        """
        return self._model.dumpData()

    def getModelList(self):
        """returns a the model names corresponding to the items in the list

        :return:
        :rtype: list<str>


        .. seealso:: :meth:`getModelItems`
        """
        return [str(s.src) for s in self.getModelItems()]

    @classmethod
    def getQtDesignerPluginInfo(cls):
        return {
            "group": "Taurus Input",
            "icon": "logos:taurus.png",
            "container": False,
            "module": "taurus.qt.qtgui.panel",
        }


if __name__ == "__main__":
    from taurus.qt.qtgui.application import TaurusApplication
    import sys

    app = TaurusApplication(cmd_line_parser=None)
    w = TaurusModelList()
    w.addModels(
        ["item%i" % i for i in range(3)]
        + [TaurusModelItem(src="src1", display="d1")]
        + [("src2", "d2")]
    )
    w.show()
    sys.exit(app.exec_())
