# -*- coding: utf-8 -*-
# noinspection PyPep8Naming
"""
***************************************************************************
    qps/cursorlocationvalue.py

    Retrieval and visualization of cursor location values from QgsMapCanvases
    ---------------------
    Beginning            : 2019-01-15 (and earlier)
    Copyright            : (C) 2020 by Benjamin Jakimow
    Email                : benjamin.jakimow@geo.hu-berlin.de
***************************************************************************
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this software. If not, see <https://www.gnu.org/licenses/>.
***************************************************************************
"""

import collections
import os
from typing import List

from qgis.PyQt.QtCore import QModelIndex, QPoint, QAbstractListModel, pyqtSignal
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QIcon, QClipboard, QColor
from qgis.PyQt.QtWidgets import QMenu, QApplication, QDockWidget
from qgis.core import QgsCoordinateReferenceSystem, QgsWkbTypes, QgsField, QgsFeature, \
    QgsMapLayer, QgsVectorLayer, QgsRasterLayer, QgsPointXY, QgsRectangle, QgsTolerance, \
    QgsFeatureRequest, QgsRasterBlock, QgsPalettedRasterRenderer, QgsRaster
from qgis.core import QgsRasterDataProvider
from qgis.gui import QgsMapCanvas
from . import DIR_UI_FILES
from .classification.classificationscheme import ClassInfo, ClassificationScheme
from .models import TreeNode, TreeModel, TreeView
from .utils import SpatialPoint, geo2px, as_py_value, loadUi


class SourceValueSet(object):
    def __init__(self, source, point: SpatialPoint):
        if not (isinstance(point, SpatialPoint)):
            raise AssertionError(f'Wring data type: {point}')
        self.source = source
        self.point = point

    def baseName(self):
        return os.path.basename(self.source)

    def crs(self):
        return QgsCoordinateReferenceSystem(self.wktCrs)


class RasterValueSet(SourceValueSet):
    class BandInfo(object):
        def __init__(self, bandIndex, bandValue, bandName,
                     is_nodata: bool = False, classInfo=None):
            if not (bandIndex >= 0):
                raise AssertionError
            if bandValue is not None:
                if not (type(bandValue) in [float, int]):
                    raise AssertionError
            if bandName is not None:
                if not (isinstance(bandName, str)):
                    raise AssertionError

            self.is_nodata: bool = bool(is_nodata)
            self.bandIndex = bandIndex
            self.bandValue = bandValue
            self.bandName: str = bandName
            self.classInfo = classInfo

    def __init__(self, source, point, pxPosition: QPoint):
        if not (isinstance(pxPosition, QPoint)):
            raise AssertionError
        super(RasterValueSet, self).__init__(source, point)
        self.pxPosition = pxPosition
        self.noDataValue = None
        self.bandValues = []


class VectorValueSet(SourceValueSet):
    class FeatureInfo(object):
        def __init__(self, fid):
            if not (isinstance(fid, int)):
                raise AssertionError
            self.fid = fid
            self.attributes = collections.OrderedDict()

    def __init__(self, source, point: SpatialPoint):
        super(VectorValueSet, self).__init__(source, point)
        self.features = []

    def addFeatureInfo(self, featureInfo):
        if not (isinstance(featureInfo, VectorValueSet.FeatureInfo)):
            raise AssertionError
        self.features.append(featureInfo)


class PixelPositionTreeNode(TreeNode):

    def __init__(self, px: QPoint, coord: SpatialPoint, *args,
                 name: str = 'Pixel',
                 zeroBased: bool = True, **kwds):

        super().__init__(*args, **kwds)
        self.setName(name)
        self.mPx: QPoint = px
        self.mGeo: SpatialPoint = coord
        self.mZeroBased = zeroBased

        if self.mZeroBased:
            self.setValues([(px.x(), px.y())])
            self.setToolTip('Pixel Coordinate (upper-left = (0,0) )')
        else:
            self.setValues([(px.x() + 1, px.y() + 1)])
            self.setToolTip('Pixel Coordinate (upper-left = (1,1) )')

    def populateContextMenu(self, menu: QMenu):

        a = menu.addAction('Copy pixel coordinate (0-based)')
        a.setToolTip('Copies the zero-based pixel coordinate (first/upper-left pixel = 0,0)')
        a.triggered.connect(lambda *args, txt=f'{self.mPx.x()}, {self.mPx.y()}':
                            self.onCopyToClipBoad(txt))

        a = menu.addAction('Copy pixel coordinate (1-based)')
        a.setToolTip('Copies the one-based pixel coordinate (first/upper-left pixel = 1,1)')
        a.triggered.connect(lambda *args, txt=f'{self.mPx.x() + 1}, {self.mPx.y() + 1}':
                            self.onCopyToClipBoad(txt))

        a = menu.addAction('Copy spatial coordinate')
        a.setToolTip('Copies the spatial coordinate as <x>,<y> pair')
        a.triggered.connect(lambda *args, txt=self.mGeo.toString():
                            self.onCopyToClipBoad(txt))

        a = menu.addAction('Copy spatial coordinate (WKT)')
        a.setToolTip('Copies the spatial coordinate as Well-Known-Type')
        a.triggered.connect(lambda *args, txt=self.mGeo.asWkt():
                            self.onCopyToClipBoad(txt))

    def onCopyToClipBoad(self, text: str):
        cb: QClipboard = QApplication.clipboard()
        cb.setText(text)


class CursorLocationInfoModel(TreeModel):
    ALWAYS_EXPAND = 'always'
    NEVER_EXPAND = 'never'
    REMAINDER = 'reminder'

    def __init__(self, parent=None):
        super().__init__(parent=parent)

        self.setColumnNames(['Band/Field', 'Value'])
        self.mCountFromZero: bool = True
        self.mCursorLocation: SpatialPoint = None

    def setCursorLocation(self, location: SpatialPoint):
        if not (isinstance(location, SpatialPoint)):
            raise AssertionError
        self.mCursorLocation = location

    def setCountFromZero(self, b: bool):
        """
        Specifies if the 1st pixel (upper left corner) is countes as 0,0 (True, default) or 1,1 (False)
        :param b: bool
        """
        if not (isinstance(b, bool)):
            raise AssertionError
        self.mCountFromZero = b

    def flags(self, index: QModelIndex):
        return Qt.ItemIsEnabled | Qt.ItemIsSelectable

    def addSourceValues(self, sourceValueSet: SourceValueSet):
        if not isinstance(sourceValueSet, SourceValueSet):
            return
        bn = os.path.basename(sourceValueSet.source)

        newSourceNodes: List[TreeNode] = []

        if isinstance(sourceValueSet, RasterValueSet):

            root = TreeNode(bn)
            root.setIcon(QIcon(':/images/themes/default/mIconRasterLayer.svg'))

            # add sub-nodes
            pxNode = PixelPositionTreeNode(sourceValueSet.pxPosition,
                                           self.mCursorLocation,
                                           name='Pixel',
                                           zeroBased=self.mCountFromZero)

            subNodes = [pxNode]

            for bv in sourceValueSet.bandValues:
                if isinstance(bv, RasterValueSet.BandInfo):
                    # n = TreeNode(name='Band {}'.format(bv.bandIndex + 1))
                    n = TreeNode(name=bv.bandName)
                    n.setToolTip('Band {} {}'.format(bv.bandIndex + 1, bv.bandName).strip())
                    n.setValues([bv.bandValue, bv.bandName])
                    subNodes.append(n)

                    if isinstance(bv.classInfo, ClassInfo):
                        nc = TreeNode(name='Class')
                        nc.setValue(bv.classInfo.name())
                        nc.setIcon(bv.classInfo.icon())
                        n.appendChildNodes(nc)

                elif isinstance(bv, QColor):
                    n = TreeNode(name='Color')
                    n.setToolTip('Color selected from screen pixel')
                    n.setValue(bv.getRgb())
                    subNodes.append(n)
            root.appendChildNodes(subNodes)
            newSourceNodes.append(root)

        if isinstance(sourceValueSet, VectorValueSet):
            if len(sourceValueSet.features) == 0:
                return

            root = TreeNode(name=bn)
            refFeature = sourceValueSet.features[0]
            if not (isinstance(refFeature, QgsFeature)):
                raise AssertionError
            typeName = QgsWkbTypes.displayString(refFeature.geometry().wkbType()).lower()
            if 'polygon' in typeName:
                root.setIcon(QIcon(r':/images/themes/default/mIconPolygonLayer.svg'))
            elif 'line' in typeName:
                root.setIcon(QIcon(r':/images/themes/default/mIconLineLayer.svg'))
            if 'point' in typeName:
                root.setIcon(QIcon(r':/images/themes/default/mIconPointLayer.svg'))

            subNodes = []
            for field in refFeature.fields():
                if not (isinstance(field, QgsField)):
                    raise AssertionError

                fieldNode = TreeNode(name=field.name())

                featureNodes = []
                for i, feature in enumerate(sourceValueSet.features):
                    if not (isinstance(feature, QgsFeature)):
                        raise AssertionError
                    nf = TreeNode(name='{}'.format(feature.id()))
                    nf.setValues([feature.attribute(field.name()), field.typeName()])
                    nf.setToolTip('Value of feature "{}" in field with name "{}"'.format(feature.id(), field.name()))
                    featureNodes.append(nf)
                fieldNode.appendChildNodes(featureNodes)
                subNodes.append(fieldNode)
            root.appendChildNodes(subNodes)
            newSourceNodes.append(root)

        self.rootNode().appendChildNodes(newSourceNodes)

    def clear(self):
        # self.beginResetModel()
        self.mRootNode.removeAllChildNodes()
        # self.endResetModel()


class CursorLocationInfoTreeView(TreeView):

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


class ComboBoxOption(object):
    def __init__(self, value, name=None, tooltip=None, icon=None):
        self.value = value
        self.name = str(value) if name is None else str(name)
        self.tooltip = tooltip
        self.icon = icon


LUT_GEOMETRY_ICONS = {}

RASTERBANDS = [
    ComboBoxOption('VISIBLE', 'Visible', 'Visible bands only.'),
    ComboBoxOption('ALL', 'All', 'All raster bands.'),

]

LAYERMODES = [
    ComboBoxOption('TOP_LAYER', 'Top layer', 'Show values of the top-most map layer only.'),
    ComboBoxOption('ALL_LAYERS', 'All layers', 'Show values of all map layers.')
]

LAYERTYPES = [
    ComboBoxOption('ALL', 'Raster and Vector', 'Show values of both, raster and vector layers.'),
    ComboBoxOption('VECTOR', 'Vector only', 'Show values of vector layers only.'),
    ComboBoxOption('RASTER', 'Raster only', 'Show values of raster layers only.')
]


class ComboBoxOptionModel(QAbstractListModel):

    def __init__(self, options, parent=None):
        super(ComboBoxOptionModel, self).__init__(parent)
        if not (isinstance(options, list)):
            raise AssertionError

        for o in options:
            if not (isinstance(o, ComboBoxOption)):
                raise AssertionError

        self.mOptions = options

    def rowCount(self, parent=None, *args, **kwargs):
        return len(self.mOptions)

    def columnCount(self, index=None, *args, **kwargs):
        return 1

    def index2option(self, index):

        if isinstance(index, QModelIndex) and index.isValid():
            return self.mOptions[index.row()]
        elif isinstance(index, int):
            return self.mOptions[index]
        return None

    def option2index(self, option):
        if not (option in self.mOptions):
            raise AssertionError(f'option does not exists: {option}')
        return self.mOptions.index(option)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        option = self.index2option(index)
        if not (isinstance(option, ComboBoxOption)):
            raise AssertionError
        value = None
        if role == Qt.DisplayRole:
            value = option.name
        if role == Qt.ToolTipRole:
            value = option.tooltip
        if role == Qt.DecorationRole:
            value = option.icon
        if role == Qt.UserRole:
            value = option
        return value


class CursorLocationInfoDock(QDockWidget):
    sigLocationRequest = pyqtSignal()
    sigCursorLocationInfoAdded = pyqtSignal()

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

        path_ui = DIR_UI_FILES / 'cursorlocationinfodock.ui'
        loadUi(path_ui, self)

        self.mMaxPoints = 1
        self.mLocationHistory = []

        self.mCrs: QgsCoordinateReferenceSystem = QgsCoordinateReferenceSystem('EPSG:4326')
        self.mCanvases = []

        self.btnCrs.crsChanged.connect(self.setCrs)
        self.btnCrs.setCrs(QgsCoordinateReferenceSystem())

        self.mLocationInfoModel = CursorLocationInfoModel()
        self.mTreeView: CursorLocationInfoTreeView
        self.mTreeView.setAutoExpansionDepth(3)
        self.mTreeView.setModel(self.mLocationInfoModel)

        self.mLayerModeModel = ComboBoxOptionModel(LAYERMODES, parent=self)
        self.mLayerTypeModel = ComboBoxOptionModel(LAYERTYPES, parent=self)
        self.mRasterBandsModel = ComboBoxOptionModel(RASTERBANDS, parent=self)

        self.cbLayerModes.setModel(self.mLayerModeModel)
        self.cbLayerTypes.setModel(self.mLayerTypeModel)
        self.cbRasterBands.setModel(self.mRasterBandsModel)
        self.actionRequestCursorLocation.triggered.connect(self.sigLocationRequest)
        self.actionReload.triggered.connect(self.reloadCursorLocation)

        self.btnActivateMapTool.setDefaultAction(self.actionRequestCursorLocation)
        self.btnReload.setDefaultAction(self.actionReload)

        self.actionAllRasterBands.triggered.connect(
            lambda: self.btnRasterBands.setDefaultAction(self.actionAllRasterBands))
        self.actionVisibleRasterBands.triggered.connect(
            lambda: self.btnRasterBands.setDefaultAction(self.actionVisibleRasterBands))

        self.updateCursorLocationInfo()

    def options(self):

        layerType = self.mLayerTypeModel.index2option(self.cbLayerTypes.currentIndex()).value
        layerMode = self.mLayerModeModel.index2option(self.cbLayerModes.currentIndex()).value
        rasterBands = self.mRasterBandsModel.index2option(self.cbRasterBands.currentIndex()).value

        return layerMode, layerType, rasterBands

    def loadCursorLocation(self, point: SpatialPoint, canvas: QgsMapCanvas):
        """
        :param point:
        :param canvas:
        :return:
        """
        if not (isinstance(canvas, QgsMapCanvas)):
            raise AssertionError
        if not (isinstance(point, SpatialPoint)):
            raise AssertionError

        self.setCursorLocation(point)
        self.setCanvas(canvas)
        self.reloadCursorLocation()

    def treeView(self) -> CursorLocationInfoTreeView:
        return self.mTreeView

    def reloadCursorLocation(self):
        """
        Call to load / re-load the data for the cursor location
        """

        ptInfo = self.cursorLocation()

        if not isinstance(ptInfo, SpatialPoint) or len(self.mCanvases) == 0:
            return

        mode, lyrtype, rasterbands = self.options()

        def layerFilter(canvas):
            if not (isinstance(canvas, QgsMapCanvas)):
                raise AssertionError
            lyrs = canvas.layers()
            if lyrtype == 'VECTOR':
                lyrs = [lyr for lyr in lyrs if isinstance(lyr, QgsVectorLayer)]
            if lyrtype == 'RASTER':
                lyrs = [lyr for lyr in lyrs if isinstance(lyr, QgsRasterLayer)]

            return lyrs

        lyrs = []
        for c in self.mCanvases:
            lyrs.extend(layerFilter(c))

        self.treeView().updateNodeExpansion(False)
        self.mLocationInfoModel.clear()
        self.mLocationInfoModel.setCursorLocation(self.cursorLocation())

        for lyr in lyrs:
            if not (isinstance(lyr, QgsMapLayer)):
                raise AssertionError
            if mode == 'TOP_LAYER' and self.mLocationInfoModel.rootNode().childCount() > 0:
                s = ""
                break

            if not (isinstance(lyr, QgsMapLayer)):
                raise AssertionError

            pointLyr = ptInfo.toCrs(lyr.crs())
            if not (isinstance(pointLyr, SpatialPoint) and lyr.extent().contains(pointLyr)):
                continue

            if isinstance(lyr, QgsRasterLayer):
                renderer = lyr.renderer()
                px = geo2px(pointLyr, lyr)
                v = RasterValueSet(lyr.name(), pointLyr, px)

                # !Note: b is not zero-based -> 1st band means b == 1
                if rasterbands == 'VISIBLE':
                    if isinstance(renderer, QgsPalettedRasterRenderer):
                        bandNumbers = renderer.usesBands()
                        # sometime the renderer is set to band 0 (which does not exist)
                        # QGIS bug
                        if bandNumbers == [0] and lyr.bandCount() > 0:
                            bandNumbers = [1]
                    else:
                        bandNumbers = renderer.usesBands()

                elif rasterbands == 'ALL':
                    bandNumbers = list(range(1, lyr.bandCount() + 1))
                else:
                    bandNumbers = [1]

                pt2 = QgsPointXY(pointLyr.x() + lyr.rasterUnitsPerPixelX() * 3,
                                 pointLyr.y() - lyr.rasterUnitsPerPixelY() * 3)
                ext2Px = QgsRectangle(pointLyr.x(), pt2.y(), pt2.x(), pointLyr.y())

                if lyr.dataProvider().name() in ['wms']:
                    for b in bandNumbers:
                        block = lyr.renderer().block(b, ext2Px, 3, 3)
                        if not (isinstance(block, QgsRasterBlock)):
                            raise AssertionError
                        v.bandValues.append(QColor(block.color(0, 0)))
                else:
                    dp: QgsRasterDataProvider = lyr.dataProvider()
                    results = dp.identify(pointLyr, QgsRaster.IdentifyFormatValue).results()
                    classScheme = None
                    if isinstance(lyr.renderer(), QgsPalettedRasterRenderer):
                        classScheme = ClassificationScheme.fromRasterRenderer(lyr.renderer())
                    for b in bandNumbers:
                        if b in results.keys():
                            bandValue = results[b]
                            if bandValue:
                                bandValue = as_py_value(bandValue, lyr.dataProvider().dataType(b))

                            classInfo: ClassInfo = None
                            if isinstance(bandValue, (int, float)) and isinstance(classScheme, ClassificationScheme):
                                classInfo = classScheme.classInfo(label=int(bandValue))
                            info = RasterValueSet.BandInfo(b - 1, bandValue, lyr.bandName(b), classInfo=classInfo)
                            v.bandValues.append(info)

                self.mLocationInfoModel.addSourceValues(v)
                s = ""

            if isinstance(lyr, QgsVectorLayer):
                # searchRect = QgsRectangle(pt, pt)

                # searchRadius = QgsTolerance.toleranceInMapUnits(1, lyr, self.mCanvas.mapRenderer(),
                # QgsTolerance.Pixels)
                searchRadius = QgsTolerance.toleranceInMapUnits(1, lyr, self.mCanvases[0].mapSettings(),
                                                                QgsTolerance.Pixels)
                # searchRadius = QgsTolerance.defaultTolerance(lyr, self.mCanvas.mapSettings()) searchRadius =
                # QgsTolerance.toleranceInProjectUnits(1, self.mCanvas.mapRenderer(), QgsTolerance.Pixels)
                searchRect = QgsRectangle()
                searchRect.setXMinimum(pointLyr.x() - searchRadius)
                searchRect.setXMaximum(pointLyr.x() + searchRadius)
                searchRect.setYMinimum(pointLyr.y() - searchRadius)
                searchRect.setYMaximum(pointLyr.y() + searchRadius)

                flags = QgsFeatureRequest.ExactIntersect
                features = lyr.getFeatures(QgsFeatureRequest()
                                           .setFilterRect(searchRect)
                                           .setFlags(flags))
                feature = QgsFeature()
                s = VectorValueSet(lyr.source(), pointLyr)
                while features.nextFeature(feature):
                    s.features.append(QgsFeature(feature))

                self.mLocationInfoModel.addSourceValues(s)
                s = ""

                pass

        self.treeView().updateNodeExpansion(True)

    def setCursorLocation(self, spatialPoint: SpatialPoint):
        """
        Set the cursor location to be loaded.
        :param spatialPoint:
        """
        if not (isinstance(spatialPoint, SpatialPoint)):
            raise AssertionError
        self.mLocationHistory.insert(0, spatialPoint)
        if len(self.mLocationHistory) > self.mMaxPoints:
            del self.mLocationHistory[self.mMaxPoints:]

        if self.mCrs is None:
            self.setCrs(spatialPoint.crs())
        self.updateCursorLocationInfo()

    def updateCursorLocationInfo(self):

        self.btnCrs.setToolTip(f'Set CRS<br>Selected CRS: {self.mCrs.description()}')

        # transform this point to targeted CRS
        pt = self.cursorLocation()
        if isinstance(pt, SpatialPoint):
            pt2 = pt.toCrs(self.mCrs)
            if isinstance(pt2, SpatialPoint):
                tt = f'{pt2.asWkt()} <br>CRS: {self.mCrs.description()}'
                self.tbX.setText('{}'.format(pt2.x()))
                self.tbY.setText('{}'.format(pt2.y()))
                self.tbX.setToolTip(tt)
                self.tbY.setToolTip(tt)
            else:
                self.tbX.setText('None')
                self.tbY.setText('None')
                tt = f'Unable to convert {pt.asWkt()} from ' \
                     f'<br>CRS 1: {pt.crs().description()} to' \
                     f'<br>CRS 2: {self.mCrs.description()}'
                self.tbX.setToolTip(tt)
                self.tbY.setToolTip(tt)

    def setCanvas(self, mapCanvas):
        self.setCanvases([mapCanvas])

    def setCanvases(self, mapCanvases):
        if not (isinstance(mapCanvases, list)):
            raise AssertionError
        for c in mapCanvases:
            if not (isinstance(c, QgsMapCanvas)):
                raise AssertionError

        if len(mapCanvases) == 0:
            self.setCrs(None)
        else:
            setNew = True
            for c in mapCanvases:
                if c in self.mCanvases:
                    setNew = False
            if setNew:
                self.setCrs(mapCanvases[0].mapSettings().destinationCrs())
        self.mCanvases = mapCanvases

    def setCrs(self, crs):
        """
        Set the coordinate reference system in which coordinates are shown
        :param crs:
        :return:
        """
        if not (isinstance(crs, QgsCoordinateReferenceSystem)):
            raise AssertionError
        if crs != self.mCrs:
            self.mCrs = crs
            self.btnCrs.setCrs(crs)
        self.updateCursorLocationInfo()

    def cursorLocation(self) -> SpatialPoint:
        """
        Returns the last location that was set.
        """
        if len(self.mLocationHistory) > 0:
            return self.mLocationHistory[0]
        else:
            return None, None
