# -*- 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-03-07'
__copyright__ = '(C) 2024 by Tiago Prudencio e Leandro França'

from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (QgsProcessing,
					   QgsProcessingException,
					   QgsGeometry,
					   QgsProcessingParameterNumber,
					   QgsExpressionContextUtils,
					   QgsProcessingParameterBoolean,
					   QgsExpressionContext,
					   QgsProcessingParameterFeatureSource,
					   QgsFeatureRequest,
					   QgsProcessingAlgorithm,
					   QgsProcessingParameterFileDestination)

from math import floor, modf
from qgis.PyQt.QtGui import QIcon
from GeoINCRA.images.Imgs import *
import subprocess
import os, re
import shutil
import platform
from pathlib import Path


class createTemplate2(QgsProcessingAlgorithm):

	VERTICE = 'VERTICE'
	LIMITE  = 'LIMITE'
	PARCELA  ='PARCELA'
	OUTPUT = 'OUTPUT'
	DEC_COORD = 'DEC_COORD'
	DEC_PREC = 'DEC_PREC'
	VER_Z = 'VER_Z'

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

	def createInstance(self):
		return createTemplate2()

	def name(self):
		return 'GeoRural2ODS'

	def displayName(self):
		return self.tr('GeoRural para Planilha ODS')

	def group(self):
		return self.tr(self.groupId())

	def groupId(self):
		return ''

	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,regularização,fundiária'.split(',')

	def shortHelpString(self):
		txt = "Cria uma planilha ODS do SIGEF, preenchendo-a diretamente através de Macros, a partir das camadas do banco de dados GeoRural trabalhado no QGIS."

		footer = '''<div>
					  <div align="center">
					  <img style="width: 100%; height: auto;" src="data:image/jpg;base64,'''+ INCRA_GeoOne +'''
					  </div>
					  <div align="right">
					  <p align="right">
					  <a href="https://geoone.com.br/pvgeoincra2/"><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/georreferenciamento-de-imveis-rurais-com-o-plugin-geoincra-1690158094835"><span style="font-weight: bold;">Acesse seu curso na GeoOne</span></a>
					  </p>
					  <a target="_blank" rel="noopener noreferrer" href="https://geoone.com.br/"><img height="80" title="GeoOne" src="data:image/png;base64,'''+ GeoOne +'''"></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(
			QgsProcessingParameterFeatureSource(
				self.VERTICE,
				self.tr('Camada Vertice'),
				[QgsProcessing.TypeVectorPoint]
			)
		)

		self.addParameter(
			QgsProcessingParameterFeatureSource(
				self.LIMITE,
				self.tr('Camada Limite'),
				[QgsProcessing.TypeVectorLine]
			)
		)

		self.addParameter(
			QgsProcessingParameterFeatureSource(
				self.PARCELA,
				self.tr('Camada Parcela'),
				[QgsProcessing.TypeVectorPolygon]
			)
		)

		self.addParameter(
			QgsProcessingParameterNumber(
				self.DEC_COORD,
				self.tr('Casas decimais das coordenadas'),
				type = QgsProcessingParameterNumber.Type.Integer,
				defaultValue = 3,
				minValue = 3
			)
		)

		self.addParameter(
			QgsProcessingParameterNumber(
				self.DEC_PREC,
				self.tr('Casas decimais das precisões e altitude'),
				type = QgsProcessingParameterNumber.Type.Integer,
				defaultValue = 2,
				minValue = 2
			)
		)

		self.addParameter(
            QgsProcessingParameterBoolean(
                self.VER_Z,
                self.tr('Verificar preenchimento de cota Z'),
                defaultValue = True
            )
        )

		self.addParameter(
			QgsProcessingParameterFileDestination(
				self.OUTPUT,
				self.tr('Planilha ODS'),
				self.tr('Planilha OpenDocument (*.ods)')
			)
		)

	def vld_0(self, vertice, limite, parcela):
		if parcela is None or parcela.featureCount() != 1:
			raise QgsProcessingException('A camada Parcela deve conter exatamente uma feição selecionada!')
		if vertice is None or vertice.featureCount() == 0:
			raise QgsProcessingException('A camada Vértice não pode estar vazia!')
		if limite is None or limite.featureCount() == 0:
			raise QgsProcessingException('A camada Limite não pode estar vazia!')

	def vld_1(self, vertice):
		for feat in vertice.getFeatures():
			id_feat = feat.id()
			sigma_x = feat['sigma_x']
			sigma_y = feat['sigma_y']
			sigma_z = feat['sigma_z']
			metodo = feat['metodo_pos']
			tipo = feat['tipo_verti']
			vert = str(feat['vertice']).strip().upper()
			if sigma_x == None or sigma_y == None or sigma_z == None:
				raise QgsProcessingException(f'Preencha os valores dos sigmas (precisões) da feição de ID {id_feat}!')
			if not (0 <= sigma_x <= 10):
				raise QgsProcessingException(f'Erro no vértice {id_feat}: sigma_x fora do intervalo (0 a 10). Valor encontrado: {sigma_x}')
			if not (0 <= sigma_y <= 10):
				raise QgsProcessingException(f'Erro no vértice {id_feat}: sigma_y fora do intervalo (0 a 10). Valor encontrado: {sigma_y}')
			if not (0 <= sigma_z <= 10):
				raise QgsProcessingException(f'Erro no vértice {id_feat}: sigma_z fora do intervalo (0 a 10). Valor encontrado: {sigma_z}')
			if metodo not in ('PG1', 'PG2', 'PG3', 'PG4', 'PG5', 'PG6', 'PG7', 'PG8', 'PG9',
							'PT1', 'PT2', 'PT3', 'PT4', 'PT5', 'PT6', 'PT7', 'PT8', 'PT9',
							'PA1', 'PA2', 'PA3', 'PS1', 'PS2', 'PS3', 'PS4', 'PB1', 'PB2'):
				raise QgsProcessingException(f'Erro no vértice {id_feat}: método de posicionamento inválido: {metodo}')
			if tipo not in ('M', 'P', 'V'):
				raise QgsProcessingException(f'Erro no vértice {id_feat}: tipo de vértice inválido: {tipo}')
			if len(vert) < 7:
				raise QgsProcessingException(f'Erro no vértice {id_feat}: nome do vértice deve ter ao menos 7 caracteres. Valor encontrado: "{vert}"')
			if vert == 'NULL':
				raise QgsProcessingException(f'Erro no vértice {id_feat}: valor do código vértice está nulo ("NULL")')
		return True  # Validação bem-sucedida

	def vld_2(self,limite,vertice):
		pontos_vertice = {feat.geometry().asPoint() for feat in vertice.getFeatures()}
		for feat in limite.getFeatures():
			if feat['tipo'] not in ('LA1', 'LA2', 'LA3', 'LA4', 'LA5', 'LA6', 'LA7', 'LN1', 'LN2', 'LN3', 'LN4', 'LN5', 'LN6'):
				raise QgsProcessingException('Verifique os valores do atributo "tipo" na camada Limite!')
			if feat['confrontan']:
				if len(feat['confrontan']) < 3:
					raise QgsProcessingException('Verifique os valores do atributo "confrontante" da camada limite!')
			else:
				raise QgsProcessingException('Verifique os valores do atributo "confrontante" da camada limite!')
			for ponto in feat.geometry().asPolyline():
				if ponto not in pontos_vertice:
					raise QgsProcessingException('Ponto de coordenadas ({}, {}) da camada Limite não tem correspondente na camada Vértice!'.format(ponto.y(), ponto.x()))

	def vld_3(self,parcela,vertice):
		pontos_vertice = {feat.geometry().asPoint() for feat in vertice.getFeatures()}
		for feat in parcela.getFeatures():
			if feat.geometry().isMultipart():
				pols = feat.geometry().asMultiPolygon()
			else:
				pols = [feat.geometry().asPolygon()]
			for pol in pols:
				for ponto in pol[0]:
					if ponto not in pontos_vertice:
						raise QgsProcessingException('Ponto de coordenadas ({}, {}) da camada Parcela não tem correspondente na camada Vértice!'.format(ponto.y(), ponto.x()))

	def vld_z (self, vertice):
		for feat1 in vertice.getFeatures():
			z = float(feat1.geometry().constGet().z())
			if str(z) == 'nan' or z == 0:
				raise QgsProcessingException('Cota Z não preenchida ou igual a zero no ponto de coordenadas na feição de id {}!'.format(feat1.id()))
			if z > 3000 or z < -10:
				raise QgsProcessingException('Cota Z com valor "{}" na feição de id {} fora dos limites permitidos!'.format(z, feat1.id()))

	def dd2dms(self, dd, n_digits=3):
		dd = abs(dd)
		frac, graus = modf(dd)
		frac, minutos = modf(frac * 60)
		segundos = round(frac * 60, n_digits)
		if segundos == 60:
			minutos += 1
			segundos = 0
		if minutos == 60:
			graus += 1
			minutos = 0
		return f"{int(graus):02d} {int(minutos):02d} {segundos:0{3+n_digits}.{n_digits}f}".replace('.', ',')


	def vertice (self,pnt,vertice,dec_coord,dec_prec):
		dec_prec = str(dec_prec)
		for feat in vertice.getFeatures():
			vert = feat.geometry().asPoint()
			if vert == pnt:
				codigo = feat['vertice'].strip()
				longitude = self.dd2dms(vert.x(), dec_coord) + ' W'
				sigma_x = ('{:.'+ dec_prec + 'f}').format(feat['sigma_x']).replace('.',',')
				latitude = self.dd2dms(vert.y(), dec_coord) + str(' S' if vert.y() < 0 else self.dd2dms(vert.y(), 3) + ' N')
				sigma_y = ('{:.'+ dec_prec + 'f}').format(feat['sigma_y']).replace('.',',')
				z = float(feat.geometry().constGet().z())
				if str(z) != 'nan':
					altitude = ('{:.'+ dec_prec + 'f}').format(z).replace('.',',')
				else:
					altitude = '0,00'
					feedback.reportError('Advertência: Ponto de código {} está com altitude igual a 0 (zero). Verifique!'.format(codigo))
				sigma_z = ('{:.'+ dec_prec + 'f}').format(feat['sigma_z']).replace('.',',')
				metodo_pos = feat['metodo_pos']
				return codigo,longitude,sigma_x,latitude,sigma_y,altitude, sigma_z,metodo_pos

	def limite (self,pnt,pnt_seg,limite):
		for feat in limite.getFeatures():
			linha = feat.geometry().asPolyline()
			for k2, vert in enumerate(linha[:-1]):
				if vert == pnt and linha[k2 + 1] == pnt_seg:
					tipo = feat['tipo']
					confrontan = feat['confrontan'].strip()
					cns = str(feat['cns']).replace('NULL', '').strip()
					matricula = str(feat['matricula']).replace('NULL', '').strip()
					return tipo,confrontan,cns,matricula
		raise QgsProcessingException(f'Erro de topologia: Limite não encontrado para ({pnt.y()}, {pnt.x()}) -> ({pnt_seg.y()}, {pnt_seg.x()})')

	def generate_table_substitution(self,k, feat, vertice, limite, dec_coord, dec_prec):
		pnt_str = []
		for k1, pnt in enumerate(feat[:-1]):
			codigo, longitude, sigma_x, latitude, sigma_y, altitude, sigma_z, metodo_pos = self.vertice(pnt, vertice, dec_coord, dec_prec)
			pnt_seg = feat[k1 + 1]
			tipo, confrontan, cns, matricula = self.limite(pnt, pnt_seg, limite)
			k = k1 + 12

			values = [codigo, longitude, sigma_x, latitude, sigma_y, altitude, sigma_z, metodo_pos, tipo, cns, matricula, confrontan]

			pnt_str.append(self.format_doc_values(k, values))

		return "\n".join(pnt_str)

	def setInf (self,n,vertice, limite, data, feat, dec_coord, dec_prec, table_prefix):
		pnt_str = self.generate_table_substitution(n, feat, vertice, limite, dec_coord, dec_prec)

		table_marker = f'#{table_prefix}_{n+1}' if n != 0 else r'#table_1\b'
		data = re.sub(table_marker, pnt_str, data)

		return data

	def format_doc_values(self, k, values):
		fields = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"]
		return "\n".join(f'\tdoc.setValue("{fields[i]}{k}", "{value}")' for i, value in enumerate(values))

	def createSheets(self, mapping, data):
		for i,feat in enumerate (mapping[0]):
			if i==0:
				act_sheets = f'\tdoc.activate("perimetro_1")\n#table_1\n'
				data = data.replace('#table',act_sheets)
			else:
				add_sheets = f'\tdoc.copySheet("perimetro_1", "perimetro_1_{i+1}", "sobre")\n#copy_sheet\n'
				act_sheets = f'\tdoc.activate("perimetro_1_{i+1}")\n\tdoc.setValue("B4", "{i+1:03d}")\n#table_1_{i+1}\n\n#activate_sheet\n'
				data = data.replace('#copy_sheet',add_sheets)
				data = data.replace('#activate_sheet',act_sheets)

		k=1
		for parcela, features in list(mapping.items())[1:]:
			for feat in features:
				k+=1
				add_sheets = f'\tdoc.copySheet("perimetro_1", "perimetro_{k}", "sobre")\n#copy_sheet\n'
				act_sheets = f'\tdoc.activate("perimetro_{k}")\n\tdoc.setValue("B4", "{parcela:03d}")\n\tdoc.setValue("B5", "Interno")\n#table_{k}\n\n#activate_sheet\n'
				data = data.replace('#copy_sheet',add_sheets)
				data = data.replace('#activate_sheet',act_sheets)
		return data

	def reorder_polygon_points(self,pontos):
		ponto_inicial = None
		max_latitude = -90  # Latitude mínima possível
		for pnt in (pontos[:-1]):
			if pnt.y() > max_latitude:
				max_latitude = pnt.y()
				ponto_inicial = pnt

		index = pontos.index(ponto_inicial)
		pontos_reordenados = pontos[index:] + pontos[1:index+1]
		return(pontos_reordenados)

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

		vertice = self.parameterAsSource(
			parameters,
			self.VERTICE,
			context
		)
		if vertice is None:
			raise QgsProcessingException(self.invalidSourceError(parameters, self.VERTICE))

		limite = self.parameterAsSource(
			parameters,
			self.LIMITE,
			context
		)
		if limite is None:
			raise QgsProcessingException(self.invalidSourceError(parameters, self.LIMITE))

		context.setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck)
		parcela = self.parameterAsSource(
			parameters,
			self.PARCELA,
			context
		)
		if parcela is None:
			raise QgsProcessingException(self.invalidSourceError(parameters, self.PARCELA))

		dec_coord = self.parameterAsInt(
			parameters,
			self.DEC_COORD,
			context
		)

		dec_prec = self.parameterAsInt(
			parameters,
			self.DEC_PREC,
			context
		)

		ver_z = self.parameterAsBool(
		   parameters,
		   self.VER_Z,
		   context
		)

		output_path = self.parameterAsString(
			parameters,
			self.OUTPUT,
			context
		)
		if not output_path:
			raise QgsProcessingException('Caminho de saída inválido!')

		# Validações
		self.vld_0(vertice, limite, parcela)
		self.vld_1(vertice)
		self.vld_2(limite,vertice)
		self.vld_3(parcela,vertice)
		if ver_z:
			# Verificar altitude Z não preenchida
			self.vld_z (vertice)

		# Detectando o sistema operacional
		system_os = platform.system()

        # Definição de caminhos para LibreOffice
		if system_os == "Windows":
			libreoffice_path = Path("C:/Program Files/LibreOffice/program/soffice.exe")
		elif system_os == "Linux":
			libreoffice_path = Path("/usr/bin/soffice")
		elif system_os == "Darwin":  # macOS
			libreoffice_path = Path("/Applications/LibreOffice.app/Contents/MacOS/soffice")
		else:
			raise OSError("Sistema operacional não suportado")

        # Definição de caminho para macros no LibreOffice
		if system_os == "Windows":
			path_macro = Path.home() / "AppData/Roaming/LibreOffice/4/user/Scripts/python"
			src_macro = Path.home() / "AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins/GeoINCRA/algorithms/shp/macro.py"
			path_ods = Path.home() / "AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins/GeoINCRA/algorithms/shp/sigef_planilha_modelo_1.3_rc5.ods"
		elif system_os == "Darwin":
			path_macro = Path.home() / "Library/Application Support/LibreOffice/4/user/Scripts/python"
			src_macro = Path.home() / "Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins/GeoINCRA/algorithms/shp/macro.py"
			path_ods = Path.home() / "Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins/GeoINCRA/algorithms/shp/sigef_planilha_modelo_1.3_rc5.ods"
		else:
			path_macro = Path.home() / "Library/Application Support/LibreOffice/4/user/Scripts/python"
			src_macro = Path.home() / "Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins/GeoINCRA/algorithms/shp/macro.py"
			path_ods = Path.home() / "Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins/GeoINCRA/algorithms/shp/sigef_planilha_modelo_1.3_rc5.ods"

        # Criando diretório, se não existir
		path_macro.mkdir(parents=True, exist_ok=True)

        # Copiando macro do QGIS para LibreOffice
		dst_macro = path_macro / 'qgis_macro.py'
		shutil.copy(src_macro, dst_macro)


		# map
		nat_ser = {1:'Particular', 2:'Contrato com Administração Pública'}
		pessoa  = {1:'Física', 2:'Jurídica'}
		situacao = {1:'Imóvel Registrado', 2:'Área Titulada não Registrada', 3:'Área não Titulada'}
		natureza = {1:'Assentamento',2:'Assentamento Parcela',3:'Estrada',4:'Ferrovia',5:'Floresta Pública',6:'Gleba Pública',7:'Particular',8:'Perímetro Urbano',9:'Terra Indígena',10:'Terreno de Marinha',11:'Terreno Marginal',12:'Território Quilombola',13:'Unidade de Conservação'}

		feature = next(parcela.getFeatures())

		with open(os.path.join(path_macro,'qgis_macro.py'),'r') as arq:
			data = arq.read()
			data = data.replace("Natureza do serviço", nat_ser[feature['nat_serv']] if feature['nat_serv'] in nat_ser else '')
			data = data.replace("Tipo de pessoa", pessoa[feature['pessoa']] if feature['pessoa'] in pessoa else '')
			data = data.replace("Nome",str(feature['nome']).replace('NULL', '').replace('\n',''))
			data = data.replace("Denominação",str(feature['denominacao']).replace('NULL', '').replace('\n',''))
			data = data.replace("Situação", situacao[feature['situacao']] if feature['situacao'] in situacao else '')
			data = data.replace("Natureza da area", natureza[feature['natureza']] if feature['natureza'] in natureza else '')
			data = data.replace("Codigo do Imovel", str(feature['sncr']).replace('NULL', '').replace('\n',''))
			data = data.replace("Codigo do cartorio", str(feature['cod_cartorio']).replace('NULL', '').replace('\n',''))
			data = data.replace("Matricula",str(feature['matricula']).replace('NULL', '').replace('\n',''))
			municipio = str(feature['municipio']).replace('NULL', '').replace('\n','') +'-'+ str(feature['uf']).replace('NULL', '').replace('\n','')
			data = data.replace("Municipio", municipio)



		geom = feature.geometry()
		polygons = geom.asMultiPolygon() if geom.isMultipart() else [geom.asPolygon()]

		mapping = {0: []}
		for i, features in enumerate(polygons):
			mapping[0].append(self.reorder_polygon_points(features[0]))
			reorder = [self.reorder_polygon_points(feat) for feat in features[1:]]
			if reorder:
				mapping[i + 1] = reorder

		data = self.createSheets(mapping, data)

		for n, feat in enumerate(mapping[0]):
			data = self.setInf (n,vertice, limite, data, feat, dec_coord, dec_prec,table_prefix="table_1")

		k=0
		for prm ,features in list(mapping.items())[1:]:
			for feat in features:
				k+=1
				data = self.setInf (k,vertice, limite, data, feat, dec_coord, dec_prec,table_prefix="table")

		data = data.replace('output_path', output_path)

		with open(os.path.join(path_macro,'qgis_macro.py'),'w') as arq:
			arq.write(data)

		#executa macro
		try:
			subprocess.call(f"{libreoffice_path} "
						" --invisible "
						f"{path_ods} "
						'vnd.sun.star.script:qgis_macro.py$create_table?language=Python&location=user'
						)
		except:
			raise QgsProcessingException("Verifique se a versão do seu LibreOffice ou o seu SO estão atualizados!")

		os.remove(os.path.join(path_macro,'qgis_macro.py')) # Remove a macro temporária

		return {self.OUTPUT: output_path}
