import json
import logging
import re
import warnings
from copy import copy
from json import JSONEncoder
from math import isnan
from typing import Any, List, Optional, Tuple

import numpy as np

from qgis.PyQt.QtCore import NULL, pyqtSignal, QAbstractTableModel, QModelIndex, QSortFilterProxyModel, \
    Qt, QVariant
from qgis.PyQt.QtCore import QObject
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QFrame, QGroupBox, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QSizePolicy, \
    QSpacerItem, QTableView, QToolButton, QVBoxLayout, QWidget
from qgis.PyQt.QtWidgets import QMenu, QAction
from qgis.core import Qgis, QgsApplication, QgsFeature, QgsField, QgsFieldFormatter, QgsFieldFormatterRegistry, \
    QgsVectorLayer
from qgis.gui import QgsCodeEditorJson, QgsEditorConfigWidget, QgsEditorWidgetFactory, QgsEditorWidgetWrapper, QgsGui
from .spectrallibraryplotunitmodels import SpectralProfilePlotXAxisUnitModel
from .spectralprofileplotwidget import SpectralProfilePlotWidget
from .. import EDITOR_WIDGET_REGISTRY_KEY
from ..core import can_store_spectral_profiles
from ..core.spectralprofile import decodeProfileValueDict, encodeProfileValueDict, prepareProfileValueDict, \
    ProfileEncoding, validateProfileValueDict
from ...unitmodel import UnitWrapper
from ...utils import SignalBlocker

logger = logging.getLogger(__name__)

SPECTRAL_PROFILE_FIELD_REPRESENT_VALUE = 'Profile'

_SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY: None
_SPECTRAL_PROFILE_FIELD_FORMATTER: None


class SpectralProfileTableModel(QAbstractTableModel):
    """
    A TableModel to show and edit spectral values of a SpectralProfile
    """

    profileChanged = pyqtSignal()

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

        self.mColumnNames = {0: '#',
                             1: 'x',
                             2: 'y',
                             3: 'bbl'}
        self.mXUnit = None
        self.mYUnit = None

        self.mValues: List[dict] = list()

        self.mLastProfile: dict = dict()
        self.mCurrentProfile: dict = dict()

        self.mBooleanBBL: bool = True

        self.mIsReadOnly: bool = False

    def setReadOnly(self, read_only: bool):
        self.mIsReadOnly = read_only is True

    def setBooleanBBL(self, b: bool):
        if not (isinstance(b, bool)):
            raise AssertionError
        self.mBooleanBBL = b

    def clear(self):
        m = copy(self.mValues)
        self.beginResetModel()
        self.mValues.clear()
        self.endResetModel()
        if m != self.mValues:
            self.profileChanged.emit()

    def bands(self) -> int:
        return self.rowCount()

    def setProfileDict(self, profile: dict):
        """
        :param values:
        :return:
        """
        if not (isinstance(profile, dict)):
            raise AssertionError

        m = copy(self.mValues)

        self.beginResetModel()
        self.mValues.clear()
        self.mLastProfile = profile
        xValues = profile.get('x', None)
        yValues = profile.get('y', [])
        bblValues = profile.get('bbl', None)
        self.mBooleanBBL = False
        for i, y in enumerate(yValues):
            x = xValues[i] if xValues else None
            bbl = None
            if bblValues:
                bbl = bblValues[i]
            # if bbl not in [0, 1]:
            #    self.mBooleanBBL = False
            item = {0: i + 1,
                    1: x,
                    2: y,
                    3: bbl}
            self.mValues.append(item)

        self.endResetModel()

        if m != self.mValues:
            self.profileChanged.emit()

    def profileDict(self) -> dict:
        if len(self.mValues) == 0:
            return dict()

        x = np.asarray([v[1] for v in self.mValues])
        y = np.asarray([v[2] for v in self.mValues])
        bbl = [v[3] for v in self.mValues]

        bbl = [1 if v is None or isnan(v) else v for v in bbl]
        if all([v == 1 for v in bbl]):
            bbl = None
        if x.dtype.name == 'object':
            x = None

        profile = prepareProfileValueDict(
            x=x,
            y=y,
            bbl=bbl
        )
        return profile

    def resetProfile(self):
        self.setProfileDict(self.mLastProfile)

    def rowCount(self, parent: QModelIndex = None, *args, **kwargs) -> int:

        return len(self.mValues)

    def columnCount(self, parent=QModelIndex()) -> int:
        return len(self.mColumnNames)

    def data(self, index, role=Qt.DisplayRole):
        if role is None or not index.isValid():
            return None
        if index.row() >= len(self.mValues):
            return False

        c = index.column()
        i = index.row()

        item = self.mValues[i]

        if role in [Qt.DisplayRole, Qt.EditRole]:
            if c == 0:
                return i + 1

            if c in [1, 2]:
                return str(item[c])

            elif c == 3:
                bbl = item[c]
                if self.mBooleanBBL:
                    return True if bbl is None else bool(bbl)
                else:
                    if bbl is None:
                        return 1
                    return bbl

        return None

    def stringToType(self, value: str):
        """
        Converts a string input into a matching int, float or datetime
        """

        t = str
        for candidate in [float, int]:
            try:
                _ = candidate(value)
                t = candidate
            except ValueError:
                break
        return t(value)

    def setData(self, index, value, role=None):
        if role is None or not index.isValid():
            return None

        c = index.column()
        i = index.row()

        item = self.mValues[i]
        itemOld = copy(item)
        modified = False

        if role == Qt.EditRole:
            if c in [1, 2]:
                # x / y values
                item[c] = self.stringToType(value)

            elif c == 3:
                # bbl values, always stored as number
                if self.mBooleanBBL:
                    bbl = int(bool(value))
                else:
                    bbl = int(value)
                item[c] = bbl

        modified = item[c] != itemOld[c]
        if modified:
            self.dataChanged.emit(index, index, [role])
            self.profileChanged.emit()
        return modified

    def flags(self, index):
        if index.isValid():
            c = index.column()
            flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
            if c in [1, 2, 3] and not self.mIsReadOnly:
                flags = flags | Qt.ItemIsEditable
            return flags
        return None

    def headerData(self, col: int, orientation: Qt.Orientation, role: int):

        if orientation == Qt.Horizontal and role in [Qt.DisplayRole, Qt.ToolTipRole]:
            return self.mColumnNames.get(col, f'{col + 1}')
        if orientation == Qt.Vertical:
            if role == Qt.ToolTipRole:
                return f'Band {col + 1}'
        return None


class InlineListEncoder(JSONEncoder):
    def encode(self, obj):
        if isinstance(obj, dict):
            items = []
            for k, v in obj.items():
                key = json.dumps(k,
                                 ensure_ascii=self.ensure_ascii,
                                 allow_nan=self.allow_nan,
                                 separators=(self.item_separator, self.key_separator))
                indent = ' ' * self.indent
                if isinstance(v, list):
                    value = json.dumps(v,
                                       ensure_ascii=self.ensure_ascii,
                                       separators=(self.item_separator, self.key_separator))  # no indent for lists
                    items.append(f'{indent}{key}: {value}')
                else:
                    value = self.encode(v)
                    items.append(f'{indent}{key}: {value}')
            text = ',\n'.join(items)
            return f'{{\n{text}\n}}'
        return super().encode(obj)


class SpectralProfileJsonEditor(QgsCodeEditorJson):
    profileChanged = pyqtSignal()

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

        self.textChanged.connect(self.profileChanged)

    def setProfile(self, d: dict):
        self.setProfileDict(d)

    def setProfileDict(self, d: dict):
        jsonData = encodeProfileValueDict(d, ProfileEncoding.Dict)

        if jsonData:
            jsonText = json.dumps(jsonData, ensure_ascii=False, allow_nan=True, indent=4, cls=InlineListEncoder)
        else:
            jsonText = None
        self.setText(jsonText)

    def profileDict(self) -> dict:
        text = self.text().strip()
        if text == '':
            return dict()
        else:
            return json.loads(self.text())

    def addSyntaxWarning(self, line: int, col: int, msg: str):
        self.addWarning(line, msg)
        self.setCursorPosition(line, col - 1)
        self.ensureLineVisible(line)


class SpectralProfileTableEditor(QFrame):
    profileChanged = pyqtSignal()

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

        super().__init__(*args, **kwds)
        self.tableView = QTableView()
        self.tableModel = SpectralProfileTableModel()
        self.tableModel.profileChanged.connect(self.profileChanged)
        self.proxyModel = QSortFilterProxyModel()
        self.tableFrame = QFrame()

        self.proxyModel.setSourceModel(self.tableModel)
        self.tableView.setModel(self.proxyModel)
        self.tableView.setSortingEnabled(True)
        self.tableView.sortByColumn(0, Qt.AscendingOrder)
        self.tableView.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
        self.tableView.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents)

        self.mXUnitModel: SpectralProfilePlotXAxisUnitModel = SpectralProfilePlotXAxisUnitModel.instance()
        self.mXUnitModel.setAllowEmptyUnit(False)

        self._x_unit: Optional[str] = None
        self._y_unit: Optional[str] = None

        m = QMenu()

        model = SpectralProfilePlotXAxisUnitModel.instance()
        for i in range(2, model.rowCount()):
            unit_wrapper: UnitWrapper = model.data(model.index(i, 0), Qt.UserRole + 1)
            a: QAction = m.addAction(unit_wrapper.unit)
            a.setText(unit_wrapper.description)
            a.setToolTip(unit_wrapper.tooltip)
            a.triggered.connect(lambda *args, unit=unit_wrapper.unit: self.setXUnit(unit))
        m.addSeparator()
        a = m.addAction('<Reset>')
        a.setToolTip('Resets the X Unit')
        a.triggered.connect(lambda *args: self.setXUnit(self._x_unit))
        a = m.addAction('<Clear>')
        a.setToolTip('Clears the X Unit')
        a.triggered.connect(lambda *args: self.setXUnit(None))

        self.btnXUnits = QToolButton()
        self.btnXUnits.setPopupMode(QToolButton.InstantPopup)
        self.btnXUnits.setMenu(m)

        # self.cbXUnit.setModel(self.mXUnitModel)
        # self.cbXUnit.currentTextChanged.connect(self.profileChanged)

        self.tbXUnit = QLineEdit()
        self.tbXUnit.textChanged.connect(self.profileChanged)

        self.tbYUnit = QLineEdit()
        self.tbYUnit.textChanged.connect(self.profileChanged)

        for tb in [self.tbXUnit, self.tbYUnit]:
            tb.setFixedWidth(100)
            tb.setMinimumWidth(50)

        hbox = QHBoxLayout()
        hbox.addWidget(QLabel('X unit'))
        hbox.addWidget(self.tbXUnit)
        hbox.addWidget(self.btnXUnits)
        hbox.addSpacerItem(QSpacerItem(10, 0))
        hbox.addWidget(QLabel('Y unit'))
        hbox.addWidget(self.tbYUnit)

        # hbox.setStretchFactor(self.tbXUnit, 2)
        # hbox.setStretchFactor(self.tbYUnit, 2)
        hbox.addStretch()

        vbox = QVBoxLayout()
        vbox.addLayout(hbox)
        vbox.addWidget(self.tableView)
        self.setLayout(vbox)
        self.setReadOnly(True)

    def setReadOnly(self, read_only: bool):
        self.tbXUnit.setEnabled(not read_only)
        self.tbYUnit.setReadOnly(read_only)
        self.btnXUnits.setEnabled(not read_only)
        self.tableModel.setReadOnly(read_only)

    def clear(self):
        self.tableModel.clear()

    def setXUnit(self, unit: str):
        if self.xUnit() != unit:
            self.tbXUnit.setText(unit)

    def setYUnit(self, unit: str):
        if self.yUnit() != unit:
            self.tbYUnit.setText(unit)

    def xUnit(self) -> str:
        return self.tbXUnit.text()
        # return self.cbXUnit.currentData(Qt.UserRole)

    def yUnit(self) -> str:
        return self.tbYUnit.text()

    def setProfile(self, d: dict):
        self.setProfileDict(d)

    def setProfileDict(self, d: dict):
        if not (isinstance(d, dict)):
            raise AssertionError
        self.tableModel.setProfileDict(d)
        self._x_unit = d.get('xUnit', None)
        self._y_unit = d.get('yUnit', None)
        self.setXUnit(self._x_unit)
        self.setYUnit(self._y_unit)

    def profileDict(self) -> dict:
        d = self.tableModel.profileDict()

        if len(d) == 0:
            # empty profile dict
            return dict()

        d = prepareProfileValueDict(prototype=d,
                                    xUnit=self.xUnit(),
                                    yUnit=self.yUnit())

        return d


class SpectralProfileEditorWidget(QGroupBox):
    VIEW_TABLE = 1
    VIEW_JSON_EDITOR = 2
    VIEW_PLOT = 3

    CNT = 0

    profileChanged = pyqtSignal()
    viewChangeRequest = pyqtSignal(int)

    def __init__(self, *args, **kwds):
        super(SpectralProfileEditorWidget, self).__init__(*args, **kwds)
        self.setWindowIcon(QIcon(':/qps/ui/icons/profile.svg'))

        # self.messageBar = QgsMessageBar()
        # the default profile (from vector layer)
        self.mDefaultProfile: Optional[dict] = None

        # the current widget to show the current profile
        self.mCurrentWidget: Optional[QWidget] = None

        # the potentially modified profile
        self.mCurrentProfile: Optional[dict] = None

        self.mReadOnly: bool = True

        self.controlBar = QHBoxLayout()
        self.controlBar.setSpacing(1)
        self.btnReset = QToolButton()
        self.btnReset.setText('Reset')
        self.btnReset.setToolTip('Resets the profile')
        self.btnReset.setIcon(QIcon(':/images/themes/default/mActionUndo.svg'))
        self.btnReset.clicked.connect(self.resetProfile)
        self.btnReset.setVisible(False)

        self.btnPlot = QToolButton()
        self.btnPlot.setText('Profile')
        self.btnPlot.setToolTip('View profile in plot.')
        self.btnPlot.setCheckable(True)
        self.btnPlot.setIcon(QIcon(':/qps/ui/icons/speclib_plot.svg'))
        # self.btnPlot.clicked.connect(lambda: self.setViewMode(self.VIEW_PLOT))
        self.btnPlot.clicked.connect(lambda: self.viewChangeRequest.emit(self.VIEW_PLOT))

        self.btnJson = QToolButton()
        self.btnJson.setText('JSON')
        self.btnJson.setToolTip('View/edit profile values in JSON editor.')
        self.btnJson.setCheckable(True)
        self.btnJson.setIcon(QIcon(':/images/themes/default/mIconFieldJson.svg'))
        # self.btnJson.clicked.connect(lambda: self.setViewMode(self.VIEW_JSON_EDITOR))
        self.btnJson.clicked.connect(lambda: self.viewChangeRequest.emit(self.VIEW_JSON_EDITOR))

        self.btnClear = QToolButton()
        self.btnClear.setText('Clear')
        self.btnClear.setToolTip('Removes the profile and replaces it with NULL')
        self.btnClear.setIcon(QIcon(':/images/themes/default/mIconClearItem.svg'))
        self.btnClear.clicked.connect(self.clear)

        self.btnTable = QToolButton()
        self.btnTable.setCheckable(True)
        self.btnTable.setText('Table')
        self.btnTable.setToolTip('Edit profile values in table editor.')
        self.btnTable.setIcon(QIcon(':/images/themes/default/mActionOpenTable.svg'))
        # self.btnTable.clicked.connect(lambda: self.setViewMode(self.VIEW_TABLE))
        self.btnTable.clicked.connect(lambda: self.viewChangeRequest.emit(self.VIEW_TABLE))

        self.controlBar.addWidget(self.btnPlot)
        self.controlBar.addWidget(self.btnJson)
        self.controlBar.addWidget(self.btnTable)
        # self.controlBar.addWidget(self.messageBar)
        self.controlBar.addSpacerItem(QSpacerItem(0, 0, hPolicy=QSizePolicy.Expanding))
        self.controlBar.addWidget(self.btnClear)
        self.controlBar.addWidget(self.btnReset)

        for btn in [self.btnPlot, self.btnJson, self.btnTable, self.btnClear, self.btnReset]:
            btn.setAutoRaise(True)

        vbox = QVBoxLayout()
        vbox.setSpacing(1)
        vbox.addLayout(self.controlBar)
        vbox.setContentsMargins(0, 0, 0, 0)
        vbox.setSpacing(5)
        self._vbox = vbox
        self.setLayout(vbox)
        self.setViewMode(self.VIEW_JSON_EDITOR)

    def setReadOnly(self, read_only: bool):
        """
        Enables / disables the widgets to modify values
        :param read_only:
        :return:
        """
        self.mReadOnly = read_only

        if isinstance(self.mCurrentWidget, (SpectralProfileJsonEditor, SpectralProfileTableEditor)):
            self.mCurrentWidget.setReadOnly(read_only)
        self.btnClear.setDisabled(read_only)
        self.btnClear.setVisible(not read_only)

    def updateCurrentWidget(self, mode: int):

        current_profile = self.currentProfile()

        if mode == self.VIEW_JSON_EDITOR and not isinstance(self.mCurrentWidget, SpectralProfileJsonEditor):
            cw_new = SpectralProfileJsonEditor()
            cw_new.setLineNumbersVisible(True)
            cw_new.setReadOnly(self.mReadOnly)
            cw_new.setFoldingVisible(True)
            if current_profile:
                cw_new.setProfile(current_profile)
            cw_new.profileChanged.connect(self.editorProfileChanged)

        elif mode == self.VIEW_TABLE and not isinstance(self.mCurrentWidget, SpectralProfileTableEditor):
            cw_new = SpectralProfileTableEditor()
            if current_profile:
                cw_new.setProfile(current_profile)
            cw_new.setReadOnly(self.mReadOnly)
            cw_new.profileChanged.connect(self.editorProfileChanged)

        elif mode == self.VIEW_PLOT and not isinstance(self.mCurrentWidget, SpectralProfilePlotWidget):
            cw_new = SpectralProfilePlotWidget()
            if current_profile:
                cw_new.setProfile(current_profile)
        else:
            # no need to create a new widget
            cw_new = None

        if isinstance(cw_new, QWidget):
            if isinstance(self.mCurrentWidget, QWidget):
                self._vbox.removeWidget(self.mCurrentWidget)
                self.mCurrentWidget.deleteLater()
            self._vbox.addWidget(cw_new)
            self.mCurrentWidget = cw_new
            # remove old CW

    def setViewMode(self, mode: int):

        if mode != self.viewMode():
            self.updateCurrentWidget(mode)
            cw = self.mCurrentWidget
            self.btnJson.setChecked(isinstance(cw, SpectralProfileJsonEditor))
            self.btnTable.setChecked(isinstance(cw, SpectralProfileTableEditor))
            self.btnPlot.setChecked(isinstance(cw, SpectralProfilePlotWidget))

    def viewMode(self) -> int:
        cw = self.mCurrentWidget
        if isinstance(cw, SpectralProfileJsonEditor):
            return self.VIEW_JSON_EDITOR
        elif isinstance(cw, SpectralProfileTableEditor):
            return self.VIEW_TABLE
        elif isinstance(cw, SpectralProfilePlotWidget):
            return self.VIEW_PLOT
        else:
            return 0

    RX_JSON_ERROR = re.compile(r'(?P<msg>.*): line.*(?P<line>\d+) column.*(?P<col>\d+).*\(char.*(?P<char>\d+)\)', re.I)

    def editorProfileChanged(self):

        w = self.mCurrentWidget

        if self.sender() != w:
            return

        success, error, d = self.validate()

        if isinstance(w, SpectralProfileJsonEditor):
            w.clearWarnings()
            w.initializeLexer()
            w.runPostLexerConfigurationTasks()

        if not success:
            match = self.RX_JSON_ERROR.match(error)
            if isinstance(w, SpectralProfileJsonEditor) and match:
                eline = int(match.group('line')) - 1
                ecol = int(match.group('col'))
                emsg = match.group('msg')
                w.addSyntaxWarning(eline, ecol, emsg)

            else:
                QgsApplication.messageLog().logMessage(error, 'SpectralProfileEditorWidget', Qgis.Critical)
                # self.messageBar.pushMessage('Error', error.splitlines()[0], error, Qgis.Warning)
        else:
            self.profileChanged.emit()

    def initConfig(self, conf: dict):
        """
        Initializes widget elements like QComboBoxes etc.
        :param conf: dict
        """
        pass

    def setProfile(self, profile: dict):
        """
        Sets the profile values to be shown
        :param values: dict() or SpectralProfile
        :return:
        """
        if profile in [None, NULL, QVariant(None)]:
            profile = dict()
        if not (isinstance(profile, dict)):
            raise AssertionError
        self.mDefaultProfile = self.mCurrentProfile = profile.copy()

        if isinstance(self.mCurrentWidget,
                      (SpectralProfileJsonEditor, SpectralProfileTableEditor, SpectralProfilePlotWidget)):
            with SignalBlocker(self.mCurrentWidget) as _:
                self.mCurrentWidget.setProfile(self.mCurrentProfile)

        # w = self.stackedWidget.currentWidget()
        # with SignalBlocker(self.jsonEditor, self.tableEditor, self.plotView) as blocker:
        #    self.jsonEditor.setProfileDict(profile)
        #    self.tableEditor.setProfileDict(profile)
        #    self.plotView.setProfile(profile)

    def resetProfile(self):
        if isinstance(self.mDefaultProfile, dict):
            self.setProfile(self.mDefaultProfile)

    def validate(self) -> Tuple[bool, str, dict]:
        """
        Validates the editor widget input.
        :return: tuple (bool, str, dict) with
           bool = is valid,
           str = error message if invalid or '',
           dict = profile dictionary if valid or empty ({})
        """
        try:
            return validateProfileValueDict(self.currentProfile(), allowEmpty=True)
        except Exception as ex:
            return False, str(ex), dict()

    def clear(self):

        d = {}
        self.setProfile(d)

    def profile(self) -> Optional[dict]:
        """
        Returns a value spectral profile dictionary collected by profileDict or None, if internal state does not
        return a valid profile dictionary,
        the returned value is None (see `validate`).
        """
        success, err, d = self.validate()
        if d == dict() or not success:
            return None
        else:
            return d

    def currentProfile(self) -> dict:
        """
        Return the data as new SpectralProfile
        :return:
        :rtype:
        """
        cw = self.mCurrentWidget
        cp = self.mCurrentProfile
        if isinstance(cw, SpectralProfileJsonEditor):
            cp = cw.profileDict()
        elif isinstance(cw, SpectralProfileTableEditor):
            cp = cw.profileDict()
        self.mCurrentProfile = cp
        return cp


class SpectralProfileEditorWidgetWrapper(QgsEditorWidgetWrapper):
    class GlobalSignals(QObject):

        viewChangeRequest = pyqtSignal(str, int)

    GLOBAL_SIGNALS = GlobalSignals()

    def __init__(self, vl: QgsVectorLayer, fieldIdx: int, editor: QWidget, parent: QWidget):
        super(SpectralProfileEditorWidgetWrapper, self).__init__(vl, fieldIdx, editor, parent)
        self.mWidget: Optional[QWidget] = None

        self.mLastValue: QVariant = NULL
        self.mLayerID = vl.id()

    def createWidget(self, parent: QWidget):
        # log('createWidget')

        if not self.isInTable(parent):
            self.mWidget = SpectralProfileEditorWidget(parent=parent)
            self.mWidget.setMinimumHeight(200)
            self.mWidget.viewChangeRequest.connect(self.requestViewChange)
            self.GLOBAL_SIGNALS.viewChangeRequest.connect(self.onViewChangeRequest)
        else:
            self.mWidget = QLabel('Profile', parent=parent)
        return self.mWidget

    def requestViewChange(self, mode: int):
        self.GLOBAL_SIGNALS.viewChangeRequest.emit(self.mLayerID, mode)

    def onViewChangeRequest(self, layer_id: str, mode: int):
        if layer_id == self.mLayerID and isinstance(self.mWidget, SpectralProfileEditorWidget):
            self.mWidget.setViewMode(mode)
            # logger.debug(f'SpectralProfileEditorWidgetWrapper.onViewChangeRequest: {mode} {self.mWidget}')

    def initWidget(self, editor: QWidget):
        # log(' initWidget')
        conf = self.config()

        if isinstance(editor, SpectralProfileEditorWidget):

            editor.profileChanged.connect(self.onValueChanged)
            editor.initConfig(conf)
            # SpectralProfileEditorWidget.CNT += 1
            # logger.debug(f'SpectralProfileEditorWidget.CNT = {SpectralProfileEditorWidget.CNT}')

        elif isinstance(editor, QLabel):
            editor.setText(f'{SPECTRAL_PROFILE_FIELD_REPRESENT_VALUE} ({self.field().typeName()})')
            editor.setToolTip('Activate Form View to edit values')

    def onValueChanged(self, *args):
        self.valuesChanged.emit(self.value())

    def valid(self, *args, **kwargs) -> bool:
        return isinstance(self.mWidget, (SpectralProfileEditorWidget, QLabel))

    def value(self, *args, **kwargs):
        value = self.mLastValue
        w = self.widget()
        if isinstance(w, SpectralProfileEditorWidget):
            p = w.profile()
            if p is None:
                value = NULL
            else:
                value = encodeProfileValueDict(p, self.field())

        return value

    def setFeature(self, feature: QgsFeature) -> None:
        super(SpectralProfileEditorWidgetWrapper, self).setFeature(feature)

    def setEnabled(self, enabled: bool):
        w = self.widget()
        if isinstance(w, SpectralProfileEditorWidget):
            w.setReadOnly(not enabled)

    def setValue(self, value: Any) -> None:
        self.mLastValue = value
        w = self.widget()
        if isinstance(w, SpectralProfileEditorWidget):
            w.setProfile(decodeProfileValueDict(value))


class SpectralProfileEditorConfigWidget(QgsEditorConfigWidget):

    def __init__(self, vl: QgsVectorLayer, fieldIdx: int, parent: QWidget):
        super(SpectralProfileEditorConfigWidget, self).__init__(vl, fieldIdx, parent)
        self.label = QLabel('A field to store spectral profiles')
        hbox = QHBoxLayout()
        hbox.addWidget(self.label)
        self.setLayout(hbox)

    def config(self, *args, **kwargs) -> dict:
        config = {
            'foo': 'bar',
        }

        return config

    def setConfig(self, config: dict):
        pass


class SpectralProfileFieldFormatter(QgsFieldFormatter):

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

    def id(self) -> str:
        return EDITOR_WIDGET_REGISTRY_KEY

    def representValue(self, layer: QgsVectorLayer, fieldIndex: int, config: dict, cache, value):

        if value in [None, NULL]:
            return QgsApplication.nullRepresentation()
        else:
            return SPECTRAL_PROFILE_FIELD_REPRESENT_VALUE
            # return f'<b>{value}</b>'
            # if value not in [NULL, None, '']:
            #    d = decodeProfileValueDict(value)
            #    return json.dumps(d, ensure_ascii=False, allow_nan=True, indent=4, cls=InlineListEncoder)
            # return ''
            # return f'{SPECTRAL_PROFILE_FIELD_REPRESENT_VALUE} ({layer.fields().at(fieldIndex).typeName()})'


class SpectralProfileEditorWidgetFactory(QgsEditorWidgetFactory):

    def __init__(self, name: str):

        super(SpectralProfileEditorWidgetFactory, self).__init__(name)

        self.mConfigurations = {}

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

        w = SpectralProfileEditorConfigWidget(layer, fieldIdx, parent)
        key = self.configKey(layer, fieldIdx)
        w.setConfig(self.readConfig(key))
        w.changed.connect(lambda *args, ww=w, k=key: self.writeConfig(key, ww.config()))
        return w

    def configKey(self, layer: QgsVectorLayer, fieldIdx: int) -> Tuple[str, int]:
        """
        Returns a tuple to be used as dictionary key to identify a layer profile_field configuration.
        :param layer: QgsVectorLayer
        :param fieldIdx: int
        :return: (str, int)
        """
        return layer.id(), fieldIdx

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

        w = SpectralProfileEditorWidgetWrapper(layer, fieldIdx, editor, parent)
        # self.editWrapper = w
        return w

    def writeConfig(self, key: tuple, config: dict):
        """
        :param key: tuple (str, int), as created with .configKey(layer, fieldIdx)
        :param config: dict with config values
        """
        self.mConfigurations[key] = config
        # print('Save config')
        # print(config)

    def readConfig(self, key: tuple):
        """
        :param key: tuple (str, int), as created with .configKey(layer, fieldIdx)
        :return: {}
        """
        return self.mConfigurations.get(key, {})

    def supportsField(self, vl: QgsVectorLayer, fieldIdx: int) -> bool:
        """
        :param vl:
        :param fieldIdx:
        :return:
        """
        field: QgsField = vl.fields().at(fieldIdx)
        return can_store_spectral_profiles(field)

    def fieldScore(self, vl: QgsVectorLayer, fieldIdx: int) -> int:
        """
        This method allows disabling this editor widget type for a certain profile_field.

        :param vl: QgsVectorLayer
        :param fieldIdx: int
        :return: int
        """
        # log(' fieldScore()')
        field = vl.fields().at(fieldIdx)
        if not (isinstance(field, QgsField)):
            raise AssertionError
        if can_store_spectral_profiles(field):
            if field.editorWidgetSetup().type() == self.name():
                return 20  # specialized support
            else:
                return 5  # basic support
        else:
            return 0  # no support


_SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY = None
_SPECTRAL_PROFILE_FIELD_FORMATTER = None


def spectralProfileEditorWidgetFactory(register: bool = True) -> SpectralProfileEditorWidgetFactory:
    global _SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY
    global _SPECTRAL_PROFILE_FIELD_FORMATTER
    if not isinstance(_SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY, SpectralProfileEditorWidgetFactory):
        _SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY = SpectralProfileEditorWidgetFactory(EDITOR_WIDGET_REGISTRY_KEY)

    if not isinstance(_SPECTRAL_PROFILE_FIELD_FORMATTER, SpectralProfileFieldFormatter):
        _SPECTRAL_PROFILE_FIELD_FORMATTER = SpectralProfileFieldFormatter()

        if register:
            fmtReg: QgsFieldFormatterRegistry = QgsApplication.instance().fieldFormatterRegistry()
            fmtReg.addFieldFormatter(_SPECTRAL_PROFILE_FIELD_FORMATTER)

    reg: QgsEditorWidgetFactory = QgsGui.editorWidgetRegistry()

    if register and EDITOR_WIDGET_REGISTRY_KEY not in reg.factories().keys():
        reg.registerWidget(EDITOR_WIDGET_REGISTRY_KEY, _SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY)

    return reg.factory(EDITOR_WIDGET_REGISTRY_KEY)


def registerSpectralProfileEditorWidget():
    warnings.warn(DeprecationWarning('Use spectralprofileeditor.spectralProfileEditorWidgetFactory(True)'),
                  stacklevel=2)
    spectralProfileEditorWidgetFactory(True)
