from contextlib import suppress
from math import nan, inf
from typing import Optional

from osgeo import gdal
from qgis.PyQt import uic
from qgis.PyQt.QtWidgets import QDoubleSpinBox, QComboBox, QCheckBox, QToolButton, QLabel, QTabWidget, \
    QLineEdit, QTableWidget, QSpinBox
from qgis.core import QgsRasterLayer, QgsSingleBandGrayRenderer, QgsRectangle, QgsMapLayer, \
    QgsContrastEnhancement, QgsRasterRenderer, QgsMultiBandColorRenderer, QgsSingleBandPseudoColorRenderer, \
    QgsMapLayerProxyModel, QgsRasterDataProvider, QgsRasterShader, QgsProject, QgsRasterTransparency
from qgis.gui import (
    QgsDockWidget, QgsMapLayerComboBox, QgsCollapsibleGroupBox, QgsColorRampButton, QgsRangeSlider
)

from enmapbox.gui.dataviews.dockmanager import DockPanelUI
from enmapbox.gui.enmapboxgui import EnMAPBox
from enmapbox.gui.mapcanvas import MapCanvas
from enmapbox.qgispluginsupport.qps.utils import SpatialExtent
from enmapbox.typeguard import typechecked
from enmapbox.utils import BlockSignals
from enmapboxprocessing.algorithm.createspectralindicesalgorithm import CreateSpectralIndicesAlgorithm
from enmapboxprocessing.rasterreader import RasterReader, metadataCache, setMetadataCache, buildMetadataCache
from enmapboxprocessing.rasterwriter import RasterWriter
from enmapboxprocessing.utils import Utils
from rasterlayerstylingapp.rasterlayerstylingbandwidget import RasterLayerStylingBandWidget
from rasterlayerstylingapp.rasterlayerstylingpercentileswidget import RasterLayerStylingPercentilesWidget


@typechecked
class RasterLayerStylingPanel(QgsDockWidget):
    # main layer
    mInfo: QLabel
    mLayer: QgsMapLayerComboBox
    mRefresh: QToolButton

    # renderer
    mRenderer: QTabWidget
    mVisualization: QComboBox
    mPseudoColorRamp: QgsColorRampButton
    mRedBand: RasterLayerStylingBandWidget
    mGreenBand: RasterLayerStylingBandWidget
    mBlueBand: RasterLayerStylingBandWidget
    mGrayBand: RasterLayerStylingBandWidget
    mPseudoBand: RasterLayerStylingBandWidget

    # linked layers
    mAddLink: QToolButton
    mRemoveLink: QToolButton
    mLinkedLayers: QTableWidget

    # min / max value settings
    mMinMaxUser: QCheckBox
    mMinMaxPercentile: QCheckBox
    mP1: QDoubleSpinBox
    mP2: QDoubleSpinBox
    mExtent: QComboBox
    mAccuracy: QComboBox
    mApply: QToolButton

    # custom transparency options
    mUseTransparency: QCheckBox
    mTransparencyValue: QSpinBox
    mTransparencySlider: QgsRangeSlider
    mTransparencyMin: QLineEdit
    mTransparencyMax: QLineEdit
    mTransparencyLower: QLineEdit
    mTransparencyUpper: QLineEdit

    mGroupBoxMinMax: QgsCollapsibleGroupBox
    mGroupBoxTransparency: QgsCollapsibleGroupBox

    RgbRendererTab, GrayRendererTab, PseudoRendererTab, DefaultRendererTab, SpectralLinkingTab = range(5)
    BandLinkage, WavelengthLinkage = range(2)
    UserDefinedStretch, CumulativeCountCutStrech, ReferenceLayerStrech = range(3)
    WholeRasterStatistics, CurrentCanvasStatistics = range(2)
    EstimatedAccuracy, ActualAccuracy = range(2)

    def __init__(self, enmapBox: EnMAPBox, parent=None):
        QgsDockWidget.__init__(self, parent)
        uic.loadUi(__file__.replace('.py', '.ui'), self)
        self.enmapBox = enmapBox
        self.originalRenderer: Optional[QgsRasterRenderer] = None
        self.originalLayer: Optional[str] = None
        self.mLayer.setProject(self.enmapBox.project())
        self.mLayer.setFilters(QgsMapLayerProxyModel.RasterLayer)
        self.mLayer.setExcludedProviders(['wms'])
        self.cache = dict()

        self.visibilityChanged.connect(self.onPanelVisibilityChanged)

        self.enmapBox.currentLayerChanged.connect(self.mLayer.setLayer)
        self.mLayer.layerChanged.connect(self.onLayerChanged)
        self.mRefresh.clicked.connect(self.onLayerChanged)

        self.mRenderer.currentChanged.connect(self.onRendererTabChanged)

        # renderer bands
        self.mVisualization.currentIndexChanged.connect(self.onVisualizationChanged)
        self.mPseudoColorRamp.setColorRampFromName('Spectral')
        self.mPseudoColorRamp.colorRampChanged.connect(self.updateRenderer)
        for mBand, name in [
            (self.mRedBand, 'Red\nband'), (self.mGreenBand, 'Green\nband'), (self.mBlueBand, 'Blue\nband'),
            (self.mGrayBand, 'Gray\nband'), (self.mPseudoBand, 'Band')
        ]:
            mBand.mName.setText(name)
            mBand.mMin.textChanged.connect(self.updateRenderer)
            mBand.mMax.textChanged.connect(self.updateRenderer)
            mBand.mSlider.valueChanged.connect(mBand.mBandNo.setBand)
            mBand.mBandNo.bandChanged.connect(mBand.mSlider.setValue)
            mBand.mBandNo.bandChanged.connect(self.onBandChanged)
            mBand.mIsBadBand.toggled.connect(self.onIsBadBandToggled)

            # waveband buttons
            for sname in CreateSpectralIndicesAlgorithm.ShortNames:
                lname = CreateSpectralIndicesAlgorithm.LongNameMapping[sname]
                wavelength, fwhm = CreateSpectralIndicesAlgorithm.WavebandMapping[sname]
                mWaveband: QToolButton = getattr(mBand, 'mWaveband' + sname)
                mWaveband.setToolTip(f'{lname} at {wavelength} Nanometers')
                mWaveband.clicked.connect(self.onWavebandClicked)

        # min / max value settings
        self.mMinMaxPercentile.toggled.connect(self.updateMinMax)
        self.mP1.valueChanged.connect(self.updateMinMax)
        self.mP2.valueChanged.connect(self.updateMinMax)
        self.mExtent.currentIndexChanged.connect(self.onStatisticsChanged)
        self.mAccuracy.currentIndexChanged.connect(self.onStatisticsChanged)
        self.mApply.clicked.connect(self.updateMinMax)

        # custom transparency options
        self.mTransparencySlider.setRangeLimits(0, 100)
        self.mTransparencySlider.setRange(0, 100)
        self.mTransparencySlider.setSingleStep(1)
        self.mTransparencySlider.rangeChanged.connect(self.onTransparencyRangeChanged)
        self.mUseTransparency.toggled.connect(self.updateRenderer)

        # spectral linking
        self.mAddLink.clicked.connect(self.onAddLinkClicked)
        self.mRemoveLink.clicked.connect(self.onRemoveLinkClicked)

        self.openedStateChanged.connect(self.onOpenStateChanged)

        # init GUI
        self.mRenderer.setCurrentIndex(self.DefaultRendererTab)

        self.initMinMax = 0

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

    def onOpenStateChanged(self, wasOpened: bool):
        panel: DockPanelUI = self.enmapBox.ui.dockPanel
        panel.mRasterLayerStyling.setChecked(wasOpened)

    def onRemoveLinkClicked(self):
        rows = [index.row() for index in self.mLinkedLayers.selectionModel().selectedRows()]
        if len(rows) == 0:
            rows = [self.mLinkedLayers.rowCount() - 1]
        self.mLinkedLayers.removeRow(rows[-1])

    def onAddLinkClicked(self):
        self.mLinkedLayers.setRowCount(self.mLinkedLayers.rowCount() + 1)
        row = self.mLinkedLayers.rowCount() - 1

        w = QgsMapLayerComboBox()  # raster layer
        w.setFilters(QgsMapLayerProxyModel.RasterLayer)
        w.setProject(self.project())

        cl = self.mLayer.currentLayer()
        if isinstance(cl, QgsMapLayer):
            w.setExceptedLayerList([cl])

        w.setAllowEmptyLayer(True)
        w.setLayer(None)
        w.row = row
        self.mLinkedLayers.setCellWidget(row, 0, w)

        w = QComboBox()  # linkage
        w.addItems(['Band', 'Wavelength'])
        w.setCurrentIndex(1)
        self.mLinkedLayers.setCellWidget(row, 1, w)

        w = QComboBox()  # stretch type
        w.addItems(['User defined', 'Cumulative count cut', 'Reference layer'])
        w.setCurrentIndex(1)
        self.mLinkedLayers.setCellWidget(row, 2, w)

        w = RasterLayerStylingPercentilesWidget()  # percentiles
        self.mLinkedLayers.setCellWidget(row, 3, w)

        w = QLineEdit()  # red min
        w.setFrame(False)
        self.mLinkedLayers.setCellWidget(row, 4, w)
        w = QLineEdit()  # red max
        w.setFrame(False)
        self.mLinkedLayers.setCellWidget(row, 5, w)
        w = QLineEdit()  # green min
        w.setFrame(False)
        self.mLinkedLayers.setCellWidget(row, 6, w)
        w = QLineEdit()  # green max
        w.setFrame(False)
        self.mLinkedLayers.setCellWidget(row, 7, w)
        w = QLineEdit()  # blue min
        w.setFrame(False)
        self.mLinkedLayers.setCellWidget(row, 8, w)
        w = QLineEdit()  # blue max
        w.setFrame(False)
        self.mLinkedLayers.setCellWidget(row, 9, w)

        w = QComboBox()  # statistics
        w.addItems(['Whole raster', 'Current canvas'])
        w.setCurrentIndex(0)
        self.mLinkedLayers.setCellWidget(row, 10, w)

        w = QComboBox()  # accuracy
        w.addItems(['Estimated (faster)', 'Actual (slower)'])
        w.setCurrentIndex(0)
        self.mLinkedLayers.setCellWidget(row, 11, w)

    def updateGui(self):
        layer: QgsRasterLayer = self.mLayer.currentLayer()

        # set waveband enabled state
        availableShortNames = dict()
        for sname in CreateSpectralIndicesAlgorithm.ShortNames:

            # let's cache the broad band matching, because it takes a second
            cacheKey = layer.source(), sname
            if cacheKey not in self.cache:
                bandNo = CreateSpectralIndicesAlgorithm.findBroadBand(layer, sname, strict=True)
                isWaveband = bandNo is not None
                self.cache[cacheKey] = isWaveband, bandNo
            isWaveband, bandNo = self.cache[cacheKey]

            for mBand in [self.mRedBand, self.mGreenBand, self.mBlueBand, self.mGrayBand, self.mPseudoBand]:
                mWaveband: QToolButton = getattr(mBand, 'mWaveband' + sname)
                mWaveband.setVisible(isWaveband)

            if isWaveband:
                availableShortNames[sname] = bandNo

        # set rgb visualizations
        visualizations = CreateSpectralIndicesAlgorithm.filterVisualizations(
            CreateSpectralIndicesAlgorithm.sentinel2Visualizations(), list(availableShortNames.keys())
        )
        self.mVisualization.clear()
        items = [''] + [f'{name} ({"-".join(snames)})' for name, snames in visualizations.items()]
        self.mVisualization.addItems(items)

        # update wavelength info
        for mBand in [self.mRedBand, self.mGreenBand, self.mBlueBand, self.mGrayBand, self.mPseudoBand]:
            self.updateWavelengthInfo(mBand)

        # update is bad band
        for mBand in [self.mRedBand, self.mGreenBand, self.mBlueBand, self.mGrayBand, self.mPseudoBand]:
            self.updateIsBadBand(mBand)

    def onVisualizationChanged(self):
        if self.mVisualization.currentText() == '':
            return
        name, snames = self.mVisualization.currentText().split(' (')
        for sname, mBand in zip(
                snames.strip(' )').split('-'), [self.mRedBand, self.mGreenBand, self.mBlueBand]
        ):
            mWaveband: QToolButton = getattr(mBand, 'mWaveband' + sname)
            mWaveband.click()

    def onPanelVisibilityChanged(self):
        self.onLayerChanged()  # trigger GUI initialization

    def onWavebandClicked(self):
        mWaveband: QToolButton = self.sender()
        mBand: RasterLayerStylingBandWidget = mWaveband.parent()
        _, sname = mWaveband.objectName().split('Waveband')
        wavelength, fwhm = CreateSpectralIndicesAlgorithm.WavebandMapping[sname]
        reader = RasterReader(self.mLayer.currentLayer())
        bandNo = reader.findWavelength(wavelength)
        mBand.mBandNo.setBand(bandNo)

    def onEnmapBoxCurrentLayerChanged(self):
        if self.isHidden():
            return

        self.mLayer.setLayer(self.enmapBox.currentLayer())

    def onLayerChanged(self):

        if self.isHidden():  # do nothing if panel is hidden
            return

        layer: QgsRasterLayer = self.mLayer.currentLayer()
        if not isinstance(layer, QgsRasterLayer) or layer.dataProvider().name() in ['wms']:
            self.disableGui()
            return

        self.originalLayerId = layer.id()
        self.originalRenderer = layer.renderer().clone()

        with BlockSignals(self.mMinMaxPercentile):
            self.mMinMaxUser.setChecked(True)

        hasNoMetadataCache = metadataCache(layer) is None
        if hasNoMetadataCache:
            cache = buildMetadataCache(layer)
            setMetadataCache(layer, cache)

        with suppress(Exception):
            layer.rendererChanged.disconnect(self.onLayerRendererChanged)

        layer.rendererChanged.connect(self.onLayerRendererChanged)

        self.updateRendererTab(layer)
        self.onRendererTabChanged()

        self.enableGui()
        self.updateGui()

    def updateRendererTab(self, layer):
        with BlockSignals(self.mRenderer):
            renderer = layer.renderer()

            if isinstance(renderer, QgsMultiBandColorRenderer):
                self.mRenderer.setCurrentIndex(0)
            elif isinstance(renderer, QgsSingleBandGrayRenderer):
                self.mRenderer.setCurrentIndex(1)
            elif isinstance(renderer, QgsSingleBandPseudoColorRenderer):
                self.mRenderer.setCurrentIndex(2)
            else:
                self.mRenderer.setCurrentIndex(3)

    def onRendererTabChanged(self):
        layer: Optional[QgsRasterLayer] = self.mLayer.currentLayer()

        if layer is None:
            return

        if self.mRenderer.currentIndex() == self.RgbRendererTab:
            for mBand in [self.mRedBand, self.mGreenBand, self.mBlueBand]:
                with BlockSignals(mBand.mBandNo, mBand.mSlider):
                    mBand.mBandNo.setLayer(layer)
                    mBand.mSlider.setRange(1, layer.bandCount())

            renderer: QgsMultiBandColorRenderer = layer.renderer()
            if not isinstance(layer.renderer(), QgsMultiBandColorRenderer):
                if isinstance(self.originalRenderer, QgsMultiBandColorRenderer):
                    if self.originalLayerId == layer.id():
                        renderer = self.originalRenderer.clone()
                else:
                    renderer = Utils.multiBandColorRenderer(
                        layer.dataProvider(), [1] * 3, [self.initMinMax] * 3, [self.initMinMax] * 3
                    )
                layer.setRenderer(renderer)

            for mBand, ce, bandNo in [
                (self.mRedBand, renderer.redContrastEnhancement(), renderer.redBand()),
                (self.mGreenBand, renderer.greenContrastEnhancement(), renderer.greenBand()),
                (self.mBlueBand, renderer.blueContrastEnhancement(), renderer.blueBand())
            ]:
                with BlockSignals(mBand.mMin, mBand.mMax, mBand.mBandNo):
                    mBand.mMin.setText(str(ce.minimumValue()))
                    mBand.mMax.setText(str(ce.maximumValue()))
                    mBand.mBandNo.setBand(bandNo)
                    mBand.mSlider.setValue(bandNo)

        elif self.mRenderer.currentIndex() == self.GrayRendererTab:
            self.mGrayBand.mBandNo.setLayer(layer)
            self.mGrayBand.mSlider.setRange(1, layer.bandCount())

            renderer: QgsSingleBandGrayRenderer = layer.renderer()
            if not isinstance(layer.renderer(), QgsSingleBandGrayRenderer):
                if isinstance(self.originalRenderer, QgsSingleBandGrayRenderer):
                    if self.originalLayerId == layer.id():
                        renderer = self.originalRenderer.clone()
                else:
                    renderer = Utils.singleBandGrayRenderer(layer.dataProvider(), 1, self.initMinMax, self.initMinMax)
                layer.setRenderer(renderer)
            ce: QgsContrastEnhancement = renderer.contrastEnhancement()

            with BlockSignals(self.mGrayBand.mMin, self.mGrayBand.mMax, self.mGrayBand.mBandNo):
                self.mGrayBand.mMin.setText(str(ce.minimumValue()))
                self.mGrayBand.mMax.setText(str(ce.maximumValue()))
                self.mGrayBand.mBandNo.setBand(renderer.inputBand())
                self.mGrayBand.mSlider.setValue(renderer.inputBand())

        elif self.mRenderer.currentIndex() == self.PseudoRendererTab:
            self.mPseudoBand.mBandNo.setLayer(layer)
            self.mPseudoBand.mSlider.setRange(1, layer.bandCount())

            renderer: QgsSingleBandPseudoColorRenderer = layer.renderer()
            if not isinstance(layer.renderer(), QgsSingleBandPseudoColorRenderer):
                if isinstance(self.originalRenderer, QgsSingleBandPseudoColorRenderer):
                    if self.originalLayerId == layer.id():
                        renderer = self.originalRenderer.clone()
                else:
                    renderer = Utils.singleBandPseudoColorRenderer(
                        layer.dataProvider(), 1, self.initMinMax, self.initMinMax, None
                    )
                layer.setRenderer(renderer)
            shader: QgsRasterShader = renderer.shader()
            shaderFunction = shader.rasterShaderFunction()

            with BlockSignals(self.mPseudoBand.mMin, self.mPseudoBand.mMax, self.mPseudoBand.mBandNo):
                self.mPseudoBand.mMin.setText(str(shaderFunction.minimumValue()))
                self.mPseudoBand.mMax.setText(str(shaderFunction.maximumValue()))
                self.mPseudoBand.mBandNo.setBand(renderer.inputBand())
                self.mPseudoBand.mSlider.setValue(renderer.inputBand())

        elif self.mRenderer.currentIndex() == self.DefaultRendererTab:
            if self.originalRenderer is not None:
                layer.setRenderer(self.originalRenderer.clone())
        elif self.mRenderer.currentIndex() == self.SpectralLinkingTab:
            pass
        else:
            raise ValueError()

        # update bad band
        for mBand in [self.mRedBand, self.mGreenBand, self.mBlueBand, self.mGrayBand, self.mPseudoBand]:
            self.updateIsBadBand(mBand)

        self.updateGroupBoxVisibility()
        self.updateMinMax()
        self.updateRenderer()

    def onStatisticsChanged(self):
        if self.mRenderer.currentIndex() == self.RgbRendererTab:
            for mBand in [self.mRedBand, self.mGreenBand, self.mBlueBand]:
                mBand.mBandNo.bandChanged.emit(mBand.mBandNo.currentBand())
        elif self.mRenderer.currentIndex() == self.GrayRendererTab:
            self.mGrayBand.mBandNo.bandChanged.emit(self.mGrayBand.mBandNo.currentBand())
        elif self.mRenderer.currentIndex() == self.PseudoRendererTab:
            self.mPseudoBand.mBandNo.bandChanged.emit(self.mPseudoBand.mBandNo.currentBand())
        else:
            raise ValueError()

    def onBandChanged(self):

        mBand: RasterLayerStylingBandWidget = self.sender().parent()

        layer: QgsRasterLayer = self.mLayer.currentLayer()
        if layer is None:
            return

        bandNo = mBand.mBandNo.currentBand()

        # Check if renderer type was changed externally.
        # If so, create a new renderer with correct type.
        if self.mRenderer.currentIndex() == self.RgbRendererTab:
            if not isinstance(layer.renderer(), QgsMultiBandColorRenderer):
                renderer = Utils.multiBandColorRenderer(
                    layer.dataProvider(), [bandNo] * 3, [self.initMinMax] * 3, [self.initMinMax] * 3
                )
                layer.setRenderer(renderer)
        elif self.mRenderer.currentIndex() == self.GrayRendererTab:
            if not isinstance(layer.renderer(), QgsSingleBandGrayRenderer):
                renderer = Utils.singleBandGrayRenderer(layer.dataProvider(), bandNo, self.initMinMax, self.initMinMax)
                layer.setRenderer(renderer)
        elif self.mRenderer.currentIndex() == self.PseudoRendererTab:
            if not isinstance(layer.renderer(), QgsSingleBandPseudoColorRenderer):
                renderer = Utils.singleBandPseudoColorRenderer(
                    layer.dataProvider(), bandNo, self.initMinMax, self.initMinMax, None
                )
                layer.setRenderer(renderer)
        elif self.mRenderer.currentIndex() == self.DefaultRendererTab:
            pass
        else:
            raise ValueError()

        self.updateWavelengthInfo(mBand)
        self.updateIsBadBand(mBand)
        self.updateMinMax()
        self.updateRenderer()

    def onIsBadBandToggled(self):
        layer: QgsRasterLayer = self.mLayer.currentLayer()
        if layer is None:
            return

        mIsBadBand: QCheckBox = self.sender()
        mBand: RasterLayerStylingBandWidget = mIsBadBand.parent()
        bandNo = mBand.mBandNo.currentBand()
        if bandNo == -1:
            return

        if mIsBadBand.isChecked():
            badBandMultiplier = 0
        else:
            badBandMultiplier = 1

        source = layer.source()

        # find all layers with same source
        layers = list()
        for layerId in self.project().mapLayers():
            aLayer = self.project().mapLayer(layerId)
            if not isinstance(aLayer, QgsRasterLayer):
                continue
            if aLayer.dataProvider().name() != 'gdal':
                continue
            if source.replace(r'\/', '') == aLayer.source().replace(r'\/', ''):
                layers.append(aLayer)

        # flush sources
        for aLayer in layers:
            Utils.setLayerDataSource(aLayer, 'gdal', source)

        # set metadata
        ds: gdal.Dataset = gdal.Open(source)
        writer = RasterWriter(ds)
        writer.setBadBandMultiplier(badBandMultiplier, bandNo)
        writer.close()
        del ds

        # reconnect sources
        for aLayer in layers:
            Utils.setLayerDataSource(aLayer, 'gdal', source)

    def onLayerRendererChanged(self):
        # the renderer of the layer may be changed from outside, so we need to update the settings

        if self.isHidden():  # do nothing if panel is hidden
            return

        layer: QgsRasterLayer = self.mLayer.currentLayer()
        if layer is not self.sender():
            return

        renderer = layer.renderer()
        if isinstance(renderer, QgsMultiBandColorRenderer):
            with BlockSignals(self.mRedBand, self.mGreenBand, self.mBlueBand):
                self.mRedBand.mBandNo.setBand(renderer.redBand())
                self.mGreenBand.mBandNo.setBand(renderer.greenBand())
                self.mBlueBand.mBandNo.setBand(renderer.blueBand())
                self.mRedBand.mSlider.setValue(renderer.redBand())
                self.mGreenBand.mSlider.setValue(renderer.greenBand())
                self.mBlueBand.mSlider.setValue(renderer.blueBand())
        elif isinstance(renderer, QgsSingleBandGrayRenderer):
            with BlockSignals(self.mGrayBand):
                self.mGrayBand.mBandNo.setBand(renderer.inputBand())
                self.mGrayBand.mSlider.setValue(renderer.inputBand())
        elif isinstance(renderer, QgsSingleBandPseudoColorRenderer):
            with BlockSignals(self.mPseudoBand.mBandNo):
                self.mPseudoBand.mBandNo.setBand(renderer.inputBand())
                self.mPseudoBand.mSlider.setValue(renderer.inputBand())
        else:
            pass

    def onTransparencyRangeChanged(self):
        layer: QgsRasterLayer = self.mLayer.currentLayer()
        if layer is None:
            return
        renderer: QgsRasterRenderer = layer.renderer()
        bandNo = renderer.usesBands()[0]
        provider: QgsRasterDataProvider = layer.dataProvider()
        p1 = self.mTransparencySlider.lowerValue()
        p2 = self.mTransparencySlider.upperValue()
        if p1 == 0 and p2 == 100:
            lower = upper = 0
        else:
            lower, upper = provider.cumulativeCut(
                bandNo, max(p1, 0) / 100., min(p2, 100) / 100., layer.extent(), int(QgsRasterLayer.SAMPLE_SIZE)
            )
            if lower is nan or upper is nan:
                lower = upper = 0
        if p1 == 0:
            self.mTransparencyLower.setText('')
        else:
            self.mTransparencyLower.setText(str(lower))
        if p2 == 100:
            self.mTransparencyUpper.setText('')
        else:
            self.mTransparencyUpper.setText(str(upper))
        self.updateRenderer()

    def updateWavelengthInfo(self, mBand: RasterLayerStylingBandWidget):
        layer: QgsRasterLayer = self.mLayer.currentLayer()
        if layer is None:
            return
        bandNo = mBand.mBandNo.currentBand()
        if bandNo == -1:
            wavelength = None
        elif bandNo > layer.bandCount():
            wavelength = None
        else:
            wavelength = RasterReader(layer).wavelength(bandNo)
        if wavelength is None:
            mBand.mWavelength.hide()
        else:
            mBand.mWavelength.show()
            mBand.mWavelength.setValue(int(wavelength))

    def updateIsBadBand(self, mBand: RasterLayerStylingBandWidget):
        layer: QgsRasterLayer = self.mLayer.currentLayer()
        if layer is None:
            return
        bandNo = mBand.mBandNo.currentBand()
        if bandNo == -1:
            mBand.mIsBadBand.hide()
        else:
            mBand.mIsBadBand.show()
            reader = RasterReader(layer)
            if reader.bandCount() < bandNo:
                return
            if RasterReader(layer).badBandMultiplier(bandNo) == 0:
                mBand.mIsBadBand.setChecked(True)
            else:
                mBand.mIsBadBand.setChecked(False)

    def updateMinMax(self):
        layer: QgsRasterLayer = self.mLayer.currentLayer()
        if layer is None:
            return
        extent = self.currentExtent(layer, self.mExtent.currentIndex())
        if extent is None:
            return

        def setCumulativeCut(bandNo: int, mBandMin: QLineEdit, mBandMax: QLineEdit):
            vmin, vmax = layer.dataProvider().cumulativeCut(
                bandNo, self.mP1.value() / 100., self.mP2.value() / 100., extent,
                self.currentSampleSize(self.mAccuracy.currentIndex())
            )
            if vmin is nan or vmax is nan:
                vmin = vmax = 0

            with BlockSignals(mBandMin, mBandMax):
                mBandMin.setText(str(vmin))
                mBandMax.setText(str(vmax))

        if self.mRenderer.currentIndex() == self.RgbRendererTab:
            if self.mMinMaxPercentile.isChecked():
                for mBand in [self.mRedBand, self.mGreenBand, self.mBlueBand]:
                    setCumulativeCut(mBand.mBandNo.currentBand(), mBand.mMin, mBand.mMax)
            elif self.mMinMaxUser.isChecked():
                pass
            else:
                raise ValueError()
        elif self.mRenderer.currentIndex() == self.GrayRendererTab:
            if self.mMinMaxPercentile.isChecked():
                setCumulativeCut(self.mGrayBand.mBandNo.currentBand(), self.mGrayBand.mMin, self.mGrayBand.mMax)
            elif self.mMinMaxUser.isChecked():
                pass
            else:
                raise ValueError()
        elif self.mRenderer.currentIndex() == self.PseudoRendererTab:
            if self.mMinMaxPercentile.isChecked():
                setCumulativeCut(self.mPseudoBand.mBandNo.currentBand(), self.mPseudoBand.mMin, self.mPseudoBand.mMax)
            elif self.mMinMaxUser.isChecked():
                pass
            else:
                raise ValueError()
        elif self.mRenderer.currentIndex() >= self.DefaultRendererTab:
            pass
        else:
            raise ValueError()

        self.updateRenderer()

    def updateRenderer(self):
        layer: QgsRasterLayer = self.mLayer.currentLayer()
        if layer is None:
            return

        if self.mRenderer.currentIndex() == self.RgbRendererTab:
            renderer: QgsMultiBandColorRenderer = layer.renderer()
            for mBand, ce, setBand in [
                (self.mRedBand, renderer.redContrastEnhancement(), renderer.setRedBand),
                (self.mGreenBand, renderer.greenContrastEnhancement(), renderer.setGreenBand),
                (self.mBlueBand, renderer.blueContrastEnhancement(), renderer.setBlueBand),
            ]:
                bandNo = mBand.mBandNo.currentBand()
                setBand(bandNo)
                ce.setMinimumValue(tofloat(mBand.mMin.text()))
                ce.setMaximumValue(tofloat(mBand.mMax.text()))
        elif self.mRenderer.currentIndex() == self.GrayRendererTab:
            bandNo = self.mGrayBand.mBandNo.currentBand()
            renderer: QgsSingleBandGrayRenderer = layer.renderer()
            renderer.setInputBand(bandNo)
            ce = renderer.contrastEnhancement()
            ce.setMinimumValue(tofloat(self.mGrayBand.mMin.text()))
            ce.setMaximumValue(tofloat(self.mGrayBand.mMax.text()))

        elif self.mRenderer.currentIndex() == self.PseudoRendererTab:
            bandNo = self.mPseudoBand.mBandNo.currentBand()
            minValue = tofloat(self.mPseudoBand.mMin.text())
            maxValue = tofloat(self.mPseudoBand.mMax.text())
            ramp = self.mPseudoColorRamp.colorRamp()
            renderer = Utils.singleBandPseudoColorRenderer(
                layer.dataProvider(), bandNo, minValue, maxValue, ramp
            )
            layer.setRenderer(renderer)
        elif self.mRenderer.currentIndex() >= self.DefaultRendererTab:
            pass
        else:
            raise ValueError()

        # set transparency
        if self.mRenderer.currentIndex() in [self.GrayRendererTab, self.PseudoRendererTab]:
            rasterTransparency = QgsRasterTransparency()
            transparentSingleValuePixelList = list()
            if self.mUseTransparency.isChecked():
                if self.mTransparencyLower.text() != '':
                    transparentSingleValuePixel = QgsRasterTransparency.TransparentSingleValuePixel()
                    transparentSingleValuePixel.min = -inf
                    transparentSingleValuePixel.max = tofloat(self.mTransparencyLower.text())
                    transparentSingleValuePixel.percentTransparent = self.mTransparencyValue.value()
                    transparentSingleValuePixelList.append(transparentSingleValuePixel)
                if self.mTransparencyUpper.text() != '':
                    transparentSingleValuePixel = QgsRasterTransparency.TransparentSingleValuePixel()
                    transparentSingleValuePixel.min = tofloat(self.mTransparencyUpper.text())
                    transparentSingleValuePixel.max = inf
                    transparentSingleValuePixel.percentTransparent = self.mTransparencyValue.value()
                    transparentSingleValuePixelList.append(transparentSingleValuePixel)
            rasterTransparency.setTransparentSingleValuePixelList(transparentSingleValuePixelList)
            layer.renderer().setRasterTransparency(rasterTransparency)

        layer.rendererChanged.emit()
        layer.triggerRepaint()
        self.updateLinkedLayerRenderer()

    def updateLinkedLayerRenderer(self):
        layer: QgsRasterLayer = self.mLayer.currentLayer()
        for row in range(self.mLinkedLayers.rowCount()):
            # get user settings
            w: QgsMapLayerComboBox = self.mLinkedLayers.cellWidget(row, 0)
            layer2: QgsRasterLayer = w.currentLayer()
            if layer2 is None:
                continue
            if layer2 is layer:
                continue
            provider2: QgsRasterDataProvider = layer2.dataProvider()

            w: QComboBox = self.mLinkedLayers.cellWidget(row, 1)
            linkageType = w.currentIndex()

            w: QComboBox = self.mLinkedLayers.cellWidget(row, 2)
            stretchType = w.currentIndex()

            w: RasterLayerStylingPercentilesWidget = self.mLinkedLayers.cellWidget(row, 3)
            p1 = w.mP1.value()
            p2 = w.mP2.value()

            w: QLineEdit = self.mLinkedLayers.cellWidget(row, 4)
            redMin = tofloat(w.text())
            w: QLineEdit = self.mLinkedLayers.cellWidget(row, 5)
            redMax = tofloat(w.text())
            w: QLineEdit = self.mLinkedLayers.cellWidget(row, 6)
            greenMin = tofloat(w.text())
            w: QLineEdit = self.mLinkedLayers.cellWidget(row, 7)
            greenMax = tofloat(w.text())
            w: QLineEdit = self.mLinkedLayers.cellWidget(row, 8)
            blueMin = tofloat(w.text())
            w: QLineEdit = self.mLinkedLayers.cellWidget(row, 9)
            blueMax = tofloat(w.text())

            w: QComboBox = self.mLinkedLayers.cellWidget(row, 10)
            statisticsType = w.currentIndex()

            w: QComboBox = self.mLinkedLayers.cellWidget(row, 11)
            accuracyType = w.currentIndex()

            # update renderer
            reader = RasterReader(layer2)
            if self.mRenderer.currentIndex() == self.RgbRendererTab:
                redWavelength = self.mRedBand.mWavelength.value()
                greenWavelength = self.mGreenBand.mWavelength.value()
                blueWavelength = self.mBlueBand.mWavelength.value()

                if linkageType == self.BandLinkage:
                    redBandNo = min(self.mRedBand.mBandNo.currentBand(), reader.bandCount())
                    greenBandNo = min(self.mGreenBand.mBandNo.currentBand(), reader.bandCount())
                    blueBandNo = min(self.mBlueBand.mBandNo.currentBand(), reader.bandCount())
                elif linkageType == self.WavelengthLinkage:
                    redBandNo = reader.findWavelength(redWavelength)
                    if redBandNo is None:
                        redBandNo = 1
                    greenBandNo = reader.findWavelength(greenWavelength)
                    if greenBandNo is None:
                        greenBandNo = 1
                    blueBandNo = reader.findWavelength(blueWavelength)
                    if blueBandNo is None:
                        blueBandNo = 1
                else:
                    raise ValueError()
                renderer: QgsMultiBandColorRenderer = layer.renderer()

                if stretchType == self.UserDefinedStretch:
                    pass
                elif stretchType == self.CumulativeCountCutStrech:
                    redMin, redMax = layer2.dataProvider().cumulativeCut(
                        redBandNo, p1 / 100., p2 / 100., self.currentExtent(layer2, statisticsType),
                        self.currentSampleSize(accuracyType)
                    )
                    greenMin, greenMax = layer2.dataProvider().cumulativeCut(
                        greenBandNo, p1 / 100., p2 / 100., self.currentExtent(layer2, statisticsType),
                        self.currentSampleSize(accuracyType)
                    )
                    blueMin, blueMax = layer2.dataProvider().cumulativeCut(
                        blueBandNo, p1 / 100., p2 / 100., self.currentExtent(layer2, statisticsType),
                        self.currentSampleSize(accuracyType)
                    )

                    if redMin is nan or redMax is nan:
                        redMin = redMax = 0
                    if greenMin is nan or greenMax is nan:
                        greenMin = greenMax = 0
                    if blueMin is nan or blueMax is nan:
                        blueMin = blueMax = 0

                elif stretchType == self.ReferenceLayerStrech:
                    ce: QgsContrastEnhancement = renderer.redContrastEnhancement()
                    redMin = ce.minimumValue()
                    redMax = ce.maximumValue()
                    ce: QgsContrastEnhancement = renderer.greenContrastEnhancement()
                    greenMin = ce.minimumValue()
                    greenMax = ce.maximumValue()
                    ce: QgsContrastEnhancement = renderer.blueContrastEnhancement()
                    blueMin = ce.minimumValue()
                    blueMax = ce.maximumValue()

                else:
                    raise ValueError()

                w: QLineEdit = self.mLinkedLayers.cellWidget(row, 4)
                w.setText(str(tofloat(redMin)))
                w: QLineEdit = self.mLinkedLayers.cellWidget(row, 5)
                w.setText(str(tofloat(redMax)))
                w: QLineEdit = self.mLinkedLayers.cellWidget(row, 6)
                w.setText(str(tofloat(greenMin)))
                w: QLineEdit = self.mLinkedLayers.cellWidget(row, 7)
                w.setText(str(tofloat(greenMax)))
                w: QLineEdit = self.mLinkedLayers.cellWidget(row, 8)
                w.setText(str(tofloat(blueMin)))
                w: QLineEdit = self.mLinkedLayers.cellWidget(row, 9)
                w.setText(str(tofloat(blueMax)))

                renderer2 = renderer.clone()
                renderer2.setRedBand(redBandNo)
                renderer2.setGreenBand(greenBandNo)
                renderer2.setBlueBand(blueBandNo)

                ce2 = QgsContrastEnhancement(provider2.dataType(redBandNo))
                ce2.setMinimumValue(redMin)
                ce2.setMaximumValue(redMax)
                ce2.setContrastEnhancementAlgorithm(
                    QgsContrastEnhancement.ContrastEnhancementAlgorithm.StretchToMinimumMaximum
                )
                renderer2.setRedContrastEnhancement(ce2)
                ce2 = QgsContrastEnhancement(provider2.dataType(greenBandNo))
                ce2.setMinimumValue(greenMin)
                ce2.setMaximumValue(greenMax)
                ce2.setContrastEnhancementAlgorithm(
                    QgsContrastEnhancement.ContrastEnhancementAlgorithm.StretchToMinimumMaximum
                )
                renderer2.setGreenContrastEnhancement(ce2)
                ce2 = QgsContrastEnhancement(provider2.dataType(blueBandNo))
                ce2.setMinimumValue(blueMin)
                ce2.setMaximumValue(blueMax)
                ce2.setContrastEnhancementAlgorithm(
                    QgsContrastEnhancement.ContrastEnhancementAlgorithm.StretchToMinimumMaximum
                )
                renderer2.setBlueContrastEnhancement(ce2)
                layer2.setRenderer(renderer2)

            elif self.mRenderer.currentIndex() == self.GrayRendererTab:
                wavelength = self.mGrayBand.mWavelength.value()
                if linkageType == self.BandLinkage:
                    bandNo = min(self.mGrayBand.mBandNo.currentBand(), reader.bandCount())
                elif linkageType == self.WavelengthLinkage:
                    bandNo = reader.findWavelength(wavelength)
                    if bandNo is None:
                        bandNo = 1
                else:
                    raise ValueError()
                renderer: QgsSingleBandGrayRenderer = layer.renderer()

                if stretchType == self.UserDefinedStretch:
                    pass
                elif stretchType == self.CumulativeCountCutStrech:
                    redMin, redMax = layer2.dataProvider().cumulativeCut(
                        bandNo, p1 / 100., p2 / 100., self.currentExtent(layer2, statisticsType),
                        self.currentSampleSize(accuracyType)
                    )
                    if redMin is nan or redMax is nan:
                        redMin = redMax = 0

                elif stretchType == self.ReferenceLayerStrech:
                    ce: QgsContrastEnhancement = renderer.contrastEnhancement()
                    redMin = ce.minimumValue()
                    redMax = ce.maximumValue()
                else:
                    raise ValueError()

                w: QLineEdit = self.mLinkedLayers.cellWidget(row, 4)
                w.setText(str(tofloat(redMin)))
                w: QLineEdit = self.mLinkedLayers.cellWidget(row, 5)
                w.setText(str(tofloat(redMax)))

                renderer2 = renderer.clone()
                renderer2.setInputBand(bandNo)
                ce2 = QgsContrastEnhancement(provider2.dataType(bandNo))
                ce2.setMinimumValue(redMin)
                ce2.setMaximumValue(redMax)
                ce2.setContrastEnhancementAlgorithm(QgsContrastEnhancement.StretchToMinimumMaximum)
                renderer2.setContrastEnhancement(ce2)
                layer2.setRenderer(renderer2)
            elif self.mRenderer.currentIndex() == self.PseudoRendererTab:
                wavelength = self.mPseudoBand.mWavelength.value()
                if linkageType == self.BandLinkage:
                    bandNo = min(self.mPseudoBand.mBandNo.currentBand(), reader.bandCount())
                elif linkageType == self.WavelengthLinkage:
                    bandNo = reader.findWavelength(wavelength)
                    if bandNo is None:
                        bandNo = 1
                else:
                    raise ValueError()
                renderer: QgsSingleBandPseudoColorRenderer = layer.renderer()

                if stretchType == self.UserDefinedStretch:
                    pass
                elif stretchType == self.CumulativeCountCutStrech:
                    redMin, redMax = layer2.dataProvider().cumulativeCut(
                        bandNo, p1 / 100., p2 / 100., self.currentExtent(layer2, statisticsType),
                        self.currentSampleSize(accuracyType)
                    )
                    if redMin is nan or redMax is nan:
                        redMin = redMax = 0
                elif stretchType == self.ReferenceLayerStrech:
                    shader: QgsRasterShader = renderer.shader()
                    redMin = shader.minimumValue()
                    redMax = shader.maximumValue()
                else:
                    raise ValueError()

                w: QLineEdit = self.mLinkedLayers.cellWidget(row, 4)
                w.setText(str(tofloat(redMin)))
                w: QLineEdit = self.mLinkedLayers.cellWidget(row, 5)
                w.setText(str(tofloat(redMax)))
                ramp = self.mPseudoColorRamp.colorRamp()
                renderer2 = Utils.singleBandPseudoColorRenderer(provider2, bandNo, redMin, redMax, ramp)
                layer2.setRenderer(renderer2)
            elif self.mRenderer.currentIndex() == self.DefaultRendererTab:
                pass
            elif self.mRenderer.currentIndex() == self.SpectralLinkingTab:
                pass
            else:
                raise ValueError()

            layer.rendererChanged.emit()
            layer2.triggerRepaint()

    def updateGroupBoxVisibility(self):
        if self.mRenderer.currentIndex() < self.DefaultRendererTab:
            self.mGroupBoxMinMax.show()
        else:
            self.mGroupBoxMinMax.hide()

        if all(
                [self.mRenderer.currentIndex() < self.DefaultRendererTab,
                 self.mRenderer.currentIndex() != self.RgbRendererTab]
        ):

            self.mGroupBoxTransparency.show()
        else:
            self.mGroupBoxTransparency.hide()

    def currentExtent(self, layer: QgsRasterLayer, statisticsType: int) -> Optional[QgsRectangle]:

        if statisticsType == self.WholeRasterStatistics:
            return layer.extent()
        elif statisticsType == self.CurrentCanvasStatistics:
            mapCanvas = self.currentMapCanvas(layer)
            if mapCanvas is None:
                return None
            return SpatialExtent(mapCanvas.crs(), mapCanvas.extent()).toCrs(layer.crs())
        else:
            raise ValueError()

    def currentMapCanvas(self, layer: QgsRasterLayer) -> Optional[MapCanvas]:

        for mapDock in self.enmapBox.dockManager().mapDocks():
            if layer in mapDock.mapCanvas().layers():
                return mapDock.mapCanvas()
        return None

    def currentSampleSize(self, accuracyType: int) -> int:
        if accuracyType == self.EstimatedAccuracy:
            return int(QgsRasterLayer.SAMPLE_SIZE)
        elif accuracyType == self.ActualAccuracy:
            return 0  # use all pixel
        else:
            raise ValueError()

    def disableGui(self):
        self.mInfo.show()
        self.mRenderer.hide()
        self.mGroupBoxMinMax.hide()
        self.mGroupBoxTransparency.hide()

    def enableGui(self):
        self.mInfo.hide()
        self.mRenderer.show()
        self.mGroupBoxMinMax.show()
        self.updateGroupBoxVisibility()


# utils
def tofloat(text: str) -> float:
    try:
        value = float(text)
        if value is nan:
            value = 0
    except Exception:
        value = 0

    return value
