# -*- coding: utf-8 -*-

"""
Doc_ValidateTopology.py
***************************************************************************
*                                                                         *
*   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 2 of the License, or     *
*   (at your option) any later version.                                   *
*                                                                         *
***************************************************************************
"""

__author__ = 'Leandro França'
__date__ = '2023-08-22'
__copyright__ = '(C) 2023, Leandro França'

from qgis.PyQt.QtCore import QMetaType
from qgis.core import (Qgis,
                       QgsApplication,
                       QgsFields,
                       QgsWkbTypes,
                       QgsField,
                       QgsFeature,
                       QgsGeometry,
                       QgsPointXY,
                       QgsFeatureSink,
                       QgsProcessingException,
                       QgsProcessingAlgorithm,
                       QgsFeatureRequest,
                       QgsProcessingUtils,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterFeatureSink)
import os
from lftools.geocapt.imgs import *
from lftools.translations.translate import translate
from lftools.geocapt.cartography import LabelConf, SymbolSimplePoint
from qgis.PyQt.QtGui import QIcon

class ValidateTopology(QgsProcessingAlgorithm):

    LOC = QgsApplication.locale()[:2]

    def tr(self, *string):
        return translate(string, self.LOC)

    def createInstance(self):
        return ValidateTopology()

    def name(self):
        return 'ValidateTopology'.lower()

    def displayName(self):
        return self.tr('Validate topology', 'Validar topologia')

    def group(self):
        return self.tr('Documents', 'Documentos')

    def groupId(self):
        return 'documents'

    def tags(self):
        return 'GeoOne,validate,validar,topology,topologia,contains,contém,aggregate,vertex,duplicate,connect,conectividade,topografia,imóvel,memorial,deed description'.split(',')

    def icon(self):
        return QIcon(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'images/document.png'))

    txt_en = "This tool performs a series of topological validations to ensure the correct generation of survey plans and deed description based on GeoOne's <b>TopoGeo</b> and <b>GeoRural</b> models."
    txt_pt = "Esta ferramenta executa uma série de validações topológicas para garantir a correta geração de plantas topográficas e memoriais descritivos baseado nos modelos <b>TopoGeo</b> e <b>GeoRural</b> da GeoOne."
    figure = 'images/tutorial/doc_validation.jpg'

    def shortHelpString(self):
        social_BW = Imgs().social_BW
        footer = '''<div align="center">
                      <img src="'''+ os.path.join(os.path.dirname(os.path.dirname(__file__)), self.figure) +'''">
                      </div>
                      <div align="right">
                      <p align="right">
                      <b>'''+self.tr('Author: Leandro Franca', 'Autor: Leandro França')+'''</b>
                      </p>'''+ social_BW + '''</div>
                    </div>'''
        return self.tr(self.txt_en, self.txt_pt) + footer

    POINT = 'POINT'
    LINE = 'LINE'
    POLYGON = 'POLYGON'
    OUTPUT = 'OUTPUT'

    def initAlgorithm(self, config=None):

        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.POINT,
                self.tr('Vertices (points)', 'Vértices (pontos)'),
                [Qgis.ProcessingSourceType.TypeVectorPoint]
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.LINE,
                self.tr('Limits (lines)', 'Limites (linhas)'),
                [Qgis.ProcessingSourceType.TypeVectorLine]
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.POLYGON,
                self.tr('Area (polygon)', 'Área (polígono)'),
                [Qgis.ProcessingSourceType.TypeVectorPolygon]
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr('Topology errors','Erros de topologia')
            )
        )


    def processAlgorithm(self, parameters, context, feedback):

        vertices = self.parameterAsSource(parameters, self.POINT, context)
        limites = self.parameterAsSource(parameters, self.LINE, context)
        context.setInvalidGeometryCheck(Qgis.InvalidGeometryCheck.GeometryNoCheck)
        area = self.parameterAsSource(parameters, self.POLYGON, context)

        # Verificar se as 3 camadas estão no mesmo SRC Geográfico
        crs_vertices = vertices.sourceCrs()
        crs_limites = limites.sourceCrs()
        crs_area = area.sourceCrs()

        if not (crs_vertices.isGeographic() and crs_limites.isGeographic() and crs_area.isGeographic()):
            raise QgsProcessingException(
                self.tr(
                    'All input layers must use a geographic CRS!',
                    'Todas as camadas de entrada devem utilizar um SRC geográfico!'
                )
            )
        
        if crs_vertices != crs_limites or crs_vertices != crs_area:
            raise QgsProcessingException(
                self.tr(
                    'All input layers must use the same CRS!',
                    'Todas as camadas de entrada devem usar o mesmo SRC!'
                )
            )
        
        tolerancia = 0.001/111000  # 1 mm em graus

        Fields = QgsFields()
        itens  = {
                     'ord' : QMetaType.Type.Int,
                     'feat_id': QMetaType.Type.Int,
                     self.tr('type','tipo') : QMetaType.Type.QString,
                     }
        for item in itens:
            Fields.append(QgsField(item, itens[item]))

        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            Fields,
            Qgis.WkbType.Point,
            vertices.sourceCrs()
        )
        if sink is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))

        feedback.pushInfo(self.tr('Validating topology of geometries...', 'Validando topologia das geometrias...' ))
        cont = 0

        def validarGeometria(layer, nome_camada):
            cont_erros_estrutura = 0
            for feat in layer.getFeatures():
                geom = feat.geometry()

                if geom is None:
                    erro = self.tr(
                        'Null geometry in feature ID {} of layer "{}"!',
                        'Geometria nula na feição de ID {} da camada "{}"!'
                    ).format(feat.id(), nome_camada)
                    cont_erros_estrutura += 1
                    feedback.reportError(erro)

                elif geom.isEmpty():
                    erro = self.tr(
                        'Empty geometry in feature ID {} of layer "{}"!',
                        'Geometria vazia na feição de ID {} da camada "{}"!'
                    ).format(feat.id(), nome_camada)
                    cont_erros_estrutura += 1
                    feedback.reportError(erro)

                elif not geom.isGeosValid():
                    # IGNORAR multipolígonos com mais de uma parte
                    if geom.isMultipart() and geom.type() == 2:  # 2 = Polygon
                        partes = geom.asMultiPolygon()
                        if len(partes) > 1:
                            continue  # ignora multipolígono com múltiplas partes

                    erro = self.tr(
                        'Invalid geometry in feature ID {} of layer "{}"!',
                        'Geometria inválida na feição de ID {} da camada "{}"!'
                    ).format(feat.id(), nome_camada)
                    cont_erros_estrutura += 1
                    feedback.reportError(erro)
            return cont_erros_estrutura

        cont_erros_estrutura = validarGeometria(vertices, self.tr('Vertices (points)', 'Vértices (pontos)'))
        cont_erros_estrutura+= validarGeometria(limites, self.tr('Limits (lines)', 'Limites (linhas)'))
        cont_erros_estrutura+= validarGeometria(area, self.tr('Area (polygon)', 'Área (polígono)'))

        # Validar limites geográficos
        def validarLimitesGeograficos(layer, nome_camada):
            ext = layer.sourceExtent()
            xmin = ext.xMinimum()
            xmax = ext.xMaximum()
            ymin = ext.yMinimum()
            ymax = ext.yMaximum()

            if xmin < -180 or xmax > 180 or ymin < -90 or ymax > 90:
                raise QgsProcessingException(
                    self.tr(
                        'The layers are defined as geographic CRS, but their coordinates are outside valid longitude/latitude limits. Check whether the CRS was assigned incorrectly.',
                        'As camadas estão definidas em SRC geográfico, porém as coordenadas estão fora dos limites válidos de longitude/latitude. Verifique se o SRC foi atribuído incorretamente.'
                    )
                )
        validarLimitesGeograficos(vertices, self.tr('Vertices (points)', 'Vértices (pontos)'))
        validarLimitesGeograficos(limites, self.tr('Limits (lines)', 'Limites (linhas)'))
        validarLimitesGeograficos(area, self.tr('Area (polygon)', 'Área (polígono)'))

        # Validações das geometrias

        feedback.pushInfo(self.tr('Checking if each vertex of the Limit layer (line) has the corresponding one of the Vertex layer (point)...', 'Verificando se cada vértice da camada Limite (linha) tem o correspondente da camada Vértice (ponto)...'))
        for feat1 in limites.getFeatures():
            geom1 = feat1.geometry()
            if geom1:
                if geom1.isMultipart():
                    linha = feat1.geometry().asMultiPolyline()[0]
                else:
                    linha = feat1.geometry().asPolyline()
                for pnt in linha:
                    corresp = False
                    for feat2 in vertices.getFeatures():
                        geom2 = feat2.geometry()
                        if geom2:
                            vert = geom2.asPoint()
                            if vert == pnt:
                                corresp = True
                                continue
                    if not corresp:
                        cont += 1
                        X, Y = pnt.x(), pnt.y()
                        erro = self.tr('Point of the "line" layer has no correspondent in the "point" layer!',
                                    'Ponto de camada "linha" não possui correspondente na camada "ponto"!')
                        fet = QgsFeature(Fields)
                        fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(X,Y)))
                        fet.setAttributes([cont, feat1.id(), erro])
                        sink.addFeature(fet, QgsFeatureSink.Flag.FastInsert)

        feedback.pushInfo(self.tr('Checking if each vertex of the Area layer (polygon) has the corresponding one of the Vertex layer (point)...', 'Verificando se cada vértice da camada Área (polígono) tem o correspondente da camada Vértice (ponto)...'))
        for feat1 in area.getFeatures():
            geom1 = feat1.geometry()
            if geom1:
                if geom1.isMultipart():
                    pols = geom1.asMultiPolygon()
                else:
                    pols = [geom1.asPolygon()]
                for aneis in pols:
                    for anel in aneis:
                        for pnt in anel:
                            corresp = False
                            for feat2 in vertices.getFeatures():
                                geom2 = feat2.geometry()
                                if geom2:
                                    vert = geom2.asPoint()
                                    if vert == pnt:
                                        corresp = True
                                        continue
                            if not corresp:
                                cont += 1
                                X, Y = pnt.x(), pnt.y()
                                erro = self.tr('Point of the "area" layer has no correspondent in the "point" layer!',
                                            'Ponto de camada "área" não possui correspondente na camada "ponto"!')
                                fet = QgsFeature(Fields)
                                fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(X,Y)))
                                fet.setAttributes([cont, feat1.id(), erro])
                                sink.addFeature(fet, QgsFeatureSink.Flag.FastInsert)

        feedback.pushInfo(self.tr('Checking if each vertex of the Vertex layer (point) has the corresponding one of the Area layer (polygon)...', 'Verificando se cada vértice da camada Vértice (ponto) tem o correspondente da camada Área (polígono)...'))
        for feat1 in vertices.getFeatures():
            geom1 = feat1.geometry()
            if geom1:
                vert = geom1.asPoint()
                corresp = False
                for feat2 in area.getFeatures():
                    geom2 = feat2.geometry()
                    if geom2:
                        if geom2.isMultipart():
                            pols = geom2.asMultiPolygon()
                        else:
                            pols = [geom2.asPolygon()]
                        for aneis in pols:
                            for anel in aneis:
                                for pnt in anel:
                                    if vert == pnt:
                                        corresp = True
                                        break
                            if corresp:
                                break
                if not corresp:
                    cont += 1
                    X, Y = vert.x(), vert.y()
                    erro = self.tr('Point of the "point" layer has no correspondent in the "area" layer!',
                                'Ponto da camada "ponto" não possui correspondente na camada "área"!')
                    fet = QgsFeature(Fields)
                    fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(X,Y)))
                    fet.setAttributes([cont, feat1.id(), erro])
                    sink.addFeature(fet, QgsFeatureSink.Flag.FastInsert)

        feedback.pushInfo(self.tr('Checking if each vertex of the Vertex layer (point) has the corresponding one of the limit layer (line)...', 'Verificando se cada vértice da camada Vértice (ponto) tem o correspondente da camada Limite (linha)...'))
        for feat1 in vertices.getFeatures():
            geom1 = feat1.geometry()
            if geom1:
                vert = geom1.asPoint()
                geom1 = geom1.buffer(tolerancia,5)
                corresp = False
                for feat2 in limites.getFeatures():
                    geom2 = feat2.geometry()
                    if geom2:
                        if geom2.intersects(geom1):
                            corresp = True
                            break
                if not corresp:
                    cont += 1
                    X, Y = vert.x(), vert.y()
                    erro = self.tr('Point of the "point" layer has no correspondent in the "line" layer!',
                                'Ponto de camada "ponto" não possui correspondente na camada "linha"!')
                    fet = QgsFeature(Fields)
                    fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(X,Y)))
                    fet.setAttributes([cont, feat1.id(), erro])
                    sink.addFeature(fet, QgsFeatureSink.Flag.FastInsert)

        feedback.pushInfo(self.tr('Checking for duplicate vertices inside the vertex (point) layer...', 'Verificando vértices duplicados dentro da camada vértice (ponto)...'))
        pontos = set()
        for feat1 in vertices.getFeatures():
            geom1 = feat1.geometry()
            if geom1:
                vert = geom1.asPoint()
                chave = (vert.x(), vert.y())
                if chave not in pontos:
                    pontos.add(chave)
                else:
                    cont += 1
                    X, Y = vert.x(), vert.y()
                    erro = self.tr('Vertex of the "point" layer is duplicated!',
                                'Vértice da camada "ponto" está duplicado!')
                    fet = QgsFeature(Fields)
                    fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(X,Y)))
                    fet.setAttributes([cont, feat1.id(), erro])
                    sink.addFeature(fet, QgsFeatureSink.Flag.FastInsert)

        feedback.pushInfo(self.tr('Checking for duplicate vertices inside the limit (line) layer...', 'Verificando vértices duplicados dentro da camada limite (linha)...'))
        for feat1 in limites.getFeatures():
            geom1 = feat1.geometry()
            if geom1:
                if geom1.isMultipart():
                    linha = feat1.geometry().asMultiPolyline()[0]
                else:
                    linha = feat1.geometry().asPolyline()
                pontos = set()
                for pnt in linha:
                    chave = (pnt.x(), pnt.y())
                    if chave not in pontos:
                        pontos.add(chave)
                    else:
                        if not pnt == linha[-1]: # fechamento de anel linear
                            cont += 1
                            X, Y = pnt.x(), pnt.y()
                            erro = self.tr('Vertex of the "line" layer is duplicated!',
                                        'Vértice da camada "linha" está duplicado!')
                            fet = QgsFeature(Fields)
                            fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(X,Y)))
                            fet.setAttributes([cont, feat1.id(), erro])
                            sink.addFeature(fet, QgsFeatureSink.Flag.FastInsert)


        feedback.pushInfo(self.tr('Checking for duplicate vertices inside the area (polygon) layer...', 'Verificando vértices duplicados dentro da camada área (polígono)...'))
        for feat1 in area.getFeatures():
            geom1 = feat1.geometry()
            if geom1:
                if geom1.isMultipart():
                    pols = geom1.asMultiPolygon()
                else:
                    pols = [geom1.asPolygon()]
                for aneis in pols:
                    for anel in aneis:
                        pontos = set()
                        for pnt in anel[:-1]:
                            chave = (pnt.x(), pnt.y())
                            if chave not in pontos:
                                pontos.add(chave)
                            else:
                                cont += 1
                                X, Y = pnt.x(), pnt.y()
                                erro = self.tr('Vertex of the "area" layer is duplicated!',
                                            'Vértice da camada "área" está duplicado!')
                                fet = QgsFeature(Fields)
                                fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(X,Y)))
                                fet.setAttributes([cont, feat1.id(), erro])
                                sink.addFeature(fet, QgsFeatureSink.Flag.FastInsert)


        feedback.pushInfo(self.tr('Checking line layer orientation...', 'Verificando orientação da camada linha...'))
        for feat1 in limites.getFeatures():
            geom1 =  feat1.geometry()
            if geom1:
                if geom1.isMultipart():
                    ultimo_pnt = geom1.asMultiPolyline()[0][-1]
                    p0 = geom1.asMultiPolyline()[0][0]
                else:
                    ultimo_pnt = geom1.asPolyline()[-1]
                    p0 = geom1.asPolyline()[0]
                for feat2 in limites.getFeatures():
                    if feat1.id() != feat2.id():
                        geom2 = feat2.geometry()
                        if geom2:
                            if geom2.isMultipart():
                                primeiro_pnt = geom2.asMultiPolyline()[0][0]
                            else:
                                primeiro_pnt = geom2.asPolyline()[0]
                            if ultimo_pnt == primeiro_pnt:
                                break
                else:
                    if ultimo_pnt != p0: # não for fechamento de anel linear
                        cont += 1
                        X, Y = ultimo_pnt.x(), ultimo_pnt.y()
                        erro = self.tr('Problem with the orientation of the vertices of the line layer!',
                                    'Problema na orientação dos vértices da camada de linhas!')
                        fet = QgsFeature(Fields)
                        fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(X,Y)))
                        fet.setAttributes([cont, feat1.id(), erro])
                        sink.addFeature(fet, QgsFeatureSink.Flag.FastInsert)

        # Checar geometrias duplicadas para linhas
        feedback.pushInfo(self.tr('Checking for duplicate geometry line layer...', 'Verificando geometria duplicada na camada de linhas...'))
        geoms = set()
        for feat1 in limites.getFeatures():
            geom = feat1.geometry()
            if geom:
                wkt = geom.asWkt()
                if wkt not in geoms:
                    geoms.add(wkt)
                else:
                    cont += 1
                    pnt = geom.centroid().asPoint()
                    X, Y = pnt.x(), pnt.y()
                    erro = self.tr('Duplicated line geometry in feature ID {}!'.format(feat1.id()),
                                'Geometria linha duplicada na feição de ID {}!'.format(feat1.id()))
                    fet = QgsFeature(Fields)
                    fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(X,Y)))
                    fet.setAttributes([cont, feat1.id(), erro])
                    sink.addFeature(fet, QgsFeatureSink.Flag.FastInsert)

        # Checar geometrias duplicadas para polígono
        feedback.pushInfo(self.tr('Checking for duplicate geometry polygon layer...', 'Verificando geometria duplicada na camada de polígonos...'))
        geoms = set()
        for feat1 in area.getFeatures():
            geom = feat1.geometry()
            if geom:
                wkt = geom.asWkt()
                if wkt not in geoms:
                    geoms.add(wkt)
                else:
                    cont += 1
                    pnt = geom.centroid().asPoint()
                    X, Y = pnt.x(), pnt.y()
                    erro = self.tr('Duplicated polygon geometry in feature ID {}!'.format(feat1.id()),
                                'Geometria polígono duplicada na feição de ID {}!'.format(feat1.id()))
                    fet = QgsFeature(Fields)
                    fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(X,Y)))
                    fet.setAttributes([cont, feat1.id(), erro])
                    sink.addFeature(fet, QgsFeatureSink.Flag.FastInsert)

        # Verificar coordenada Z igual a Zero
        feedback.pushInfo(self.tr('Checking if any vertex has a dimension-Z equal to Zero...', 'Verificando se algum vértice tem cota Z igual a Zero...'))
        for feat1 in vertices.getFeatures():
            geom1 =  feat1.geometry()
            if geom1:
                try:
                    z = float(geom1.constGet().z())
                except Exception:
                    z = float('nan')
                pnt = geom1.asPoint()
                if str(z) == 'nan' or z == 0:
                    cont += 1
                    X, Y = pnt.x(), pnt.y()
                    erro = self.tr('Z altitude not filled in correctly!',
                                'Altitude Z não preenchida corretamente!')
                    fet = QgsFeature(Fields)
                    fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(X,Y)))
                    fet.setAttributes([cont, feat1.id(), erro])
                    sink.addFeature(fet, QgsFeatureSink.Flag.FastInsert)

        def avaliar_erros(cont):
            if cont == 0:
                return "🎉"
            elif cont <= 5:
                return "🙁"
            elif cont <= 20:
                return "😢"
            else:
                return "😱"

        total_erros = cont + cont_erros_estrutura

        if total_erros > 0:

            msg = self.tr(
                ' {} inconsistencies detected ({} topological errors and {} structural geometry errors)!',
                ' Foram encontradas {} inconsistências ({} erros topológicos e {} erros estruturais de geometria)!'
            ).format(total_erros, cont, cont_erros_estrutura)

            feedback.reportError(avaliar_erros(total_erros) + msg)

        else:
            feedback.pushInfo(
                avaliar_erros(total_erros) +
                self.tr(
                    ' Congratulations! No topological inconsistency was detected.',
                    ' Parabéns! Nenhuma inconsistência topológica foi detectada.'
                )
            )

        feedback.pushInfo(self.tr('Operation completed successfully!', 'Operação finalizada com sucesso!'))
        feedback.pushInfo('Leandro França - Eng Cart')
        self.SAIDA = dest_id
        return {self.OUTPUT: dest_id}

    def postProcessAlgorithm(self, context, feedback):
        layer = QgsProcessingUtils.mapLayerFromString(self.SAIDA, context)
        # Rotulação
        labeling = LabelConf(self.tr('type','tipo'))
        layer.setLabeling(labeling)
        layer.setLabelsEnabled(True)
        layer.triggerRepaint()
        # Simbologia
        renderer = SymbolSimplePoint(layer)
        layer.setRenderer(renderer)
        layer.triggerRepaint()

        return {}
