"""
***************************************************************************
    layerconfigwidget/vectorlayerfields.py
        - widgets and helpers to handle QgsVectorLayer fields
    -----------------------------------------------------------------------
    begin                : 2020-02-24
    copyright            : (C) 2020 Benjamin Jakimow
    email                : benjamin.jakimow@geo.hu-berlin.de

***************************************************************************
    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 3 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 software. If not, see <https://www.gnu.org/licenses/>.
***************************************************************************
"""
import sys

from qgis.PyQt.QtCore import QSortFilterProxyModel, QModelIndex, pyqtSignal, QAbstractItemModel
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtGui import QResizeEvent, QStandardItemModel, QStandardItem
from qgis.PyQt.QtWidgets import QMessageBox, QDialog, QTableView, QStackedWidget, QMenu, QComboBox, QVBoxLayout, QLabel, \
    QWidget
from qgis.core import QgsMapLayer, QgsVectorLayer, QgsField, QgsFieldModel, QgsEditorWidgetSetup
from qgis.gui import QgsEditorWidgetFactory, QgsCollapsibleGroupBox, QgsEditorConfigWidget, QgsGui, \
    QgsMapLayerConfigWidgetFactory
from .core import QpsMapLayerConfigWidget
from ..layerproperties import AddAttributeDialog
from ..utils import loadUi


class LayerFieldsTableModel(QgsFieldModel):

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        assert isinstance(self, QAbstractItemModel)
        self.cnId = 'Id'
        self.cnName = 'Name'
        self.cnAlias = 'Alias'
        self.cnType = 'Type'
        self.cnTypeName = 'Type name'
        self.cnLength = 'Length'
        self.cnPrecision = 'Precision'
        self.cnComment = 'Comment'
        self.cnWMS = 'WMS'
        self.cnWFS = 'WFS'

        self.mColumnNames = [self.cnId, self.cnName, self.cnAlias, self.cnType, self.cnTypeName, self.cnLength,
                             self.cnPrecision, self.cnComment, self.cnWMS, self.cnWFS]

    def columnNames(self):
        return self.mColumnNames[:]

    def columnCount(self, parent):
        return len(self.mColumnNames)

    def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole):
        """
        Returns header data
        :param col: int
        :param orientation: Qt.Horizontal | Qt.Vertical
        :param role:
        :return: value
        """
        if Qt is None:
            return None
        if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
            return self.mColumnNames[col]
        elif orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole:
            return col
        return None

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

        if not isinstance(self.layer(), QgsMapLayer):
            return None

        cn = self.mColumnNames[index.column()]
        field = self.layer().fields().at(index.row())
        if not isinstance(field, QgsField):
            return None
        if role == Qt.ItemDataRole.DisplayRole:
            if cn == self.cnId:
                return index.row()
            if cn == self.cnName:
                return field.name()
            if cn == self.cnAlias:
                return field.alias()
            if cn == self.cnType:
                return field.type()
            if cn == self.cnTypeName:
                return field.typeName()
            if cn == self.cnLength:
                return field.length()
            if cn == self.cnPrecision:
                return field.precision()
            if cn == self.cnComment:
                return field.comment()
            if cn == self.cnWMS:
                return None
            if cn == self.cnWFS:
                return None
        if role in [QgsFieldModel.CustomRole.FieldNameRole,
                    QgsFieldModel.CustomRole.FieldIndexRole,
                    QgsFieldModel.CustomRole.ExpressionRole,
                    QgsFieldModel.CustomRole.IsExpressionRole,
                    QgsFieldModel.CustomRole.ExpressionValidityRole,
                    QgsFieldModel.CustomRole.FieldTypeRole,
                    QgsFieldModel.CustomRole.FieldOriginRole,
                    QgsFieldModel.CustomRole.IsEmptyRole,
                    QgsFieldModel.CustomRole.EditorWidgetType,
                    QgsFieldModel.CustomRole.JoinedFieldIsEditable
                    ]:
            return super().data(index, role)


class LayerFieldsListModel(QgsFieldModel):
    """
    A model to show the QgsFields of an QgsVectorLayer as vertical list
    Inherits QgsFieldModel and allows to change the name of the 1st column.
    """

    def __init__(self, parent=None):
        """
        Constructor
        :param parent:
        """
        super(LayerFieldsListModel, self).__init__(parent)
        self.mColumnNames = ['Fields']

    def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole):
        """
        Returns header data
        :param col: int
        :param orientation: Qt.Horizontal | Qt.Vertical
        :param role:
        :return: value
        """
        if Qt is None:
            return None
        if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
            return self.mColumnNames[col]
        elif orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole:
            return col
        return None

    def setHeaderData(self, col, orientation, value, role=Qt.ItemDataRole.EditRole):
        """
        Sets the header data.
        :param col: int
        :param orientation:
        :param value: any
        :param role:
        """
        result = False

        if role == Qt.ItemDataRole.EditRole:
            if orientation == Qt.Orientation.Horizontal and col < len(self.mColumnNames) and isinstance(value, str):
                self.mColumnNames[col] = value
                result = True

        if result is True:
            self.headerDataChanged.emit(orientation, col, col)
        return result


class LayerAttributeFormConfigEditorWidget(QWidget):
    class ConfigInfo(QStandardItem):
        """
        Describes a QgsEditorWidgetFactory configuration.
        """

        def __init__(self, key: str, factory: QgsEditorWidgetFactory, configWidget: QgsEditorConfigWidget):
            super(LayerAttributeFormConfigEditorWidget.ConfigInfo, self).__init__()

            assert isinstance(key, str)
            assert isinstance(factory, QgsEditorWidgetFactory)
            assert isinstance(configWidget, QgsEditorConfigWidget)
            self.mKey = key
            self.mFactory = factory
            self.mConfigWidget = configWidget
            self.setText(factory.name())
            self.setToolTip(factory.name())
            self.mInitialConfig = dict(configWidget.config())

        def resetConfig(self):
            """
            Resets the widget to its initial values
            """
            self.mConfigWidget.setConfig(dict(self.mInitialConfig))

        def factoryKey(self) -> str:
            """
            Returns the QgsEditorWidgetFactory key, e.g. "CheckBox"
            :return: str
            """
            return self.mKey

        def factoryName(self) -> str:
            """
            Returns the QgsEditorWidgetFactory name, e.g. "Checkbox"
            :return: str
            """
            return self.factory().name()

        def config(self) -> dict:
            """
            Returns the config dictionary
            :return: dict
            """
            return self.mConfigWidget.config()

        def configWidget(self) -> QgsEditorConfigWidget:
            """
            Returns the QgsEditorConfigWidget
            :return: QgsEditorConfigWidget
            """
            return self.mConfigWidget

        def factory(self) -> QgsEditorWidgetFactory:
            """
            Returns the QgsEditorWidgetFactory
            :return: QgsEditorWidgetFactory
            """
            return self.mFactory

        def editorWidgetSetup(self) -> QgsEditorWidgetSetup:
            """
            Creates a QgsEditorWidgetSetup
            :return: QgsEditorWidgetSetup
            """
            return QgsEditorWidgetSetup(self.factoryKey(), self.config())

    sigChanged = pyqtSignal(object)

    def __init__(self, parent, layer: QgsVectorLayer, index: int):
        super(LayerAttributeFormConfigEditorWidget, self).__init__(parent)

        self.setLayout(QVBoxLayout())

        assert isinstance(layer, QgsVectorLayer)
        assert isinstance(index, int)

        self.mLayer = layer
        self.mField = layer.fields().at(index)
        assert isinstance(self.mField, QgsField)
        self.mFieldIndex = index

        self.mFieldNameLabel = QLabel(parent)
        self.mFieldNameLabel.setText(self.mField.name())

        self.layout().addWidget(self.mFieldNameLabel)

        self.gbWidgetType = QgsCollapsibleGroupBox(self)
        self.gbWidgetType.setTitle('Widget Type')
        self.gbWidgetType.setLayout(QVBoxLayout())
        self.cbWidgetType = QComboBox(self.gbWidgetType)

        self.stackedWidget = QStackedWidget(self.gbWidgetType)
        self.gbWidgetType.layout().addWidget(self.cbWidgetType)
        self.gbWidgetType.layout().addWidget(self.stackedWidget)

        currentSetup = self.mLayer.editorWidgetSetup(self.mFieldIndex)

        refkey = currentSetup.type()
        if refkey == '':
            refkey = QgsGui.editorWidgetRegistry().findBest(self.mLayer, self.mField.name()).type()

        self.mItemModel = QStandardItemModel(parent=self.cbWidgetType)

        iCurrent = -1
        i = 0
        factories = QgsGui.editorWidgetRegistry().factories()
        for key, fac in factories.items():
            assert isinstance(key, str)
            assert isinstance(fac, QgsEditorWidgetFactory)
            score = fac.fieldScore(self.mLayer, self.mFieldIndex)
            configWidget = fac.configWidget(self.mLayer, self.mFieldIndex, self.stackedWidget)

            if isinstance(configWidget, QgsEditorConfigWidget):
                configWidget.changed.connect(lambda: self.sigChanged.emit(self))
                self.stackedWidget.addWidget(configWidget)
                confItem = LayerAttributeFormConfigEditorWidget.ConfigInfo(key, fac, configWidget)
                if key == refkey:
                    iCurrent = i
                confItem.setEnabled(score > 0)
                confItem.setData(self, role=Qt.ItemDataRole.UserRole)
                self.mItemModel.appendRow(confItem)

                i += 1

        self.cbWidgetType.setModel(self.mItemModel)
        self.cbWidgetType.currentIndexChanged.connect(self.updateConfigWidget)

        self.layout().addWidget(self.gbWidgetType)
        self.layout().addStretch()
        self.cbWidgetType.setCurrentIndex(iCurrent)

        conf = self.currentFieldConfig()
        self.mInitialFactoryKey = None
        self.mInitialConf = None
        if isinstance(conf, LayerAttributeFormConfigEditorWidget.ConfigInfo):
            self.mInitialFactoryKey = conf.factoryKey()
            self.mInitialConf = conf.config()

    def setFactory(self, factoryKey: str):
        """
        Shows the QgsEditorConfigWidget of QgsEditorWidgetFactory `factoryKey`
        :param factoryKey: str
        """
        for i in range(self.mItemModel.rowCount()):
            confItem = self.mItemModel.item(i)
            assert isinstance(confItem, LayerAttributeFormConfigEditorWidget.ConfigInfo)
            if confItem.factoryKey() == factoryKey:
                self.cbWidgetType.setCurrentIndex(i)
                break

    def changed(self) -> bool:
        """
        Returns True if the QgsEditorWidgetFactory or its configuration has been changed
        :return: bool
        """

        recentConfigInfo = self.currentFieldConfig()
        if isinstance(recentConfigInfo, LayerAttributeFormConfigEditorWidget.ConfigInfo):
            if self.mInitialFactoryKey != recentConfigInfo.factoryKey():
                return True
            elif self.mInitialConf != recentConfigInfo.config():
                return True

        return False

    def apply(self):
        """
        Applies the
        :return:
        """
        if self.changed():
            configInfo = self.currentFieldConfig()
            self.mInitialConf = configInfo.config()
            self.mInitialFactoryKey = configInfo.factoryKey()
            setup = QgsEditorWidgetSetup(self.mInitialFactoryKey, self.mInitialConf)
            self.mLayer.setEditorWidgetSetup(self.mFieldIndex, setup)

    def reset(self):
        """
        Resets the widget to its initial status
        """
        if self.changed():
            self.setFactory(self.mInitialFactoryKey)
            self.currentEditorConfigWidget().setConfig(self.mInitialConf)

    def currentFieldConfig(self) -> ConfigInfo:
        i = self.cbWidgetType.currentIndex()
        return self.mItemModel.item(i)

    def currentEditorConfigWidget(self) -> QgsEditorConfigWidget:
        return self.currentFieldConfig().configWidget()

    def updateConfigWidget(self, index):
        self.stackedWidget.setCurrentIndex(index)
        fieldConfig = self.currentFieldConfig()
        if isinstance(fieldConfig, LayerAttributeFormConfigEditorWidget.ConfigInfo):
            self.sigChanged.emit(self)


class LayerAttributeFormConfigWidget(QpsMapLayerConfigWidget):
    """
    A widget to set QgsVectorLayer field settings
    """

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

        from .core import configWidgetUi
        loadUi(configWidgetUi('layerattributeformconfigwidget.ui'), self)

        assert isinstance(self.mapLayer(), QgsVectorLayer)
        self.scrollArea.resizeEvent = self.onScrollAreaResize
        self.mFieldModel = LayerFieldsListModel(self)
        self.mFieldModel.setLayer(self.mapLayer())
        self.treeView.setModel(self.mFieldModel)
        self.treeView.selectionModel().currentRowChanged.connect(self.onSelectedFieldChanged)
        self.updateFieldWidgets()

    def menuButtonMenu(self) -> QMenu:
        m = QMenu('Reset')
        a = m.addAction('Reset')
        a.triggered.connect(self.onReset)
        return m

    def onSelectedFieldChanged(self, index1: QModelIndex, index2: QModelIndex):
        """
        Shows the widget for the selected QgsField
        :param index1:
        :param index2:
        """
        if isinstance(index1, QModelIndex) and index1.isValid():
            r = index1.row()
            if r < 0 or r >= self.stackedWidget.count():
                s = ""
            self.stackedWidget.setCurrentIndex(r)

    def onScrollAreaResize(self, resizeEvent: QResizeEvent):
        """
        Forces the stackedWidget's width to fit into the scrollAreas viewport
        :param resizeEvent: QResizeEvent
        """
        assert isinstance(resizeEvent, QResizeEvent)
        self.stackedWidget.setMaximumWidth(resizeEvent.size().width())
        s = ""

    def onReset(self):

        sw = self.stackedWidget
        assert isinstance(sw, QStackedWidget)

        for i in range(sw.count()):
            w = sw.widget(i)
            assert isinstance(w, LayerAttributeFormConfigEditorWidget)
            w.reset()
        self.onSettingsChanged()

    def apply(self):
        """
        Applies all changes to the QgsVectorLayer
        :return:
        """

        sw = self.stackedWidget
        assert isinstance(sw, QStackedWidget)

        for i in range(sw.count()):
            w = sw.widget(i)
            assert isinstance(w, LayerAttributeFormConfigEditorWidget)
            w.apply()
        self.onSettingsChanged()

    def updateFieldWidgets(self):
        """
        Empties the stackedWidget and populates it with a FieldConfigEditor
        for each QgsVectorLayer field.
        """
        sw = self.stackedWidget
        assert isinstance(sw, QStackedWidget)
        i = sw.count() - 1
        while i >= 0:
            w = sw.widget(i)
            w.setParent(None)
            i -= 1

        lyr = self.mapLayer()
        if isinstance(lyr, QgsVectorLayer):
            for i in range(lyr.fields().count()):
                w = LayerAttributeFormConfigEditorWidget(sw, lyr, i)
                w.sigChanged.connect(self.onSettingsChanged)
                sw.addWidget(w)

        self.onSettingsChanged()

    def onSettingsChanged(self):
        """
        Enables/disables buttons
        :return:
        """
        b = False
        for i in range(self.stackedWidget.count()):
            w = self.stackedWidget.widget(i)
            assert isinstance(w, LayerAttributeFormConfigEditorWidget)
            if w.changed():
                b = True
                break


class LayerAttributeFormConfigWidgetFactory(QgsMapLayerConfigWidgetFactory):
    def __init__(self, title='Attributes Form', icon=QIcon(':/images/themes/default/mActionFormView.svg')):
        super().__init__(title, icon)
        self.setSupportLayerPropertiesDialog(True)
        self.setSupportsStyleDock(False)

    def createWidget(self, layer, canvas, dockWidget=False, parent=None):
        return LayerAttributeFormConfigWidget(layer, canvas, parent=parent)

    def supportsLayer(self, layer) -> bool:
        return isinstance(layer, QgsVectorLayer)

    def supportLayerPropertiesDialog(self):
        return True

    def supportsStyleDock(self):
        return False


class LayerFieldsConfigWidget(QpsMapLayerConfigWidget):
    """
    A widget to edit the fields of a QgsVectorLayer
    """

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

        from .core import configWidgetUi
        loadUi(configWidgetUi('layerfieldsconfigwidget.ui'), self)

        lyr = self.mapLayer()
        self.mFieldModel = LayerFieldsTableModel()
        self.mFieldModel.setLayer(lyr)

        self.mProxyModel = QSortFilterProxyModel()
        self.mProxyModel.setSourceModel(self.mFieldModel)
        assert isinstance(self.tableView, QTableView)
        self.tableView.setModel(self.mProxyModel)
        self.tableView.selectionModel().selectionChanged.connect(self.validate)
        self.btnAddField.setDefaultAction(self.actionAddField)
        self.btnRemoveField.setDefaultAction(self.actionRemoveField)
        self.btnToggleEditing.setDefaultAction(self.actionToggleEditing)
        self.btnFieldCalculator.setDefaultAction(self.actionFieldCalculator)

        self.actionAddField.triggered.connect(self.onAddField)
        self.actionRemoveField.triggered.connect(self.onRemoveField)
        self.actionToggleEditing.toggled.connect(self.onToggleEditing)

        self.actionFieldCalculator.setEnabled(False)
        self.actionFieldCalculator.setVisible(False)

        self.validate()

        lyr.editingStarted.connect(self.validate)
        lyr.editingStopped.connect(self.validate)

    def validate(self, *args):

        bEdit = isinstance(self.mapLayer(), QgsVectorLayer) and self.mapLayer().isEditable()
        bSelected = len(self.tableView.selectionModel().selectedRows()) > 0

        self.actionAddField.setEnabled(bEdit)
        self.actionRemoveField.setEnabled(bEdit and bSelected)
        self.actionToggleEditing.setChecked(bEdit)

    def onAddField(self):
        lyr = self.mapLayer()
        if isinstance(lyr, QgsVectorLayer) and lyr.isEditable():
            d = AddAttributeDialog(lyr)
            d.exec_()
            if d.result() == QDialog.DialogCode.Accepted:
                field = d.field()
                lyr.addAttribute(field)

    def onToggleEditing(self, b):

        lyr = self.mapLayer()
        if not isinstance(lyr, QgsVectorLayer):
            return

        isEditable = lyr.isEditable()
        if isEditable == b:
            return

        errors = None
        if b:
            lyr.startEditing()
        else:
            if lyr.isModified():
                result = QMessageBox.question(self, 'Leaving edit mode', 'Save changes?',
                                              buttons=QMessageBox.StandardButton.No | QMessageBox.StandardButton.Yes,
                                              defaultButton=QMessageBox.StandardButton.Yes)
                if result == QMessageBox.StandardButton.Yes:
                    if not lyr.commitChanges():
                        errors = lyr.commitErrors()
                else:
                    lyr.rollBack()
            else:
                lyr.commitChanges()
        if errors:
            print(str(errors), file=sys.stderr)

    def onRemoveField(self):

        lyr = self.mapLayer()
        if not isinstance(lyr, QgsVectorLayer):
            return
        indices = [self.mProxyModel.mapToSource(idx).data(QgsFieldModel.CustomRole.FieldIndexRole) for idx in
                   self.tableView.selectionModel().selectedRows()]

        if not lyr.deleteAttributes(indices):
            errors = lyr.errors()
        self.validate()

    def apply(self):

        lyr = self.mapLayer()
        if not isinstance(lyr, QgsVectorLayer):
            return
        if lyr.isEditable():
            lyr.commitChanges()
            lyr.startEditing()

    def syncToLayer(self, *args):
        super().syncToLayer(*args)
        self.mFieldModel.setLayer(self.mapLayer())


class LayerFieldsConfigWidgetFactory(QgsMapLayerConfigWidgetFactory):
    def __init__(self, title='Fields', icon=QIcon(':/images/themes/default/mSourceFields.svg')):
        super().__init__(title, icon)
        self.setSupportLayerPropertiesDialog(True)
        self.setSupportsStyleDock(False)

    def createWidget(self, layer, canvas, dockWidget=False, parent=None):
        return LayerFieldsConfigWidget(layer, canvas, parent=parent)

    def supportsLayer(self, layer) -> bool:
        return isinstance(layer, QgsVectorLayer)

    def supportLayerPropertiesDialog(self):
        return True

    def supportsStyleDock(self):
        return False
