# pylint: disable=E1101,F0401

from __future__ import absolute_import, division, print_function

from qtpy import QtCore, QtGui, QtWidgets, PYQT5
from qtpy.QtCore import Qt
from glue.core.hub import HubListener
from glue.core import message as m
from glue.core.decorators import memoize
from glue import core
from glue.core.qt.mime import LAYERS_MIME_TYPE
from glue.icons.qt import layer_icon

from glue.core.qt.style_dialog import StyleDialog
from glue.utils import nonpartial
from glue.utils.qt import PyMimeData

DATA_IDX = 0
SUBSET_IDX = 1


def full_edit_factory(item, pos):
    StyleDialog.dropdown_editor(item, pos)


def restricted_edit_factory(item, pos):
    StyleDialog.dropdown_editor(item, pos, edit_label=False)


class Item(object):
    edit_factory = None
    glue_data = None
    flags = Qt.ItemIsEnabled
    tooltip = None

    def font(self):
        return QtGui.QFont()

    def icon(self):
        return None

    @property
    def label(self):
        return self._label


class DataCollectionItem(Item):

    def __init__(self, dc):
        self.dc = dc
        self.row = 0
        self.column = 0
        self.parent = None
        self._label = ''
        self.children_count = 2

    @memoize
    def child(self, row):
        if row == DATA_IDX:
            return DataListItem(self.dc, self)
        if row == SUBSET_IDX:
            return SubsetListItem(self.dc, self)
        return None


class DataListItem(Item):

    def __init__(self, dc, parent):
        self.dc = dc
        self.parent = parent
        self.row = DATA_IDX
        self.column = 0
        self._label = 'Data'

    @memoize
    def child(self, row):
        if row < len(self.dc):
            return DataItem(self.dc, row, self)

    @property
    def children_count(self):
        return len(self.dc)

    def font(self):
        result = QtGui.QFont()
        result.setBold(True)
        return result


class DataItem(Item):
    edit_factory = full_edit_factory
    flags = (Qt.ItemIsSelectable | Qt.ItemIsEnabled |
             Qt.ItemIsDragEnabled)

    def __init__(self, dc, row, parent):
        self.dc = dc
        self.row = row
        self.parent = parent
        self.column = 0
        self.children_count = 0

    @property
    def data(self):
        return self.dc[self.row]

    @property
    def glue_data(self):
        return self.data

    @property
    def label(self):
        return self.data.label

    @label.setter
    def label(self, value):
        self.data.label = value

    @property
    def tooltip(self):
        # Return the label as the tooltip - this is useful if filenames are
        # really long and don't fit in the window.
        return self.label

    @property
    def style(self):
        return self.data.style

    def icon(self):
        return layer_icon(self.data)


class SubsetListItem(Item):

    def __init__(self, dc, parent):
        self.dc = dc
        self.parent = parent
        self.row = SUBSET_IDX
        self._label = 'Subsets'
        self.column = 0

    @memoize
    def child(self, row):
        if row < len(self.dc.subset_groups):
            return SubsetGroupItem(self.dc, row, self)

    @property
    def children_count(self):
        return len(self.dc.subset_groups)

    def font(self):
        result = QtGui.QFont()
        result.setBold(True)
        return result


class SubsetGroupItem(Item):
    edit_factory = full_edit_factory
    flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

    def __init__(self, dc, row, parent):
        self.parent = parent
        self.dc = dc
        self.row = row
        self.column = 0

    @property
    def subset_group(self):
        return self.dc.subset_groups[self.row]

    @property
    def glue_data(self):
        return self.subset_group

    @property
    def label(self):
        return self.subset_group.label

    @label.setter
    def label(self, value):
        self.subset_group.label = value

    @property
    def tooltip(self):
        if type(self.subset_group.subset_state) == core.subset.SubsetState:
            return "Empty subset"

        atts = self.subset_group.subset_state.attributes
        atts = [a for a in atts if isinstance(a, core.ComponentID)]

        if len(atts) > 0:
            lbl = ', '.join(a.label for a in atts)
            return "Selection on %s" % lbl

    @property
    def style(self):
        return self.subset_group.style

    @property
    def children_count(self):
        return len(self.subset_group.subsets)

    @memoize
    def child(self, row):
        return SubsetItem(self.dc, self.subset_group, row, self)

    def icon(self):
        return layer_icon(self.subset_group)


class SubsetItem(Item):
    edit_factory = restricted_edit_factory
    flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled

    def __init__(self, dc, subset_group, subset_idx, parent):
        self.parent = parent
        self.subset_group = subset_group
        self.row = subset_idx
        self.parent = parent
        self.children_count = 0
        self.column = 0

    @property
    def subset(self):
        return self.subset_group.subsets[self.row]

    @property
    def label(self):
        return self.subset.verbose_label

    def icon(self):
        return layer_icon(self.subset)

    @property
    def style(self):
        return self.subset.style

    @property
    def glue_data(self):
        return self.subset


class DataCollectionModel(QtCore.QAbstractItemModel, HubListener):
    new_item = QtCore.Signal(QtCore.QModelIndex)

    def __init__(self, data_collection, parent=None):
        QtCore.QAbstractItemModel.__init__(self, parent)
        HubListener.__init__(self)

        self.data_collection = data_collection
        self.root = DataCollectionItem(data_collection)
        self._items = {}   # map hashes of Model pointers to model items
        # without this reference, PySide clobbers instance
        # data of model items
        self.register_to_hub(self.data_collection.hub)
        if not PYQT5:
            self.setSupportedDragActions(Qt.CopyAction)

    def supportedDragActions(self):
        return Qt.CopyAction

    def index(self, row, column, parent=QtCore.QModelIndex()):
        if column != 0:
            return QtCore.QModelIndex()

        if not parent.isValid():
            parent_item = self.root
        else:
            parent_item = self._get_item(parent)
            if parent_item is None:
                return QtCore.QModelIndex()

        child_item = parent_item.child(row)
        if child_item:
            return self._make_index(row, column, child_item)
        else:
            return QtCore.QModelIndex()

    def _get_item(self, index):
        if not index.isValid():
            return None
        return self._items.get(id(index.internalPointer()), None)

    def _make_index(self, row, column, item):
        if item is not None:
            result = self.createIndex(row, column, item)
            self._items[id(result.internalPointer())] = item
            assert result.internalPointer() is item
            return result
        return self.createIndex(row, column)

    def to_indices(self, items):
        """Translate a list of Data, Subset, or SubsetGroups
        to a list of indices"""
        result = []
        for item in items:
            if isinstance(item, core.Data):
                idx = self.data_index(list(self.data_collection).index(item))
            elif isinstance(item, core.SubsetGroup):
                idx = self.subsets_index(
                    self.data_collection.subset_groups.index(item))
            elif isinstance(item, core.subset_group.GroupedSubset):
                grp = item.group
                idx = self.subsets_index(
                    self.data_collection.subset_groups.index(grp))
                row = list(self.data_collection).index(item.data)
                idx = self.index(row, idx)
            else:
                raise NotImplementedError(type(item))
            result.append(idx)
        return result

    def flags(self, index=QtCore.QModelIndex()):
        item = self._get_item(index)
        if item is None:
            return Qt.NoItemFlags
        else:
            return item.flags

    def data(self, index, role):
        if not index.isValid():
            return

        dispatch = {
            Qt.DisplayRole: self._display_data,
            Qt.FontRole: self._font_data,
            Qt.DecorationRole: self._icon_data,
            Qt.UserRole: self._user_data,
            Qt.ToolTipRole: self._tooltip_data}

        if role in dispatch:
            return dispatch[role](index)

    def setData(self, index, value, role=Qt.EditRole):
        if role != Qt.EditRole:
            return False
        try:
            self._get_item(index).label = value
            return True
        except AttributeError:
            return False

    def _tooltip_data(self, index):
        tooltip = self._get_item(index).tooltip
        return tooltip

    def _user_data(self, index):
        return self._get_item(index)

    def _display_data(self, index):
        return self._get_item(index).label

    def _font_data(self, index):
        item = self._get_item(index)
        return item.font()

    def _icon_data(self, index):
        return self._get_item(index).icon()

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        return ''

    def data_index(self, data_number=None):
        """
        Fetch the QtCore.QModelIndex for a given data index,
        or the index for the parent data item

        :param data_number: position of data set to fetch, or None
        """
        base = self.index(DATA_IDX, 0)
        if data_number is None:
            return base
        return self.index(data_number, 0, base)

    def subsets_index(self, subset_number=None):
        """
        Fetch the QtCore.QModelIndex for a given subset,
        or the index for the parent subset item

        :param data_number: position of subset group to fetch, or None
        """
        base = self.index(SUBSET_IDX, 0)
        assert isinstance(self._get_item(base), SubsetListItem)
        if subset_number is None:
            return base
        return self.index(subset_number, 0, base)

    def rowCount(self, index=QtCore.QModelIndex()):
        item = self._get_item(index)

        if item is None:
            return self.root.children_count

        return item.children_count

    def parent(self, index=None):

        if index is None:  # overloaded QtCore.QObject.parent()
            return QtCore.QObject.parent(self)

        item = self._get_item(index)
        if item is None:
            return QtCore.QModelIndex()

        return self._make_index(item.row, item.column, item.parent)

    def columnCount(self, index):
        return 1

    def register_to_hub(self, hub):
        for msg in [m.DataCollectionDeleteMessage,
                    m.SubsetDeleteMessage]:
            hub.subscribe(self, msg, lambda x: self.invalidate())

        hub.subscribe(self, m.DataCollectionAddMessage, self._on_add_data)
        hub.subscribe(self, m.SubsetCreateMessage, self._on_add_subset)

    def _on_add_data(self, message):
        self.invalidate()
        idx = self.data_index(len(self.data_collection) - 1)
        self.new_item.emit(idx)

    def _on_add_subset(self, message):
        self.invalidate()
        idx = self.subsets_index(len(self.data_collection.subset_groups) - 1)
        self.new_item.emit(idx)

    def invalidate(self):
        self.root = DataCollectionItem(self.data_collection)
        self._items.clear()
        if not PYQT5:
            self.reset()
        self.layoutChanged.emit()

    def glue_data(self, indices):
        """ Given a list of indices, return a list of all selected
        Data, Subset, and SubsetGroup objects.
        """
        items = [self._get_item(idx) for idx in indices]
        items = [item.glue_data for item in items]
        return items

    def mimeData(self, indices):
        data = self.glue_data(indices)
        result = PyMimeData(data, **{LAYERS_MIME_TYPE: data})
        self._mime = result  # hold reference to prevent segfault
        return result

    def mimeTypes(self):
        return [LAYERS_MIME_TYPE]


class DataCollectionView(QtWidgets.QTreeView):
    selection_changed = QtCore.Signal()

    def __init__(self, parent=None):
        super(DataCollectionView, self).__init__(parent)
        self.doubleClicked.connect(self._edit)

        # this keeps the full-row of the selection bar in-sync
        self.pressed.connect(nonpartial(self._update_viewport))

        # only edit label on model.new_item
        self.setItemDelegate(LabeledDelegate())
        self.setEditTriggers(self.NoEditTriggers)

        self.setIconSize(QtCore.QSize(16, 16))

        self._timer = QtCore.QTimer(self)
        self._timer.timeout.connect(nonpartial(self._update_viewport))
        self._timer.start(1000)

    def _update_viewport(self):
        # We have to do this here to make sure we always get the latest
        # viewport instance.
        self.viewport().update()

    def selected_layers(self):
        idxs = self.selectedIndexes()
        return self._model.glue_data(idxs)

    def set_selected_layers(self, layers):
        sm = self.selectionModel()
        idxs = self._model.to_indices(layers)
        self.select_indices(*idxs)

    def select_indices(self, *indices):
        sm = self.selectionModel()
        sm.clearSelection()
        for idx in indices:
            sm.select(idx, sm.Select)

    def set_data_collection(self, data_collection):
        self._model = DataCollectionModel(data_collection)
        self.setModel(self._model)

        sm = QtCore.QItemSelectionModel(self._model)
        sm.selectionChanged.connect(lambda *args:
                                    self.selection_changed.emit())
        self.setSelectionModel(sm)

        self.setRootIsDecorated(False)
        self.setExpandsOnDoubleClick(False)
        self.expandToDepth(0)
        self._model.layoutChanged.connect(lambda: self.expandToDepth(0))
        self._model.layoutChanged.connect(self.selection_changed.emit)
        self._model.new_item.connect(self.select_indices)

        self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        self.setDragEnabled(True)
        self.setDropIndicatorShown(True)
        self.setDragDropMode(QtWidgets.QAbstractItemView.DragOnly)

    def edit_label(self, index):
        if not (self._model.flags(index) & Qt.ItemIsEditable):
            return
        self.edit(index)

    def _edit(self, index):
        item = self._model.data(index, role=Qt.UserRole)
        if item is None or item.edit_factory is None:
            return

        rect = self.visualRect(index)
        pos = self.mapToGlobal(rect.bottomLeft())
        pos.setY(pos.y() + 1)
        item.edit_factory(pos)


class LabeledDelegate(QtWidgets.QStyledItemDelegate):

    """ Add placeholder text to default delegate """

    def setEditorData(self, editor, index):
        super(LabeledDelegate, self).setEditorData(editor, index)
        label = index.model().data(index, role=Qt.DisplayRole)
        editor.selectAll()
        editor.setText(label)


if __name__ == "__main__":

    from glue.utils.qt import get_qapp
    from qtpy import QtWidgets
    from glue.core import Data, DataCollection

    app = get_qapp()

    dc = DataCollection()
    dc.append(Data(label='w'))

    view = DataCollectionView()
    view.set_data_collection(dc)
    view.show()
    view.raise_()
    dc.extend([Data(label='x', x=[1, 2, 3]),
               Data(label='y', y=[1, 2, 3]),
               Data(label='z', z=[1, 2, 3])])
    app.exec_()
