from __future__ import annotations

from typing import Any

from numpy import ndarray, iscomplexobj, asarray
from pyqtgraph import PlotDataItem

from ..Qt import QtGui, QtCore, QtWidgets
from .._py.valueeditor import Ui_MaskDialog


class ValueEditWidget(QtWidgets.QWidget, Ui_MaskDialog):
    requestData = QtCore.pyqtSignal(str)
    maskSignal = QtCore.pyqtSignal(str, list)
    itemChanged = QtCore.pyqtSignal(str, tuple, object)
    itemDeleted = QtCore.pyqtSignal(str, list)
    itemAdded = QtCore.pyqtSignal(str)
    values_selected = QtCore.pyqtSignal(str, str)
    split_signal = QtCore.pyqtSignal(str, int)

    def __init__(self, parent=None):
        super().__init__(parent=parent)

        self.setupUi(self)

        self.connected_figure = None
        self.shown_set = None
        self.items = {}

        self.model = ValueModel()
        self.model.itemChanged.connect(self.update_items)

        self.selection_model = QtCore.QItemSelectionModel(self.model)
        self.selection_model.selectionChanged.connect(self.show_position)

        self.tableView.setModel(self.model)
        self.tableView.setSelectionModel(self.selection_model)
        self.tableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.tableView.customContextMenuRequested.connect(self.ctx)

        self.selection_real = PlotDataItem(x=[], y=[], symbolSize=25, symbol='x',
                                           pen=None, symbolPen='#c9308e', symbolBrush='#c9308e')
        self.selection_imag = PlotDataItem(x=[], y=[], symbolSize=25, symbol='+',
                                           pen=None, symbolPen='#dcdcdc', symbolBrush='#dcdcdc')

    def __call__(self, items: dict):
        self.items = items

        self.graph_combobox.blockSignals(True)
        self.graph_combobox.clear()
        for k, v in items.items():
            self.graph_combobox.addItem(k[1], userData=k[0])
        self.graph_combobox.blockSignals(False)

        idx = self.graph_combobox.findData(self.connected_figure)
        if idx == -1:
            idx = 0

        self.graph_combobox.setCurrentIndex(idx)
        self.graph_combobox.currentIndexChanged.emit(idx)

        return self

    @QtCore.pyqtSlot(int, name='on_graph_combobox_currentIndexChanged')
    def _populate_sets(self, idx: int):
        if idx == -1:
            self.graph_combobox.setCurrentIndex(0)
            return

        self.set_combobox.blockSignals(True)
        self.set_combobox.clear()
        old_figure = self.connected_figure
        self.connected_figure = self.graph_combobox.currentData()

        if self.items:
            for sid, name in self.items[(self.graph_combobox.currentData(), self.graph_combobox.currentText())]:
                self.set_combobox.addItem(name, userData=sid)
        self.set_combobox.blockSignals(False)

        sidx = self.set_combobox.findData(self.shown_set)
        if sidx == -1:
            sidx = 0

        self.set_combobox.setCurrentIndex(sidx)
        self.set_combobox.currentIndexChanged.emit(sidx)

        self.values_selected.emit(old_figure, self.connected_figure)

    @QtCore.pyqtSlot(int, name='on_set_combobox_currentIndexChanged')
    def _populate_table(self, idx):
        self.selection_model.clearSelection()
        self.shown_set = self.set_combobox.itemData(idx)
        self.requestData.emit(self.set_combobox.itemData(idx))

    def set_data(self, data: list, mask: ndarray):
        self.selection_model.clearSelection()
        self.model.loadData(data, mask)
        self.spinBox.setMaximum(self.model.rowCount())

    def remove_graph(self):
        # remove everything
        self.connected_figure = None
        self.items = {}
        self.selection_model.clear()
        while self.model.rowCount():
            self.model.removeRow(0)
        self.graph_combobox.clear()
        self.set_combobox.clear()

    def ctx(self, pos: QtCore.QPoint):
        idx = self.tableView.indexAt(pos)
        if not idx.isValid():
            return

        menu = QtWidgets.QMenu()
        menu.addSeparator()

        hide_action = menu.addAction('Hide/Show')
        del_action = menu.addAction('Delete')
        split_action = menu.addAction('Split after selection')
        cpc_action = menu.addAction('Copy to clipboard')
        action = menu.exec(self.tableView.viewport().mapToGlobal(pos))

        if action == hide_action:
            self.mask_row()
        elif action == del_action:
            self.delete_item()
        elif action == cpc_action:
            self.copy_selection()
        elif action == split_action:
            self.split()

    def keyPressEvent(self, evt):
        if evt.matches(QtGui.QKeySequence.Copy):
            self.copy_selection()
        elif evt.key() == QtCore.Qt.Key_Delete:
            self.delete_item()
        else:
            super().keyPressEvent(evt)

    def copy_selection(self) -> str:
        table = ''
        for r in self.selection_model.selectedRows():
            table += self.model.data(r) + '\t'
            table += '\t'.join([self.model.data(r.sibling(r.row(), i)) for i in [1, 2]]) + '\n'

        QtWidgets.QApplication.clipboard().setText(table)

        return table

    @QtCore.pyqtSlot(name='on_split_button_clicked')
    def split(self):
        self.split_signal.emit(self.set_combobox.currentData(),
                               self.selection_model.selectedRows()[-1].row()+1)

    @QtCore.pyqtSlot(name='on_mask_button_clicked')
    def mask_row(self):
        for r in self.selection_model.selectedRows():
            self.model.setData(r, not self.model.data(r, ValueModel.maskRole), ValueModel.maskRole)
        self.maskSignal.emit(self.set_combobox.currentData(), self.model.mask)

    @QtCore.pyqtSlot(name='on_unmaskbutton_clicked')
    def unmask(self):
        self.model.unmask()
        self.maskSignal.emit(self.set_combobox.currentData(), self.model.mask)

    @QtCore.pyqtSlot(name='on_delete_button_clicked')
    def delete_item(self):
        idx = [r.row() for r in self.selection_model.selectedRows()]
        success = False
        for i in sorted(idx, reverse=True):
            success = self.model.removeRow(i)

        if success:
            self.itemDeleted.emit(self.set_combobox.currentData(), idx)
            self.spinBox.setMaximum(self.spinBox.maximum()-len(idx))

    @QtCore.pyqtSlot(name='on_add_button_clicked')
    def new_value(self):
        success = self.model.addRows()
        if success:
            self.spinBox.setMaximum(self.spinBox.maximum()+1)
            self.itemAdded.emit(self.set_combobox.currentData())

    @QtCore.pyqtSlot(int, int, str)
    def update_items(self, col, row, val):
        sid = self.set_combobox.currentData()
        new_value = complex(val)
        new_value = new_value.real if new_value.imag == 0 else new_value

        self.itemChanged.emit(sid, (col, row), new_value)

    @QtCore.pyqtSlot(QtCore.QItemSelection, QtCore.QItemSelection)
    def show_position(self, *_):
        xvals = []
        yvals = []
        for idx in self.selection_model.selectedRows():
            xvals.append(float(self.model.data(idx)))
            try:
                yvals.append(float(self.model.data(idx.sibling(idx.row(), 1))))
            except ValueError:
                yvals.append(complex(self.model.data(idx.sibling(idx.row(), 1))))

        yvals = asarray(yvals)

        if iscomplexobj(yvals):
            self.selection_real.setData(x=xvals, y=yvals.real)
            self.selection_imag.setData(x=xvals, y=yvals.imag)
        else:
            self.selection_real.setData(x=xvals, y=yvals)
            self.selection_imag.setData(x=[], y=[])

    @QtCore.pyqtSlot(name='on_toolButton_clicked')
    def goto(self):
        self.tableView.scrollTo(self.model.index(self.spinBox.value()-1, 0))


class ValueModel(QtCore.QAbstractTableModel):
    """
    TableModel with lazy loading
    """
    itemChanged = QtCore.pyqtSignal(int, int, str)
    load_number = 20
    maskRole = QtCore.Qt.UserRole+321

    def __init__(self, parent=None):
        super().__init__(parent=parent)

        self._data = None
        self.total_rows = 0
        self.rows_loaded = 0
        self.mask = None
        self.headers = ['x', 'y', '\u0394y']
        for i, hd in enumerate(self.headers):
            self.setHeaderData(i, QtCore.Qt.Horizontal, hd)

    def rowCount(self, *args, **kwargs) -> int:
        return self.total_rows

    def columnCount(self, *args, **kwargs) -> int:
        return len(self.headers)

    def loadData(self, data: list[ndarray], mask: ndarray):
        self.beginResetModel()
        self._data = []
        for x, y, y_err in zip(*data):
            self._data.append([x, y, y_err])
        self.total_rows = len(self._data)

        self.mask = mask.tolist()

        self.endResetModel()
        self.dataChanged.emit(self.index(0, 0), self.index(0, 1), [QtCore.Qt.DisplayRole])

    def data(self, idx: QtCore.QModelIndex, role=QtCore.Qt.DisplayRole) -> Any:
        if not idx.isValid():
            return

        row = idx.row()
        if role in [QtCore.Qt.DisplayRole, QtCore.Qt.EditRole]:
            val = self._data[row][idx.column()]
            if isinstance(val, complex):
                return f'{val.real:.8g}{val.imag:+.8g}j'
            else:
                return f'{val:.8g}'

        elif role == QtCore.Qt.BackgroundRole:
            pal = QtGui.QGuiApplication.palette()
            if not self.mask[row]:
                return pal.color(QtGui.QPalette.Disabled, QtGui.QPalette.Base)
            else:
                return pal.color(QtGui.QPalette.Base)

        elif role == QtCore.Qt.ForegroundRole:
            pal = QtGui.QGuiApplication.palette()
            if not self.mask[row]:
                return pal.color(QtGui.QPalette.Disabled, QtGui.QPalette.Text)
            else:
                return pal.color(QtGui.QPalette.Text)

        elif role == ValueModel.maskRole:
            return self.mask[row]

        else:
            return

    def setData(self, idx: QtCore.QModelIndex, value: str | bool, role=QtCore.Qt.DisplayRole) -> Any:
        col, row = idx.column(), idx.row()

        if role == ValueModel.maskRole:
            self.mask[row] = bool(value)
            self.dataChanged.emit(self.index(0, 0), self.index(0, 1), [role])

            return True

        if value:
            if role == QtCore.Qt.EditRole:
                try:
                    value = complex(value)
                except ValueError:
                    # not a number
                    return False
                self._data[row][col] = value.real if value.imag == 0 else value
                self.itemChanged.emit(col, row, str(value))
                self.dataChanged.emit(self.index(0, 0), self.index(0, 1), [role])

            else:
                return super().setData(idx, value, role=role)

            return True

        else:
            return False

    def headerData(self, section: int, orientation, role=QtCore.Qt.DisplayRole) -> Any:
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                return self.headers[section]
            else:
                return str(section+1)

        return

    def canFetchMore(self, idx: QtCore.QModelIndex) -> bool:
        if not idx.isValid():
            return False

        return self.total_rows > self.rows_loaded

    def fetchMore(self, idx: QtCore.QModelIndex):
        remaining = self.total_rows - self.rows_loaded
        to_be_loaded = min(remaining, ValueModel.load_number)

        self.beginInsertRows(QtCore.QModelIndex(), self.rows_loaded, self.rows_loaded + to_be_loaded - 1)
        self.rows_loaded += to_be_loaded
        self.endInsertRows()

    def flags(self, idx: QtCore.QModelIndex) -> QtCore.Qt.ItemFlag:
        return QtCore.QAbstractTableModel.flags(self, idx) | QtCore.Qt.ItemIsEditable

    def removeRows(self, pos: int, rows: int, parent=None, *args, **kwargs) -> bool:
        self.beginRemoveRows(parent, pos, pos+rows-1)

        for _ in range(rows):
            self._data.pop(pos)
            self.mask.pop(pos)

        self.endRemoveRows()
        self.total_rows -= rows

        return True

    def addRows(self, num=1):
        return self.insertRows(self.rowCount(), num)

    def insertRows(self, pos: int, rows: int, parent=QtCore.QModelIndex(), *args, **kwargs):
        self.beginInsertRows(parent, pos, pos+rows-1)

        for _ in range(rows):
            self._data.insert(pos, [0.0] * self.columnCount())
            self.mask.insert(pos, True)
        self.total_rows += rows

        self.endInsertRows()

        return True

    def unmask(self):
        self.mask = [True] * self.total_rows
        self.dataChanged.emit(self.index(0, 0), self.index(0, 1), [ValueModel.maskRole])
