import bisect
import enum
import math
from typing import List, Optional, Union

import numpy as np
from qgis.PyQt.QtCore import QAbstractListModel, QDate, QDateTime, QMetaType, QModelIndex, Qt, QTime, QVariant
from qgis.core import QgsEditorWidgetSetup, QgsExpressionContext, QgsExpressionContextUtils, QgsField, QgsVectorLayer
from qgis.gui import QgsDateEdit, QgsDateTimeEdit, QgsDoubleSpinBox, QgsEditorConfigWidget, QgsEditorWidgetFactory, \
    QgsEditorWidgetWrapper, QgsFieldExpressionWidget, QgsGui, QgsSpinBox, QgsTimeEdit
from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel
from qgis.PyQt.QtWidgets import QComboBox, QLineEdit, QWidget

from eotimeseriesviewer import DIR_UI
from eotimeseriesviewer.qgispluginsupport.qps.classification.classificationscheme import ClassificationScheme, \
    ClassificationSchemeWidget
from eotimeseriesviewer.qgispluginsupport.qps.unitmodel import datetime64
from eotimeseriesviewer.qgispluginsupport.qps.utils import loadUi

labelEditorWidgetFactory = None
EDITOR_WIDGET_REGISTRY_KEY = 'EOTSV Quick Label'


class LabelShortcutType(enum.Flag):
    """Enumeration for shortcuts to be derived from a TimeSeriesDate instance"""
    Off = enum.auto()
    Date = enum.auto()
    DateTime = enum.auto()
    Time = enum.auto()
    DOY = enum.auto()
    Year = enum.auto()
    DecimalYear = enum.auto()
    Sensor = enum.auto()
    SourceImage = enum.auto()
    Customized = enum.auto()
    Classification = enum.auto()

    Autogenerated = Date | DateTime | Time | DOY | Year | Sensor | DecimalYear | SourceImage | Customized

    # Classification = 'Classification'

    @classmethod
    def fromConfig(cls, config: dict) -> Optional['LabelShortcutType']:
        if isinstance(config, dict) and LabelConfigurationKey.LabelType in config:
            return cls.fromValue(config[LabelConfigurationKey.LabelType])
        return None

    @classmethod
    def fromValue(cls, value: Union[str, int, 'LabelShortcutType']) -> 'LabelShortcutType':
        for t in LabelShortcutType:
            if value in [t, t.name, t.value]:
                return t
        raise NotImplementedError(f'Cannot get LabelShortcutType from "{value}"')

    def __str__(self):
        return str(self.name)


shortCutsAny = LabelShortcutType.Off | LabelShortcutType.Customized
shortCutsInt = shortCutsAny | LabelShortcutType.Year | LabelShortcutType.DOY | LabelShortcutType.Classification
shortCutsFloat = shortCutsAny | LabelShortcutType.Year | LabelShortcutType.DOY | LabelShortcutType.DecimalYear

QUICK_LABEL_SHORTCUT_TYPES = {
    QMetaType.QString:
        shortCutsInt | shortCutsFloat
        | LabelShortcutType.Sensor | LabelShortcutType.SourceImage
        | LabelShortcutType.Date | LabelShortcutType.DateTime | LabelShortcutType.Time,
    QMetaType.Int: shortCutsInt,
    QMetaType.Long: shortCutsInt,
    QMetaType.LongLong: shortCutsInt,
    QMetaType.UInt: shortCutsInt,
    QMetaType.ULong: shortCutsInt,
    QMetaType.ULongLong: shortCutsInt,
    QMetaType.Float: shortCutsFloat,
    QMetaType.Double: shortCutsFloat,
    QMetaType.QDate: shortCutsAny | LabelShortcutType.Date,
    QMetaType.QDateTime: shortCutsAny | LabelShortcutType.Date | LabelShortcutType.DateTime,
    QMetaType.QTime: shortCutsAny | LabelShortcutType.Time,
}

QUICK_LABEL_SHORTCUT_TYPE_TOOLTIPS = {
    LabelShortcutType.Off: 'Off / None',
    LabelShortcutType.Customized: 'Labels are generated using a customized field expression',
    LabelShortcutType.Classification: 'Labels are generated using a shortcut for a classification class',
    LabelShortcutType.Time: 'Adds a time stamp',
    LabelShortcutType.Date: 'Adds a date stamp',
    LabelShortcutType.DateTime: 'Adds a date-time stamp',
    LabelShortcutType.DecimalYear: 'Adds the decimal year',
    LabelShortcutType.DOY: 'Add the day of year',
    LabelShortcutType.SourceImage: 'Adds the source image path',
    LabelShortcutType.Sensor: 'Add the sensor name'
}


def registerLabelShortcutEditorWidget():
    reg = QgsGui.editorWidgetRegistry()
    global labelEditorWidgetFactory
    if EDITOR_WIDGET_REGISTRY_KEY not in reg.factories().keys():
        labelEditorWidgetFactory = LabelShortcutWidgetFactory(EDITOR_WIDGET_REGISTRY_KEY)
        reg.registerWidget(EDITOR_WIDGET_REGISTRY_KEY, labelEditorWidgetFactory)
    else:
        labelEditorWidgetFactory = reg.factories()[EDITOR_WIDGET_REGISTRY_KEY]


class LabelShortcutEditorConfigWidget(QgsEditorConfigWidget):

    def __init__(self, vl: QgsVectorLayer, fieldIdx: int, parent: QWidget):

        super(LabelShortcutEditorConfigWidget, self).__init__(vl, fieldIdx, parent)
        loadUi(DIR_UI / 'labelshortcuteditorconfigwidget.ui', self)

        self.cbLabelType: QComboBox
        self.cbLabelGroup: QComboBox
        assert isinstance(vl, QgsVectorLayer)
        field = vl.fields().at(fieldIdx)
        assert isinstance(field, QgsField)

        self.labelFieldInfo.setText(f'{field.name()} ({field.typeName()})')

        self.shortcutModel = LabelShortcutTypeModel(field)
        self.cbLabelType.setModel(self.shortcutModel)
        self.cbLabelType.currentIndexChanged[int].connect(self.onLabelTypeChanged)

        self.mLabelGroupModel: LabelShortcutGroupModel = LabelShortcutGroupModel.instance()
        self.cbLabelGroup.setModel(self.mLabelGroupModel)
        self.cbLabelGroup.setEditable(True)
        self.cbLabelGroup.currentIndexChanged.connect(self.changed.emit)

        self.btnAddGroup.setDefaultAction(self.actionAddGroup)
        self.btnRemoveGroup.setDefaultAction(self.actionRemoveGroup)
        self.actionAddGroup.triggered.connect(self.onAddGroup)
        self.actionRemoveGroup.triggered.connect(self.onRemoveGroup)

        # self.setConfig(vl.editorWidgetSetup(fieldIdx).config())
        self.mLastConfig = {}

        self.setShortcutType(LabelShortcutType.Off)
        self.onLabelTypeChanged()

    def classificationSchemeWidget(self) -> ClassificationSchemeWidget:
        return self.classificationWidget

    def classificationScheme(self) -> Optional[ClassificationScheme]:
        if self.shortcutType() == LabelShortcutType.Classification:
            return self.classificationSchemeWidget().classificationScheme()
        else:
            return None

    def customExpression(self) -> Optional[str]:
        if self.shortcutType() == LabelShortcutType.Customized:
            w: QgsFieldExpressionWidget = self.fieldExpressionWidget
            return w.expression()
        else:
            return None

    def onLabelTypeChanged(self):
        currentType = self.shortcutType()
        b = currentType == LabelShortcutType.Customized
        self.fieldExpressionWidget.setVisible(b)
        self.fieldExpressionWidget.setEnabled(b)

        b = currentType == LabelShortcutType.Classification
        self.classificationWidget.setVisible(b)
        self.classificationWidget.setEnabled(b)

        self.changed.emit()

    def setShortcutType(self, labelType: LabelShortcutType):
        assert isinstance(labelType, LabelShortcutType)
        currentType = self.shortcutType()
        cb: QComboBox = self.cbLabelType
        if currentType != labelType and self.shortcutModel.isEnabled(labelType):
            for i in range(cb.count()):
                if labelType == cb.itemData(i, Qt.UserRole):
                    cb.setCurrentIndex(i)
                    break

    def shortcutType(self) -> LabelShortcutType:

        return self.cbLabelType.currentData(Qt.UserRole)

    def onAddGroup(self):
        self.mLabelGroupModel.addGroup(self.labelGroup())

    def onRemoveGroup(self):
        self.mLabelGroupModel.removeGroup(self.labelGroup())

    def config(self, *args, **kwargs) -> dict:
        """
        Return the widget configuration
        :param args:
        :type args:
        :param kwargs:
        :type kwargs:
        :return:
        :rtype:
        """

        labelType = self.cbLabelType.currentData()

        conf = {LabelConfigurationKey.LabelType: labelType.name,
                LabelConfigurationKey.LabelGroup: self.labelGroup()}

        if cs := self.classificationScheme():
            conf[LabelConfigurationKey.LabelClassification] = cs.asMap()

        if expression := self.customExpression():
            conf[LabelConfigurationKey.LabelExpression] = expression

        return conf

    def addLabelGroup(self, grp: str):

        m: QStandardItemModel = self.cbLabelGroup.model()

        if grp not in ['', None] and isinstance(m, QStandardItemModel):
            exists = False
            for r in range(m.rowCount()):
                item = m.item(r)
                if grp == item.data(Qt.DisplayRole):
                    exists = True
                    break
            if not exists:
                m.appendRow(QStandardItem(grp))

    def setLabelGroup(self, grp):

        if grp is None:
            grp = ''
        else:
            grp = str(grp).strip()

        i = self.cbLabelGroup.findText(grp)
        if i == -1 and grp != '':
            self.addLabelGroup(grp)
            i = self.cbLabelGroup.findText(grp)

        if i >= 0:
            self.cbLabelGroup.setCurrentIndex(i)
            s = ""
        else:
            self.cbLabelGroup.setCurrentText(grp)

    def labelGroup(self) -> str:
        return self.cbLabelGroup.currentText()

    def fieldName(self) -> str:
        field = self.layer().fields().at(self.field())
        return field.name()

    def createExpressionContext(self) -> QgsExpressionContext:

        context = QgsExpressionContext()
        context.appendScope(QgsExpressionContextUtils.globalScope())
        context.appendScope(QgsExpressionContextUtils.layerScope(self.layer()))

        return context

    def setConfig(self, config: dict):
        assert isinstance(config, dict)

        labelType: LabelShortcutType = LabelShortcutType.fromValue(
            config.get(LabelConfigurationKey.LabelType, LabelShortcutType.Off)
        )

        assert isinstance(labelType, LabelShortcutType)
        labelGroup: str = config.get(LabelConfigurationKey.LabelGroup, '')
        self.setLabelGroup(labelGroup)

        if cs := config.get(LabelConfigurationKey.LabelClassification, None):
            if isinstance(cs, dict):
                cs = ClassificationScheme.fromMap(cs)
                if isinstance(cs, ClassificationScheme):
                    self.classificationSchemeWidget().setClassificationScheme(cs)

        if expr := config.get(LabelConfigurationKey.LabelExpression, None):
            self.fieldExpressionWidget.setExpression(expr)

        if self.shortcutModel.isEnabled(labelType):
            i = self.cbLabelType.findData(labelType)
            self.cbLabelType.setCurrentIndex(i)

    def onIndexChanged(self, *args):
        self.changed.emit()


class LabelShortcutEditorWidgetWrapper(QgsEditorWidgetWrapper):

    def __init__(self, vl: QgsVectorLayer, fieldIdx: int, editor: QWidget, parent: QWidget):
        super(LabelShortcutEditorWidgetWrapper, self).__init__(vl, fieldIdx, editor, parent)

    def createWidget(self, parent: QWidget = None) -> QWidget:
        """
        Create the data input widget
        :param parent: QWidget
        :return: QLineEdit | QgsDateTimeEdit | QSpinBox
        """
        # log('createWidget')
        # labelType = self.configLabelType()
        fieldType = self.field().type()
        if fieldType == QVariant.Date:
            return QgsDateEdit(parent)
        elif fieldType == QVariant.DateTime:
            return QgsDateTimeEdit(parent)
        elif fieldType == QVariant.Time:
            return QgsTimeEdit(parent)
        elif fieldType == QVariant.Double:
            return QgsDoubleSpinBox(parent)
        elif fieldType == QVariant.Int:
            return QgsSpinBox(parent)
        else:
            return QLineEdit(parent)

    def initWidget(self, editor: QWidget):
        # log(' initWidget')

        # if isinstance(editor, ClassificationSchemeComboBox):
        #    cs = self.configClassificationScheme()
        #    if isinstance(cs, ClassificationScheme):
        #        self.mEditor.setClassificationScheme(cs)
        #        self.mEditor.currentIndexChanged.connect(self.onValueChanged)

        if isinstance(editor, QLineEdit):
            editor.textChanged.connect(self.onValueChanged)
        elif isinstance(editor, (QgsTimeEdit, QgsDateEdit, QgsDateTimeEdit)):
            if isinstance(editor, QgsDateEdit):
                editor.setDisplayFormat('yyyy-MM-dd')
            elif isinstance(editor, QgsDateTimeEdit):
                editor.setDisplayFormat('yyyy-MM-dd HH:mm')
            elif isinstance(editor, QgsTimeEdit):
                pass
            editor.clear()
            editor.valueChanged.connect(self.onValueChanged)
        elif isinstance(editor, (QgsDoubleSpinBox, QgsSpinBox)):
            editor.valueChanged.connect(self.onValueChanged)

        else:
            s = ""

    def onValueChanged(self, *args):
        self.valueChanged.emit(self.value())
        s = ""

    def valid(self, *args, **kwargs) -> bool:
        """
        Returns True if a valid editor widget exists
        :param args:
        :param kwargs:
        :return: bool
        """
        # return isinstance(self.mEditor, (ClassificationSchemeComboBox, QLineEdit))
        return isinstance(self.widget(), (QLineEdit, QgsDateTimeEdit, QgsTimeEdit,
                                          QgsDateEdit, QgsSpinBox, QgsDoubleSpinBox))

    def value(self, *args, **kwargs):
        """
        Returns the value
        :param args:
        :param kwargs:
        :return:
        """
        typeCode = self.field().type()

        editor = self.widget()
        if isinstance(editor, QLineEdit):
            value = editor.text()
            dt64 = None
            try:
                dt64 = datetime64(value)
            except Exception:
                pass

            if isinstance(dt64, np.datetime64) and np.isfinite(dt64):
                if typeCode == QVariant.DateTime:
                    return QDateTime(dt64.astype(object))
                elif typeCode == QVariant.Date:
                    return QDate(dt64.astype(object))
            if typeCode == QVariant.String:
                return value

        elif isinstance(editor, QgsDateTimeEdit):
            if typeCode == QVariant.DateTime:
                return editor.dateTime()
            elif typeCode == QVariant.Date:
                return editor.date()
            elif typeCode == QVariant.String:
                return str(editor.dateTime())

        elif isinstance(editor, (QgsSpinBox, QgsDoubleSpinBox)):
            return editor.value()
        else:
            s = ""
        return self.defaultValue()

    def setEnabled(self, enabled: bool):
        editor = self.widget()
        if isinstance(editor, QWidget):
            editor.setEnabled(enabled)

    # def setFeature(self, feature:QgsFeature):
    #    s = ""

    def setValue(self, value):

        # if isinstance(self.mEditor, ClassificationSchemeComboBox):
        #    cs = self.mEditor.classificationScheme()
        #    if isinstance(cs, ClassificationScheme) and len(cs) > 0:
        #        i = cs.classIndexFromValue(value)
        #        self.mEditor.setCurrentIndex(max(i, 0))
        # elif isinstance(self.mEditor, QLineEdit):
        w = self.widget()

        if value in [None, QVariant()]:
            if isinstance(w, (QgsTimeEdit, QgsDateEdit, QgsDateTimeEdit)):
                w.setEmpty()
            elif isinstance(w, (QgsSpinBox, QgsDoubleSpinBox)):
                w.clear()
            elif isinstance(w, QLineEdit):
                w.clear()
            else:
                s = ""
        else:
            if isinstance(w, QgsTimeEdit):
                w.setTime(QTime(value))
            elif isinstance(w, QgsDateEdit):
                w.setDate(QDate(value))
            elif isinstance(w, QgsDateTimeEdit):
                w.setDateTime(QDateTime(value))
            elif isinstance(w, (QgsSpinBox, QgsDoubleSpinBox)):
                if w.maximum() <= value:
                    e = int(math.log10(value)) + 1
                    w.setMaximum(int(10 ** e))
                w.setClearValue(value)
                w.setValue(value)
            elif isinstance(w, QLineEdit):
                w.setText(str(value))
            else:
                s = ""


class LabelShortcutWidgetFactory(QgsEditorWidgetFactory):
    """
    A QgsEditorWidgetFactory to create widgets for EOTSV Quick Labeling
    """

    @staticmethod
    def instance():
        return QgsGui.editorWidgetRegistry().factory(EDITOR_WIDGET_REGISTRY_KEY)

    def __init__(self, name: str):

        super(LabelShortcutWidgetFactory, self).__init__(name)

        self.mLabelGroupModel: QStandardItemModel = QStandardItemModel()
        self.mLabelGroupModel.appendRow(QStandardItem(''))

    def name(self) -> str:
        return EDITOR_WIDGET_REGISTRY_KEY

    def configWidget(self, layer: QgsVectorLayer, fieldIdx: int, parent=QWidget) -> LabelShortcutEditorConfigWidget:
        """
        Returns a SpectralProfileEditorConfigWidget
        :param layer: QgsVectorLayer
        :param fieldIdx: int
        :param parent: QWidget
        :return: SpectralProfileEditorConfigWidget
        """

        w = LabelShortcutEditorConfigWidget(layer, fieldIdx, parent)
        w.setConfig(layer.editorWidgetSetup(fieldIdx).config())
        return w

    def create(self, layer: QgsVectorLayer, fieldIdx: int, editor: QWidget,
               parent: QWidget) -> LabelShortcutEditorWidgetWrapper:
        """
        Create a LabelShortcutEditorWidgetWrapper
        :param layer: QgsVectorLayer
        :param fieldIdx: int
        :param editor: QWidget
        :param parent: QWidget
        :return: ClassificationSchemeEditorWidgetWrapper
        """
        w = LabelShortcutEditorWidgetWrapper(layer, fieldIdx, editor, parent)

        return w

    def fieldScore(self, vl: QgsVectorLayer, fieldIdx: int) -> int:
        """
        This method allows disabling this editor widget type for a certain field.
        0: not supported: none String fields
        5: maybe support String fields with length <= 400
        20: specialized support: String fields with length > 400

        :param vl: QgsVectorLayer
        :param fieldIdx: int
        :return: int
        """
        # log(' fieldScore()')
        if fieldIdx < 0:
            return 0
        field = vl.fields().at(fieldIdx)
        assert isinstance(field, QgsField)
        if field.type() in QUICK_LABEL_SHORTCUT_TYPES:
            return 5
        else:
            return 0  # no support

    def supportsField(self, vl: QgsVectorLayer, idx: int) -> True:
        """
        :param vl: vectorlayers
        :param idx:
        :return: bool
        """
        field = vl.fields().at(idx)
        if isinstance(field, QgsField) and field.type() in QUICK_LABEL_SHORTCUT_TYPES:
            return True
        return False


class LabelShortcutGroupModel(QAbstractListModel):
    _INSTANCE = None

    @classmethod
    def instance(cls) -> 'LabelShortcutGroupModel':
        if cls._INSTANCE is None:
            cls._INSTANCE = LabelShortcutGroupModel()
        return cls._INSTANCE

    def __init__(self, *args, **kwds):

        super().__init__(*args, **kwds)

        self.mGroups: List[str] = ['']

    def rowCount(self, parent=None, *args, **kwargs):
        return len(self.mGroups)

    def addGroup(self, name: str):

        if name not in self.mGroups:
            i = bisect.bisect(self.mGroups, name)
            self.beginInsertRows(QModelIndex(), i, i)
            self.mGroups.insert(i, name)
            self.endInsertRows()

    def removeGroup(self, name: str):
        if name in self.mGroups:
            i = self.mGroups.index(name)

            self.beginRemoveRows(QModelIndex(), i, i)
            del self.mGroups[i]
            self.endRemoveRows()

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

        if role == Qt.DisplayRole:
            return self.mGroups[index.row()]

        if role == Qt.ToolTipRole:
            grp = self.mGroups[index.row()]

            if grp == '':
                return 'Empty (Default group)'
            else:
                return f'Group "{grp}"'

        if role == Qt.EditRole:
            return self.mGroups[index.row()]

        return None


class LabelShortcutTypeModel(QAbstractListModel):

    def __init__(self, field: QgsField, **kwds):

        super().__init__(**kwds)
        assert isinstance(field, QgsField)
        self.mField = field

        self.mShortcutTypes = []
        self.mShortcutEnabled = dict()
        for t in LabelShortcutType:
            self.mShortcutTypes.append(t)
            enabled = t == LabelShortcutType.Off or t in QUICK_LABEL_SHORTCUT_TYPES.get(field.type(), [])
            self.mShortcutEnabled[t] = enabled

        self.mToolTips = {
            LabelShortcutType.Classification: 'Classification value',
            LabelShortcutType.Sensor: 'Sensor name',
            LabelShortcutType.Date: 'Date',
            LabelShortcutType.DateTime: 'Date-Time',
            LabelShortcutType.DOY: 'Day of year (DOY) [int]',
            LabelShortcutType.DecimalYear: 'Decimal year [float]',
            LabelShortcutType.SourceImage: 'Source image path',
        }

    def headerData(self, section, orientation, role=None):
        if orientation == Qt.Horizontal:
            return 'Label Type'

    def flags(self, index: QModelIndex):

        if not index.isValid():
            return Qt.NoItemFlags

        lt = self.mShortcutTypes[index.row()]
        flags = Qt.NoItemFlags
        if self.mShortcutEnabled[lt]:
            flags = flags | Qt.ItemIsEnabled | Qt.ItemIsSelectable

        return flags

    def rowCount(self, parent=None, *args, **kwargs):
        return len(self.mShortcutTypes)

    def isEnabled(self, shortcutType: LabelShortcutType) -> bool:
        """Returns True if the shortcut type can be used with the given QgsField type"""
        return self.mShortcutEnabled.get(shortcutType, False)

    def data(self, index: QModelIndex, role=None):

        if not index.isValid():
            return None

        st = self.mShortcutTypes[index.row()]

        if role == Qt.DisplayRole:
            return st.name
        if role == Qt.ToolTipRole:
            return QUICK_LABEL_SHORTCUT_TYPE_TOOLTIPS.get(st, st.name)
        if role == Qt.UserRole:
            return st
        return None


def createWidgetSetup(labelType: LabelShortcutType,
                      group: str = '',
                      classification: Optional[ClassificationScheme] = None,
                      expression: Optional[str] = None,
                      ) -> QgsEditorWidgetSetup:
    """
    Creates the QgsEditorWidgetSetup for designate quick-label fields
    :param labelType: LabelShortcutType
    :param group: str
    :param classification: ClassificationScheme, optional
    :param expression: customized expression, optional
    :return:
    """
    assert isinstance(labelType, LabelShortcutType)

    conf = {LabelConfigurationKey.LabelType: labelType.value,
            LabelConfigurationKey.LabelGroup: group}

    if isinstance(classification, ClassificationScheme):
        # assert labelType == LabelShortcutType.Classification
        conf[LabelConfigurationKey.LabelClassification] = classification.asMap()

    if isinstance(expression, str):
        # assert labelType == LabelShortcutType.Customized
        conf[LabelConfigurationKey.LabelExpression] = expression

    return QgsEditorWidgetSetup(EDITOR_WIDGET_REGISTRY_KEY, conf)


class LabelConfigurationKey(object):
    LabelType = 'labelType'
    LabelGroup = 'labelGroup'
    LabelClassification = 'classification'
    LabelExpression = 'expression'


def shortcuts(field: QgsField) -> LabelShortcutType:
    """
    Returns the possible LabelShortCutTypes for a QgsField
    :param field:
    :type field:
    :return: [list]
    """
    assert isinstance(field, QgsField)
    return QUICK_LABEL_SHORTCUT_TYPES.get(field.type(), LabelShortcutType.Off)
