from qgis.core import QgsProject, QgsMessageLog, Qgis, QgsVectorLayer, QgsWkbTypes, QgsMapSettings, QgsMapRendererCustomPainterJob, QgsGeometry, QgsFeature, QgsFeatureRequest, QgsField, QgsDefaultValue, QgsDistanceArea, QgsEditorWidgetSetup
from qgis.PyQt.QtWidgets import QDialog, QCheckBox, QGraphicsScene, QGraphicsPixmapItem, QGraphicsLineItem
from PyQt5.QtGui import QImage, QPainter, QPixmap, QColor, QPen
from qgis.PyQt.QtCore import Qt, QSize, QTimer, QVariant
from qgis.utils import iface
from qgis.PyQt import uic
import math
import os


FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'GeoprocessarPoligonosReal.ui'))

class GeoPoligonosManager(QDialog, FORM_CLASS):
    def __init__(self, parent=None):
        """Constructor."""
        super(GeoPoligonosManager, self).__init__(parent)
        # Configura a interface do usuário a partir do Designer.
        self.setupUi(self)

        # Altera o título da janela
        self.setWindowTitle("Geoprocessamento de Polígonos")

        self.iface = iface

        self.scenePoligono = QGraphicsScene()
        self.graphicsView.setScene(self.scenePoligono)

        self._orig_geoms = {}  # Inicialize aqui!

        self._recorte_history = []  # lista de tuplas: (dict de orig_geoms, geometria atual de cada feição)

        # Preenche o comboBox com camadas de linha
        self.populate_combo_boxes()

        # Sempre que uma feição for adicionada, removida ou modificada na camada ativa
        self._camada_signal_connections = []

        self.update_layer_connections()

        # Conecta os sinais aos slots
        self.connect_signals()

    def connect_signals(self):

       # Conecta a mudança de seleção no comboBoxCamada para atualizar o checkBoxSeleciona
        self.comboBoxCamada.currentIndexChanged.connect(self.update_checkBoxSeleciona)
        self.comboBoxCamada.currentIndexChanged.connect(self.display_polygon)  # Conexão para atualizar o graphicsView

        # Conecta também para atualizar os sinais da camada (selectionChanged)
        self.comboBoxCamada.currentIndexChanged.connect(self.update_layer_connections)

        # Conecta o clique do botão pushButtonExecutar para executar
        self.pushButtonExecutar.clicked.connect(self.executar_recorte)

        # Conecta sinais do projeto para atualizar comboBox quando camadas forem adicionadas, removidas ou renomeadas
        QgsProject.instance().layersAdded.connect(self.populate_combo_boxes)
        QgsProject.instance().layersRemoved.connect(self.populate_combo_boxes)
        QgsProject.instance().layerWillBeRemoved.connect(self.populate_combo_boxes)

        # Sinais para atualizar um ao mudar o outro
        self.comboBoxCamada.currentIndexChanged.connect(self.update_comboBoxCamadaSobrep)
        self.comboBoxCamadaSobrep.currentIndexChanged.connect(self.update_comboBoxCamada)

        # Atualiza o checkbox sobreposição ao mudar a camada sobreposição
        self.comboBoxCamadaSobrep.currentIndexChanged.connect(self.update_checkBoxSelecionaSobrepos)

        # Conecta o botão pushButtonFechar
        self.pushButtonFechar.clicked.connect(self.close)

        QgsProject.instance().layerWillBeRemoved.connect(self._on_layer_removida)

        self.comboBoxCamadaSobrep.currentIndexChanged.connect(self.update_layer_connections_sobrep)

        self.comboBoxCamada.currentIndexChanged.connect(self._atualiza_estado_botao_executar)
        self.comboBoxCamadaSobrep.currentIndexChanged.connect(self._atualiza_estado_botao_executar)
        self.radioButtonDiferenca.toggled.connect(self._atualiza_estado_botao_executar)
        self.radioButtonItersecao.toggled.connect(self._atualiza_estado_botao_executar)
        self.radioButtonUniao.toggled.connect(self._atualiza_estado_botao_executar)
        QgsProject.instance().layersAdded.connect(self._atualiza_estado_botao_executar)
        QgsProject.instance().layersRemoved.connect(self._atualiza_estado_botao_executar)

        # Atualiza ao adicionar/remover camada
        QgsProject.instance().layersAdded.connect(lambda layers: self.display_polygon())
        QgsProject.instance().layersRemoved.connect(lambda layer_ids: self.display_polygon())

    def showEvent(self, event):
        """
        Sobrescreve o evento de exibição do diálogo para resetar os Widgets.
        """
        super(GeoPoligonosManager, self).showEvent(event)

        # Reset do checkBoxSeleciona
        checkBox = self.findChild(QCheckBox, 'checkBoxSeleciona')
        if checkBox:
            checkBox.setChecked(False)
            checkBox.setEnabled(False)

        checkbox = self.findChild(QCheckBox, 'checkBoxSelecionaSobrepos')
        if checkbox:
            checkbox.setChecked(False)
            checkbox.setEnabled(False)

        self.populate_combo_boxes()  # Atualiza o comboBoxCamada com as camadas disponíveis

        # Atualiza o estado do checkBoxSeleciona com base nas feições selecionadas
        self.update_checkBoxSeleciona()
        self.update_checkBoxSelecionaSobrepos()
        self.update_layer_connections_sobrep()

        self.display_polygon() # Ajusta a visualização quando o diálogo é mostrado

        self.update_layer_connections()  # Conecta os sinais da camada atual

        self._atualiza_estado_botao_executar()

    def _log_message(self, message, level=Qgis.Info):
        QgsMessageLog.logMessage(message, 'RECORTE', level=level)

    def mostrar_mensagem(self, texto, tipo, duracao=3, caminho_pasta=None, caminho_arquivos=None):
        """
        Exibe uma mensagem na barra de mensagens do QGIS, proporcionando feedback visual ao usuário.
        
        Funcionalidades:
        - Suporte a mensagens do tipo "Erro", "Sucesso" ou "Aviso", exibindo ícone e cor apropriados.
        - Duração da mensagem configurável (em segundos).
        - Opção de inserir botões para abrir uma pasta ou executar um arquivo diretamente da mensagem (apenas para "Sucesso").
        
        Parâmetros:

        texto : str
            Texto da mensagem a ser exibida.
        tipo : str
            Tipo da mensagem: "Erro", "Sucesso" ou "Aviso".
        duracao : int, opcional
            Tempo de exibição da mensagem em segundos (padrão: 3).
        caminho_pasta : str, opcional
            Caminho de uma pasta para abrir via botão (apenas para "Sucesso").
        caminho_arquivos : str, opcional
            Caminho de um arquivo para executar via botão (apenas para "Sucesso").
        """
        bar = self.iface.messageBar()  # Obtém a barra de mensagens do QGIS

        if tipo == "Erro":
            # Exibe mensagem de erro (ícone vermelho)
            bar.pushMessage("Erro", texto, level=Qgis.Critical, duration=duracao)

        elif tipo == "Sucesso":
            # Cria mensagem de sucesso (ícone azul/informativo)
            msg = bar.createMessage("Sucesso", texto)

            # Adiciona botão para abrir pasta, se informado
            if caminho_pasta:
                botao_abrir_pasta = QPushButton("Abrir Pasta")
                botao_abrir_pasta.clicked.connect(lambda: os.startfile(caminho_pasta))
                msg.layout().insertWidget(1, botao_abrir_pasta)  # Insere botão à esquerda do texto

            # Adiciona botão para executar arquivo, se informado
            if caminho_arquivos:
                botao_executar = QPushButton("Executar")
                botao_executar.clicked.connect(lambda: os.startfile(caminho_arquivos))
                msg.layout().insertWidget(2, botao_executar)

            # Exibe a mensagem na barra
            bar.pushWidget(msg, level=Qgis.Info, duration=duracao)

        elif tipo == "Aviso":
            # Exibe mensagem de aviso (ícone amarelo)
            bar.pushMessage("Aviso", texto, level=Qgis.Warning, duration=duracao)

    def update_checkBoxSeleciona(self):
        """
        Atualiza o estado do checkBoxSeleciona com base na seleção de feições da camada atualmente selecionada.

        Parâmetros:
        self : objeto
            Referência à instância atual da classe.

        A função realiza as seguintes ações:
        - Obtém o ID da camada atualmente selecionada no comboBoxCamada.
        - Se uma camada válida for encontrada, verifica a quantidade de feições selecionadas na camada.
        - Se houver feições selecionadas, o checkBoxSeleciona é ativado.
        - Se não houver feições selecionadas ou a camada não for válida, o checkBoxSeleciona é desativado e desmarcado.
        """
        layer_id = self.comboBoxCamada.currentData()  # Obtém o ID da camada selecionada no comboBoxCamada
        if layer_id:  # Verifica se há uma camada selecionada
            layer = QgsProject.instance().mapLayer(layer_id)  # Obtém a camada correspondente ao ID
            if layer:  # Verifica se a camada existe
                selected_features = layer.selectedFeatureCount()  # Conta o número de feições selecionadas na camada
                if selected_features > 0:  # Se houver feições selecionadas, ativa o checkBoxSeleciona
                    self.findChild(QCheckBox, 'checkBoxSeleciona').setEnabled(True)
                else:  # Se não houver feições selecionadas, desativa o checkBoxSeleciona e o desmarca
                    self.findChild(QCheckBox, 'checkBoxSeleciona').setEnabled(False)
                    self.findChild(QCheckBox, 'checkBoxSeleciona').setChecked(False)
            else:  # Se a camada não for válida, desativa o checkBoxSeleciona e o desmarca
                self.findChild(QCheckBox, 'checkBoxSeleciona').setEnabled(False)
                self.findChild(QCheckBox, 'checkBoxSeleciona').setChecked(False)
        else:  # Se não houver uma camada selecionada, desativa o checkBoxSeleciona e o desmarca
            self.findChild(QCheckBox, 'checkBoxSeleciona').setEnabled(False)
            self.findChild(QCheckBox, 'checkBoxSeleciona').setChecked(False)

    def update_layer_connections(self):
        """
        Conecta os sinais da camada selecionada no comboBoxCamada para garantir que 
        a interface (incluindo o graphicsView e checkBoxSeleciona) seja atualizada 
        sempre que houver mudanças na seleção ou nas feições (adição, remoção, alteração).
        """
        # Desconecta anteriores (evita múltiplas conexões)
        for old_signal in getattr(self, "_camada_signal_connections", []):
            try:
                old_signal[0].featureAdded.disconnect(old_signal[1])
                old_signal[0].featureDeleted.disconnect(old_signal[2])
                old_signal[0].geometryChanged.disconnect(old_signal[3])
            except Exception:
                pass
        self._camada_signal_connections = []

        layer_id = self.comboBoxCamada.currentData()
        if layer_id:
            layer = QgsProject.instance().mapLayer(layer_id)
            if layer:
                # Conecta sinais relevantes para atualizar o graphicsView
                def on_feature_added(fid): self.display_polygon()
                def on_feature_deleted(fid): self.display_polygon()
                def on_geometry_changed(fid, geom): self.display_polygon()
                layer.featureAdded.connect(on_feature_added)
                layer.featureDeleted.connect(on_feature_deleted)
                layer.geometryChanged.connect(on_geometry_changed)
                self._camada_signal_connections.append(
                    (layer, on_feature_added, on_feature_deleted, on_geometry_changed))
                # Já conecta para atualizar seleção também
                layer.selectionChanged.connect(self.update_checkBoxSeleciona)
                layer.selectionChanged.connect(self.display_polygon)
                self.update_checkBoxSeleciona()
        else:
            self.update_checkBoxSeleciona()

    def _atualiza_estado_botao_executar(self):
        # Checa se um radiobutton está selecionado
        algum_radio = (
            self.radioButtonDiferenca.isChecked() or
            self.radioButtonItersecao.isChecked() or
            self.radioButtonUniao.isChecked())
        # Checa se há camadas nos combos
        camada_id  = self.comboBoxCamada.currentData()
        sobrep_id  = self.comboBoxCamadaSobrep.currentData()
        camada     = QgsProject.instance().mapLayer(camada_id) if camada_id else None
        sobrep     = QgsProject.instance().mapLayer(sobrep_id) if sobrep_id else None
        camadas_ok = camada is not None and sobrep is not None

        # SRC iguais?
        src_iguais = camadas_ok and camada.crs() == sobrep.crs()

        # Mensagem se SRC diferentes
        if camadas_ok and not src_iguais:
            self.mostrar_mensagem(
                "As camadas selecionadas possuem sistemas de referência (SRC) diferentes. "
                "Operações podem gerar resultados inesperados.",
                "Aviso", duracao=3)

        # Ativa botão apenas se tudo ok
        self.pushButtonExecutar.setEnabled(algum_radio and camadas_ok and src_iguais)

    def display_polygon(self):
        """
        Atualiza a exibição do(s) polígono(s) no QGraphicsView.
        - Se houver feições selecionadas, exibe e aproxima para elas.
        - Caso contrário, exibe todos os polígonos da camada.
        """
        self.scenePoligono.clear()
        layer_id = self.comboBoxCamada.currentData()
        layer = QgsProject.instance().mapLayer(layer_id)

        if not layer or not isinstance(layer, QgsVectorLayer):
            return

        # Filtra apenas polígonos
        feats = [f for f in layer.getFeatures() if f.geometry().type() == QgsWkbTypes.PolygonGeometry]
        if not feats:
            if self.isVisible():
                self.mostrar_mensagem("A camada de polígono não contém feições.", "Erro")
            return

        selected = layer.selectedFeatures()
        if selected:
            # Unifica os polígonos selecionados
            geom = QgsGeometry.unaryUnion([f.geometry() for f in selected])
            bounding_box = geom.boundingBox()
        else:
            # Unifica todos os polígonos da camada
            geom = QgsGeometry.unaryUnion([f.geometry() for f in feats])
            bounding_box = geom.boundingBox()

        if geom is None or geom.isEmpty() or bounding_box.isNull():
            if self.isVisible():
                self.mostrar_mensagem("A geometria não contém dados válidos.", "Erro")
            return

        # Configuração do renderizador
        map_settings = QgsMapSettings()
        map_settings.setLayers([layer])
        map_settings.setBackgroundColor(QColor(255, 255, 255))
        width = self.graphicsView.viewport().width()
        height = self.graphicsView.viewport().height()
        map_settings.setOutputSize(QSize(width, height))
        map_settings.setExtent(bounding_box)

        # Renderiza a imagem
        image = QImage(width, height, QImage.Format_ARGB32)
        image.fill(Qt.transparent)
        painter = QPainter(image)
        render_job = QgsMapRendererCustomPainterJob(map_settings, painter)
        render_job.start()
        render_job.waitForFinished()
        painter.end()

        # Exibe no QGraphicsView
        pixmap = QPixmap.fromImage(image)
        pixmap_item = QGraphicsPixmapItem(pixmap)
        self.scenePoligono.addItem(pixmap_item)
        self.graphicsView.setSceneRect(pixmap_item.boundingRect())
        self.graphicsView.fitInView(self.scenePoligono.sceneRect(), Qt.KeepAspectRatio)

    def update_comboBoxCamada(self):
        """
        Atualiza comboBoxCamada para não permitir seleção igual à do comboBoxCamadaSobrep.
        (Opcional: caso queira evitar a inversão)
        """
        sobrep_id = self.comboBoxCamadaSobrep.currentData()
        current_id = self.comboBoxCamada.currentData()
        if current_id == sobrep_id:
            # Seleciona outra camada, se possível
            for i in range(self.comboBoxCamada.count()):
                if self.comboBoxCamada.itemData(i) != sobrep_id:
                    self.comboBoxCamada.setCurrentIndex(i)
                    break

    def update_checkBoxSelecionaSobrepos(self):
        """
        Atualiza o estado do checkBoxSelecionaSobrepos com base na seleção de feições
        da camada atualmente selecionada no comboBoxCamadaSobrep.
        """
        layer_id = self.comboBoxCamadaSobrep.currentData()  # ID da camada de sobreposição
        checkbox = self.findChild(QCheckBox, 'checkBoxSelecionaSobrepos')
        if layer_id and checkbox:
            layer = QgsProject.instance().mapLayer(layer_id)
            if layer:
                selected_features = layer.selectedFeatureCount()
                if selected_features > 0:
                    checkbox.setEnabled(True)
                else:
                    checkbox.setEnabled(False)
                    checkbox.setChecked(False)
            else:
                checkbox.setEnabled(False)
                checkbox.setChecked(False)
        else:
            if checkbox:
                checkbox.setEnabled(False)
                checkbox.setChecked(False)

    def update_layer_connections_sobrep(self):
        """
        Conecta (exclusivamente) o sinal selectionChanged da camada escolhida
        no comboBoxCamadaSobrep ao update_checkBoxSelecionaSobrepos.
        """
        # Desconecta o layer anterior, se houver
        try:
            if hasattr(self, "_sobrep_layer_conectado") and self._sobrep_layer_conectado:
                self._sobrep_layer_conectado.selectionChanged.disconnect(
                    self.update_checkBoxSelecionaSobrepos)
        except Exception:
            pass

        layer_id = self.comboBoxCamadaSobrep.currentData()
        novo_layer = QgsProject.instance().mapLayer(layer_id) if layer_id else None

        if novo_layer:
            novo_layer.selectionChanged.connect(self.update_checkBoxSelecionaSobrepos)
            self._sobrep_layer_conectado = novo_layer
        else:
            self._sobrep_layer_conectado = None

        # Atualiza imediatamente o estado do checkbox
        self.update_checkBoxSelecionaSobrepos()

    def populate_combo_boxes(self):
        """
        Atualiza as opções dos comboBoxCamada e comboBoxCamadaSobrep, listando todas as camadas de polígonos disponíveis no projeto e preservando a seleção atual sempre que possível.
        """
        # Desconecta nameChanged antigos
        if hasattr(self, "_layer_name_change_connections"):
            for lyr, slot in self._layer_name_change_connections:
                try:
                    lyr.nameChanged.disconnect(slot)
                except Exception:
                    pass
        self._layer_name_change_connections = []

        current_layer_id  = self.comboBoxCamada.currentData()
        current_sobrep_id = self.comboBoxCamadaSobrep.currentData()

        layer_list = QgsProject.instance().mapLayers().values()
        polygon_layers = [
            layer for layer in layer_list
            if isinstance(layer, QgsVectorLayer)
            and QgsWkbTypes.geometryType(layer.wkbType()) == QgsWkbTypes.PolygonGeometry]

        # Repovoa comboBoxCamada
        self.comboBoxCamada.blockSignals(True)
        self.comboBoxCamada.clear()
        for layer in polygon_layers:
            self.comboBoxCamada.addItem(layer.name(), layer.id())
        if current_layer_id:
            idx = self.comboBoxCamada.findData(current_layer_id)
            if idx != -1:
                self.comboBoxCamada.setCurrentIndex(idx)
        self.comboBoxCamada.blockSignals(False)

        # Repovoa comboBoxCamadaSobrep
        self.update_comboBoxCamadaSobrep()   # já cuida de excluir camada repetida

        # Conecta nameChanged de cada camada
        def _refresh():       # slot simples que repovoa tudo
            self.populate_combo_boxes()

        for layer in polygon_layers:
            layer.nameChanged.connect(_refresh)
            self._layer_name_change_connections.append((layer, _refresh))

    def update_comboBoxCamadaSobrep(self):
        """
        Atualiza as opções do comboBoxCamadaSobrep, exibindo apenas camadas de polígono diferentes da camada selecionada no comboBoxCamada.
        Mantém a seleção atual sempre que possível.
        """
        camada_id = self.comboBoxCamada.currentData()
        current_sobrep_id = self.comboBoxCamadaSobrep.currentData()

        layer_list = QgsProject.instance().mapLayers().values()
        polygon_layers = [
            layer for layer in layer_list 
            if isinstance(layer, QgsVectorLayer) 
            and QgsWkbTypes.geometryType(layer.wkbType()) == QgsWkbTypes.PolygonGeometry
            and layer.id() != camada_id]

        # Remove a camada usada diretamente para edição caso checkboxNova esteja desmarcado
        if not self.checkBoxNova.isChecked():
            if current_sobrep_id in [layer.id() for layer in polygon_layers]:
                polygon_layers = [layer for layer in polygon_layers if layer.id() != current_sobrep_id]

        self.comboBoxCamadaSobrep.blockSignals(True)
        self.comboBoxCamadaSobrep.clear()
        for layer in polygon_layers:
            self.comboBoxCamadaSobrep.addItem(layer.name(), layer.id())
        self.comboBoxCamadaSobrep.blockSignals(False)

        if self.comboBoxCamadaSobrep.count() > 0:
            if current_sobrep_id:
                idx = self.comboBoxCamadaSobrep.findData(current_sobrep_id)
                self.comboBoxCamadaSobrep.setCurrentIndex(idx if idx != -1 else 0)
            else:
                self.comboBoxCamadaSobrep.setCurrentIndex(0)

        # Garante que o checkbox reflita a seleção da NOVA camada
        self.update_layer_connections_sobrep()

    def _desativa_atualizacao_ao_vivo(self):
        """
        Desconecta todos os sinais de atualização em tempo real e limpa referências.
        """
        try:
            if getattr(self, "recorte_layer", None):
                self.recorte_layer.geometryChanged.disconnect(self._on_recorte_geom_changed)
                self.recorte_layer.featureAdded.disconnect(self._on_recorte_feat_added)
                self.recorte_layer.featureDeleted.disconnect(self._on_recorte_feat_deleted)
                # Desconecte estes também!
                self.recorte_layer.featureAdded.disconnect(self._on_feature_added_resultado)
                self.recorte_layer.geometryChanged.disconnect(self._on_geometry_changed_resultado)
            if getattr(self, "mascara_layer", None):
                self.mascara_layer.geometryChanged.disconnect(self._on_mascara_alterada)
                self.mascara_layer.featureAdded.disconnect(self._on_mascara_alterada)
                self.mascara_layer.featureDeleted.disconnect(self._on_mascara_alterada)
        except Exception:
            pass

        self.recorte_layer = None
        self.mascara_layer = None
        self._union_mascara = None
        self._orig_geoms = {}
        self._recorte_history = []

    def _ativa_atualizacao_ao_vivo(self):
        """
        Liga/desliga sinais para atualização em tempo real após criar a camada Recorte.
        """
        # evita múltiplas conexões
        try:
            self.recorte_layer.geometryChanged.disconnect(self._on_recorte_geom_changed)
            self.mascara_layer.geometryChanged.disconnect(self._on_mascara_alterada)
            self.mascara_layer.featureAdded.disconnect(self._on_mascara_alterada)
            self.mascara_layer.featureDeleted.disconnect(self._on_mascara_alterada)
        except Exception:
            pass

        # pré-calcula a união da máscara
        self._union_mascara = QgsGeometry.unaryUnion(
            [f.geometry() for f in self.mascara_layer.getFeatures()])

        # conecta sinais
        self.recorte_layer.geometryChanged.connect(self._on_recorte_geom_changed)

        self.mascara_layer.geometryChanged.connect(self._on_mascara_alterada)
        self.mascara_layer.featureAdded.connect(self._on_mascara_alterada)
        self.mascara_layer.featureDeleted.connect(self._on_mascara_alterada)

        self.recorte_layer.featureAdded.connect(self._on_recorte_feat_added) #novo
        self.recorte_layer.featureDeleted.connect(self._on_recorte_feat_deleted)

    def _on_recorte_feat_deleted(self, fid):
        """
        Remove a referência à geometria original da feição deletada da camada de recorte.
        Garante que o dicionário _orig_geoms não fique com dados órfãos.
        """
        if fid in self._orig_geoms:
            del self._orig_geoms[fid]

    def _coletar_parametros_recorte(self):
        """
        Coleta e valida todos os parâmetros necessários para o recorte.
        Retorna tuple de dados prontos ou None se erro.
        """
        entrada_id = self.comboBoxCamada.currentData()
        sobrep_id = self.comboBoxCamadaSobrep.currentData()

        camada_entrada = QgsProject.instance().mapLayer(entrada_id)
        camada_sobrep = QgsProject.instance().mapLayer(sobrep_id)

        if not camada_entrada or not camada_sobrep:
            self.mostrar_mensagem("Selecione corretamente as camadas.", "Erro")
            return None

        feats_entrada = (
            camada_entrada.selectedFeatures()
            if self.checkBoxSeleciona.isChecked()
            else list(camada_entrada.getFeatures()))

        feats_sobrep = (
            camada_sobrep.selectedFeatures()
            if self.checkBoxSelecionaSobrepos.isChecked()
            else list(camada_sobrep.getFeatures()))

        if not feats_entrada or not feats_sobrep:
            self.mostrar_mensagem("Necessário ao menos uma feição de cada camada.", "Erro")
            return None

        geom_sobrep = QgsGeometry.unaryUnion([f.geometry() for f in feats_sobrep])

        if not geom_sobrep or geom_sobrep.isEmpty():
            self.mostrar_mensagem("Geometria inválida para sobreposição.", "Erro")
            return None

        fid_to_original_geom = {feat.id(): QgsGeometry(feat.geometry()) for feat in feats_entrada}

        return (camada_entrada, camada_sobrep, feats_entrada, feats_sobrep, geom_sobrep, fid_to_original_geom)

    def closeEvent(self, event):
        # ao fechar, desativa atualizações
        self._desativa_atualizacao_ao_vivo()
        super().closeEvent(event)

    def _on_layer_removida(self, layer_id):
        """Desconecta os sinais se a camada recorte ou máscara for removida."""
        if hasattr(self, "recorte_layer") and self.recorte_layer:
            if self.recorte_layer.id() == layer_id:
                self._desativa_atualizacao_ao_vivo()
        if hasattr(self, "mascara_layer") and self.mascara_layer:
            if self.mascara_layer.id() == layer_id:
                self._desativa_atualizacao_ao_vivo()

    def executar_recorte(self):
        """
        Executa a operação selecionada (diferença, interseção ou união) entre as camadas escolhidas.
        Valida parâmetros, processa a operação e ativa ou desativa atualizações dinâmicas conforme necessário.
        """
        # Identifica a operação escolhida
        if self.radioButtonDiferenca.isChecked():
            operacao = 'diferenca'
        elif self.radioButtonItersecao.isChecked():
            operacao = 'intersecao'
        elif self.radioButtonUniao.isChecked():
            operacao = 'uniao'
        else:
            self.mostrar_mensagem(
                "Selecione o modo de operação (Recortar, Diferença, Interseção ou União).", "Aviso")
            return

        # Coleta parâmetros
        params = self._coletar_parametros_recorte()
        if params is None:
            return

        camada_entrada, camada_sobrep, feats_entrada, feats_sobrep, geom_sobrep, fid_to_original_geom = params

        # Executa operação
        resultado_layer = self._processar_recorte_operacao(operacao, camada_entrada, camada_sobrep, feats_entrada, geom_sobrep, fid_to_original_geom)
        if not resultado_layer:
            return

        # Pós-processamento
        self.recorte_layer = resultado_layer
        self.mascara_layer = camada_sobrep

        if operacao in ('diferenca', 'intersecao'):
            # só estes modos precisam do “ao vivo”
            self._operacao_atual = operacao
            self._ativa_atualizacao_ao_vivo()
        else:
            # União (e outros futuros) não usam atualização dinâmica
            self._desativa_atualizacao_ao_vivo()

    def _on_mascara_alterada(self, *args):
        """
        Atualiza a geometria das feições da camada de recorte quando a máscara (camada de sobreposição) é alterada.
        Aplica a operação selecionada (diferença ou interseção) para cada feição.
        """
        if (not self.recorte_layer or not QgsProject.instance().mapLayer(self.recorte_layer.id())):
            self._desativa_atualizacao_ao_vivo()
            return

        self._union_mascara = QgsGeometry.unaryUnion(
            [f.geometry() for f in self.mascara_layer.getFeatures()])

        try:
            self.recorte_layer.blockSignals(True)
            for feat in self.recorte_layer.getFeatures():
                orig = self._orig_geoms.get(feat.id())
                if not orig:
                    continue
                dx = feat.geometry().centroid().asPoint().x() - orig.centroid().asPoint().x()
                dy = feat.geometry().centroid().asPoint().y() - orig.centroid().asPoint().y()
                geom = QgsGeometry(orig)
                geom.translate(dx, dy)
                # Aplica operação conforme self._operacao_atual
                if getattr(self, '_operacao_atual', 'diferenca') == 'intersecao':
                    nova_geom = geom.intersection(self._union_mascara)
                else:
                    nova_geom = geom.difference(self._union_mascara)
                self.recorte_layer.blockSignals(True)
                self.recorte_layer.changeGeometry(feat.id(), nova_geom)
                # atualiza área/perímetro também:
                self.atualizar_valores_poligono(self.recorte_layer, feat.id())
            self.recorte_layer.blockSignals(False)
        finally:
            self.recorte_layer.blockSignals(False)

    def _on_recorte_geom_changed(self, fid, nova_geom):
        """
        Trata a alteração de geometria de uma feição na camada de recorte.
        Aplica a operação selecionada (diferença ou interseção) em relação à máscara e atualiza os atributos de área e perímetro.
        """
        operacao = getattr(self, '_operacao_atual', 'diferenca')
        if not self.recorte_layer:
            return
        if not QgsProject.instance().mapLayer(self.recorte_layer.id()):
            self._desativa_atualizacao_ao_vivo()
            return

        orig = self._orig_geoms.get(fid)
        if not orig:
            return
        if not self._union_mascara:
            return

        if operacao == 'intersecao':
            nova_geom_final = nova_geom.intersection(self._union_mascara)
        else:
            bb_orig = orig.boundingBox()
            bb_new  = nova_geom.boundingBox()
            dx = bb_new.xMinimum() - bb_orig.xMinimum()
            dy = bb_new.yMinimum() - bb_orig.yMinimum()
            geom = QgsGeometry(orig)
            geom.translate(dx, dy)
            nova_geom_final = geom.difference(self._union_mascara)

        if nova_geom_final and not nova_geom_final.isEmpty():
            self.recorte_layer.blockSignals(True)
            self.recorte_layer.changeGeometry(fid, nova_geom_final)
            self.recorte_layer.blockSignals(False)

            # Aguarda o fim da edição da geometria para atualizar os atributos
            def delayed_update():
                if self.recorte_layer and QgsProject.instance().mapLayer(self.recorte_layer.id()):
                    self.atualizar_valores_poligono(self.recorte_layer, fid)

            QTimer.singleShot(0, delayed_update)

    def _on_recorte_feat_added(self, fid):
        """
        Ajusta a geometria da feição adicionada conforme a operação (diferença/interseção).
        """
        feat = next(self.recorte_layer.getFeatures(QgsFeatureRequest(fid)), None)
        if not feat:
            return

        # Guarda original
        self._orig_geoms[fid] = QgsGeometry(feat.geometry())

        # Aplica operação correta conforme o modo atual
        operacao = getattr(self, '_operacao_atual', 'diferenca')
        if operacao == 'intersecao':
            geom_rec = feat.geometry().intersection(self._union_mascara)
        else:
            geom_rec = feat.geometry().difference(self._union_mascara)

        if geom_rec and not geom_rec.isEmpty():
            self.recorte_layer.blockSignals(True)
            self.recorte_layer.changeGeometry(fid, geom_rec)
            self.recorte_layer.blockSignals(False)

        # COMMIT SEGURO
        layer_id = self.recorte_layer.id()  # captura id agora!
        def safe_commit():
            layer = QgsProject.instance().mapLayer(layer_id)
            if not layer or not layer.isEditable():
                # Camada já foi removida ou não está mais em edição → nada a fazer
                return
            if not layer.commitChanges():  # tenta salvar
                return
            # Reabre edição **apenas se** ainda existir e você precisar
            layer.startEditing()
        QTimer.singleShot(0, safe_commit)

    def _processar_recorte_operacao(self, operacao, camada_entrada, camada_sobrep, feats_entrada, geom_sobrep, fid_to_original_geom):
        """
        Processa a operação geométrica (diferença, interseção, união).
        Retorna a camada resultado (memory ou camada de sobreposição).
        """
        nome_base = camada_entrada.name()

        if operacao == 'diferenca':
            nome_resultado = f"{nome_base}_diferenca"
        elif operacao == 'intersecao':
            nome_resultado = f"{nome_base}_interseção"
        elif operacao == 'uniao':
            nome_resultado = f"{nome_base}_uniao"
        else:
            nome_resultado = f"{nome_base}_resultado"

        if self.checkBoxNova.isChecked():
            # 1) Criação da camada resultado (pronto para trocar pelo CamadaPoligonosManager)
            resultado_layer = self._criar_camada_resultado(operacao, camada_entrada, nome_resultado)

            # 2) Adiciona feições resultado
            self._adicionar_feicoes_resultado(
                resultado_layer, operacao, feats_entrada, geom_sobrep, camada_entrada, fid_to_original_geom)

            QgsProject.instance().addMapLayer(resultado_layer)
            self._mensagem_sucesso_operacao(operacao)
            return resultado_layer

        else:
            resultado_layer = camada_sobrep
            resultado_layer.startEditing()
            resultado_layer.dataProvider().truncate()

            for feat in feats_entrada:
                nova_geom = self._calcular_geometria_operacao(operacao, feat.geometry(), geom_sobrep)
                if nova_geom and not nova_geom.isEmpty():
                    nova_feat = QgsFeature(feat)
                    nova_feat.setGeometry(nova_geom)
                    resultado_layer.addFeature(nova_feat)

            resultado_layer.commitChanges()
            self._orig_geoms = {}
            feats_novas = list(resultado_layer.getFeatures())
            for i, f in enumerate(feats_novas):
                if i < len(feats_entrada):
                    self._orig_geoms[f.id()] = QgsGeometry(fid_to_original_geom[feats_entrada[i].id()])

            resultado_layer.triggerRepaint()
            self._mensagem_sucesso_operacao(operacao, sobreposicao=True)
            return resultado_layer

    def _criar_camada_resultado(self, operacao, camada_entrada, nome_resultado):
        """
        Cria e configura uma nova camada de memória para armazenar o resultado da operação de geoprocessamento.
        Define campos necessários, adiciona campos de área, perímetro e orig_fid, e conecta sinais para atualização de atributos.
        """
        epsg = camada_entrada.crs().authid()

        # Sempre usar MultiPolygon para operações de geoprocessamento
        geom_type = "MultiPolygon"
        uri = f"{geom_type}?crs={epsg}"

        resultado_layer = QgsVectorLayer(uri, nome_resultado, "memory")
        resultado_layer.startEditing()
        campos = camada_entrada.fields().toList()
        campos.append(QgsField('orig_fid', QVariant.Int))
        resultado_layer.dataProvider().addAttributes(campos)
        resultado_layer.updateFields()

        self.configura_campos(resultado_layer)

        resultado_layer.featureAdded.connect(lambda fid: self.atualizar_valores_poligono(resultado_layer, fid))
        resultado_layer.geometryChanged.connect(lambda fid, geom: self.atualizar_valores_poligono(resultado_layer, fid))

        return resultado_layer

    def _on_feature_added_resultado(self, fid):
        """
        Callback para atualizar área e perímetro quando uma nova feição é adicionada à camada de resultado.
        """
        if hasattr(self, "recorte_layer") and self.recorte_layer:
            self.atualizar_valores_poligono(self.recorte_layer, fid)

    def _on_geometry_changed_resultado(self, fid, geom):
        """
        Callback para atualizar área e perímetro quando a geometria de uma feição na camada de resultado é alterada.
        """
        if hasattr(self, "recorte_layer") and self.recorte_layer:
            self.atualizar_valores_poligono(self.recorte_layer, fid)

    def _adicionar_feicoes_resultado(self, resultado_layer, operacao, feats_entrada, geom_sobrep, camada_entrada, fid_to_original_geom):
        """
        Adiciona as feições resultantes da operação na camada de saída.
        """
        for feat in feats_entrada:
            nova_geom = self._calcular_geometria_operacao(operacao, feat.geometry(), geom_sobrep)
            if nova_geom and not nova_geom.isEmpty():
                nova_feat = self._criar_feat_resultado(resultado_layer, camada_entrada, feat, nova_geom)
                resultado_layer.addFeature(nova_feat)

        resultado_layer.commitChanges()
        self._orig_geoms = {}
        for f in resultado_layer.getFeatures():
            orig_fid = f['orig_fid']
            self._orig_geoms[f.id()] = QgsGeometry(fid_to_original_geom[orig_fid])

        # Atualiza área/perímetro de todas as feições
        for feat in resultado_layer.getFeatures():
            self.atualizar_valores_poligono(resultado_layer, feat.id())

    def _calcular_geometria_operacao(self, operacao, geom, geom_sobrep):
        """
        Realiza a operação geométrica correta entre a geometria e a máscara.
        """
        if operacao == 'diferenca':
            return geom.difference(geom_sobrep)
        elif operacao == 'intersecao':
            return geom.intersection(geom_sobrep)
        elif operacao == 'uniao':
            return geom.combine(geom_sobrep)
        else:
            return None

    def _mensagem_sucesso_operacao(self, operacao, sobreposicao=False):
        """
        Exibe mensagem de sucesso adequada ao tipo de operação.
        """
        if operacao == 'intersecao':
            msg = "Camada de interseção criada." if not sobreposicao else "Camada de sobreposição atualizada com interseção."
        elif operacao == 'uniao':
            msg = "Camada de união criada." if not sobreposicao else "Camada de sobreposição atualizada com união."
        else:
            msg = "Operação concluída."
        self.mostrar_mensagem(msg, "Sucesso")

    def configura_campos(self, camada):
        """
        Garante que a camada possua os campos 'ID', 'Perimetro' e 'Area'.
        Se já existem, apenas atualiza restrições e widgets. Se não, adiciona.
        """
        prov = camada.dataProvider()
        fields_to_add = []

        # Verifica campos existentes
        idx_id = camada.fields().indexOf("ID")
        idx_perimetro = camada.fields().indexOf("Perimetro")
        idx_area = camada.fields().indexOf("Area")

        # Campos que faltam são adicionados
        if idx_id == -1:
            id_field = QgsField("ID", QVariant.Int)
            # Restrições
            constraints = QgsFieldConstraints()
            constraints.setConstraint(QgsFieldConstraints.ConstraintUnique)
            constraints.setConstraint(QgsFieldConstraints.ConstraintNotNull)
            id_field.setConstraints(constraints)
            fields_to_add.append(id_field)
        if idx_perimetro == -1:
            perimetro_field = QgsField("Perimetro", QVariant.Double, "double", 20, 3)
            fields_to_add.append(perimetro_field)
        if idx_area == -1:
            area_field = QgsField("Area", QVariant.Double, "double", 20, 3)
            fields_to_add.append(area_field)

        # Adiciona campos que faltam
        if fields_to_add:
            prov.addAttributes(fields_to_add)
            camada.updateFields()

        # Atualiza índices após possível adição
        idx_id = camada.fields().indexOf("ID")
        idx_perimetro = camada.fields().indexOf("Perimetro")
        idx_area = camada.fields().indexOf("Area")

        # Configura valor default do ID sequencial
        if idx_id != -1:
            expr = 'coalesce(maximum("ID"),0) + 1'
            camada.setDefaultValueDefinition(idx_id, QgsDefaultValue(expr))

        # Oculta Perímetro e Área (editor widget)
        widget_setup_oculto = QgsEditorWidgetSetup("Hidden", {})
        if idx_perimetro != -1:
            camada.setEditorWidgetSetup(idx_perimetro, widget_setup_oculto)
        if idx_area != -1:
            camada.setEditorWidgetSetup(idx_area, widget_setup_oculto)

    def _criar_feat_resultado(self, resultado_layer, camada_entrada, feat, nova_geom):
        """
        Cria a feição de saída, preenchendo atributos necessários.
        Calcula área e perímetro a partir da geometria recortada.
        """
        nova_feat = QgsFeature(resultado_layer.fields())
        for field in camada_entrada.fields():
            nova_feat.setAttribute(field.name(), feat[field.name()])
        nova_feat.setAttribute('orig_fid', feat.id())
        nova_feat.setGeometry(nova_geom)

        # Calcula área e perímetro da nova geometria
        idx_area = resultado_layer.fields().indexOf("Area")
        idx_perimetro = resultado_layer.fields().indexOf("Perimetro")
        if idx_area != -1 or idx_perimetro != -1:
            d = QgsDistanceArea()
            d.setSourceCrs(resultado_layer.crs(), QgsProject.instance().transformContext())
            d.setEllipsoid(QgsProject.instance().crs().ellipsoidAcronym())
            if resultado_layer.crs().isGeographic():
                perimetro = round(d.measurePerimeter(nova_geom), 3)
                area = round(d.measureArea(nova_geom), 3)
            else:
                perimetro = round(nova_geom.length(), 3)
                area = round(nova_geom.area(), 3)
            if idx_perimetro != -1:
                nova_feat.setAttribute(idx_perimetro, perimetro)
            if idx_area != -1:
                nova_feat.setAttribute(idx_area, area)

        return nova_feat

    def atualizar_valores_poligono(self, camada: QgsVectorLayer, fid: int, geom_override=None):
        """
        Atualiza os valores de 'Perimetro' e 'Area' de um polígono quando ele é adicionado ou sua geometria é alterada.
        Considera cálculos específicos caso o sistema de referência seja geográfico.
        """
        idx_perimetro = camada.fields().indexOf("Perimetro")
        idx_area = camada.fields().indexOf("Area")
        if idx_perimetro == -1 or idx_area == -1:
            return

        if geom_override is not None:
            geom = geom_override
        else:
            feature = camada.getFeature(fid)
            if not (feature.isValid() and feature.geometry() and not feature.geometry().isEmpty()):
                return
            geom = feature.geometry()

        d = QgsDistanceArea()
        d.setSourceCrs(camada.crs(), QgsProject.instance().transformContext())
        d.setEllipsoid(QgsProject.instance().crs().ellipsoidAcronym())
        if camada.crs().isGeographic():
            perimetro = round(d.measurePerimeter(geom), 3)
            area = round(d.measureArea(geom), 3)
        else:
            perimetro = round(geom.length(), 3)
            area = round(geom.area(), 3)

        camada.changeAttributeValue(fid, idx_perimetro, perimetro)
        camada.changeAttributeValue(fid, idx_area, area)






