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

"""
/***************************************************************************
 GeoINCRA
                                 A QGIS plugin
 Georreferenciamento de Imóveis Rurais
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2022-02-13
        copyright            : (C) 2022 by Tiago Prudencio e Leandro França
        email                : contato@geoone.com.br
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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__ = 'Tiago Prudencio e Leandro França'
__date__ = '2024-08-10'
__copyright__ = '(C) 2024 by Tiago Prudencio e Leandro França'

from qgis.PyQt.QtCore import QCoreApplication, QMetaType
from qgis.core import *
from qgis.PyQt.QtGui import QIcon, QFont, QColor
import zipfile
import os


class LayersFromSheet(QgsProcessingAlgorithm):

    ODS = 'ODS'
    VERTICE = 'VERTICE'
    LIMITE = 'LIMITE'
    PARCELA = 'PARCELA'

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return LayersFromSheet()

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

    def displayName(self):
        return self.tr('a. Importar planilha ODS (.ods)')

    def group(self):
        return self.tr('4. 📥 Importação como camadas no QGIS')

    def groupId(self):
        return 'importacao_camadas_qgis'

    def icon(self):
        return QIcon(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'images/geoincra_pb.png'))
    
    def tags(self):
        return 'GeoOne,GeoRural,INCRA,Sigef,memorial,ODS,planilha,conversão,tranformar,descritivo,documento,cartório,matrícula,regularização,fundiária,layer,geopackage'.split(',')
    
    def shortHelpString(self):
        txt = 'Esta ferramenta importa uma <b>Planilha ODS</b> no padrão do SIGEF/INCRA, carregando as camadas vétice (ponto), limite (linha) e parcela (polígono) no modelo GeoRural.'

        footer = '''<div>
                      <div align="center">
                      <a target="_blank" rel="noopener noreferrer" href="https://geoone.com.br/pvgeoincra/"><img title="Inscreva-se no curso de GeoINCRA" style="width: 100%; height: auto;" src="'''+ os.path.join(os.path.dirname(os.path.dirname(__file__)), 'images/INCRA_GeoOne.jpg') +'''"></a>
                      </div>
                      <div align="right">
                      <p align="right">
                      <a href="https://geoone.com.br/pvgeoincra/"><span style="font-weight: bold;">Conheça o curso de GeoINCRA no QGIS</span></a>
                      </p>
                      <p align="right">
                      <a href="https://portal.geoone.com.br/m/lessons/geoincra?classId=4081"><span style="font-weight: bold;">Acesse a aula sobre esta ferramenta no curso de GeoINCRA no GeoOne</span></a>
                      </p>
                      <a target="_blank" rel="noopener noreferrer" href="https://geoone.com.br/"><img title="GeoOne" width="280"  src="'''+ os.path.join(os.path.dirname(os.path.dirname(__file__)), 'images/GeoOne.png') +'''"></a>
                      <p><i>"Mapeamento automatizado, fácil e direto ao ponto é na GeoOne!"</i></p>
                      </div>
                    </div>'''
        return txt + footer

    def initAlgorithm(self, config=None):

        self.addParameter(
        QgsProcessingParameterFile(
            self.ODS,
            self.tr('Planilha ODS do Sigef'),
            fileFilter= 'Planilha ODF (*.ods *.ODS)'
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.VERTICE,
                self.tr('Vértices da planilha')
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.LIMITE,
                self.tr('Limites da planilha')
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.PARCELA,
                self.tr('Parcela da planilha')
            )
        )

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

        arquivo_ods = self.parameterAsString(
            parameters,
            self.ODS,
            context
        )

        if arquivo_ods is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.ODS))
        
        from ..dependencies import ensure_defusedxml
        if not ensure_defusedxml(feedback):
            raise QgsProcessingException(
                self.tr('Could not install or load defusedxml.')
            )
        from defusedxml import ElementTree as ET

        # Ler meridiano central da ODS
        with zipfile.ZipFile(arquivo_ods, 'r') as ods:
            with ods.open('content.xml') as content_file:
                tree = ET.parse(content_file)
                root = tree.getroot()
        # Namespaces usados no content.xml
        ns = {'form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'}
        # Procurar apenas o listbox com nome lstMeridianos
        listbox_meridianos = root.find(".//form:listbox[@form:name='lstMeridianos']", ns)
        if listbox_meridianos is not None:
            for option in listbox_meridianos.findall('form:option', ns):
                if option.get('{urn:oasis:names:tc:opendocument:xmlns:form:1.0}current-selected') == 'true':
                    MC = option.get('{urn:oasis:names:tc:opendocument:xmlns:form:1.0}label')


        # Iterar no arquivo ODS
        feedback.pushInfo('Lendo dados da planilha ODS...')
        with zipfile.ZipFile(arquivo_ods, 'r') as ods:
            with ods.open('content.xml') as content_file:
                tree = ET.parse(content_file)
                root = tree.getroot()

        # Namespace do OpenDocument
        ns = {
            'table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
            'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'
        }

        dic_perimetros = {}
        lista_perimetros = []

        for tabela in root.findall('.//table:table', ns):
            nome_aba = tabela.get('{urn:oasis:names:tc:opendocument:xmlns:table:1.0}name')
            if 'perimetro' in nome_aba:
                lista_perimetros += [nome_aba]
                dic_perimetros[nome_aba] = {'nome':'', 'lado':'', 'src':'', 'mc': '', 'hemisf': ''}
                vertices_lista = []
                for linha_idx, linha in enumerate(tabela.findall('table:table-row', ns)):
                    colunas = linha.findall('table:table-cell', ns)
                    linha_lista = []
                    for col_idx, celula in enumerate(colunas):
                        textos = celula.findall('text:p', ns)
                        valor = ' '.join([t.text for t in textos if t.text])
                        # print(f"[{linha_idx},{col_idx}] = {valor}")
                        if linha_idx > 10:
                            linha_lista += [valor] if valor else ['']
                        elif linha_idx == 2 and col_idx == 1:
                            dic_perimetros[nome_aba]['nome'] = valor  if valor else ''
                        elif linha_idx == 4 and col_idx == 1:
                            dic_perimetros[nome_aba]['lado'] = valor if valor else ''
                        elif linha_idx == 8 and col_idx == 1:
                            dic_perimetros[nome_aba]['src'] = valor if valor else ''
                        elif linha_idx == 8 and col_idx == 3:
                            dic_perimetros[nome_aba]['mc'] = MC
                        elif linha_idx == 8 and col_idx == 5:
                            dic_perimetros[nome_aba]['hemisf'] = valor if valor else ''
                    if linha_lista:
                        if linha_lista[0] != '':
                            if len(linha_lista) == 14:
                                vertices_lista += [linha_lista[0:-2]]
                            else:
                                vertices_lista += [linha_lista[0:9] + ['', ''] + [linha_lista[10]]]
                dic_perimetros[nome_aba]['vertices'] = vertices_lista

        # Número de perímetros da ODS
        num_tab = len(lista_perimetros)
        if num_tab < 1:
            raise QgsProcessingException("Verifique se a planilha ODS carregada segue o modelo do SIGEF/INCRA!")

        # Pegar dados de SRC
        feedback.pushInfo('Lendo Sistema de Referência de Coordenadas (SRC)...')
        try:
            src = dic_perimetros[lista_perimetros[0]]['src']
            hemisf = dic_perimetros[lista_perimetros[0]]['hemisf']
            mc = dic_perimetros[lista_perimetros[0]]['mc']
            if src == 'Geográfica':
                SRC = QgsCoordinateReferenceSystem('EPSG:4674')
            else:
                fuso = round((183+int(mc))/6)
                if hemisf[0].upper() == 'S':
                    SRC = QgsCoordinateReferenceSystem('EPSG:' + str(31960+fuso))
                elif hemisf[0].upper() == 'N':
                    SRC = QgsCoordinateReferenceSystem('EPSG:' + str(31954+fuso))
        except:
            raise QgsProcessingException("Erro ao carregar a planilha ODS!")

        feedback.pushInfo('O SRC da planilha é {}'.format(SRC.authid()))

        # Pegar dados do imóvel
        feedback.pushInfo('Obtendo dados da planilha ODS...')
        idendificacao = QgsVectorLayer(arquivo_ods + '|layername=identificacao', "planilha", "ogr")
        dic_nat_ser = {'Particular':1, 'Contrato com Administração Pública':2}
        dic_pessoa, dic_situacao  = {'Física':1, 'Jurídica':2}, {'Imóvel Registrado':1, 'Área Titulada não Registrada':2, 'Área não Titulada':3}
        dic_natureza = {'Assentamento':1,'Assentamento Parcela':2,'Estrada':3,'Ferrovia':4,'Floresta Pública':5,'Gleba Pública':6,'Particular':7,'Perímetro Urbano':8,'Terra Indígena':9,'Terreno de Marinha':10,'Terreno Marginal':11,'Território Quilombola':12,'Unidade de Conservação':13}
        for k, lin in enumerate(idendificacao.getFeatures()):
            valor = str(lin[1]).replace('NULL','')
            if k == 5:
                nome = valor
            elif k == 1:
                nat_serv = dic_nat_ser[valor]
            elif k == 4:
                pessoa = dic_pessoa[valor]
            elif k == 10:
                situacao = dic_situacao[valor]
            elif k == 11:
                natureza = dic_natureza[valor]
            elif k == 6:
                cpf_cnpj = valor
            elif k == 9:
                denominacao = valor
            elif k == 12:
                sncr = valor
            elif k == 13:
                cod_cartorio = valor
            elif k == 14:
                matricula_parcela = valor
            elif k == 16:
                try:
                    municipio = valor.split('-')[0]
                    uf = valor.split('-')[1]
                except:
                    municipio = ''
                    uf = ''

        # Criar camada de Pontos
        feedback.pushInfo('Criando camada Vértice...')
        Fields1 = QgsFields()
        itens  = {   'indice': QMetaType.Type.Int,
                     'vertice': QMetaType.Type.QString,
                     'tipo_verti': QMetaType.Type.QString,
                     'metodo_pos' : QMetaType.Type.QString,
                     'sigma_x' : QMetaType.Type.Double,
                     'sigma_y' : QMetaType.Type.Double,
                     'sigma_z' : QMetaType.Type.Double,
                     'lote' : QMetaType.Type.QString,
                     }
        for item in itens:
            Fields1.append(QgsField(item, itens[item]))

        (sink1, dest_id1) = self.parameterAsSink(
            parameters,
            self.VERTICE,
            context,
            Fields1,
            QgsWkbTypes.PointZ,
            SRC
        )
        if sink1 is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.VERTICE))

        # Criar camada de Linhas
        feedback.pushInfo('Criando camada Limite...')
        Fields2 = QgsFields()
        itens  = {   'tipo': QMetaType.Type.QString,
                     'confrontan': QMetaType.Type.QString,
                     'cns': QMetaType.Type.QString,
                     'matricula' : QMetaType.Type.QString,
                     'lote' : QMetaType.Type.QString,
                     }
        for item in itens:
            Fields2.append(QgsField(item, itens[item]))

        (sink2, dest_id2) = self.parameterAsSink(
            parameters,
            self.LIMITE,
            context,
            Fields2,
            QgsWkbTypes.LineStringZ,
            SRC
        )
        if sink2 is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.LIMITE))


        # Criar camada de Polígono
        feedback.pushInfo('Criando camada Parcela...')
        Fields3 = QgsFields()
        itens  = {   'nome': QMetaType.Type.QString,
                     'nat_serv': QMetaType.Type.Int,
                     'pessoa': QMetaType.Type.Int,
                     'cpf_cnpj' : QMetaType.Type.QString,
                     'denominacao': QMetaType.Type.QString,
                     'situacao': QMetaType.Type.Int,
                     'natureza': QMetaType.Type.Int,
                     'sncr': QMetaType.Type.QString,
                     'matricula': QMetaType.Type.QString,
                     'cod_cartorio': QMetaType.Type.QString,
                     'municipio': QMetaType.Type.QString,
                     'uf': QMetaType.Type.QString,
                     'resp_tec': QMetaType.Type.QString,
                     'reg_prof': QMetaType.Type.QString,
                     'data': QMetaType.Type.QDate,
                     'lote' : QMetaType.Type.QString,
                     }
        for item in itens:
            Fields3.append(QgsField(item, itens[item]))

        (sink3, dest_id3) = self.parameterAsSink(
            parameters,
            self.PARCELA,
            context,
            Fields3,
            QgsWkbTypes.MultiPolygonZ,
            SRC
        )
        if sink3 is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.PARCELA))

        for contagem, perimetro in enumerate(lista_perimetros):

            feedback.pushInfo('Alimentando camada Vértice (pontos) do {}...'.format(perimetro))

            lista = dic_perimetros[perimetro]['vertices']

            cont = 0
            pnts = []
            for item in lista:
                if src == 'Geográfica':
                    lon = item[1].replace(',','.').split(' ')
                    X = (float(lon[0]) + float(lon[1])/60 + float(lon[2])/3600)*(-1 if lon[3] == 'W' else 1)
                    lat = item[3].replace(',','.').split(' ')
                    Y = (float(lat[0]) + float(lat[1])/60 + float(lat[2])/3600)*(-1 if lat[3] == 'S' else 1)
                else:
                    X = float(item[1].replace(',','.'))
                    Y = float(item[3].replace(',','.'))
                Z = float(item[5].replace(',','.'))
                feat = QgsFeature(Fields1)
                pnts += [QgsPoint(X,Y,Z)]
                feat.setGeometry(QgsGeometry(QgsPoint(X,Y,Z)))
                cont += 1
                feat['indice'] = cont
                codigo = str(item[0])
                feat['vertice'] = codigo
                feat['tipo_verti'] = codigo.split('-')[1]
                feat['metodo_pos'] = str(item[7])
                feat['sigma_x'] = float(item[2].replace(',','.'))
                feat['sigma_y'] = float(item[4].replace(',','.'))
                feat['sigma_z'] = float(item[6].replace(',','.'))
                feat['lote'] = str(contagem+1)
                sink1.addFeature(feat, QgsFeatureSink.FastInsert)
                if feedback.isCanceled():
                    break


            feedback.pushInfo('Alimentando camada Limite (linha) do {}...'.format(perimetro))
            linha = []
            anterior_tipoLim = str(lista[0][8]).replace('NULL','')
            anterior_cns = str(lista[0][9]).replace('NULL','')
            anterior_mat = str(lista[0][10]).replace('NULL','')
            anterior_confr = str(lista[0][11]).replace('NULL','')

            for k, item in enumerate(lista):
                linha += [pnts[k]]
                tipoLim = str(item[8]).replace('NULL','')
                cns = str(item[9]).replace('NULL','')
                matricula = str(item[10]).replace('NULL','')
                confrontante = str(item[11]).replace('NULL','')
                if (tipoLim+cns+matricula+confrontante) != (anterior_tipoLim+anterior_cns+anterior_mat+anterior_confr):
                    feat = QgsFeature(Fields2)
                    feat.setGeometry(QgsGeometry(QgsLineString(linha)))
                    feat['tipo'] = anterior_tipoLim
                    feat['confrontan'] = anterior_confr
                    feat['cns'] = anterior_cns
                    feat['matricula'] = anterior_mat
                    feat['lote'] = str(contagem+1)
                    sink2.addFeature(feat, QgsFeatureSink.FastInsert)
                    anterior_tipoLim = tipoLim
                    anterior_cns = cns
                    anterior_mat = matricula
                    anterior_confr = confrontante
                    linha = [pnts[k]]
                if feedback.isCanceled():
                    break

            # Último segmento
            feat = QgsFeature(Fields2)
            if (tipoLim+cns+matricula+confrontante) == (anterior_tipoLim+anterior_cns+anterior_mat+anterior_confr):
                linha += [pnts[0]]
                feat['tipo'] = anterior_tipoLim
                feat['confrontan'] = anterior_confr
                feat['cns'] = anterior_cns
                feat['matricula'] = anterior_mat
            else:
                linha = [pnts[k], pnts[0]]
                feat['tipo'] = tipoLim
                feat['confrontan'] = confrontante
                feat['cns'] = cns
                feat['matricula'] = matricula

            feat['lote'] = str(contagem+1)
            feat.setGeometry(QgsGeometry(QgsLineString(linha)))
            sink2.addFeature(feat, QgsFeatureSink.FastInsert)


            feedback.pushInfo('Alimentando camada Parcela (polígono) do {}...'.format(perimetro))
            feat = QgsFeature(Fields3)
            feat['nome'] = nome
            feat['nat_serv'] = nat_serv
            feat['pessoa'] = pessoa
            feat['cpf_cnpj'] = cpf_cnpj
            feat['denominacao'] = denominacao
            feat['situacao'] = situacao
            feat['natureza'] = natureza
            feat['sncr'] = sncr
            feat['matricula'] = matricula_parcela
            feat['cod_cartorio'] = cod_cartorio
            feat['municipio'] = municipio
            feat['uf'] = uf
            feat['lote'] = str(contagem+1)
            mPol = QgsMultiPolygon()
            anel = QgsLineString(pnts+[pnts[0]])
            pol = QgsPolygon(anel)
            mPol.addGeometry(pol)
            newGeom = QgsGeometry(mPol)
            feat.setGeometry(newGeom)
            sink3.addFeature(feat, QgsFeatureSink.FastInsert)

            self.SAIDA_VERTICE = dest_id1
            self.SAIDA_LIMITE = dest_id2
            self.SAIDA_PARCELA = dest_id3

        return {self.VERTICE: dest_id1,
                self.LIMITE: dest_id2,
                self.PARCELA: dest_id3}

    def postProcessAlgorithm(self, context, feedback):
        # Recupera camadas (saídas do processamento)
        camada_vertice = QgsProcessingUtils.mapLayerFromString(self.SAIDA_VERTICE, context)
        camada_limite  = QgsProcessingUtils.mapLayerFromString(self.SAIDA_LIMITE, context)
        camada_parcela = QgsProcessingUtils.mapLayerFromString(self.SAIDA_PARCELA, context)

        try:

            # -------------------------------------------------------
            # 1) Criar/posicionar grupo no TOPO e inserir as 3 camadas
            # -------------------------------------------------------
            proj = QgsProject.instance()
            root = proj.layerTreeRoot()

            group_name = "Camadas da planilha ODS"
            group = root.findGroup(group_name)

            # cria no topo, ou recria no topo se já existir em outro lugar
            if group is None:
                group = root.insertGroup(0, group_name)  # topo do painel
            else:
                idx = root.children().index(group)
                if idx != 0:
                    root.removeChildNode(group)
                    group = root.insertGroup(0, group_name)

            # opcional: limpar conteúdo antigo do grupo a cada execução
            # group.removeAllChildren()

            # garante que as layers estão no projeto (registry)
            for lyr in (camada_vertice, camada_limite, camada_parcela):
                if proj.mapLayer(lyr.id()) is None:
                    proj.addMapLayer(lyr, False)

            # remove nodes existentes dessas layers (de onde estiverem), evitando duplicação
            def _detach_layer_node(layer):
                node = root.findLayer(layer.id())
                if node is not None and node.parent() is not None:
                    node.parent().removeChildNode(node)

            _detach_layer_node(camada_vertice)
            _detach_layer_node(camada_limite)
            _detach_layer_node(camada_parcela)

            # insere no grupo na ordem desejada (topo -> baixo)
            group.insertLayer(0, camada_vertice)
            group.insertLayer(1, camada_limite)
            group.insertLayer(2, camada_parcela)
            group.setExpanded(True)

            # -----------------------------
            # Helper rotulação (sem placement)
            # -----------------------------
            def _apply_labeling(layer, expr, font_size=10, color="white",
                                buffer_color="black", buffer_size=0.8, bold=True):
                label_settings = QgsPalLayerSettings()
                label_settings.fieldName = expr
                label_settings.isExpression = True
                label_settings.enabled = True

                text_format = QgsTextFormat()
                font = QFont("Arial", int(font_size))
                font.setBold(bool(bold))
                text_format.setFont(font)
                text_format.setSize(float(font_size))
                text_format.setColor(QColor(color))

                buffer_settings = QgsTextBufferSettings()
                buffer_settings.setEnabled(True)
                buffer_settings.setSize(float(buffer_size))
                buffer_settings.setColor(QColor(buffer_color))
                text_format.setBuffer(buffer_settings)

                label_settings.setFormat(text_format)

                labeling = QgsVectorLayerSimpleLabeling(label_settings)
                layer.setLabeling(labeling)
                layer.setLabelsEnabled(True)

            # -----------------------------
            # 2) VÉRTICE: simbologia + rotulação
            # -----------------------------
            symbol_vert = QgsMarkerSymbol.createSimple({
                'name': 'circle',
                'color': '255,0,0',
                'outline_color': '0,0,0',
                'outline_style': 'solid',
                'size': '2',
                'size_unit': 'MM'
            })
            camada_vertice.setRenderer(QgsSingleSymbolRenderer(symbol_vert))
            _apply_labeling(
                camada_vertice,
                expr='"vertice"',
                font_size=9,
                color="white",
                buffer_color="black",
                buffer_size=0.9,
                bold=True
            )
            camada_vertice.triggerRepaint()

            # -----------------------------
            # 3) LIMITE: categorizado por (confrontan-tipo) + rótulo com \n
            # -----------------------------
            expr_cat = "\"confrontan\" || '-' || \"tipo\""
            expr = QgsExpression(expr_cat)

            ctx = QgsExpressionContext()
            ctx.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(camada_limite))

            values = set()
            for f in camada_limite.getFeatures():
                ctx.setFeature(f)
                v = expr.evaluate(ctx)
                values.add("" if v is None else str(v))

            categories = []
            for v in sorted(values):
                sym = QgsLineSymbol.createSimple({'width': '0.6', 'width_unit': 'MM'})
                # cor determinística por hash
                h = abs(hash(v)) % 360
                sym.setColor(QColor.fromHsv(h, 200, 230))
                categories.append(QgsRendererCategory(v, sym, v))

            camada_limite.setRenderer(QgsCategorizedSymbolRenderer(expr_cat, categories))

            _apply_labeling(
                camada_limite,
                expr="\"confrontan\" || '\\n' || \"tipo\"",
                font_size=9,
                color="white",
                buffer_color="black",
                buffer_size=0.9,
                bold=True
            )
            camada_limite.triggerRepaint()

            # -----------------------------
            # 4) PARCELA: rosa claro 50% + rotulação com "nome"
            # -----------------------------
            sym_parc = QgsFillSymbol.createSimple({
                'color': '255,182,193,128',      # rosa claro com alpha
                'outline_color': '255,105,180,200',
                'outline_width': '0.6',
                'outline_width_unit': 'MM'
            })
            sym_parc.setOpacity(0.5)

            camada_parcela.setRenderer(QgsSingleSymbolRenderer(sym_parc))
            _apply_labeling(
                camada_parcela,
                expr="\"nome\"",
                font_size=10,
                color="black",
                buffer_color="white",
                buffer_size=1.0,
                bold=True
            )
            camada_parcela.triggerRepaint()

            return {}
        
        except:
            feedback.pushWarning("Não foi possível aplicar estilos na camada!")
            return {}