# -*- coding: utf-8 -*-
"""
Demonstrates GLVolumeItem for displaying volumetric data.

"""
import enum
import sys
import time
import typing
from pathlib import Path
from typing import Optional

import numpy as np
import pyqtgraph.opengl as gl
from OpenGL.GL import glEnd, glVertex3f, glColor4f, glLineWidth, GL_LINE_SMOOTH, GL_LINE_SMOOTH_HINT, GL_NICEST, \
    glEnable, glHint, glBegin, GL_LINES
from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem, GLOptions
from pyqtgraph.opengl.GLViewWidget import GLViewWidget

from enmapbox.qgispluginsupport.qps.layerproperties import showLayerPropertiesDialog
from enmapbox.qgispluginsupport.qps.maptools import SpatialExtentMapTool
from enmapbox.qgispluginsupport.qps.simplewidgets import SliderSpinBox, DoubleSliderSpinBox
from enmapbox.qgispluginsupport.qps.utils import loadUi, SpatialExtent
from qgis.PyQt.QtCore import pyqtSignal
from qgis.PyQt.QtGui import QColor, QVector3D, QMatrix4x4
from qgis.PyQt.QtWidgets import QMainWindow, QCheckBox, QLineEdit
from qgis.core import QgsRasterLayer, Qgis, QgsRasterRenderer, QgsRectangle, QgsCoordinateReferenceSystem, \
    QgsTaskManager, QgsApplication, QgsSingleBandGrayRenderer, QgsMultiBandColorRenderer, QgsPalettedRasterRenderer, \
    QgsContrastEnhancement, QgsSingleBandPseudoColorRenderer, QgsRasterMinMaxOrigin, QgsProject, \
    QgsTask, QgsMapLayerProxyModel, QgsRasterBlock, QgsRasterBlockFeedback, QgsSingleBandColorDataRenderer
from qgis.gui import QgsMapCanvas, QgsMapLayerComboBox
from . import NAME, VERSION

KEY_GL_ITEM_GROUP = 'CUBEVIEW/GL_ITEM_GROUP'
KEY_DEFAULT_TRANSFORM = 'CUBEVIEW/DEFAULT_TRANSFORM'
QGIS2NUMPY_DATA_TYPES = {Qgis.Byte: np.byte,
                         Qgis.UInt16: np.uint16,
                         Qgis.Int16: np.int16,
                         Qgis.UInt32: np.uint32,
                         Qgis.Int32: np.int32,
                         Qgis.Float32: np.float32,
                         Qgis.Float64: np.float64,
                         Qgis.CFloat32: complex,
                         Qgis.CFloat64: np.complex64,
                         Qgis.ARGB32: np.uint32,
                         Qgis.ARGB32_Premultiplied: np.uint32}


class TaskMock(QgsTask):
    def __init__(self):
        super(TaskMock, self).__init__()


def qaRed(array: np.ndarray) -> np.ndarray:
    return (array >> 16) & 0xff


def qaGreen(array: np.ndarray) -> np.ndarray:
    return (array >> 8) & 0xff


def qaBlue(array: np.ndarray) -> np.ndarray:
    return array & 0xff


def qaAlpha(array: np.ndarray) -> np.ndarray:
    return array >> 24


class GLItem(enum.Enum):
    Text = 'TEXT'
    SliceX = 'SLICE_X'
    SliceY = 'SLICE_Y'
    SliceZ = 'SLICE_Z'
    Cube = 'CUBE'
    Box = 'IMAGE_BOX_FULL'
    BoxSubset = 'IMAGE_BOX_SUBSET'
    Axes = 'AXES'
    TopPlane = 'TOPPLANE'


class ImageCubeGLWidget(GLViewWidget):

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

        self.mTextLabels = []

        self.mShowCameraInfo = True

    def addTextLabel(self, pos: QVector3D, text: str, color=QColor('white')):
        self.mTextLabels.append((pos, text, color))

    def clearTextLabels(self):
        self.mTextLabels.clear()

    def setShowCameraInfo(self, b: bool):
        self.mShowCameraInfo = b

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

        from OpenGL.GL import glEnable, glDisable, GL_DEPTH_TEST
        glEnable(GL_DEPTH_TEST)

        GLViewWidget.paintGL(self, *args, **kwds)

        glDisable(GL_DEPTH_TEST)
        for (pos, text, color) in self.mTextLabels:
            self.qglColor(color)
            pos: QVector3D
            self.renderText(pos.x(), pos.y(), pos.z(), text)

        dist = self.opts['distance']
        elev = self.opts['elevation']
        azim = self.opts['azimuth']

        if self.mShowCameraInfo:
            info = 'dist: {} elev: {} azim: {}'.format(dist, elev, azim)
            self.renderText(2, 10, info)
            c = self.opts['center']
            info = 'center: {} {} {}'.format(c.x(), c.y(), c.z())
            self.renderText(2, 20, info)


def samplingGrid(layer: QgsRasterLayer, extent: QgsRectangle, ncb: int = 1, max_size: int = 2 * 2 ** 20) -> tuple:
    """
    :param layer:
    :param extent: QgsRectangles extent to show from image
    :param ncb: number of color bands to return. Will be multiplied by 4 for RGBA
                1 = standard RGB image, 144 = for 144 input bands (cube)
    :param max_size: max. size in bytes
    :return: nnl, nns = lines a sample to sample the extent
    """
    if not (ncb >= 1):
        raise ValueError('Number of color bands must be >= 1')

    ns, nl = layer.width(), layer.height()
    lW, lH = layer.extent().width(), layer.extent().height()
    eW, eH = extent.width(), extent.height()

    """
    nnl / eH = nl / lH
    nns / eW = ns / lW
    """
    nnl = int(nl * eH / lH)
    nns = int(ns * eW / lW)

    TOTAL_SIZE = nnl * nns * ncb * 4
    if TOTAL_SIZE < max_size:
        return nnl, nns
    else:
        """
        Eq. 1.  nnl * nns * ncb * 4 = MAX_SIZE
                nnl = MAX_SIZE / (nns * ncb * 4)
        Eq. 2.  eW / eH = nns / nnl
                nns = eW / eH * nnl

        Eq. 2 in 1
                nnl = MAX_SIZE * eH
                      eW * nnl * ncb * 4

                nnl^2 = MAX_SIZE * eH / (eW * ncb * 4)

                nnl = sqrt(MAX_SIZE * eH / (eW * ncb * 4))
        """

        nnl = np.sqrt(max_size * eH / (eW * ncb * 4))
        nns = eW / eH * nnl
        nnl, nns = int(nnl), int(nns)
        return nnl, nns


class ImageCubeRenderTask(QgsTask):
    """
    A job to render a single image cube into a RGBA array.
    """

    def __init__(
        self,
        gl_item: GLItem,
        layer: QgsRasterLayer,
        renderer: Optional[QgsRasterRenderer] = None,
        extent: Optional[QgsRectangle] = None,
        max_bytes: int = 1024,
    ):

        super().__init__(description='Render image cube',
                         flags=QgsTask.Silent | QgsTask.CanCancel | QgsTask.CancelWithoutPrompt)

        self.mLayer: QgsRasterLayer = layer.clone()
        if not isinstance(self.mLayer, QgsRasterLayer):
            raise TypeError('layer must be a QgsRasterLayer')
        if renderer is None:
            renderer = layer.renderer()
        renderer = renderer.clone()
        renderer.setInput(layer.dataProvider())
        self.mLayer.setRenderer(renderer)
        self.mRenderer = renderer
        self.mGLItem: GLItem = gl_item
        self.mDuration = None
        if extent is None:
            extent = self.mLayer.extent()
        self.mExtent = QgsRectangle(extent)
        self.mMaxBytes = max_bytes
        self.mRGBA2D: Optional[np.ndarray] = None
        self.mRGBA3D: Optional[np.ndarray] = None
        self.mError: Optional[Exception] = None

    def canCancel(self) -> bool:
        return True

    def setBandFunction(self, renderer: QgsRasterRenderer):
        """
        returns a set-band function for the given renderer
        """
        if isinstance(renderer, QgsSingleBandGrayRenderer):
            return renderer.setInputBand
        elif isinstance(renderer, QgsSingleBandPseudoColorRenderer):
            return renderer.setInputBand
        elif isinstance(renderer, QgsSingleBandColorDataRenderer):
            return renderer.setInputBand
        elif isinstance(renderer, QgsMultiBandColorRenderer):
            def setBand(*args):
                return None

            return setBand
        elif isinstance(renderer, QgsPalettedRasterRenderer):
            return renderer.setInputBand
        else:
            return renderer.setInputBand

    def run(self):
        """
        Renders image data into RGBA Arrays
        :param task: QgsTask
        :param dump: serialized list of ImageCubeRenderJobs
        :return:
        """
        lyr = self.mLayer

        extent: QgsRectangle = self.mExtent
        nb = lyr.bandCount()
        # ns = lyr.width()
        # nl = lyr.height()

        renderer = lyr.renderer()
        setBandFunc = self.setBandFunction(lyr.renderer())

        if self.mGLItem == GLItem.Cube:
            feedback = QgsRasterBlockFeedback()
            feedback.setPreviewOnly(True)
            feedback.setRenderPartialOutput(True)

            h, w = samplingGrid(lyr, extent, max_size=self.mMaxBytes, ncb=nb)

            # x, y, z, RGBA
            rgba = np.empty((h, w, nb, 4), dtype=np.uint8)

            for iB, b in enumerate(range(nb)):
                setBandFunc(b + 1)
                block: QgsRasterBlock = renderer.block(0, extent, w, h, feedback=feedback)
                if not (block.isValid() and block.dataType() != Qgis.UnknownDataType):
                    feedback.appendError('band {} is invalid'.format(b))
                    continue

                colorArray = np.frombuffer(block.data(), dtype=QGIS2NUMPY_DATA_TYPES[block.dataType()])

                rgba[:, :, b, 0] = qaRed(colorArray).reshape((h, w))  # np.asarray([qRed(c) for c in colorArray])
                rgba[:, :, b, 1] = qaGreen(colorArray).reshape((h, w))  # np.asarray([qGreen(c) for c in colorArray])
                rgba[:, :, b, 2] = qaBlue(colorArray).reshape((h, w))  # np.asarray([qBlue(c) for c in colorArray])
                rgba[:, :, b, 3] = qaAlpha(colorArray).reshape((h, w))  # np.asarray([qAlpha(c) for c in colorArray])

                self.setProgress(100 * iB / nb)

            rgba = np.rot90(rgba, axes=(0, 1))
            rgba = np.flip(rgba, 0)
            # rgba = np.flip(rgba, 2)
            self.mRGBA3D = rgba

        elif self.mGLItem == GLItem.TopPlane:

            h, w = samplingGrid(lyr, self.mExtent, max_size=self.mMaxBytes, ncb=1)
            block: QgsRasterBlock = renderer.block(1, self.mExtent, w, h)
            colorArray: np.ndarray = np.frombuffer(block.data(), dtype=QGIS2NUMPY_DATA_TYPES[block.dataType()])

            rgba = np.empty((h, w, 4), dtype=np.ubyte)

            rgba[..., 0] = qaRed(colorArray).reshape((h, w))
            rgba[..., 1] = qaGreen(colorArray).reshape((h, w))
            rgba[..., 2] = qaBlue(colorArray).reshape((h, w))
            rgba[..., 3] = qaAlpha(colorArray).reshape((h, w))

            rgba = np.rot90(rgba, axes=(0, 1))
            rgba = np.flip(rgba, 0)
            self.mRGBA2D = rgba
        else:
            self.mError = RuntimeError(f'unknown GLItem: {self.mGLItem}')
            return False

        self.setProgress(100)
        return True


#
# def renderImageData(task: QgsTask, results: List[Dict]):
#     """
#     Renders image data into RGBA Arrays
#     :param task: QgsTask
#     :param dump: serialized list of ImageCubeRenderJobs
#     :return:
#     """
#
#     # jobs = skops.loads(dump)
#
#     results = []
#     # n = len(jobs)
#
#     renderCallsTotal = 0
#     renderCallsDone = 0
#     # for job in results:
#     #
#     #     # assert isinstance(job, ImageCubeRenderJob)
#     #     if job.id() == GLItem.Cube:
#     #         renderCallsTotal += job.mLayerShape[0]
#     #     elif job.id() == GLItem.TopPlane:
#     #         renderCallsTotal += 1
#
#     for i, job in enumerate(jobs):
#         t0 = time.time()
#         if task.isCanceled():
#             return skops.dumps(results)
#
#         assert isinstance(job, ImageCubeRenderJob)
#         lyr = job.rasterLayer()
#         renderer = job.renderer()
#
#         assert isinstance(lyr, QgsRasterLayer)
#         assert isinstance(renderer, QgsRasterRenderer)
#
#         nb = lyr.bandCount()
#         # ns = lyr.width()
#         # nl = lyr.height()
#
#         if job.id() == GLItem.Cube:
#             feedback = QgsRasterBlockFeedback()
#             feedback.setPreviewOnly(True)
#             feedback.setRenderPartialOutput(True)
#
#             lyr.setRenderer(renderer)
#             ext = job.extent()
#             h, w = samplingGrid(lyr, ext, max_size=job.mMaxBytes, ncb=nb)
#
#             if isinstance(renderer, QgsSingleBandGrayRenderer):
#                 setBand = renderer.setInputBand
#             elif isinstance(renderer, QgsSingleBandPseudoColorRenderer):
#                 setBand = renderer.setInputBand
#             elif isinstance(renderer, QgsSingleBandColorDataRenderer):
#                 def setBand(*args):
#                     return None
#             elif isinstance(renderer, QgsMultiBandColorRenderer):
#                 def setBand(*args):
#                     return None
#             elif isinstance(renderer, QgsPalettedRasterRenderer):
#
#                 def onSetBand(b: int):
#                     nonlocal renderer
#                     # nonlocal lyr
#                     renderer = QgsPalettedRasterRenderer(lyr.dataProvider(), b, renderer.classes())
#                     lyr.setRenderer(renderer)
#
#                 setBand = onSetBand
#
#             else:
#                 raise NotImplementedError()
#
#             # x, y, z, RGBA
#             rgba = np.empty((h, w, nb, 4), dtype=np.uint8)
#
#             for b in range(nb):
#                 setBand(b + 1)
#
#                 block = renderer.block(0, ext, w, h, feedback=feedback)
#
#                 assert isinstance(block, QgsRasterBlock)
#                 assert block.isValid()
#                 assert block.dataType() != Qgis.UnknownDataType
#
#                 colorArray = np.frombuffer(block.data(), dtype=QGIS2NUMPY_DATA_TYPES[block.dataType()])
#
#                 rgba[:, :, b, 0] = qaRed(colorArray).reshape((h, w))  # np.asarray([qRed(c) for c in colorArray])
#                 rgba[:, :, b, 1] = qaGreen(colorArray).reshape((h, w))  # np.asarray([qGreen(c) for c in colorArray])
#                 rgba[:, :, b, 2] = qaBlue(colorArray).reshape((h, w))  # np.asarray([qBlue(c) for c in colorArray])
#                 rgba[:, :, b, 3] = qaAlpha(colorArray).reshape((h, w))  # np.asarray([qAlpha(c) for c in colorArray])
#
#                 renderCallsDone += 1
#                 if isinstance(task, TaskMock):
#                     QApplication.processEvents()
#
#                 task.setProgress(100 * renderCallsDone / renderCallsTotal)
#
#             rgba = np.rot90(rgba, axes=(0, 1))
#             rgba = np.flip(rgba, 0)
#             # rgba = np.flip(rgba, 2)
#
#             job.setRGBA3D(rgba)
#
#         elif job.id() == GLItem.TopPlane:
#             lyr.setRenderer(renderer)
#             ext = job.extent()
#             h, w = samplingGrid(lyr, ext, max_size=job.mMaxBytes, ncb=1)
#             block = renderer.block(1, ext, w, h)
#             assert isinstance(block, QgsRasterBlock)
#             colorArray = np.frombuffer(block.data(), dtype=QGIS2NUMPY_DATA_TYPES[block.dataType()])
#
#             rgba = np.empty((h, w, 4), dtype=np.ubyte)
#
#             rgba[..., 0] = qaRed(colorArray).reshape((h, w))
#             rgba[..., 1] = qaGreen(colorArray).reshape((h, w))
#             rgba[..., 2] = qaBlue(colorArray).reshape((h, w))
#             rgba[..., 3] = qaAlpha(colorArray).reshape((h, w))
#
#             rgba = np.rot90(rgba, axes=(0, 1))
#             rgba = np.flip(rgba, 0)
#             renderCallsDone += 1
#             task.setProgress(100 * renderCallsDone / renderCallsTotal)
#
#             job.setRGBA2D(rgba)
#
#         job.mDuration = time.time() - t0
#
#         if job.rgba2D() is not None or job.rgba3D() is not None:
#             results.append(job)
#
#     task.setProgress(100)
#     return skops.dumps(results)


class ImageCubeAxisItem(gl.GLAxisItem):
    """
    **Bases:** :class:`GLGraphicsItem <pyqtgraph.opengl.GLGraphicsItem>`

    Displays three lines indicating the origin and orientation of a local coordinate system.

    """

    def __init__(self, *args, **kwds):
        super(ImageCubeAxisItem, self).__init__(*args, **kwds)
        self.mLineWidth = 2.0
        self.antialias = True
        self.mColorZ = QColor('green')
        self.mColorY = QColor('yellow')
        self.mColorX = QColor('blue')

    def setLineWidth(self, w: float):
        if not (w >= 0):
            raise ValueError('Line width must be >= 0')
        self.mLineWidth = w

    def paint(self):
        glLineWidth(self.mLineWidth)
        self.setupGLState()

        if self.antialias:
            glEnable(GL_LINE_SMOOTH)
            glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)

        glBegin(GL_LINES)

        x, y, z = self.size()
        glColor4f(*self.mColorZ.getRgb())  # z is green
        glVertex3f(0, 0, 0)
        glVertex3f(0, 0, z)

        glColor4f(*self.mColorY.getRgb())  # y is yellow
        glVertex3f(0, 0, 0)
        glVertex3f(0, y, 0)

        glColor4f(*self.mColorX.getRgb())  # x is blue
        glVertex3f(0, 0, 0)
        glVertex3f(x, 0, 0)
        glEnd()


#
# class ImageCubeRenderJob(object):
#     """
#     Serializable object that describes a render job to get image-cube color values
#     """
#
#     class JobType(enum.Enum):
#         Normal = 1
#         SliceX = 2
#         SliceY = 3
#         SliceZ = 4
#
#     def __init__(self,
#                  id: GLItem,
#                  layer: QgsRasterLayer,
#                  renderer: Union[QgsRasterRenderer, str],
#                  extent: Optional[QgsRectangle] = None):
#         """
#         Describes a render job to get image-cube color values.
#         """
#         self.mID: GLItem = id
#         self.mUri: str = layer.source()
#         self.mDataProvider: str = layer.dataProvider().name()
#         if isinstance(renderer, QgsRasterRenderer):
#             self.mRendererXML = rendererToXml(renderer).toString()
#         elif isinstance(renderer, str):
#             self.mRendererXML: str = renderer
#         else:
#             raise TypeError('renderer must be QgsRasterRenderer or str')
#
#         self.mExtent: QgsRectangle
#         if isinstance(extent, QgsRectangle):
#             self.mExtent = QgsRectangle(extent)
#         else:
#             self.mExtent = layer.extent()
#
#         self.mMaxBytes: int = 1024 ** 2
#         self.mLayerShape: List[int] = [layer.bandCount(), layer.height(), layer.width()]
#         self.mDuration = None
#         self.mRGBA2D: Optional[np.ndarray] = None
#         self.mRGBA3D: Optional[np.ndarray] = None
#
#     def map(self) -> Dict[str, Any]:
#
#         d = {
#             'id': str(self.mID),
#             'uri': self.mUri,
#             'data_provider': self.mDataProvider,
#             'renderer_xml': self.mRendererXML,
#             'extent': self.mExtent.asWktPolygon(),
#             'max_bytes': self.mMaxBytes,
#             'layer_shape': self.mLayerShape,
#             'duration': self.mDuration,
#             'rgba2D': self.mRGBA2D,
#             'rgba3D': self.mRGBA3D,
#         }
#
#         return d
#
#     def json(self) -> str:
#         """
#         Returns a JSON string representation of the job.
#         """
#         return json.dumps(self.map(), ensure_ascii=False)
#
#     @staticmethod
#     def fromJson(json_str: str) -> 'ImageCubeRenderJob':
#         """
#         Creates an ImageCubeRenderJob object from a JSON string.
#         """
#         d = json.loads(json_str)
#
#         lyr = QgsRasterLayer()
#         return ImageCubeRenderJob(lyr)
#
#     def setMaxBytes(self, n: int):
#         if not (n > 1024):
#             raise ValueError('Max bytes must be greater than 1024')
#         self.mMaxBytes = n
#
#     def setExtent(self, extent: QgsRectangle):
#         if not isinstance(extent, QgsRectangle):
#             raise TypeError('Extent must be a QgsRectangle')
#         self.mExtent = extent
#
#     def extent(self) -> QgsRectangle:
#         return QgsRectangle(self.mExtent)
#
#     def __eq__(self, other) -> bool:
#         if not isinstance(other, ImageCubeRenderJob):
#             return False
#         else:
#             return (
#                 self.mID == other.mID
#                 and self.mUri == other.mUri
#                 and self.mRendererXML == other.mRendererXML
#                 and self.mExtent == other.mExtent
#                 and self.mMaxBytes == other.mMaxBytes
#             )
#
#     def __hash__(self):
#         return hash((self.mID, self.mUri, self.mRendererXML))
#
#     def id(self) -> GLItem:
#         return self.mID
#
#     def rasterLayer(self) -> QgsRasterLayer:
#         return QgsRasterLayer(self.mUri, str(self.id().value), self.mDataProvider)
#
#     def renderer(self) -> QgsRasterRenderer:
#         return rendererFromXml(self.mRendererXML)
#
#     def setRGBA2D(self, array: np.ndarray):
#         assert isinstance(array, np.ndarray)
#         assert array.ndim == 3
#         assert array.shape[2] == 4
#         self.mRGBA2D = array
#
#     def rgba2D(self) -> np.ndarray:
#         return self.mRGBA2D
#
#     def setRGBA3D(self, array: np.ndarray):
#         assert isinstance(array, np.ndarray)
#         assert array.ndim == 4
#         assert array.shape[3] == 4
#         self.mRGBA3D = array
#
#     def rgba3D(self) -> np.ndarray:
#         return self.mRGBA3D
#

class ImageCubeRGBAData(object):

    def __init__(self, array: np.ndarray, layer: QgsRasterLayer, render_extent: QgsRectangle):

        self.mRGBA = array
        self.mLayerExtent: QgsRectangle = layer.extent()
        self.mRenderExtent: QgsRectangle = render_extent
        self.nb: int = layer.bandCount()
        self.ns: int = layer.width()
        self.nl: int = layer.height()

    def subsetDimensions(self):
        """
        Returns the subset dimensions in pixel-space coordinates
        :param lyr: QgsRasterLayer - Original Raster Layer
        :param subsetExtent: QgsRectangle of layer subsets
        :param rgba: np.ndarray
        :return: ox, oy, ob, sx, sy, sb
        """
        rgba = self.mRGBA
        if rgba.ndim == 4:
            nns, nnl, _, _ = rgba.shape
        elif rgba.ndim == 3:
            nns, nnl, _ = rgba.shape
        else:
            raise Exception('Error: {}.ndim must be 3 or 4'.format(rgba))

        ns, nl = self.ns, self.nl
        ext: QgsRectangle = self.mLayerExtent

        px_size_x = ext.width() / ns
        px_size_y = ext.height() / nl

        subsetExtent = self.mRenderExtent
        px_size_subset_x = subsetExtent.width() / nns
        px_size_subset_y = subsetExtent.height() / nnl

        ox = (subsetExtent.xMinimum() - ext.xMinimum()) / px_size_x
        oy = (ext.yMaximum() - subsetExtent.yMaximum()) / px_size_y
        ob = 0
        sb = 1
        sx = px_size_subset_x / px_size_x
        sy = px_size_subset_y / px_size_y

        return ox, oy, ob, sx, sy, sb


class ImageCubeWidget(QMainWindow):
    sigExtentRequested = pyqtSignal(QMainWindow)

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        pathUi = Path(__file__).parent / 'imagecube.ui'
        loadUi(pathUi, self)
        self.setWindowTitle('Image Cube')
        self.mCanvas = QgsMapCanvas()
        self.mCanvas.setVisible(False)
        self.mMapTools: typing.List[SpatialExtentMapTool] = []
        self.mSliceRenderer: Optional[QgsRasterRenderer] = None
        self.mTopPlaneRenderer: Optional[QgsRasterRenderer] = None

        self.mProject: QgsProject = QgsProject.instance()

        self.mBandScaleFactor = 1

        self.mSpatialExtent = SpatialExtent(QgsCoordinateReferenceSystem())

        self.mMaxSizeTopPlane = 10 * 2 ** 20  # MByte
        self.mMaxSizeCube = 20 * 2 ** 20  # MByte
        self.sbCacheTopPlane.setValue(int(self.mMaxSizeTopPlane / 2 ** 20))
        self.sbCacheCube.setValue(int(self.mMaxSizeCube / 2 ** 20))

        def setTopPlaneCache(mBytes: int):
            self.mMaxSizeTopPlane = mBytes * 2 ** 20

        def setCubeCache(mBytes: int):
            self.mMaxSizeCube = mBytes * 2 ** 20

        self.sbCacheTopPlane.valueChanged.connect(setTopPlaneCache)
        self.sbCacheCube.valueChanged.connect(setCubeCache)

        self.mRGBATopPlane: Optional[ImageCubeRGBAData] = None
        self.mRGBACube: Optional[ImageCubeRGBAData] = None

        self.mCubeSliceDensity = 2
        self.mSliceSliceDensity = 2

        self.mMapLayerComboBox: QgsMapLayerComboBox
        self.mMapLayerComboBox.setAllowEmptyLayer(True)
        self.mMapLayerComboBox.setFilters(QgsMapLayerProxyModel.RasterLayer)
        self.mMapLayerComboBox.layerChanged.connect(self.onLayerChanged)

        self.mLastJobs = dict()
        self.btnLoadData.clicked.connect(self.actionLoadData.trigger)
        self.btnSetRendererTopPlane.clicked.connect(self.actionSetRendererTopPlane.trigger)
        self.btnSetRendererSlices.clicked.connect(self.actionSetRendererSlices.trigger)
        self.btnResetGLView.clicked.connect(self.actionResetGLView.trigger)
        self.btnSetExtent.setDefaultAction(self.actionSetExtent)

        self.actionValidate.triggered.connect(self.onValidate)
        self.actionLoadData.triggered.connect(lambda *args: self.startDataLoading(top=True, cube=True))
        self.actionSetRendererTopPlane.triggered.connect(self.onSetTopPlaneRenderer)
        self.actionSetRendererSlices.triggered.connect(self.onSetSliceRenderer)
        self.actionResetGLView.triggered.connect(self.resetCameraPosition)
        self.actionSetExtent.triggered.connect(self.onExtentRequested)

        self.spinBoxX: SliderSpinBox
        self.spinBoxY: SliderSpinBox
        self.spinBoxZ: SliderSpinBox
        for sb in [self.spinBoxX, self.spinBoxY, self.spinBoxZ]:
            sb.setMinimum(1)
            sb.spinbox.setMinimumWidth(50)
        self.spinBoxX.sigValueChanged.connect(lambda: self.drawSlice(GLItem.SliceX))
        self.spinBoxY.sigValueChanged.connect(lambda: self.drawSlice(GLItem.SliceY))
        self.spinBoxZ.sigValueChanged.connect(lambda: self.drawSlice(GLItem.SliceZ))

        self.doubleSpinBoxZScale: DoubleSliderSpinBox
        self.doubleSpinBoxZScale.setMinimum(0.1)
        self.doubleSpinBoxZScale.setMaximum(10)
        self.doubleSpinBoxZScale.setSingleStep(0.1)
        self.doubleSpinBoxZScale.setValue(1)
        self.doubleSpinBoxZScale.sigValueChanged.connect(self.onZScaleChanged)

        # register GLITem visibility checkboxes

        self.cbShowTopPlane.setProperty(KEY_GL_ITEM_GROUP, GLItem.TopPlane)
        self.cbShowCube.setProperty(KEY_GL_ITEM_GROUP, GLItem.Cube)
        self.cbShowSliceX.setProperty(KEY_GL_ITEM_GROUP, GLItem.SliceX)
        self.cbShowSliceY.setProperty(KEY_GL_ITEM_GROUP, GLItem.SliceY)
        self.cbShowSliceZ.setProperty(KEY_GL_ITEM_GROUP, GLItem.SliceZ)
        self.cbShowBoxImage.setProperty(KEY_GL_ITEM_GROUP, GLItem.Box)
        self.cbShowBoxSubset.setProperty(KEY_GL_ITEM_GROUP, GLItem.BoxSubset)
        self.cbShowAxis.setProperty(KEY_GL_ITEM_GROUP, GLItem.Axes)

        for cb in [self.cbShowTopPlane, self.cbShowCube,
                   self.cbShowSliceX, self.cbShowSliceY, self.cbShowSliceZ,
                   self.cbShowBoxImage, self.cbShowBoxSubset,
                   self.cbShowAxis
                   ]:
            cb: QCheckBox
            glItem = cb.property(KEY_GL_ITEM_GROUP)
            cb.clicked.connect(lambda b, glItem=glItem: self.setGLItemVisbility(glItem, b))

        self.cbSmooth.clicked.connect(self.onGLParametersChanged)
        self.sbSliceDensity.valueChanged.connect(self.onGLParametersChanged)
        self.glViewWidget().setShowCameraInfo(self.cbDebug.isChecked())
        self.cbDebug.clicked.connect(self.glViewWidget().setShowCameraInfo)

        self.mTasks = dict()

        w = self.glViewWidget()

        # center = w.opts['center']
        # dist = w.opts['distance']
        # elev = w.opts['elevation'] #* np.pi/180.
        # azim = w.opts['azimuth'] #* np.pi/180.

        self.mDefaultCAM = {}
        for k in ['distance', 'elevation', 'azimuth']:
            self.mDefaultCAM[k] = w.opts[k]

        self.onLayerChanged(self.mMapLayerComboBox.currentLayer())
        # hide slices
        # .setSlicesVisibility(False)

    def setProject(self, project: QgsProject):
        self.mProject = project
        self.mMapLayerComboBox.setProject(project)

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

    def setSlicesVisibility(self, b: bool):
        for cb in [self.cbShowSliceX, self.cbShowSliceY, self.cbShowSliceZ]:
            cb.setChecked(b)

    def debug(self) -> bool:
        return self.cbDebug.isChecked()

    def glItemGroupItems(self, key: GLItem) -> list:
        return [i for i in self.glViewWidget().items if i.property(KEY_GL_ITEM_GROUP) == key]

    def setGLItemGroupItems(self, key: GLItem, items):
        """
        Adds a group of items identified by a key
        :param key:
        :param items:
        :return:
        """
        if not isinstance(items, list):
            items = [items]
        itemsOld = self.glItemGroupItems(key)

        for item in itemsOld:
            self.glViewWidget().removeItem(item)

        for item in items:
            item.setProperty(KEY_GL_ITEM_GROUP, key)
            self.glViewWidget().addItem(item)

    def resetCameraPosition(self):
        distance = self.mDefaultCAM['distance']
        elevation = self.mDefaultCAM['elevation']
        azimuth = self.mDefaultCAM['azimuth']
        if True:
            nb, nl, ns = self.layerDims()

            if ns is None:
                ns = nl = nb = 1

            center = QVector3D(0.5 * ns, -0.5 * nl, -0.5 * nb)
            elevation = 22  # °
            azimuth = -66
            self.glViewWidget().opts['center'] = center
            self.glViewWidget().update()
            distance = center.length() * 5

        self.glViewWidget().setCameraPosition(distance=distance, elevation=elevation, azimuth=azimuth)

    def layerDims(self) -> tuple:
        lyr = self.rasterLayer()
        if isinstance(lyr, QgsRasterLayer):
            ns = lyr.width()
            nl = lyr.height()
            nb = lyr.bandCount()
            return (nb, nl, ns)
        else:
            return (None, None, None)

    def cubeDims(self) -> tuple:
        """
        Returns the cube dimensions in (nb, nl, ns) order
        :return:
        """
        if isinstance(self.mRGBACube, np.ndarray):
            nns, nnl, nnb = self.mRGBACube.shape[0:3]
            return nnb, nnl, nns
        else:
            return None, None, None

    def topPlaneDims(self) -> tuple:
        """
        Returns the TOPPLANE dimensions as (nl, ns)
        :return: tuple
        """
        if isinstance(self.mRGBATopPlane, np.ndarray):
            nns, nnl = self.mRGBATopPlane.shape[0:2]
            return nnl, nns
        else:
            return None, None

    def rasterLayer(self) -> Optional[QgsRasterLayer]:
        return self.mMapLayerComboBox.currentLayer()

    def sliceRenderer(self) -> Optional[QgsRasterRenderer]:
        return self.mSliceRenderer

    def topPlaneRenderer(self) -> Optional[QgsRasterRenderer]:
        return self.mTopPlaneRenderer

    def onExtentRequested(self):
        self.mMapTools.clear()
        self.sigExtentRequested.emit(self)

    def createExtentRequestMapTool(self, canvas: QgsMapCanvas):
        mt = SpatialExtentMapTool(canvas)
        mt.sigSpatialExtentSelected.connect(lambda crs, ext: self.setSpatialExtent(SpatialExtent(crs, ext)))
        canvas.setMapTool(mt)
        self.mMapTools.append(mt)

    def onSetSliceRenderer(self):

        lyr = self.rasterLayer()
        if isinstance(lyr, QgsRasterLayer):
            lyr2 = QgsRasterLayer(lyr.source(), lyr.name(), lyr.dataProvider().name())
            r = self.sliceRenderer()
            if isinstance(r, QgsRasterRenderer):
                lyr2.setRenderer(r)
            showLayerPropertiesDialog(lyr2, None)

            self.setSliceRenderer(lyr2.renderer())

        self.onValidate()

    def onSetTopPlaneRenderer(self):

        lyr = self.rasterLayer()
        if isinstance(lyr, QgsRasterLayer):
            lyr2 = QgsRasterLayer(lyr.source(), lyr.name(), lyr.dataProvider().name())
            r = self.topPlaneRenderer()
            if isinstance(r, QgsRasterRenderer):
                lyr2.setRenderer(r)
            showLayerPropertiesDialog(lyr2, self.mCanvas)
            self.setTopPlaneRenderer(lyr2.renderer())

        self.onValidate()

    def setSliceRenderer(self, renderer: QgsRasterRenderer):
        self.mSliceRenderer = renderer.clone()
        self.startDataLoading(cube=True)

    def setTopPlaneRenderer(self, renderer: QgsRasterRenderer):
        self.mTopPlaneRenderer = renderer.clone()
        self.startDataLoading(top=True)

    def reloadData(self):
        self.mLastJobs.clear()
        self.startDataLoading(top=True, cube=True)

    def setExtent(self, extent: QgsRectangle):
        self.setSpatialExtent(SpatialExtent(self.crs(), extent))

    def setCrs(self, crs: QgsCoordinateReferenceSystem):
        self.setSpatialExtent(self.spatialExtent().toCrs(crs))

    def extent(self) -> QgsRectangle:
        return QgsRectangle(self.spatialExtent())

    def crs(self) -> QgsCoordinateReferenceSystem:
        return self.spatialExtent().crs()

    def spatialExtent(self) -> SpatialExtent:
        return self.mSpatialExtent

    def setSpatialExtent(self, spatialExtent: SpatialExtent):
        self.mSpatialExtent: SpatialExtent = spatialExtent
        self.tbExtent: QLineEdit
        info = '{},{}:{},{}:{}'.format(
            spatialExtent.xMinimum(), spatialExtent.yMaximum(),
            spatialExtent.xMaximum(), spatialExtent.yMaximum(),
            spatialExtent.crs().description())

        self.tbExtent.setText(info)
        self.tbExtent.setToolTip(info)
        self.reloadData()

    def print(self, msg: str, file=sys.stdout):
        if self.debug():
            print(msg, file=file)

    def startDataLoading(self, cube: bool = False, top: bool = False):

        lyr = self.rasterLayer()
        r_top = self.topPlaneRenderer()
        r_slice = self.sliceRenderer()

        if not isinstance(lyr, QgsRasterLayer):
            return

        ext = self.spatialExtent().toCrs(lyr.crs())

        tasks = []
        if isinstance(r_top, QgsRasterRenderer):
            tasks.append(
                ImageCubeRenderTask(
                    GLItem.TopPlane,
                    lyr,
                    extent=ext,
                    renderer=r_top,
                    max_bytes=self.mMaxSizeTopPlane
                )
            )
        if isinstance(r_slice, QgsRasterRenderer):
            tasks.append(
                ImageCubeRenderTask(
                    GLItem.Cube,
                    lyr,
                    extent=ext,
                    renderer=r_slice,
                    max_bytes=self.mMaxSizeCube
                )
            )

        run_async: bool = self.cbUseTaskManager.isChecked()
        tm: QgsTaskManager = QgsApplication.taskManager()

        for qgsTask in tasks:
            tid = id(qgsTask)
            self.mTasks[tid] = qgsTask
            qgsTask.progressChanged.connect(lambda p: self.progressBar.setValue(int(p)))
            qgsTask.taskCompleted.connect(self.onTaskCompleted)
            qgsTask.taskTerminated.connect(self.onTaskTerminated)

            if run_async:
                tm.addTask(qgsTask)
            else:
                if qgsTask.run():
                    qgsTask.taskCompleted()
                else:
                    qgsTask.taskTerminated()

    def onTaskCompleted(self, *args):
        t = self.sender()
        if isinstance(t, ImageCubeRenderTask):
            self.mTasks.pop(id(t))

            if t.mGLItem == GLItem.Cube and isinstance(t.mRGBA3D, np.ndarray):
                data = ImageCubeRGBAData(t.mRGBA3D, t.mLayer, t.mExtent)
                self.setRGBACube(data)
                self.drawCube()
                self.drawSlice(GLItem.SliceX)
                self.drawSlice(GLItem.SliceY)
                self.drawSlice(GLItem.SliceZ)

            elif t.mGLItem == GLItem.TopPlane and isinstance(t.mRGBA2D, np.ndarray):
                data = ImageCubeRGBAData(t.mRGBA2D, t.mLayer, t.mExtent)
                self.setRGBATopPlane(data)
                self.drawTopPlane()
            else:
                raise NotImplementedError()

    def onTaskTerminated(self, *args):
        t = self.sender()
        if isinstance(t, ImageCubeRenderTask):
            self.mTasks.pop(id(t))

    def onRemoveTask(self, tid):
        if tid in self.mTasks.keys():
            del self.mTasks[tid]

    def setGLItemVisbility(self, key, b: bool):
        for item in self.glItemGroupItems(key):
            item: GLGraphicsItem
            item.setVisible(b)

    def glItemVisibility(self, key: GLItem) -> bool:
        for cb in self.findChildren(QCheckBox):
            cb: QCheckBox
            if cb.property(KEY_GL_ITEM_GROUP) == key:
                return cb.isChecked()

        return False

    def glViewWidget(self) -> ImageCubeGLWidget:
        return self.openglWidget

    def smooth(self) -> bool:
        return self.cbSmooth.isChecked()

    def sliceDensity(self) -> int:
        return self.sbSliceDensity.value()

    def onValidate(self) -> bool:

        b = True
        b &= isinstance(self.topPlaneRenderer(), QgsRasterRenderer)
        b &= isinstance(self.sliceRenderer(), QgsRasterRenderer)
        b &= isinstance(self.rasterLayer(), QgsRasterLayer) and self.rasterLayer().isValid()

        self.btnLoadData.setEnabled(b)

        return b

    def config(self) -> dict:
        lyr = self.rasterLayer()
        c = {'uri': lyr.source(), 'provider': lyr.dataProvider().name(),
             'x': self.x(), 'y': self.y(), 'z': self.z(),
             'rendererTop': self.topPlaneRenderer(), 'rendererSlices': self.sliceRenderer()
             }
        return c

    def setX(self, x: int):
        if not (0 < x <= self.spinBoxX.maximum()):
            raise ValueError("Invalid X value")
        self.spinBoxX.setValue(x)

    def x(self) -> int:
        return self.spinBoxX.value()

    def setY(self, y: int):
        if not (0 < y <= self.spinBoxY.maximum()):
            raise ValueError("Invalid Y value")
        self.spinBoxY.setValue(y)

    def y(self) -> int:
        return self.spinBoxY.value()

    def z(self) -> int:
        return self.spinBoxZ.value()

    def setZ(self, z: int):
        if not (0 < z <= self.spinBoxZ.maximum()):
            raise ValueError("Invalid Z value")
        self.spinBoxZ.setValue(z)

    def setRGBATopPlane(self, data: ImageCubeRGBAData):

        rgba = data.mRGBA
        if not (
            isinstance(rgba, np.ndarray)
            and rgba.ndim == 3  # noqa
            and rgba.shape[2] == 4  # noqa
            and rgba.dtype == np.uint8  # noqa
        ):
            raise ValueError("Invalid RGBA top plane")

        self.mRGBATopPlane = data

    def drawSubsetExtent(self):

        pass

    def drawTopPlane(self):

        if not isinstance(self.mRGBATopPlane, ImageCubeRGBAData):
            return
        rgba = self.mRGBATopPlane.mRGBA
        if not (
            isinstance(rgba, np.ndarray)
            and rgba.ndim == 3  # noqa
            and rgba.shape[2] == 4  # noqa
            and rgba.dtype == np.uint8  # noqa
        ):
            raise ValueError("Invalid RGBA top plane")

        ox, oy, ob, sx, sy, sb = self.mRGBATopPlane.subsetDimensions()

        item = gl.GLImageItem(self.mRGBATopPlane)
        # scale as it would be in 1:1:1 scaled axis space
        item.scale(sx, sy, 1)
        item.translate(ox, oy, 0)
        item.rotate(180, 1, 0, 0)

        item.setProperty(KEY_DEFAULT_TRANSFORM, item.transform())

        # scale Z Dimension
        item.translate(0, 0, 1)

        # item.translate(-ns / 2, -nl / 2, job.sliceIndex())
        # item.rotate(-90, 0, 0, 1)
        self.setGLItemGroupItems(GLItem.TopPlane, item)
        item.setVisible(self.glItemVisibility(GLItem.TopPlane))
        # self.setGLItemVisbility(GLItem.TopPlane, )

    def onGLParametersChanged(self):
        self.drawCube()
        self.drawTopPlane()

    def setRGBACube(self, data: ImageCubeRGBAData):
        self.mRGBACube = data

        # set allowed slices ranges to extend subset range
        ox, oy, ob, sx, sy, sb = data.subsetDimensions()
        # layer and cube dimensions
        rgba = self.mRGBACube.mRGBA
        nns, nnl, nnb = rgba.shape[0:3]

        rangeX = [int(ox + 1), int(ox + nns * sx)]
        rangeY = [int(oy + 1), int(oy + nnl * sy)]
        rangeZ = [int(ob + 1), int(ob + nnb * sb)]
        self.spinBoxX.setRange(*rangeX)
        self.spinBoxY.setRange(*rangeY)
        self.spinBoxZ.setRange(*rangeZ)

        self.spinBoxX.slider.setPageStep(int(nns * 0.1))
        self.spinBoxY.slider.setPageStep(int(nnl * 0.1))
        self.spinBoxZ.slider.setPageStep(int(nnb * 0.1))

        if True:
            # show subset extent range plot item
            box = gl.GLBoxItem(size=QVector3D(nns * sx, nnl * sx, nnb * sb))
            box.translate(ox, oy, ob)
            box.rotate(180, 1, 0, 0, local=False)
            box.setProperty(KEY_DEFAULT_TRANSFORM, box.transform())
            box.scale(1, 1, self.zScale())

            box.setVisible(self.glItemVisibility(GLItem.BoxSubset))
            self.setGLItemGroupItems(GLItem.BoxSubset, box)

    def drawCube(self):

        if not isinstance(self.mRGBACube, ImageCubeRGBAData):
            return None

        rgba = self.mRGBACube.mRGBA
        if not (
            isinstance(rgba, np.ndarray)
            and rgba.ndim == 4  # noqa
            and rgba.shape[3] == 4  # noqa
            and rgba.dtype == np.uint8  # noqa

        ):
            raise ValueError("Invalid RGBA cube")

        ox, oy, ob, sx, sy, sb = self.mRGBACube.subsetDimensions()

        # layer and cube dimensions
        nns, nnl, nnb = rgba.shape[0:3]
        # nb, nl, ns = self.layerDims()

        # x = x2*sx
        iBlock = 0
        if True:
            items = []
            isVisible = self.glItemVisibility(GLItem.Cube)
            stepX = stepY = 250
            stepZ = nnb

            x = 0
            while x < nns:
                x1 = x
                x2 = min(x1 + stepX, nns)
                y = 0
                while y < nnl:
                    y1 = y
                    y2 = min(y1 + stepY, nnl)
                    z = 0

                    while z < nnb:
                        z1 = z
                        z2 = min(z1 + stepZ, nnb)
                        block = rgba[x1:x2, y1:y2, z1:z2, :]

                        blockW, blockH = block.shape[0:2]
                        iBlock += 1
                        blockW *= sx  # width of block in px size of original layer resolution
                        blockH *= sy
                        # self.print('x:[{} {}], y:[{} {}], z:[{} {}]'.format(x,x2-1,y,y2-1,z,z2-1))
                        z = z2

                        # do not plot empty blocks
                        if np.all(block == 0):
                            continue
                        # block = np.flip(block, axis=2)
                        from OpenGL.GL import GL_ALPHA_TEST

                        glOptions = GLOptions['translucent']
                        glOptions[GL_ALPHA_TEST] = True
                        item = gl.GLVolumeItem(block, sliceDensity=self.sliceDensity(), smooth=self.smooth(),
                                               glOptions=glOptions)

                        # item.scale(sx, sy, sb)

                        offsetX = ox + x * sx
                        offsetY = oy + y * sy
                        item.scale(sx, sy, sb)
                        item.translate(offsetX, offsetY, 0, local=False)
                        item.rotate(180, 1, 0, 0, local=False)
                        item.setVisible(isVisible)
                        # item.rotate(180, 0, 0, 1, local=False)
                        # item.rotate(180, 0, 1, 0, local=False)
                        item.setProperty(KEY_DEFAULT_TRANSFORM, item.transform())

                        # scale to zScale
                        item.scale(1, 1, self.zScale())
                        # item.translate(0, 0, -z2 * sb * self.zScale())

                        if False:
                            if iBlock % 2 != 0:  # use this switch to exclude single blocks
                                items.append(item)
                        else:
                            items.append(item)

                    y = y2
                x = x2

            self.setGLItemGroupItems(GLItem.Cube, items)

    def drawSlice(self, key: GLItem):
        if key not in [GLItem.SliceX, GLItem.SliceY, GLItem.SliceZ]:
            raise ValueError(f"Invalid key: {key}")

        items = self.glItemGroupItems(key)
        if items:
            for item in items:
                self.glViewWidget().removeItem(item)

        if not isinstance(self.mRGBACube, ImageCubeRGBAData):
            return None

        rgba = self.mRGBACube.mRGBA
        if not (
            rgba.ndim == 4 and rgba.shape[3] == 4 and rgba.dtype == np.uint8
        ):
            raise ValueError("Invalid RGBA cube")

        t0 = time.time()

        ox, oy, ob, sx, sy, sb = self.mRGBACube.subsetDimensions()

        # layer and cube dimensions
        nns, nnl, nnb = rgba.shape[0:3]

        # calculate indices xx, yy, zz to copy from 3D cube array
        xx0, xxe = 0, nns
        yy0, yye = 0, nnl
        zz0, zze = 0, nnb

        if key == GLItem.SliceX:
            if self.x() < ox or self.x() > ox + xxe * sx:
                return
            xx0 = min(int(self.x() / sx), nns - 1)
            xxe = xx0 + 1

        elif key == GLItem.SliceY:
            if self.y() < oy or self.y() > oy + yye * sy:
                return
            yy0 = min(int(self.y() / sy), nnl - 1)
            yye = yy0 + 1

        elif key == GLItem.SliceZ:
            if self.z() < ob or self.z() > ob + zze * sy:
                return

            zz0 = min(int(self.z() / sb), nnb - 1)
            zze = zz0 + 1

        # self.print('x: {} {}\ny: {} {}\nz:{} {}'.format(x0,x1,y0,y1,z0,z1))
        items = []
        isVisible = self.glItemVisibility(key)

        if True:  # print volumetric slice
            stepX = stepY = stepZ = 250
            xx1 = xx0
            while xx1 < xxe:
                xx2 = min(xx1 + stepX, xxe)
                yy1 = yy0
                while yy1 < yye:
                    yy2 = min(yy1 + stepY, yye)
                    zz1 = zz0
                    while zz1 < zze:
                        zz2 = min(zz1 + stepZ, zze)
                        self.print('GET BLOCK {}:{},{}:{},{}:{}'.format(xx1, xx2, yy1, yy2, zz1, zz2))
                        block = rgba[xx1:xx2, yy1:yy2, zz1:zz2, :]

                        offsetX = ox + xx1 * sx
                        offsetY = oy + yy1 * sy
                        offsetZ = ob + zz1 * sb * self.zScale()
                        zz1 = zz2

                        # do not plot empty blocks
                        if np.all(block == 0):
                            continue

                        item = gl.GLVolumeItem(block, sliceDensity=self.sliceDensity(), smooth=self.smooth())
                        item.scale(sx, sy, sb)
                        item.setVisible(isVisible)

                        item.translate(offsetX, offsetY, offsetZ, local=False)
                        item.rotate(180, 1, 0, 0, local=False)
                        item.setProperty(KEY_DEFAULT_TRANSFORM, item.transform())
                        item.scale(1, 1, self.zScale())
                        if False:
                            if key == GLItem.SliceX:

                                item.scale(1, 1, self.zScale())
                            elif key == GLItem.SliceY:
                                # item.translate(offsetX, self.y(), offsetZ, local=False)
                                # item.rotate(180, 1, 0, 0)
                                item.setProperty(KEY_DEFAULT_TRANSFORM, item.transform())

                            elif key == GLItem.SliceZ:
                                # item.translate(offsetX, offsetY, self.z(), local=False)
                                # item.rotate(180, 1, 0, 0)
                                # item.rotate(180, 0, 1, 0)
                                item.setProperty(KEY_DEFAULT_TRANSFORM, item.transform())
                                item.scale(1, 1, self.zScale())
                                # item.translate(ns, 0, -self.z()*self.zScale())

                        items.append(item)
                    yy1 = yy2
                xx1 = xx2

            self.setGLItemGroupItems(key, items)

        self.print('{} ADDED {}'.format(key, time.time() - t0))

    def zScale(self) -> float:
        return self.doubleSpinBoxZScale.value()

    def setZScale(self, z: float):
        self.doubleSpinBoxZScale.setValue(z)

    def onZScaleChanged(self):
        z = self.zScale()
        nb, nl, ns = self.layerDims()
        for item in self.glViewWidget().items:
            if not isinstance(item, GLGraphicsItem):
                raise AssertionError("item is not a GLGraphicsItem")

            transformDefault = item.property(KEY_DEFAULT_TRANSFORM)
            key = item.property(KEY_GL_ITEM_GROUP)
            if isinstance(transformDefault, QMatrix4x4):
                if key == GLItem.SliceZ:

                    z = -self.z() * self.zScale()
                    transformDefault[2, 3] = z
                    item.setTransform(transformDefault)

                else:
                    item.setTransform(transformDefault)
                    item.scale(1, 1, z)  # scale will update the item

    def onLayerChanged(self, lyr):

        self.print('LAYER CHANGED')
        b = isinstance(lyr, QgsRasterLayer)

        toRemove = self.glViewWidget().items[:]
        for item in toRemove:
            # item._setView(None)
            self.glViewWidget().removeItem(item)
        self.progressBar.setValue(0)
        self.mRGBACube = None
        self.mRGBATopPlane = None
        self.print('LAYER CHANGED CLEANUP')
        if b:
            self.setWindowTitle(f'{NAME} {VERSION} - {lyr.name()}')
            nb = lyr.bandCount()
            ns = lyr.width()
            nl = lyr.height()

            minEdge = 0.1 * min(ns, nl)

            if nb < minEdge:
                self.mBandScaleFactor = minEdge / nb
            else:
                self.mBandScaleFactor = 1

            x = self.x()
            y = self.y()
            z = self.z()

            self.spinBoxX.setRange(1, ns)
            self.spinBoxY.setRange(1, nl)
            self.spinBoxZ.setRange(1, nb)

            self.setX(min(x, ns))
            self.setY(min(y, nl))
            self.setZ(min(z, nb))

            self.mCanvas.setLayers([lyr])

            self.setSpatialExtent(SpatialExtent.fromLayer(lyr))
            self.setTopPlaneRenderer(lyr.renderer().clone())

            # set slice renderer, optimize for band
            l2 = QgsRasterLayer(lyr.source())
            if not isinstance(self.sliceRenderer(), QgsRasterRenderer):
                band = self.z()
                renderer = QgsSingleBandGrayRenderer(l2.dataProvider(), band)
                l2.setRenderer(renderer)

            else:
                renderer = self.sliceRenderer()
                if isinstance(renderer, QgsSingleBandPseudoColorRenderer):
                    renderer.setInputBand(self.z())
                    l2.setRenderer(renderer)
                elif isinstance(renderer, QgsSingleBandGrayRenderer):
                    renderer.setInputBand(self.z())
                    l2.setRenderer(renderer)
                elif isinstance(renderer, QgsMultiBandColorRenderer):
                    l2.setRenderer(QgsRasterLayer(l2.source(), '', l2.dataProvider().name()).renderer())

            l2.setContrastEnhancement(QgsContrastEnhancement.StretchToMinimumMaximum, QgsRasterMinMaxOrigin.MinMax)
            self.setSliceRenderer(l2.renderer().clone())

            w = self.glViewWidget()

            # w.addTextLabel(QVector3D(ns, 0, 0), 'x')
            # w.addTextLabel(QVector3D(0, -nl, 0), 'y')
            # w.addTextLabel(QVector3D(0, 0, -nb), 'bands')

            box = gl.GLBoxItem(size=QVector3D(ns, -nl, -nb))
            # box.translate(0, 0, nb)
            box.setProperty(KEY_DEFAULT_TRANSFORM, box.transform())
            box.scale(1, 1, self.zScale())
            box.setVisible(self.glItemVisibility(GLItem.Box))
            self.setGLItemGroupItems(GLItem.Box, box)

            # ax = gl.GLAxisItem(size=QVector3D(ns, -nl, -nb))
            ax = ImageCubeAxisItem(size=QVector3D(ns, -nl, -nb))

            # ax.translate(0, nl, nb)
            # ax.rotate(180, 0, 1, 0)
            ax.setProperty(KEY_DEFAULT_TRANSFORM, ax.transform())
            ax.scale(1, 1, self.zScale())
            ax.setVisible(self.glItemVisibility(GLItem.Axes))
            self.setGLItemGroupItems(GLItem.Axes, ax)

            # w = self.glViewWidget()
            # w.mTextLabels.clear()
            # w.addTextLabel(QVector3D(0, 0, 0), 'Bands')
            # w.addTextLabel(QVector3D(0, nl+1, nb), 'Lines')
            # w.addTextLabel(QVector3D(ns+1, 0, nb), 'Columns')

            self.resetCameraPosition()

            # for i in w.items:
            #    w.removeItem(i)

            # g = gl.GLGridItem(color='w')
            # w.addItem(g)

        else:
            self.mCanvas.setLayers([])
            self.spinBoxX.setRange(1, 1)
            self.spinBoxY.setRange(1, 1)
            self.spinBoxZ.setRange(1, 1)

            b = False
            self.mBandScaleFactor = 1

            self.setWindowTitle(f'{NAME} {VERSION} - <no raster layer selected>')

        for w in [self.gbRendering, self.gbPlotting, self.gbOpenGLOptions]:
            w.setEnabled(b)

        if self.onValidate():
            self.reloadData()

    def setRasterLayer(self, lyr: QgsRasterLayer):

        if isinstance(lyr, QgsRasterLayer):
            if lyr != self.rasterLayer():
                self.project().addMapLayer(lyr)
                self.mMapLayerComboBox.setLayer(lyr)

        if lyr is None:
            self.mMapLayerComboBox.setLayer(None)

    def close(self):
        self.mMapLayerComboBox.setLayer(None)
        super().close()
