"""
***************************************************************************
    spectralibraryplotmodelitems.py

    Items to described plot components in a spectral library plot.
    ---------------------
    Beginning            : January 2022
    Copyright            : (C) 2023 by 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 json
import re
import sys
from typing import Any, List, Optional, Union

import numpy as np
from pyqtgraph import InfiniteLine, PlotDataItem
from pyqtgraph.widgets.PlotWidget import PlotWidget

from qgis.PyQt.QtCore import QAbstractItemModel, QMimeData, QModelIndex, QSize, Qt
from qgis.PyQt.QtCore import QObject, pyqtSignal
from qgis.PyQt.QtGui import QColor, QIcon, QPixmap, QStandardItem, QStandardItemModel
from qgis.PyQt.QtWidgets import QCheckBox, QComboBox, QDoubleSpinBox, QHBoxLayout, QLineEdit, QMenu, QSizePolicy, \
    QSpinBox, QWidget
from qgis.PyQt.QtXml import QDomDocument, QDomElement
from qgis.core import Qgis, QgsExpression, QgsExpressionContext, QgsExpressionContextGenerator, \
    QgsExpressionContextScope, QgsExpressionContextUtils, QgsFeature, QgsFeatureRenderer, QgsFeatureRequest, QgsField, \
    QgsHillshadeRenderer, QgsMapLayer, QgsMultiBandColorRenderer, QgsPalettedRasterRenderer, QgsProject, QgsProperty, \
    QgsPropertyDefinition, QgsRasterContourRenderer, QgsRasterLayer, QgsRasterRenderer, QgsReadWriteContext, \
    QgsRenderContext, QgsSingleBandColorDataRenderer, QgsSingleBandGrayRenderer, QgsSingleBandPseudoColorRenderer, \
    QgsTextFormat, QgsVectorLayer, QgsXmlUtils
from qgis.gui import QgsColorButton, QgsDoubleSpinBox, QgsFieldExpressionWidget, QgsMapLayerComboBox, \
    QgsPropertyOverrideButton, QgsSpinBox
from ..core import is_profile_field, is_spectral_library, profile_field_names
from ..core.spectralprofile import decodeProfileValueDict
from ...editors.pythoncodeeditor import PythonCodeWidget, PythonCodeDialog
from ...layerfielddialog import LayerFieldWidget
from ...plotstyling.plotstyling import PlotStyle, PlotStyleButton, PlotWidgetStyle, PlotStyleWidget
from ...qgsrasterlayerproperties import QgsRasterLayerSpectralProperties
from ...unitmodel import BAND_INDEX, BAND_NUMBER, UnitConverterFunctionModel
from ...utils import featureSymbolScope

WARNING_ICON = QIcon(r':/images/themes/default/mIconWarning.svg')


class SpectralProfileColorPropertyWidget(QWidget):
    """
    Widget to specify the SpectralProfile colors.

    """

    class ContextGenerator(QgsExpressionContextGenerator):

        def __init__(self, widget):
            super().__init__()

            self.mWidget: 'SpectralProfileColorPropertyWidget' = widget

        def createExpressionContext(self) -> QgsExpressionContext:
            layer = self.mWidget.mPropertyOverrideButton.vectorLayer()
            if not isinstance(layer, QgsVectorLayer):
                return QgsExpressionContext()

            context: QgsExpressionContext = layer.createExpressionContext()
            feature: Optional[QgsFeature] = None
            for f in layer.getFeatures():
                feature = f
                break

            if isinstance(feature, QgsFeature):
                renderContext = QgsRenderContext()
                context.setFeature(feature)
                renderer = layer.renderer()
                if isinstance(renderer, QgsFeatureRenderer):
                    symbols = renderer.symbols(renderContext)
                    if len(symbols) > 0:
                        symbol = symbols[0]
                        j = context.indexOfScope('Symbol')
                        if j < 0:
                            symbolScope = QgsExpressionContextScope('Symbol')
                            context.appendScope(symbolScope)
                        else:
                            symbolScope: QgsExpressionContextScope = context.scope(j)
                        QgsExpressionContextUtils.updateSymbolScope(symbol, symbolScope)
            return context

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        self.setWindowIcon(QIcon(':/images/themes/default/mIconColorBox.svg'))
        self.mContextGenerator = SpectralProfileColorPropertyWidget.ContextGenerator(self)
        # self.mContext: QgsExpressionContext = QgsExpressionContext()
        # self.mRenderContext: QgsRenderContext = QgsRenderContext()
        # self.mRenderer: QgsFeatureRenderer = None
        self.mDefaultColor = QColor('green')
        self.mColorButton = QgsColorButton()
        self.mColorButton.colorChanged.connect(self.onButtonColorChanged)

        self.mColorButton.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed))
        self.mPropertyOverrideButton = QgsPropertyOverrideButton()
        self.mPropertyOverrideButton.registerLinkedWidget(self.mColorButton)
        self.mPropertyOverrideButton.registerExpressionContextGenerator(self.mContextGenerator)
        # self.mPropertyOverrideButton.aboutToShowMenu.connect(self.updateOverrideMenu)
        hl = QHBoxLayout()
        hl.addWidget(self.mColorButton)
        hl.addWidget(self.mPropertyOverrideButton)
        hl.setSpacing(2)
        hl.setContentsMargins(0, 0, 0, 0)
        self.sizePolicy().setHorizontalPolicy(QSizePolicy.Policy.Preferred)
        self.setLayout(hl)

        self.mPropertyDefinition = QgsPropertyDefinition()
        self.mPropertyDefinition.setName('Profile line color')

    def setLayer(self, layer: QgsVectorLayer):

        self.mPropertyOverrideButton.setVectorLayer(layer)
        self.mPropertyOverrideButton.updateFieldLists()

    def onButtonColorChanged(self, color: QColor):
        self.mPropertyOverrideButton.setActive(False)

    def setDefaultColor(self, color: QColor):
        self.mDefaultColor = QColor(color)

    def setToProperty(self, property: QgsProperty):
        if not (isinstance(property, QgsProperty)):
            raise AssertionError

        if property.propertyType() == QgsProperty.Type.StaticProperty:
            self.mColorButton.setColor(
                property.valueAsColor(self.mContextGenerator.createExpressionContext(), self.mDefaultColor)[0])
            self.mPropertyOverrideButton.setActive(False)
        else:
            self.mPropertyOverrideButton.setActive(True)
            self.mPropertyOverrideButton.setToProperty(property)
        # self.mColorButton.setColor(property.valueAsColor())

    def toProperty(self) -> QgsProperty:

        if self.mPropertyOverrideButton.isActive():
            return self.mPropertyOverrideButton.toProperty()
        else:
            prop = QgsProperty()
            prop.setStaticValue(self.mColorButton.color())
            return prop


class PropertyItemBase(QStandardItem):
    """
    Base class to be used by others
    """

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

    def __ne__(self, other):
        return not self.__eq__(other)

    def firstColumnSpanned(self) -> bool:
        return len(self.propertyRow()) == 1

    def propertyRow(self) -> List[QStandardItem]:
        return [self]

    def model(self) -> QStandardItemModel:
        return super().model()

    def populateContextMenu(self, menu: QMenu):
        pass

    def previewPixmap(self, size: QSize) -> QPixmap:
        return None

    def hasPixmap(self) -> bool:
        return False

    def data(self, role: int = ...) -> Any:

        if role == Qt.ItemDataRole.UserRole:
            return self
            # return None
        else:
            return super().data(role)


class PropertyLabel(QStandardItem):
    """
    The label lined to a PropertyItem
    """

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        self.setCheckable(False)
        self.setEditable(False)
        self.setDropEnabled(False)
        self.setDragEnabled(False)
        # self.mSignals = PropertyLabel.Signals()

    def propertyItem(self) -> 'PropertyItem':
        """
        Returns the PropertyItem paired with this PropertyLabel.
        Should be in the column left to it
        """
        item = self.model().index(self.row(), self.column() + 1,
                                  parent=self.parent().index()).data(Qt.ItemDataRole.UserRole)

        if isinstance(item, PropertyItem) and item.label() == self:
            return item

    def data(self, role: int = ...) -> Any:
        if role == Qt.ItemDataRole.UserRole:
            return self
        if role == Qt.ItemDataRole.ForegroundRole:
            # quick fix to avoid overwriting the TreeViewDelegetes standard color
            return None
        return super().data(role)


class PropertyItem(PropertyItemBase):
    """
    Controls a single property parameter.
    Is paired with a PropertyLabel and should be owned by a parent PropertyGroup
    .propertyRow() -> [PropertyLabel, PropertyItem]
    """

    def __init__(self, labelName: str, *args, **kwds):
        if not (isinstance(labelName, str)):
            raise AssertionError
        if 'tooltip' in kwds:
            tt = kwds.pop('tooltip')
        else:
            tt = None
        if 'key' in kwds:
            key = kwds.pop('key')
        else:
            key = re.sub(r'[-_]|\s', '_', labelName.lower())

        super().__init__(*args, **kwds)
        if not (isinstance(key, str) and ' ' not in key):
            raise AssertionError
        self.mKey = key
        self.setEditable(False)
        self.setDragEnabled(False)
        self.setDropEnabled(False)
        if labelName is None:
            labelName = key
        self.mLabel = PropertyLabel(labelName)

        if tt:
            self.mLabel.setToolTip(tt)
            self.setToolTip(tt)

    def __eq__(self, other):
        if not isinstance(other, PropertyItem):
            return False
        if self.__class__.__name__ != other.__class__.__name__:
            return False

        return (
            self.key() == other.key()
            and self.data(Qt.ItemDataRole.DisplayRole) == other.data(Qt.ItemDataRole.DisplayRole)  # noqa: W503
        )

    def __ne__(self, other):
        return not self.__eq__(other)

    def setToolTip(self, tooltip: str):

        self.label().setToolTip(tooltip)
        super().setToolTip(tooltip)

    def itemIsChecked(self) -> bool:

        if self.label().isCheckable():
            return self.label().checkState() == Qt.CheckState.Checked
        return None

    def setItemCheckable(self, b: bool):
        self.label().setCheckable(b)
        self.label().setEditable(b)

    def setItemChecked(self, b: bool):
        self.label().setCheckState(Qt.CheckState.Checked if b is True else Qt.CheckState.Unchecked)

    # def signals(self):
    #    return self.mSignals

    def expressionContext(self) -> QgsExpressionContext:
        """
        Returns an expression context that can be used
        to evaluate QgsExpressions.
        :return:
        """
        parent = self.parent()
        if parent is not None and hasattr(parent, 'expressionContextGenerator'):
            return parent.expressionContextGenerator().createExpressionContext()

        context = QgsExpressionContext()
        return context

    def createEditor(self, parent):

        return None

    def clone(self):
        raise NotImplementedError()

    def connectPlotModel(self, model: QStandardItemModel):
        pass

    def setEditorData(self, editor: QWidget, index: QModelIndex):
        pass

    def setModelData(self, w, bridge, index):
        pass

    def key(self) -> str:
        return self.mKey

    def label(self) -> PropertyLabel:
        return self.mLabel

    def propertyRow(self) -> List[QStandardItem]:
        return [self.label(), self]

    def project(self) -> Optional[QgsProject]:
        p = self.parent()
        if isinstance(p, PropertyItemGroup):
            return p.project()
        return None


class PropertyItemGroup(PropertyItemBase):
    """
    Represents a group of properties.
    """

    def __init__(self, *args, project: Optional[QgsProject] = None, **kwds):
        super().__init__(*args, **kwds)
        self.mMissingValues: bool = False
        self.mZValue = 0
        # self.mSignals = PropertyItemGroup.Signals()
        self.mFirstColumnSpanned = True

        if project is None:
            project = QgsProject.instance()
        self.mProject: QgsProject = project

    def setProject(self, project: QgsProject):
        if not (isinstance(project, QgsProject)):
            raise AssertionError
        self.mProject = project

    def project(self) -> QgsProject:
        return self.mProject

    def __eq__(self, other):

        if not (isinstance(other, PropertyItemGroup) and self.__class__.__name__ == other.__class__.__name__):
            return False

        ud1 = self.data(Qt.ItemDataRole.DisplayRole)
        ud2 = other.data(Qt.ItemDataRole.DisplayRole)

        b = (self.checkState() == other.checkState()) and (ud1 == ud2)

        if self.rowCount() != other.rowCount():
            return False
        for p1, p2 in zip(self.propertyItems(), other.propertyItems()):
            if p1 != p2:
                return False
        return b

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return super().__repr__() + f' "{self.data(Qt.ItemDataRole.DisplayRole)}"'

    def disconnectGroup(self):
        """
        Should implement all actions required to remove this property item from the plot
        """
        pass

    def isRemovable(self) -> bool:
        return True

    def zValue(self) -> int:
        return self.mZValue

    def asMap(self) -> dict:
        """
        Returns the settings as dict which can be serialized as JSON string.
        """
        raise NotImplementedError(f'Missing .asMap() in {self.__class__.__name__}')

    def fromMap(self, settings: dict):
        raise NotImplementedError(f'Missing .fromMap() in {self.__class__.__name__}')

    def createPlotStyle(self, feature: QgsFeature, fieldIndex: int) -> Optional[PlotStyle]:

        return None

    def plotDataItems(self) -> List[PlotDataItem]:
        """
        Returns a list with all pyqtgraph plot data items
        """
        return []

    def propertyItems(self) -> List['PropertyItem']:
        items = []
        for r in range(self.rowCount()):
            child = self.child(r, 1)
            if isinstance(child, PropertyItem):
                items.append(child)
        return items

    def initBasicSettings(self):
        self.setUserTristate(False)
        self.setCheckable(True)
        self.setCheckState(Qt.CheckState.Checked)
        self.setDropEnabled(False)
        self.setDragEnabled(False)

    def signals(self) -> 'PropertyItemGroup.Signals':
        return self.mSignals

    def __hash__(self):
        return hash(id(self))

    def setValuesMissing(self, missing: bool):
        self.mMissingValues = missing

    def setCheckState(self, checkState: Qt.CheckState) -> None:
        super().setCheckState(checkState)

        c = QColor() if self.isVisible() else QColor('grey')

        for r in range(self.rowCount()):
            self.child(r, 0).setForeground(c)

    def setVisible(self, visible: bool):
        if visible in [Qt.CheckState.Checked, visible is True]:
            self.setCheckState(Qt.CheckState.Checked)
        else:
            self.setCheckState(Qt.CheckState.Unchecked)

    def isVisible(self) -> bool:
        """
        Returns True if plot items related to this control item should be visible in the plot
        """
        return self.checkState() == Qt.CheckState.Checked

    def data(self, role: int = ...) -> Any:

        if role == Qt.ItemDataRole.DecorationRole and self.mMissingValues:
            return QIcon(WARNING_ICON)

        return super().data(role)

    def setData(self, value: Any, role: int = ...) -> None:
        value = super().setData(value, role)

        if role == Qt.ItemDataRole.CheckStateRole:
            # self.mSignals.requestPlotUpdate.emit()
            is_visible = self.isVisible()
            for item in self.plotDataItems():
                item.setVisible(is_visible)
            self.emitDataChanged()
            # if is_visible:
            #    self.mSignals.requestPlotUpdate.emit()
        return value

    def update(self):
        pass

    MIME_TYPE = 'application/SpectralProfilePlot/PropertyItems'

    @staticmethod
    def toMimeData(propertyGroups: List['PropertyItemGroup']):

        for g in propertyGroups:
            if not (isinstance(g, PropertyItemGroup)):
                raise AssertionError

        md = QMimeData()
        # context = QgsReadWriteContext()
        doc = QDomDocument()
        root = doc.createElement('PropertyItemGroups')
        doc.appendChild(root)
        for grp in propertyGroups:
            if isinstance(grp, PropertyItemGroup):
                data = grp.asMap()
                node = doc.createElement('PropertyItemGroup')
                node.setAttribute('type', grp.__class__.__name__)
                tn = doc.createTextNode(json.dumps(data, ensure_ascii=False))
                node.appendChild(tn)
                root.appendChild(node)
        md.setData(PropertyItemGroup.MIME_TYPE, doc.toByteArray())
        return md

    @staticmethod
    def fromMimeData(mimeData: QMimeData) -> List['ProfileVisualizationGroup']:

        # context = QgsReadWriteContext()

        groups = []
        if mimeData.hasFormat(PropertyItemGroup.MIME_TYPE):
            ba = mimeData.data(PropertyItemGroup.MIME_TYPE)
            doc = QDomDocument()
            doc.setContent(ba)
            root = doc.firstChildElement('PropertyItemGroups')
            if not root.isNull():
                # print(nodeXmlString(root))
                grpNode = root.firstChild().toElement()
                while not grpNode.isNull():
                    if grpNode.nodeName() == 'PropertyItemGroup':
                        grpNode = grpNode.toElement()
                        t = grpNode.attribute('type')
                        grp = None
                        if t == ProfileVisualizationGroup.__name__:
                            grp = ProfileVisualizationGroup()
                            data = grpNode.text()
                            data = json.loads(data)
                            grp.fromMap(data)

                        if grp:
                            groups.append(grp)
                        else:
                            pass
                    grpNode = grpNode.nextSibling()
        return groups


class LegendSettingsGroup(PropertyItemGroup):

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        self.mZValue = -1
        self.setText('Legend')
        self.setIcon(QIcon())

        self.setUserTristate(False)
        self.setCheckable(True)
        self.setCheckState(Qt.CheckState.Unchecked)
        self.setDropEnabled(False)
        self.setDragEnabled(False)

        # self.mP_MaxProfiles = QgsPropertyItem('legend_max_profiles')
        # self.mP_MaxProfiles.setDefinition(QgsPropertyDefinition(
        #    'Max. Profiles', 'Maximum number of profiles listed in legend',
        #    QgsPropertyDefinition.StandardPropertyTemplate.IntegerPositive))
        # self.mP_MaxProfiles.setProperty(QgsProperty.fromValue(64))
        # labelTextSize

        self.m_columns = QgsPropertyItem('legend_columns')
        self.m_columns.setDefinition(QgsPropertyDefinition(
            'Columns', 'Number of columns in legend',
            QgsPropertyDefinition.StandardPropertyTemplate.IntegerPositiveGreaterZero
        ))
        self.m_columns.setProperty(QgsProperty.fromValue(1))
        # self.m_textsize = PropertyItem('text_size', 'Text Size')
        self.m_textsize = TextItem('Text Size')
        self.m_textsize.setText('9px')
        self.m_textsize.setToolTip('Text size in legend')

        for pItem in [  # self.mPLegend,
            # self.mP_MaxProfiles,
            self.m_columns, self.m_textsize
        ]:
            self.appendRow(pItem.propertyRow())

    def asMap(self) -> dict:
        d = {
            'show': self.checkState() == Qt.CheckState.Checked,
            'text_size': self.m_textsize.text(),
            # 'max_items': self.mP_MaxProfiles.value(),
            'columns': max(1, self.m_columns.value()),
        }

        return d


class GeneralSettingsGroup(PropertyItemGroup):
    """
    General Plot Settings
    """

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        self.mZValue = -1
        self.setText('General Settings')
        self.setCheckable(False)
        self.setEnabled(True)
        self.setEditable(False)
        self.setIcon(QIcon(':/images/themes/default/console/iconSettingsConsole.svg'))

        self.mProfileStats = GeneralSettingsProfileStats()

        self.mShowToolTips = QgsPropertyItemBool('Tooltips',
                                                 tooltip='Show tooltips',
                                                 value=False)

        self.mShowToolTips.setProperty(QgsProperty.fromValue(False))

        self.mP_SortBands = QgsPropertyItemBool('Sort Bands',
                                                tooltip='Sort bands by their x values.',
                                                value=True)

        self.mP_BadBands = QgsPropertyItemBool('Bad Bands',
                                               tooltip='Show or hide values with a bad band value != 1.',
                                               value=True)

        self.mP_MaxProfiles = QgsPropertyItem('MaxProfiles')
        self.mP_MaxProfiles.setDefinition(QgsPropertyDefinition(
            'Max. Profiles', 'Maximum number of profiles that can be plotted.',
            QgsPropertyDefinition.StandardPropertyTemplate.IntegerPositive))
        self.mP_MaxProfiles.setProperty(QgsProperty.fromValue(256))

        self.mP_Antialiasing = QgsPropertyItemBool('Antialias',
                                                   tooltip='Enable antialias. Can decrease rendering speed.',
                                                   value=True)

        self.mP_BG = QgsPropertyItem('BG')
        self.mP_BG.setDefinition(QgsPropertyDefinition(
            'Background', 'Plot background color',
            QgsPropertyDefinition.StandardPropertyTemplate.ColorWithAlpha))
        self.mP_BG.setProperty(QgsProperty.fromValue(QColor('black')))

        self.mP_FG = QgsPropertyItem('FG')
        self.mP_FG.setDefinition(QgsPropertyDefinition(
            'Foreground', 'Plot foreground color',
            QgsPropertyDefinition.StandardPropertyTemplate.ColorWithAlpha))
        self.mP_FG.setProperty(QgsProperty.fromValue(QColor('white')))

        self.mP_SC = QgsPropertyItem('SC')
        self.mP_SC.setDefinition(QgsPropertyDefinition(
            'Selection', 'Color of selected profiles',
            QgsPropertyDefinition.StandardPropertyTemplate.ColorWithAlpha))
        self.mP_SC.setProperty(QgsProperty.fromValue(QColor('yellow')))

        self.mP_CH = QgsPropertyItem('CH')
        self.mP_CH.setDefinition(QgsPropertyDefinition(
            'Crosshair', 'Show a crosshair and set its color',
            QgsPropertyDefinition.StandardPropertyTemplate.ColorWithAlpha))
        self.mP_CH.setProperty(QgsProperty.fromValue(QColor('yellow')))
        self.mP_CH.setItemCheckable(True)
        self.mP_CH.setItemChecked(True)

        # self.mProfileCandidates = PlotStyleItem('Candidates')
        #
        # tt = 'Highlight profile candidates using a different style<br>' \
        #      'If activated and unless other defined, use the style defined here.'
        #
        # self.mProfileCandidates.setToolTip(tt)
        #
        # default_candidate_style = PlotStyle()
        # default_candidate_style.setMarkerColor('green')
        # default_candidate_style.setLineColor('green')
        # default_candidate_style.setLineWidth(2)
        # default_candidate_style.setLineStyle(Qt.SolidLine)
        #
        # self.mProfileCandidates.setPlotStyle(default_candidate_style)
        # self.mProfileCandidates.setItemCheckable(True)
        # self.mProfileCandidates.setItemChecked(True)
        # self.mProfileCandidates.setEditColors(True)

        self.mLegendGroup = LegendSettingsGroup(self)
        for pItem in [  # self.mPLegend,
            # self.mProfileCandidates,
            self.mLegendGroup,
            self.mP_CH,
            self.mProfileStats,
            self.mShowToolTips,
            self.mP_SortBands, self.mP_BadBands, self.mP_Antialiasing,
            self.mP_MaxProfiles,
            self.mP_BG, self.mP_FG, self.mP_SC,

        ]:
            self.appendRow(pItem.propertyRow())

        self.mContext: QgsExpressionContext = QgsExpressionContext()

        self.mMissingValues = False

    def fromMap(self, settings: dict):
        TRUE = [True, 1]
        if 'max_profiles' in settings:
            self.setMaximumProfiles(int(settings['max_profiles']))
        if 'show_bad_bands' in settings:
            self.mP_BadBands.setValue(settings['show_bad_bands'] in TRUE)
        if 'sort_bands' in settings:
            self.mP_SortBands.setValue(settings['sort_bands'] in TRUE)
        if 'show_crosshair' in settings:
            self.mP_CH.setValue(settings['show_crosshair'] in TRUE)
        if 'antialiasing' in settings:
            self.mP_Antialiasing.setValue(settings['antialiasing'] in TRUE)
        if 'color_bg' in settings:
            self.mP_BG.setValue(QColor(settings['color_bg']))
        if 'color_fg' in settings:
            self.mP_FG.setValue(QColor(settings['color_fg']))
        if 'color_sc' in settings:
            self.mP_SC.setValue(QColor(settings['color_sc']))
        if 'color_ch' in settings:
            self.mP_CH.setValue(QColor(settings['color_ch']))

    def asMap(self) -> dict:

        d = {
            'max_profiles': self.maximumProfiles(),
            'show_bad_bands': self.showBadBands(),
            'sort_bands': self.sortBands(),
            'show_crosshair': self.mP_CH.itemIsChecked(),
            'antialiasing': self.mP_Antialiasing.value(),
            'color_bg': self.backgroundColor().name(),
            'color_fg': self.foregroundColor().name(),
            'color_sc': self.selectionColor().name(),
            'color_ch': self.crosshairColor().name(),
            'show_tooltips': self.showToolTips(),
            'legend': self.mLegendGroup.asMap(),
            'statistics': self.mProfileStats.asMap()
        }
        return d

    def populateContextMenu(self, menu: QMenu):

        m = menu.addMenu('Color Theme')

        for style in PlotWidgetStyle.plotWidgetStyles():
            a = m.addAction(style.name)
            a.setIcon(QIcon(style.icon))
            a.triggered.connect(lambda *args, s=style: self.setPlotWidgetStyle(s))

    def maximumProfiles(self) -> int:
        return self.mP_MaxProfiles.value()

    def setShowLegend(self, value: bool):
        state = Qt.CheckState.Checked if value else Qt.CheckState.Unchecked
        self.mLegendGroup.setCheckState(state)

    def setMaximumProfiles(self, n: int):
        if not (n >= 0):
            raise AssertionError
        self.mP_MaxProfiles.setProperty(QgsProperty.fromValue(n))

    def expressionContext(self) -> QgsExpressionContext:
        return self.mContext

    def plotWidgetStyle(self) -> PlotWidgetStyle:

        style = PlotWidgetStyle(bg=self.backgroundColor(),
                                fg=self.foregroundColor(),
                                tc=self.foregroundColor(),
                                cc=self.crosshairColor(),
                                sc=self.selectionColor())

        return style

    def setPlotWidgetStyle(self, style: PlotWidgetStyle):

        self.mP_BG.setProperty(QgsProperty.fromValue(style.backgroundColor))
        self.mP_FG.setProperty(QgsProperty.fromValue(style.foregroundColor))
        self.mP_CH.setProperty(QgsProperty.fromValue(style.crosshairColor))
        self.mP_SC.setProperty(QgsProperty.fromValue(style.selectionColor))

        from .spectralprofileplotmodel import SpectralProfilePlotModel
        model: SpectralProfilePlotModel = self.model()
        if isinstance(model, SpectralProfilePlotModel):
            model.mDefaultSymbolRenderer.symbol().setColor(style.foregroundColor)

            for vis in model.visualizations():
                vis.setPlotWidgetStyle(style)
        self.emitDataChanged()

    def antialias(self) -> bool:
        return self.mP_Antialiasing.property().valueAsBool(self.expressionContext())[0]

    def backgroundColor(self) -> QColor:
        return self.mP_BG.property().valueAsColor(self.expressionContext())[0]

    def foregroundColor(self) -> QColor:
        return self.mP_FG.property().valueAsColor(self.expressionContext())[0]

    def selectionColor(self) -> QColor:
        return self.mP_SC.property().valueAsColor(self.expressionContext())[0]

    def crosshairColor(self) -> QColor:
        return self.mP_CH.property().valueAsColor(self.expressionContext())[0]

    def showToolTips(self) -> bool:
        return self.mShowToolTips.property().valueAsBool(self.expressionContext())[0]

    def showBadBands(self) -> bool:
        return self.mP_BadBands.property().valueAsBool(self.expressionContext(), False)[0]

    def sortBands(self) -> bool:
        return self.mP_SortBands.property().valueAsBool(self.expressionContext(), True)[0]

    def isRemovable(self) -> bool:
        return False

    def isVisible(self) -> bool:
        return True


class PythonCodeItem(PropertyItem):
    """
    A property item to collect python code
    """

    class Signals(QObject):

        validationRequest = pyqtSignal(dict)

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

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

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

        self.setEditable(True)
        self.mCode: str = ''
        self.mIsValid: bool = True
        self.signals = PythonCodeItem.Signals()
        self.mDialogHelpText = 'Enter python code'

    def setDialogHelpText(self, text: str):
        self.mDialogHelpText = text

    def code(self) -> str:
        return self.mCode

    def createEditor(self, parent):
        w = PythonCodeWidget(parent=parent)
        w.setCode(self.mCode)
        w.setDialogHelpText(self.mDialogHelpText)
        w.validationRequest.connect(self.signals.validationRequest.emit)
        return w

    def data(self, role: int = ...) -> Any:

        if role == Qt.ItemDataRole.DisplayRole:
            return self.mCode

        if role == Qt.ItemDataRole.ToolTipRole:
            return self.mCode

        if role == Qt.ItemDataRole.ForegroundRole:
            if not self.mIsValid:
                return QColor('red')

        return super().data(role)

    def setModelData(self, editor: PythonCodeWidget, model: QAbstractItemModel, index: QModelIndex):
        index.data()
        if editor.isValid():
            self.setCode(editor.code())

    def setCode(self, code: str):
        if not (isinstance(code, str)):
            raise AssertionError
        expr_old = self.mCode
        if code != expr_old:
            self.mCode = code
            self.emitDataChanged()

    def setEditorData(self, editor: PythonCodeWidget, index: QModelIndex):

        parent = self.parent()
        if isinstance(parent, ProfileVisualizationGroup):
            if layer := parent.layer():
                editor.setLayer(layer)
        editor.setCode(self.mCode)

    def heightHint(self) -> int:
        h = 10
        return h


class PlotStyleItem(PropertyItem):
    """
    A property item to control a PlotStyle
    """

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        self.mPlotStyle = PlotStyle()
        self.setEditable(True)

        self.mVisibilityFlags = None
        self.mEditColors: bool = False

    def clone(self):
        item = PlotStyleItem(self.key())
        item.setPlotStyle(self.plotStyle().clone())
        return item

    def __eq__(self, other):
        return super().__eq__(other) and self.plotStyle() == other.plotStyle()

    def setEditColors(self, b):
        self.mEditColors = b is True

    def setPlotStyle(self, plotStyle: PlotStyle):
        if plotStyle != self.mPlotStyle:
            self.mPlotStyle = plotStyle
            self.emitDataChanged()

    def setVisibilitFlags(self, flags: PlotStyleWidget.VisibilityFlags):

        self.mVisibilityFlags = flags

    def plotStyle(self) -> PlotStyle:
        style = self.mPlotStyle.clone()

        from .spectralprofileplotmodel import SpectralProfilePlotModel
        model = self.model()
        if isinstance(model, SpectralProfilePlotModel):
            gsettings: GeneralSettingsGroup = model.generalSettings()
            bc = gsettings.backgroundColor()
            al = gsettings.antialias()

            style.setBackgroundColor(bc)
            style.setAntialias(al)

        return style

    def populateContextMenu(self, menu: QMenu):

        parent = self.parent()

        if isinstance(parent, ProfileVisualizationGroup):
            style = parent.plotStyle()
        else:
            style = self.plotStyle()

        a = menu.addAction('Copy Style')
        a.triggered.connect(lambda *args, s=style: s.toClipboard())

        cbStyle = PlotStyle.fromClipboard()
        a = menu.addAction('Paste Style')
        a.setEnabled(isinstance(cbStyle, PlotStyle))
        a.triggered.connect(lambda *args, s=cbStyle: self.setPlotStyle(cbStyle))

    def createEditor(self, parent):

        w = PlotStyleButton(parent=parent)
        w.setMinimumSize(5, 5)
        if self.mVisibilityFlags:
            w.setVisibilityFlags(self.mVisibilityFlags)
        else:
            w.setColorWidgetVisibility(self.mEditColors)
            w.setVisibilityCheckboxVisible(self.isCheckable())

        w.setToolTip('Set curve style')

        return w

    def setEditorData(self, editor: QWidget, index: QModelIndex):
        if isinstance(editor, PlotStyleButton):
            grp = self.parent()
            if isinstance(grp, ProfileVisualizationGroup) and self.key() == 'style':
                plot_style = grp.plotStyle(add_symbol_scope=True)
            else:
                plot_style = self.plotStyle()

            editor.setPlotStyle(plot_style)

    def setModelData(self, w, bridge, index):
        if isinstance(w, PlotStyleButton):
            self.setPlotStyle(w.plotStyle())

    def writeXml(self, parentNode: QDomElement, context: QgsReadWriteContext, attribute: bool = False):
        doc: QDomDocument = parentNode.ownerDocument()
        xml_tag = self.key()
        node = doc.createElement(xml_tag)
        self.mPlotStyle.writeXml(node, doc)
        parentNode.appendChild(node)

    def readXml(self, parentNode: QDomElement, context: QgsReadWriteContext, attribute: bool = False):
        node = parentNode.firstChildElement(self.key()).toElement()
        if not node.isNull():
            style = PlotStyle.readXml(node)
            if isinstance(style, PlotStyle):
                self.setPlotStyle(style)


class SpectralProfileLayerFieldItem(PropertyItem):

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

        self.mFieldName: Optional[str] = None
        self.mLayerID: Optional[str] = None

        super().__init__(*args, **kwds)
        self.mEditor = None
        self.setEditable(True)

    def layer(self) -> Optional[QgsVectorLayer]:

        if p := self.project():
            lyr = p.mapLayer(self.mLayerID)
            if isinstance(lyr, QgsVectorLayer):
                return lyr
        return None

    def populateContextMenu(self, menu: QMenu):
        from .spectralprofileplotmodel import SpectralProfilePlotModel

        def plotModel() -> Optional[SpectralProfilePlotModel]:
            if not self.mLayerID:
                return None
            m = self.model()
            if isinstance(m, SpectralProfilePlotModel):
                return m
            else:
                return None

        def onShowLayerProperties(*args):
            if model := plotModel():
                model.sigOpenLayerPropertiesRequest.emit(self.mLayerID)

        def onOpenAttributeTableRequest(*args):
            if model := plotModel():
                model.sigOpenAttributeTableRequest.emit(self.mLayerID)

        def onSpectralProcessingDialogRequest(*args):
            if model := plotModel():
                model.sigOpenSpectralProcessingRequest.emit(self.mLayerID)

        layer = self.layer()
        if isinstance(layer, QgsVectorLayer):
            a = menu.addAction('Layer properties')
            a.setIcon(QIcon(':/images/themes/default/propertyicons/system.svg'))
            a.setToolTip(f'Open the layer properties for layer "{layer.name()}"')
            a.triggered.connect(onShowLayerProperties)

            a = menu.addAction('Attribute table')
            a.setIcon(QIcon(':/images/themes/default/mActionOpenTable.svg'))
            a.setToolTip(f'Open an attribute table for layer "{layer.name()}"')
            a.triggered.connect(onOpenAttributeTableRequest)

            a = menu.addAction('Spectral Processing')
            a.setIcon(QIcon(':/qps/ui/icons/profile_processing.svg'))
            a.setToolTip(f'Open a spectral processing dialog for layer "{layer.name()}"')
            a.triggered.connect(onSpectralProcessingDialogRequest)

    def createEditor(self, parent):
        w = LayerFieldWidget(parent=parent)
        return w

    def setEditorData(self, editor: QWidget, index: QModelIndex):

        # if isinstance(parentItem, ProfileVisualizationGroup):
        if isinstance(editor, LayerFieldWidget):
            if p := self.project():
                editor.setProject(p)
            editor.setLayerFilter(lambda lyr: is_spectral_library(lyr))
            editor.setFieldFilter(lambda field: is_profile_field(field))

            lyr = self.layer()
            if isinstance(lyr, QgsVectorLayer):
                editor.setLayerField(lyr, self.mFieldName)

    def setModelData(self, editor: QWidget, bridge, index: QModelIndex):

        if isinstance(editor, LayerFieldWidget):
            layer, field = editor.layerField()
            self.setLayerField(layer, field)
            # self.mLayerID = layer.id()
            # self.mFieldName = field
            # self.emitDataChanged()

    def setLayerField(self,
                      layer_id: Union[None, str, QgsVectorLayer],
                      field_name: Union[None, str, QgsField]):

        if isinstance(layer_id, QgsVectorLayer):
            layer_id = layer_id.id()

        if field_name is None and isinstance(layer_id, str):
            lyr = self.project().mapLayer(layer_id)
            for n in profile_field_names(lyr):
                field_name = n
                break
        elif isinstance(field_name, QgsField):
            field_name = field_name.name()

        if not (layer_id is None or isinstance(layer_id, str)):
            raise AssertionError

        if layer_id != self.mLayerID or field_name != self.mFieldName:
            self.mLayerID = layer_id
            self.mFieldName = field_name

            self.emitDataChanged()

            grp = self.parent()
            if isinstance(grp, ProfileVisualizationGroup):
                grp.emitDataChanged()

    def field(self) -> Optional[str]:
        return self.mFieldName

    def data(self, role: int = ...) -> Any:

        missing_layer = self.mLayerID in ['', None]
        missing_field = self.mFieldName in ['', None]
        if role == Qt.ItemDataRole.DisplayRole:

            if missing_layer:
                return '<select layer>'
            elif missing_field:
                return '<select field>'
            else:
                return self.mFieldName

        if role == Qt.ItemDataRole.ToolTipRole:
            lyr = self.layer()
            field = self.field()
            if not isinstance(lyr, QgsVectorLayer):
                tt = 'Layer and spectral profile field undefined'
            else:
                tt = f'Layer: "{lyr.name()}" Field: {field}<br>Layer ID: {lyr.id()}<br>Layer Source: {lyr.source()}'
            return tt

        if role == Qt.ItemDataRole.ForegroundRole:

            if missing_field or missing_layer:
                return QColor('red')

        return super().data(role)


class TextItem(PropertyItem):

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

        self.setEditable(True)

    def text(self):
        return self.data(Qt.ItemDataRole.EditRole)

    def setText(self, text: str):
        if not (isinstance(text, str)):
            raise AssertionError
        self.setData(text, Qt.ItemDataRole.EditRole)

    def createEditor(self, parent: QWidget) -> QLineEdit:
        return QLineEdit(parent)

    def setEditorData(self, editor: QLineEdit, index: QModelIndex):
        editor.setText(self.text())

    def setModelData(self, editor: QLineEdit, bridge, index: QModelIndex):
        if isinstance(editor, QLineEdit):
            self.setText(editor.text())


class QgsTextFormatItem(PropertyItem):

    def __init__(self, *args, **kwds):
        super(self).__init__(*args, **kwds)
        self.mTextFormat = QgsTextFormat()
        self.setEditable(True)


class QgsPropertyItem(PropertyItem):

    def __init__(self, *args, **kwds):
        self.mProperty: Optional[QgsProperty] = None
        self.mDefinition: Optional[QgsPropertyDefinition] = None
        super().__init__(*args, **kwds)
        self.setEditable(True)

    def update(self):
        self.setText(self.mProperty.valueAsString(QgsExpressionContext()))

    def writeXml(self, parentNode: QDomElement, context: QgsReadWriteContext, attribute: bool = False):
        doc: QDomDocument = parentNode.ownerDocument()
        xml_tag = self.key()
        node = QgsXmlUtils.writeVariant(self.property(), doc)
        node.setTagName(xml_tag)
        parentNode.appendChild(node)

    def readXml(self, parentNode: QDomElement, context: QgsReadWriteContext, attribute: bool = False) -> bool:

        xml_tag = self.key()
        child = parentNode.firstChildElement(xml_tag).toElement()
        if not child.isNull():
            property = QgsXmlUtils.readVariant(child)
            if isinstance(property, QgsProperty):
                # workaround https://github.com/qgis/QGIS/issues/47127
                property.setActive(True)
                self.setProperty(property)
                return True
        return False

    def value(self, context=QgsExpressionContext(), defaultValue=None):
        return self.mProperty.value(context, defaultValue)[0]

    def setValue(self, value):
        p = self.property()
        if isinstance(value, QgsProperty):
            self.setProperty(value)
        elif p.propertyType() == QgsProperty.Type.StaticProperty:
            self.setProperty(QgsProperty.fromValue(value))
        elif p.propertyType() == QgsProperty.Type.FieldBasedProperty:
            self.setProperty(QgsProperty.fromField(value))
        elif p.propertyType() == QgsProperty.Type.ExpressionBasedProperty:
            self.setProperty(QgsProperty.fromExpression(str(value)))

    def property(self) -> QgsProperty:
        return self.mProperty

    def setProperty(self, property: QgsProperty):
        if not (isinstance(property, QgsProperty)):
            raise AssertionError
        if not (isinstance(self.mDefinition, QgsPropertyDefinition)):
            raise AssertionError('Call setDefinition(propertyDefinition) first')
        b = self.mProperty != property
        self.mProperty = property
        if b:
            # print(self.key())
            self.emitDataChanged()

    def setDefinition(self, propertyDefinition: QgsPropertyDefinition):
        if not (isinstance(propertyDefinition, QgsPropertyDefinition)):
            raise AssertionError
        if not (self.mDefinition is None):
            raise AssertionError('property definition is immutable and already set')
        self.mDefinition = propertyDefinition
        self.label().setText(propertyDefinition.name())
        self.label().setToolTip(propertyDefinition.description())
        self.setToolTip(propertyDefinition.description())

    def definition(self) -> QgsPropertyDefinition:
        return self.mDefinition

    def data(self, role: int = ...) -> Any:

        if self.mProperty is None:
            return None
        p = self.property()

        if role == Qt.ItemDataRole.DisplayRole:
            if p.propertyType() == QgsProperty.Type.ExpressionBasedProperty:
                return p.expressionString()
            elif p.propertyType() == QgsProperty.Type.FieldBasedProperty:
                return p.field()
            else:
                v, success = p.value(QgsExpressionContext())
                if success:
                    if isinstance(v, QColor):
                        return v.name()
                    else:
                        return v
        if role == Qt.ItemDataRole.DecorationRole:
            if self.isColorProperty():
                v, success = p.value(QgsExpressionContext())
                if success and isinstance(v, QColor):
                    return v

        if role == Qt.ItemDataRole.ToolTipRole:
            return self.definition().description()

        return super().data(role)

    def isColorProperty(self) -> bool:
        return self.definition().standardTemplate() in [QgsPropertyDefinition.StandardPropertyTemplate.ColorWithAlpha,
                                                        QgsPropertyDefinition.StandardPropertyTemplate.ColorNoAlpha]

    def createEditor(self, parent):
        # speclib: Optional[QgsVectorLayer] = self.speclib()
        template = self.definition().standardTemplate()

        if self.isColorProperty():
            w = SpectralProfileColorPropertyWidget(parent=parent)

        elif template == QgsPropertyDefinition.StandardPropertyTemplate.Boolean:
            w = QComboBox(parent=parent)
            w.addItem('True', True)
            w.addItem('False', False)

        elif template in [QgsPropertyDefinition.StandardPropertyTemplate.Integer,
                          QgsPropertyDefinition.StandardPropertyTemplate.IntegerPositive,
                          QgsPropertyDefinition.StandardPropertyTemplate.IntegerPositiveGreaterZero]:

            w = QgsSpinBox(parent=parent)

        elif template in [QgsPropertyDefinition.StandardPropertyTemplate.Double,
                          QgsPropertyDefinition.StandardPropertyTemplate.DoublePositive,
                          QgsPropertyDefinition.StandardPropertyTemplate.Double0To1]:
            w = QgsDoubleSpinBox(parent=parent)

        else:

            w = QgsFieldExpressionWidget(parent=parent)
            w.setAllowEmptyFieldName(True)
            w.setExpressionDialogTitle(self.definition().name())
            w.setToolTip(self.definition().description())
            w.setExpression(self.property().expressionString())

        return w

    def setEditorData(self, editor: QWidget, index: QModelIndex):

        grp = self.parent()
        if isinstance(grp, ProfileVisualizationGroup):
            lyr = grp.layer()
        else:
            lyr = None

        if isinstance(editor, QgsFieldExpressionWidget):
            editor.setProperty('lastexpr', self.property().expressionString())
            if isinstance(grp, ProfileVisualizationGroup):
                editor.registerExpressionContextGenerator(grp.expressionContextGenerator())
            if isinstance(lyr, QgsVectorLayer):
                editor.setLayer(lyr)

        elif isinstance(editor, SpectralProfileColorPropertyWidget):
            editor.setToProperty(self.property())
            if isinstance(lyr, QgsVectorLayer):
                editor.setLayer(lyr)

        elif isinstance(editor, (QSpinBox, QgsSpinBox)):
            template = self.definition().standardTemplate()

            if template == QgsPropertyDefinition.StandardPropertyTemplate.IntegerPositive:
                v_min = 0
            elif template == QgsPropertyDefinition.StandardPropertyTemplate.IntegerPositiveGreaterZero:
                v_min = 1
            else:
                v_min = -2147483648

            v_max = 2147483647
            editor.setMinimum(v_min)
            editor.setMaximum(v_max)
            value = self.value(defaultValue=v_min)
            if isinstance(editor, QgsSpinBox):
                editor.setClearValue(0)
                editor.setShowClearButton(True)
            editor.setValue(value)

        elif isinstance(editor, (QDoubleSpinBox, QgsDoubleSpinBox)):
            template = self.definition().standardTemplate()

            if template in [QgsPropertyDefinition.StandardPropertyTemplate.DoublePositive,
                            QgsPropertyDefinition.StandardPropertyTemplate.Double0To1]:
                v_min = 0.0
            else:
                v_min = sys.float_info.min

            if template in [QgsPropertyDefinition.StandardPropertyTemplate.Double0To1]:
                v_max = 1.0
            else:
                v_max = sys.float_info.max

            editor.setMinimum(v_min)
            editor.setMaximum(v_max)

            value = self.value(self.expressionContext(), defaultValue=0.0)
            if isinstance(editor, QgsDoubleSpinBox):
                editor.setShowClearButton(True)
                editor.setClearValue(value)
            editor.setValue(value)
        elif isinstance(editor, QLineEdit):

            value, success = self.property().valueAsString(self.expressionContext())
            editor.setText(value)

        elif isinstance(editor, QCheckBox):
            b = self.property().valueAsBool(self.expressionContext())[0]
            editor.setCheckState(Qt.CheckState.Checked if b else Qt.CheckState.Unchecked)

        elif isinstance(editor, QComboBox):
            value, success = self.property().value(self.expressionContext())
            if success:
                for r in range(editor.count()):
                    if editor.itemData(r) == value:
                        editor.setCurrentIndex(r)
                        break

    def setModelData(self, w, bridge, index):
        property: QgsProperty = None

        if isinstance(w, QgsFieldExpressionWidget):
            expr = w.asExpression()
            if w.isValidExpression() or expr == '' and w.allowEmptyFieldName():
                property = QgsProperty.fromExpression(expr)

        elif isinstance(w, SpectralProfileColorPropertyWidget):
            property = w.toProperty()

        elif isinstance(w, QCheckBox):
            property = QgsProperty.fromValue(w.isChecked())

        elif isinstance(w, QComboBox):
            property = QgsProperty.fromValue(w.currentData(Qt.ItemDataRole.UserRole))

        elif isinstance(w, (QgsSpinBox, QgsDoubleSpinBox)):
            property = QgsProperty.fromValue(w.value())

        elif isinstance(w, QLineEdit):
            property = QgsProperty.fromValue(w.text())

        if isinstance(property, QgsProperty):
            self.setProperty(property)


class QgsPropertyItemBool(QgsPropertyItem):

    def __init__(self, *args, value: bool = False, **kwds):
        super().__init__(*args, **kwds)

        pdef = QgsPropertyDefinition(
            self.label().text(), self.toolTip(),
            QgsPropertyDefinition.StandardPropertyTemplate.Boolean)

        self.setDefinition(pdef)
        self.setValue(QgsProperty.fromValue(value is True))


class ProfileColorPropertyItem(QgsPropertyItem):
    """
    A property item to collect a color or color expression.
    """

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

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

    def setColor(self, color: Union[str, QColor]):
        """Sets the color as fixed color value"""
        c = QColor(color)
        p = self.property()
        p.setStaticValue(c)
        self.emitDataChanged()

    def setColorExpression(self, expression: str):
        if not (isinstance(expression, str)):
            raise AssertionError
        p = self.property()
        p.setExpressionString(expression)
        self.emitDataChanged()

    def colorExpression(self) -> str:
        """
        Returns the current color as expression string
        :return:
        """
        p = self.property()
        if p.propertyType() == Qgis.PropertyType.Expression:
            color_expression = p.expressionString()
        elif p.propertyType() == Qgis.PropertyType.Static:
            color_expression = p.staticValue()
            if isinstance(color_expression, QColor):
                color_expression = f"'{color_expression.name()}'"
        else:
            color_expression = "'white'"
        return color_expression

    def populateContextMenu(self, menu: QMenu):

        if self.isColorProperty():
            a = menu.addAction('Use vector symbol color')
            a.setToolTip('Use map vector symbol colors as profile color.')
            a.setIcon(QIcon(r':/qps/ui/icons/speclib_usevectorrenderer.svg'))
            a.triggered.connect(self.setToSymbolColor)

    def setToSymbolColor(self, *args):
        if self.isColorProperty():
            self.setProperty(QgsProperty.fromExpression('@symbol_color'))


class RasterRendererGroup(PropertyItemGroup):
    """
    Visualizes the bands of a QgsRasterLayer
    """

    def __init__(self, *args, layer: QgsRasterLayer = None, **kwds):
        super().__init__(*args, **kwds)
        self.mZValue = 0
        self.setIcon(QIcon(':/images/themes/default/rendererCategorizedSymbol.svg'))
        self.setData('Renderer', Qt.ItemDataRole.DisplayRole)
        self.setData('Raster Layer Renderer', Qt.ItemDataRole.ToolTipRole)

        # self.mPropertyNames[LayerRendererVisualization.PIX_TYPE] = 'Renderer'
        # self.mPropertyTooltips[LayerRendererVisualization.PIX_TYPE] = 'raster layer renderer type'

        self.mLayerID = None
        self.mSpectralProperties: Optional[QgsRasterLayerSpectralProperties] = None

        self.mUnitConverter: UnitConverterFunctionModel = UnitConverterFunctionModel.instance()
        self.mIsVisible: bool = True

        self.mBarR: InfiniteLine = InfiniteLine(pos=1, angle=90, movable=True)
        self.mBarB: InfiniteLine = InfiniteLine(pos=2, angle=90, movable=True)
        self.mBarG: InfiniteLine = InfiniteLine(pos=3, angle=90, movable=True)
        self.mBarA: InfiniteLine = InfiniteLine(pos=3, angle=90, movable=True)

        for bar in self.bandPlotItems():
            bar.setCursor(Qt.CursorShape.SizeHorCursor)

        self.mXUnit: str = BAND_NUMBER
        self.mBarR.sigPositionChangeFinished.connect(self.updateToRenderer)
        self.mBarG.sigPositionChangeFinished.connect(self.updateToRenderer)
        self.mBarB.sigPositionChangeFinished.connect(self.updateToRenderer)
        self.mBarA.sigPositionChangeFinished.connect(self.updateToRenderer)

        self.mItemRenderer = PropertyItem('Renderer')
        self.mItemBandR = PropertyItem('Red')
        self.mItemBandG = PropertyItem('Green')
        self.mItemBandB = PropertyItem('Blue')
        self.mItemBandA = PropertyItem('Alpha')

        for item in self.bandPlotItems():
            item.setVisible(False)

        if isinstance(layer, QgsRasterLayer):
            self.setLayer(layer)

        self.setUserTristate(False)
        self.setCheckable(True)
        self.setCheckState(Qt.CheckState.Checked)
        self.setDropEnabled(False)
        self.setDragEnabled(False)

        self.updateLayerName()

    def createEditor(self, parent):
        # speclib: Optional[QgsVectorLayer] = self.speclib()

        return QgsMapLayerComboBox(parent=parent)

    def setEditorData(self, editor: QWidget, index: QModelIndex):

        if isinstance(editor, QgsMapLayerComboBox):
            editor.setFilters(Qgis.LayerFilter.RasterLayer)

            layer = self.layer()
            p = self.project()
            if isinstance(layer, QgsRasterLayer):
                if layer.id() not in p.mapLayers():
                    p2 = layer.project()
                    if isinstance(p2, QgsProject) and layer.id() in p2.mapLayers():
                        p = p2
            if isinstance(p, QgsProject):
                editor.setProject(p)

            if isinstance(layer, QgsRasterLayer):
                editor.setLayer(layer)

    def setModelData(self, editor: QWidget, model: QAbstractItemModel, index: QModelIndex):

        if isinstance(editor, QgsMapLayerComboBox):
            new_layer = editor.currentLayer()
            if isinstance(new_layer, QgsMapLayer) and new_layer.id() != self.mLayerID:
                self.setLayer(new_layer)

    # def updateBarVisiblity(self):
    #     model = self.model()
    #     from .spectralprofileplotmodel import SpectralProfilePlotModel
    #     if isinstance(model, SpectralProfilePlotModel):
    #         plotItem = model.plotWidget().getPlotItem()
    #         for bar in self.bandPlotItems():
    #             pass

    def connectPlotModel(self, model):

        from .spectralprofileplotmodel import SpectralProfilePlotModel
        if not (isinstance(model, SpectralProfilePlotModel)):
            raise AssertionError

        model.sigXUnitChanged.connect(self.setXUnit)
        self.setXUnit(model.xUnit().unit)
        # self.updateBarVisiblity()
        # # self.updateBarVisiblity()
        for bar in self.bandPlotItems():
            model.plotWidget().getPlotItem().addItem(bar)
        # model.sigXUnitChanged.connect(self.updateBarVisiblity)

    def clone(self) -> QStandardItem:
        item = RasterRendererGroup()
        item.setLayer(self.layer())
        item.setVisible(self.isVisible())
        return item

    def setXUnit(self, xUnit: str):
        if not (xUnit is None or isinstance(xUnit, str)):
            raise AssertionError
        if xUnit is None:
            xUnit = BAND_NUMBER
        self.mXUnit = xUnit
        self.updateFromRenderer()

    def layerId(self) -> str:
        return self.mLayerID

    def layer(self) -> Optional[QgsRasterLayer]:
        """
        Returns the layer instance relating to the stored layer id.
        :return: QgsRasterLayer or None
        """

        lyr = self.project().mapLayer(self.mLayerID)

        if not isinstance(lyr, QgsRasterLayer):
            lyr = QgsProject.instance().mapLayer(self.mLayerID)

        return lyr

    def setLayer(self, layer: QgsRasterLayer):
        if not (isinstance(layer, QgsRasterLayer) and layer.isValid()):
            raise AssertionError

        lid = layer.id()
        if lid == self.mLayerID:
            # layer already linked
            return

        if layer.project() and layer.project() != self.project():
            self.setProject(layer.project())

        self.onLayerRemoved()
        self.mSpectralProperties = QgsRasterLayerSpectralProperties.fromRasterLayer(layer)
        self.mLayerID = layer.id()

        layer.rendererChanged.connect(self.updateFromRenderer)
        layer.willBeDeleted.connect(self.onLayerRemoved)
        layer.nameChanged.connect(self.updateLayerName)

        self.updateFromRenderer()
        self.updateLayerName()

    def onLayerRemoved(self):
        self.disconnectGroup()

    def plotWidget(self) -> Optional[PlotWidget]:
        model = self.model()
        if model:
            return model.plotWidget()
        return None

    def connectGroup(self):

        pw: PlotWidget = self.plotWidget()
        if pw:
            for bar in self.bandPlotItems():
                if bar not in pw.items():
                    pw.addItem(bar)

    def disconnectGroup(self):
        pw = self.plotWidget()
        if pw:
            pitem = pw.getPlotItem()
            for bar in self.bandPlotItems():
                if bar in pitem.items:
                    pitem.removeItem(bar)

    def updateToRenderer(self):

        layer = self.layer()
        if not isinstance(layer, QgsRasterLayer):
            return

        renderer: QgsRasterRenderer = layer.renderer().clone()

        if self.mBarA.isVisible():
            bandA = self.xValueToBand(self.mBarA.pos().x())
            if bandA:
                renderer.setAlphaBand(bandA)

        bandR = self.xValueToBand(self.mBarR.pos().x())
        if isinstance(renderer, QgsMultiBandColorRenderer):
            bandG = self.xValueToBand(self.mBarG.pos().x())
            bandB = self.xValueToBand(self.mBarB.pos().x())
            if bandR:
                renderer.setRedBand(bandR)
            if bandG:
                renderer.setGreenBand(bandG)
            if bandB:
                renderer.setBlueBand(bandB)

        elif bandR and getattr(renderer, 'setInputBand'):
            renderer.setInputBand(bandR)

        layer.setRenderer(renderer)
        layer.triggerRepaint()
        # convert to band unit

    def xValueToBand(self, pos: float) -> int:

        band = None
        if self.mXUnit == BAND_NUMBER:
            band = int(round(pos))
        elif self.mXUnit == BAND_INDEX:
            band = int(round(pos)) + 1
        else:
            wl = self.mSpectralProperties.wavelengths()
            wlu = self.mSpectralProperties.wavelengthUnits()

            if wlu:
                func = self.mUnitConverter.convertFunction(self.mXUnit, wlu[0])
                new_wlu = func(pos)
                if new_wlu is not None:
                    band = np.argmin(np.abs(np.asarray(wl) - new_wlu)) + 1
        if isinstance(band, int):
            band = max(band, 0)
            band = min(band, self.mSpectralProperties.bandCount())
        return band

    def bandToXValue(self, band: int) -> Optional[float]:

        if not isinstance(self.mSpectralProperties, QgsRasterLayerSpectralProperties):
            return None

        if self.mXUnit == BAND_NUMBER:
            return band
        elif self.mXUnit == BAND_INDEX:
            return band - 1
        else:
            wl = self.mSpectralProperties.wavelengths()
            wlu = self.mSpectralProperties.wavelengthUnits()
            if len(wlu) >= band:
                wlu = wlu[band - 1]
            else:
                wlu = wlu[0]
            if wlu:
                func = self.mUnitConverter.convertFunction(wlu, self.mXUnit)
                return func(wl[band - 1])

        return None

    def setData(self, value: Any, role: int = ...) -> None:
        super(RasterRendererGroup, self).setData(value, role)

    def plotDataItems(self) -> List[PlotDataItem]:
        """
        Returns the activated plot data items
        Note that bandPlotItems() returns all plot items, even those that are not used and should be hidden.
        """
        plotItems = []

        activeItems = self.propertyItems()
        if self.mItemBandR in activeItems:
            plotItems.append(self.mBarR)
        if self.mItemBandG in activeItems:
            plotItems.append(self.mBarG)
        if self.mItemBandB in activeItems:
            plotItems.append(self.mBarB)
        if self.mItemBandA in activeItems:
            plotItems.append(self.mBarA)

        return plotItems

    def setBandPosition(self, band: int, bandBar: InfiniteLine, bandItem: PropertyItem) -> bool:
        bandBar.setToolTip(bandBar.name())
        bandItem.setData(band, Qt.ItemDataRole.DisplayRole)
        if isinstance(band, int) and band > 0:
            xValue = self.bandToXValue(band)
            if xValue:
                bandBar.setPos(xValue)
                return True
        return False

    def updateLayerName(self):
        lyr = self.layer()
        if isinstance(lyr, QgsRasterLayer):
            self.setText(lyr.name())
        else:
            self.setText('<layer not set>')

    def updateFromRenderer(self):

        for r in reversed(range(self.rowCount())):
            self.takeRow(r)

        is_checked = self.isVisible()
        layer = self.layer()
        if not (
            isinstance(layer, QgsRasterLayer) and layer.isValid()
            and isinstance(layer.renderer(), QgsRasterRenderer)  # noqa: W503
        ):
            for b in self.bandPlotItems():
                b.setVisible(False)
            self.setValuesMissing(True)
            return
        else:
            self.setValuesMissing(False)

        layerName = layer.name()
        renderer = layer.renderer()
        renderer: QgsRasterRenderer
        rendererName = renderer.type()

        bandR = bandG = bandB = bandA = None

        if renderer.alphaBand() > 0:
            bandA = renderer.alphaBand()

        # is_rgb = False
        if isinstance(renderer, QgsMultiBandColorRenderer):
            # rendererName = 'Multi Band Color'
            bandR = renderer.redBand()
            bandG = renderer.greenBand()
            bandB = renderer.blueBand()
            # is_rgb = True
        elif isinstance(renderer, (QgsSingleBandGrayRenderer,
                                   QgsPalettedRasterRenderer,
                                   QgsHillshadeRenderer,
                                   QgsRasterContourRenderer,
                                   QgsSingleBandColorDataRenderer,
                                   QgsSingleBandPseudoColorRenderer)
                        ):

            self.mBarR.setPen(color='grey')
            if hasattr(renderer, 'inputBand'):
                bandR = renderer.inputBand()
            elif hasattr(renderer, 'band'):
                bandR = renderer.band()
            elif hasattr(renderer, 'grayBand'):
                bandR = renderer.grayBand()

        # emptyPen = QPen()

        self.mItemRenderer.setText(rendererName)

        if len(renderer.usesBands()) >= 3:
            self.mBarR.setName(f'{layerName} red band {bandR}')
            self.mItemBandR.label().setText('Red')
            self.mBarG.setName(f'{layerName} green band {bandG}')
            self.mBarB.setName(f'{layerName} blue band {bandB}')
            self.mBarR.setPen(color='red')
            self.mBarG.setPen(color='green')
            self.mBarB.setPen(color='blue')

        else:
            self.mBarR.setName(f'{layerName} band {bandR}')
            self.mBarR.setPen(color='grey')
            self.mItemBandR.label().setText('Band')

        self.mBarA.setName(f'{layerName} alpha band {bandA}')

        # note the order!
        # in any case we want to evaluate setBandPosition first, although items may be hidden
        self.mBarR.setVisible(self.setBandPosition(bandR, self.mBarR, self.mItemBandR) and is_checked)
        self.mBarG.setVisible(self.setBandPosition(bandG, self.mBarG, self.mItemBandG) and is_checked)
        self.mBarB.setVisible(self.setBandPosition(bandB, self.mBarB, self.mItemBandB) and is_checked)
        self.mBarA.setVisible(self.setBandPosition(bandA, self.mBarA, self.mItemBandA) and is_checked)

        self.appendRow(self.mItemRenderer.propertyRow())
        if bandR:
            self.appendRow(self.mItemBandR.propertyRow())
        if bandG:
            self.appendRow(self.mItemBandG.propertyRow())
        if bandB:
            self.appendRow(self.mItemBandB.propertyRow())
        if bandA:
            self.appendRow(self.mItemBandA.propertyRow())

    def bandPlotItems(self) -> List[InfiniteLine]:
        return [self.mBarR, self.mBarG, self.mBarB, self.mBarA]


class GeneralSettingsProfileStats(PropertyItemGroup):

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

        self.setCheckable(True)
        self.setCheckState(Qt.CheckState.Checked)
        self.setText('Statistics')

        self.mNormalized = QgsPropertyItemBool('Normalized',
                                               tooltip='Show deviations like StdDev and RMSE normalized '
                                                       'by mean in a second plot',
                                               value=True)

        self._items = [self.mNormalized]
        for item in self._items:
            self.appendRow(item.propertyRow())

    def asMap(self) -> dict:

        d = {'show': self.checkState() == Qt.CheckState.Checked}
        for item in self._items:
            d[item.key()] = item.value()

        return d


class ProfileStatsGroup(PropertyItemGroup):

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

        self.setCheckable(False)
        self.setEnabled(True)
        self.setEditable(False)

        self.setData('Statistics', role=Qt.ItemDataRole.DisplayRole)

        self.mMean = PlotStyleItem('Mean', tooltip='Mean')
        self.mStd = PlotStyleItem('StDev', tooltip='Standard deviation')
        self.mRMSE = PlotStyleItem('RMSE', tooltip='Root mean square error')
        self.mMAE = PlotStyleItem('MAE', tooltip='Mean absolute error')
        self.mQ1 = PlotStyleItem('Q1', tooltip='1st quartile')
        self.mQ2 = PlotStyleItem('Median', tooltip='Median')
        self.mQ3 = PlotStyleItem('Q3', tooltip='3rd quartile')
        self.mCount = PlotStyleItem('Count', tooltip='Count of observations per channel / wavelength')
        self.mRange = PlotStyleItem('Range', tooltip='Range = Max - Min')

        self.mMetricItems = [self.mMean, self.mStd, self.mRMSE, self.mMAE,
                             self.mQ1, self.mQ2, self.mQ3, self.mCount, self.mRange]

        f = PlotStyleWidget.VisibilityFlags

        default_style = PlotStyle()
        default_style.setMarkerSymbol(None)
        default_style.setLineColor('green')
        default_style.setLineStyle(Qt.PenStyle.SolidLine)
        default_style.setLineWidth(3)

        visFlags = f.Line | f.Color | f.Size | f.Type
        for pItem in self.mMetricItems:
            pItem: PlotStyleItem
            pItem.setPlotStyle(default_style)
            pItem.setVisibilitFlags(visFlags)
            pItem.setEditable(True)
            pItem.setItemCheckable(True)
            pItem.setItemChecked(False)
            pItem.setEditColors(True)
            self.appendRow(pItem.propertyRow())

    def map(self) -> dict:
        d = {}

        for item in self.mMetricItems:
            item: PlotStyleItem
            if item.itemIsChecked():
                d[item.key().lower()] = item.plotStyle().map()

        return d


def lists_to_numpy_array(raw_data: dict, keys=('x', 'y', 'bbl')):
    """
    Converts all list values into numpy arrays.
    :param raw_data:
    :param keys:
    :return:
    """
    for k in keys:
        if k in raw_data and isinstance(raw_data[k], list):
            raw_data[k] = np.asarray(raw_data[k])


class ProfileVisualizationGroup(PropertyItemGroup):
    """
    Controls the visualization for a set of profiles
    """
    MIME_TYPE = 'application/SpectralProfilePlotVisualization'

    class ExpressionContextGenerator(QgsExpressionContextGenerator):

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

        def createExpressionContext(self):
            context = QgsExpressionContext()
            context.appendScope(QgsExpressionContextUtils.globalScope())
            lyr = self.grp.layer()
            context.appendScope(QgsExpressionContextUtils.projectScope(self.grp.project()))

            if isinstance(lyr, QgsVectorLayer) and lyr.isValid():
                context.appendScope(QgsExpressionContextUtils.layerScope(lyr))

            # myscope = QgsExpressionContextScope('myscope')
            # myscope.setVariable('MYVAR', 42)
            # context.appendScope(myscope)
            return context

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

        self.mExpressionContextGenerator = self.ExpressionContextGenerator(self)

        # foreground and background colors that are used for preview icons
        self.mPlotWidgetStyle: PlotWidgetStyle = PlotWidgetStyle.default()

        self.mZValue = 2
        self.mAutoName: bool = True
        self.setIcon(QIcon(':/qps/ui/icons/profile.svg'))
        self.mFirstColumnSpanned = False

        self.mProject: QgsProject = QgsProject.instance()

        self.mPlotDataItems: List[PlotDataItem] = []

        self.mPField = SpectralProfileLayerFieldItem('Field')

        self.mProfileCandidates = PlotStyleItem('Candidates')

        tt = 'Highlight profile candidates using a different style<br>' \
             'If activated and unless other defined, use the style defined here.'

        self.mProfileCandidates.setToolTip(tt)

        default_candidate_style = PlotStyle()
        default_candidate_style.setMarkerColor('green')
        default_candidate_style.setLineColor('green')
        default_candidate_style.setLineWidth(2)
        default_candidate_style.setLineStyle(Qt.PenStyle.SolidLine)

        self.mProfileCandidates.setPlotStyle(default_candidate_style)
        self.mProfileCandidates.setItemCheckable(True)
        self.mProfileCandidates.setItemChecked(True)
        self.mProfileCandidates.setEditColors(True)

        self.mPStyle = PlotStyleItem('Style')
        self.mPStyle.setEditColors(False)

        self.mPLabel = QgsPropertyItem('Label')
        self.mPLabel.setDefinition(QgsPropertyDefinition(
            'Label', 'A label to describe the plotted profiles',
            QgsPropertyDefinition.StandardPropertyTemplate.String))
        self.mPLabel.setProperty(QgsProperty.fromExpression('$id'))

        self.mPFilter = QgsPropertyItem('Filter')
        self.mPFilter.setDefinition(QgsPropertyDefinition(
            'Filter', 'Filter for feature rows', QgsPropertyDefinition.StandardPropertyTemplate.String))
        self.mPFilter.setProperty(QgsProperty.fromExpression(''))

        self.mPCode = PythonCodeItem('Data')
        self.mPCode.setToolTip('Modify profile data with python expressions')
        self.mPCode.setDialogHelpText('<h1>Modify profile data</h1><br>'
                                      'Set or overwrite the following raw profile data using python code:'
                                      '<table>'
                                      '<tr><th>Variable</th><th>Description</th></tr>'
                                      '<tr><td>y</td><td>numpy array with profile values</td></tr>'
                                      '<tr><td>x</td><td>numpy array with profile x values, e.g. wavelengths</td></tr>'
                                      '</table>'
                                      'Example 1: <code>y *= 100</code> scale profile values by 100<br>'
                                      "Example 2: <code>xUnit = 'nm'</code> specify the wavelength unit")
        self.mPCode.signals.validationRequest.connect(self._validate_data_expression)

        self.mPColor: ProfileColorPropertyItem = ProfileColorPropertyItem('Color')
        self.mPColor.setDefinition(QgsPropertyDefinition(
            'Color', 'Color of spectral profile', QgsPropertyDefinition.StandardPropertyTemplate.ColorWithAlpha))
        self.mPColor.setProperty(QgsProperty.fromValue('@symbol_color'))

        self.mStats = ProfileStatsGroup()
        # self.mPColor.signals().dataChanged.connect(lambda : self.setPlotStyle(self.generatePlotStyle()))
        for pItem in [self.mPField, self.mPLabel, self.mPFilter, self.mPCode,
                      self.mPColor, self.mPStyle, self.mProfileCandidates,
                      self.mStats]:
            self.appendRow(pItem.propertyRow())

        self.setUserTristate(False)
        self.setCheckable(True)
        self.setCheckState(Qt.CheckState.Checked)
        self.setDropEnabled(False)
        self.setDragEnabled(False)

    def _validate_data_expression(self, data):

        # 1. compile expression

        code = data[PythonCodeDialog.VALKEY_CODE]

        code = ['import numpy as np', 'y = np.asarray(y)', code, ]
        error = None
        compiled_code = None
        try:
            compiled_code = compile('\n'.join(code), f'<band expression: "{code}">', 'exec')
        except Exception as ex:
            error = str(ex)

        feature = data.get(PythonCodeDialog.VALKEY_FEATURE)
        field = self.mPField.field()
        if not error and isinstance(feature, QgsFeature) and isinstance(field, str):
            # 2. execute code
            try:
                kwds = decodeProfileValueDict(feature.attribute(field))
                kwds['f'] = feature
                lists_to_numpy_array(kwds)
                exec(compiled_code, kwds, kwds)  # nosec: B102 # User-defined scipy/numpy code execution
                if not ('y' in kwds):
                    raise AssertionError('Missing y in kwds')

            except Exception as ex:
                error = str(ex)
        data[PythonCodeDialog.VALKEY_ERROR] = error

        if error:
            data[PythonCodeDialog.VALKEY_PREVIEW_TEXT] = '<b><span style="color:red">error</span></b>'
            data[PythonCodeDialog.VALKEY_PREVIEW_TOOLTIP] = f'<span style="color:red">{error}</span>'
        else:
            results = []
            for k in ['y', 'x', 'xUnit']:
                if k in kwds:
                    results.append(f'{k}={kwds[k]}')
            data[PythonCodeDialog.VALKEY_PREVIEW_TEXT] = f"{'<br>'.join(results)}"
            data[PythonCodeDialog.VALKEY_PREVIEW_TOOLTIP] = f"Results:\n{'<br>'.join(results)}"
            pass

    def fromMap(self, data: dict):

        self.setLayerField(data.get('field', None))
        if name := data.get('name', None):
            self.setText(name)

    def asMap(self) -> dict:

        layer_id = self.layerId()
        layer_src = layer_name = layer_provider = None
        if layer_id:
            lyr = self.project().mapLayer(layer_id)
            if isinstance(lyr, QgsVectorLayer):
                layer_src = lyr.source()
                layer_name = lyr.name()
                layer_provider = lyr.providerType()

        color_expression = self.colorExpression()
        plot_style = self.plotStyle()

        candidate_style = self.profileCandidateStyle().map()
        candidate_show = self.mProfileCandidates.itemIsChecked()

        settings = {
            'vis_id': id(self),
            'name': self.text(),
            'field_name': self.fieldName(),
            'layer_id': layer_id,
            'layer_source': layer_src,
            'layer_name': layer_name,
            'layer_provider': layer_provider,
            'label_expression': self.labelExpression(),
            'filter_expression': self.filterExpression(),
            'color_expression': color_expression,
            'tooltip_expression': self.labelExpression(),
            'data_expression': self.dataExpression(),
            'plot_style': plot_style.map(),
            'statistics': self.mStats.map(),
            'candidate_style': candidate_style,
            'show_candidates': candidate_show,
        }
        return settings

    def profileCandidateStyle(self) -> PlotStyle:
        """
        Returns the plot style to be used as default for profile candidates
        :return: PlotStyle
        """
        style = self.mProfileCandidates.plotStyle()
        # style.setAntialias(self.antialias())
        return style

    def setColorExpression(self, expression: str):

        self.mPColor.setColorExpression(expression)

    def colorExpression(self) -> str:
        """
        Returns the color as QGIS expression string
        :return: str
        """
        return self.mPColor.colorExpression()

    def propertyRow(self) -> List[QStandardItem]:
        return [self]

    def expressionContextGenerator(self) -> QgsExpressionContextGenerator:
        return self.mExpressionContextGenerator

    def createExpressionContextScope(self) -> QgsExpressionContextScope:

        scope = QgsExpressionContextScope('profile_visualization')
        # todo: add scope variables
        scope.setVariable('vis_name', self.text(), isStatic=True)
        return scope

    def clone(self) -> 'ProfileVisualizationGroup':
        v = ProfileVisualizationGroup()
        v.fromMap(self.asMap())
        v.setEditable(self.isEditable())
        v.setVisible(self.isVisible())
        v.setCheckable(self.isCheckable())

        return v

    def setPlotWidgetStyle(self, style: PlotWidgetStyle):
        """

        :param style:
        :return:
        """
        if not (isinstance(style, PlotWidgetStyle)):
            raise AssertionError
        if style != self.mPlotWidgetStyle:
            self.mPlotWidgetStyle = style
            self.setColor(style.foregroundColor)
            self.emitDataChanged()

    def data(self, role: int = ...) -> Any:
        if role == Qt.ItemDataRole.DisplayRole:
            if self.mAutoName:
                return self.autoName()
        if role == Qt.ItemDataRole.ToolTipRole:
            return self.mPField.toolTip()
        return super().data(role)

    def setColor(self, color: Union[str, QColor]):
        self.mPColor.setColor(color)

    def autoName(self) -> str:
        """
        Create a name for the profile visualization
        from the layer and field name
        :return: str
        """
        lyr = self.layer()
        fn = self.fieldName()

        if isinstance(lyr, QgsVectorLayer):
            return f'{lyr.name()}:{fn}'
        return 'Missing layer'

    def update(self):
        is_complete = self.isComplete()
        self.setValuesMissing(not is_complete)
        self.mPField.label().setIcon(QIcon() if is_complete else QIcon(WARNING_ICON))

    def isComplete(self) -> bool:

        has_layer = isinstance(self.layerId(), str)
        has_field = isinstance(self.fieldName(), str)

        return has_layer and has_field

    def setFilterExpression(self, expression):
        if isinstance(expression, QgsExpression):
            expression = expression.expression()
        if not (isinstance(expression, str)):
            raise AssertionError
        p = self.mPFilter.property()
        p.setExpressionString(expression)
        self.mPFilter.setProperty(p)

    def filterExpression(self) -> str:
        return self.filterProperty().expressionString()

    def filterProperty(self) -> QgsProperty:
        """
        Returns the filter expression that describes included profiles
        :return: str
        """
        return self.mPFilter.property()

    def setLabelExpression(self, expression):
        if isinstance(expression, QgsExpression):
            expression = expression.expression()
        if not (isinstance(expression, str)):
            raise AssertionError
        p = self.mPLabel.property()
        p.setExpressionString(expression)
        self.mPLabel.setProperty(p)

    def setDataExpression(self, expression: str):
        self.mPCode.setCode(expression)

    def dataExpression(self) -> str:
        return self.mPCode.code()

    def labelExpression(self) -> str:
        return self.mPLabel.property().expressionString()

    def labelProperty(self) -> QgsProperty:
        """
        Returns the expression that returns the name for a single profile
        :return: str
        """
        return self.mPLabel.property()

    def setLayerField(self, layer: Union[QgsVectorLayer, str], field: Union[QgsField, str, None]):
        self.mPField.setLayerField(layer, field)

    def fieldName(self) -> Optional[str]:
        return self.mPField.mFieldName

    def layerId(self) -> Optional[str]:
        return self.mPField.mLayerID

    def layer(self) -> Optional[QgsMapLayer]:
        """Returns the layer instance relating to the layerId.
        Requires that the layer is stored in the provided QgsProject instance.
        """
        lid = self.layerId()
        if lid:
            return self.project().mapLayer(self.layerId())
        else:
            return None

    def setPlotStyle(self, style: PlotStyle):
        """Set the profile style"""
        self.mPStyle.setPlotStyle(style)
        # trigger update of the group icon
        self.emitDataChanged()

    def setCandidatePlotStyle(self, style: PlotStyle):
        """Set the profile style for profile candidates"""
        self.mProfileCandidates.setPlotStyle(style)

    def populateContextMenu(self, menu: QMenu):

        for item in [self.mPField, self.mPColor]:
            item.populateContextMenu(menu)

    def plotStyle(self, add_symbol_scope: bool = False) -> PlotStyle:
        """
        Creates a PlotStyle that uses the color
        as line and marker color. In case of a color expression, the plot foreground color will be used.
        Antialias flag is taken from general settings.
        :return: PlotStyle
        """
        style = self.mPStyle.plotStyle().clone()

        expr = QgsExpression(self.colorExpression())
        context = self.expressionContextGenerator().createExpressionContext()

        lyr = self.layer()

        if add_symbol_scope and isinstance(lyr, QgsVectorLayer) and isinstance(lyr.renderer(), QgsFeatureRenderer):

            request = QgsFeatureRequest()
            filter = self.filterExpression()
            if filter != '':
                request.setFilterExpression(filter)

            # get color from 1st feature
            for feature in lyr.getFeatures(request):
                context.setFeature(feature)
                context.appendScope(featureSymbolScope(feature, renderer=lyr.renderer(), context=context))
                break

        from .spectralprofileplotmodel import SpectralProfilePlotModel
        model = self.model()
        if isinstance(model, SpectralProfilePlotModel):
            gsettings: GeneralSettingsGroup = model.generalSettings()
            bc = gsettings.backgroundColor()
            fc = gsettings.foregroundColor()
            al = gsettings.antialias()
        else:
            bc = QColor(self.mPlotWidgetStyle.backgroundColor)
            fc = QColor(self.mPlotWidgetStyle.foregroundColor)
            al = False

        color = QColor(expr.evaluate(context))
        if not color.isValid():
            color = fc
        style.setBackgroundColor(bc)
        style.setLineColor(color)
        style.setMarkerColor(color)
        style.setAntialias(al)

        return style

    def generateTooltip(self, context: QgsExpressionContext, label: str = None) -> str:
        tooltip = '<html><body><table>'
        if label is None:
            label = self.generateLabel(context)
        fid = context.feature().id()
        fname = context.variable('field_name')
        if label:
            tooltip += f'\n<tr><td>Label</td><td>{label}</td></tr>'
        if fid:
            tooltip += f'\n<tr><td>FID</td><td>{fid}</td></tr>'
        if fname not in [None, '']:
            tooltip += f'<tr><td>Field</td><td>{fname}</td></tr>'
        tooltip += '\n</table></body></html>'
        return tooltip

    def generateLabel(self, context: QgsExpressionContext):
        defaultLabel = ''
        if context.feature().isValid():
            defaultLabel = f'{context.feature().id()}, {self.fieldName()}'
        label, success = self.labelProperty().valueAsString(context, defaultString=defaultLabel)
        if success:
            return label
        else:
            return defaultLabel

    def plotDataItems(self) -> List[PlotDataItem]:
        """
        Returns a list with all pyqtgraph plot data items
        """
        return self.mPlotDataItems[:]
