# -*- coding: utf-8 -*-
"""
/***************************************************************************
 VectorToMap - QGIS Plugin
 Automates the generation of print layouts for vector features.
 Author: Matheus Durso
 ***************************************************************************/
"""
import math
import re
import unicodedata
import os
import gc
import traceback
from qgis.PyQt import sip
from qgis.PyQt.QtCore import QTranslator, QCoreApplication, Qt, QTimer
from qgis.PyQt.QtGui import QIcon, QPixmap, QColor
from qgis.PyQt.QtWidgets import QAction, QCheckBox, QTableWidgetItem, QFrame, QPushButton, QDialogButtonBox, QButtonGroup, QApplication, QGridLayout, QFileDialog, QMessageBox
from qgis.core import (
    QgsProject, QgsPrintLayout, QgsLayoutSize, QgsUnitTypes, QgsLayoutItemMap, 
    QgsLayoutPoint, QgsMapLayerProxyModel, QgsFeatureRequest, QgsSettings, 
    QgsLayoutExporter, QgsRectangle, QgsCoordinateTransform, QgsLayoutItemPage, 
    QgsLayoutItemLabel, QgsLayoutItem, QgsWkbTypes, QgsExpression, QgsVectorLayer, NULL
)
# Resources generated by Plugin Builder
from .resources import *
from .vector_to_map_dialog import VectorToMapDialog


class VectorToMap:

    ####################################################################################
    # --- 1. CICLO DE VIDA E CONFIGURAÇÕES INICIAIS ---
    ####################################################################################

    def __init__(self, iface):
        """Initialize the plugin and define global state variables."""

        self.iface = iface
        self.plugin_dir = os.path.dirname(__file__)
        self.actions = []
        self.menu = self.tr(u'&VectorToMap')
        self.first_start = True
        self.is_rendering = False # Flag to prevent infinite rendering loops
        self.colunas_atuais = []
        
        self.preview_data_cache = None
        self.ultimo_id_camada = None
        self.cache_val_atlas = None
        self.clones_preview = []
        
        # Determina a língua de exibição
        locale = QgsSettings().value('locale/userLocale')[0:2] 

        if locale != 'pt':
            # O path vai buscar vector_to_map_en.qm ou vector_to_map_es.qm
            path = os.path.join(self.plugin_dir, 'i18n', f'vector_to_map_{locale}.qm')
            
            # Se o espanhol não existir por algum motivo, ele volta pro inglês
            if not os.path.exists(path):
                path = os.path.join(self.plugin_dir, 'i18n', 'vector_to_map_en.qm')

            if os.path.exists(path):
                self.translator = QTranslator()
                self.translator.load(path)
                QCoreApplication.installTranslator(self.translator)
    

    #Indica um trecho que deve ser traduzido
    def tr(self, message):
        """Translate strings using the QGIS internationalization system."""

        return QCoreApplication.translate('VectorToMap', message)
    

    #Determina variáveis de sistema e configuração de aparência
    def add_action(self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None):
        """Helper function to add buttons and menus to the QGIS interface."""

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)
        if status_tip: action.setStatusTip(status_tip)
        if whats_this: action.setWhatsThis(whats_this)
        if add_to_toolbar: self.iface.addToolBarIcon(action)
        if add_to_menu: self.iface.addPluginToMenu(self.menu, action)
        self.actions.append(action)
        return action
    

    def initGui(self):
        """Cria as entradas de menu e a barra de ferramentas personalizada."""

        icon_path = ':/plugins/vector_to_map/icon.png'
        
        # 1. CRIAÇÃO DA BARRA DE FERRAMENTAS EXCLUSIVA
        self.toolbar = self.iface.addToolBar(u'VectorToMap')
        self.toolbar.setObjectName(u'VectorToMap')

        #Botão Render Preview
        self.btn_render = QPushButton(self.tr("Preview"))

        # 2. CRIAÇÃO DA AÇÃO PRINCIPAL
        self.action = self.add_action(
            icon_path,
            text=self.tr(u'VectorToMap'),
            callback=self.run,
            parent=self.iface.mainWindow(),
            add_to_toolbar=True,
            add_to_menu=True
        )

        # Adiciona atalho para o plugin
        self.action.setShortcut("Ctrl+Alt+V")


    def unload(self):
        """Limpa os recursos do plugin ao ser desativado."""
        
        # 1. Limpa as camadas de preview da memória
        self.limpar_clones_preview()

        # 2. Remove as ações dos menus e barras
        for action in self.actions:
            self.iface.removePluginMenu(self.menu, action)
            self.iface.removeToolBarIcon(action)
        
        # 3. Limpeza segura da Toolbar (Foco QGIS 4.0)
        if hasattr(self, 'toolbar') and self.toolbar:
            if not sip.isdeleted(self.toolbar):
                self.iface.mainWindow().removeToolBar(self.toolbar)
                # No QGIS 4.0, deleteLater() é a forma elegante de dizer ao Qt: 
                # "Delete isso assim que terminar de processar os eventos atuais"
                self.toolbar.deleteLater() 
            del self.toolbar

        # 4. Limpeza segura do Diálogo
        if hasattr(self, 'dlg') and self.dlg:
            if not sip.isdeleted(self.dlg):
                self.dlg.close()
                self.dlg.deleteLater()
            del self.dlg

        # 5. Coleta de lixo final
        gc.collect()

    ##################################################################################
    # --- 2. INTERFACE E DIÁLOGO ---
    ##################################################################################

    def run(self):
        """Initialize the UI and set up window flags and button logic."""
        if self.first_start:
            self.first_start = False
            self.dlg = VectorToMapDialog()

            # 1. Tenta desconectar qualquer link automático que o Qt Designer criou
            try:
                self.dlg.button_box.accepted.disconnect()
                self.dlg.button_box.rejected.disconnect()
            except:
                pass # Caso não haja conexão prévia

            # --- ESTILO CONSOLIDADO V0.3.0 (AZUL PROFISSIONAL & CLEAN) ---
            estilo_consolidado = """
                /* 1. ESTILO GERAL DAS BARRAS DE PROGRESSO */
                QProgressBar {
                    border: 1px solid #dcdcdc;
                    border-radius: 4px;
                    text-align: center;
                    background-color: #f0f0f0;
                    height: 14px;
                    font-size: 10px;
                    color: #333;
                }
                QProgressBar::chunk {
                    background-color: #3498db; /* Azul Claro */
                    border-radius: 3px;
                }

                /* 2. BARRA DA PREVIEW (ULTRA-FINA ESTILO YOUTUBE) */
                QProgressBar#previewProgressBar {
                    border: 1px solid #dcdcdc;
                    border-radius: 4px;
                    background-color: #f0f0f0;
                    height: 3px;
                    color: #333; 
                }
                QProgressBar#previewProgressBar::chunk {
                    background-color: #3498db;
                    border-radius: 3px;
                }

                /* 3. BARRAS DE ROLAGEM MODERNAS (SCROLLBARS) */
                QScrollBar:vertical, QScrollBar:horizontal {
                    border: none;
                    background: #f8f8f8;
                    width: 8px;
                    height: 8px;
                    margin: 0px;
                }
                QScrollBar::handle {
                    background: #cccccc;
                    border-radius: 4px;
                    min-height: 20px;
                    min-width: 20px;
                }
                QScrollBar::handle:hover {
                    background: #707070; /* cinza */
                }
                QScrollBar::add-line, QScrollBar::sub-line {
                    border: none;
                    background: none;
                    height: 0px;
                    width: 0px;
                }
                /* 4. PADRONIZAÇÃO DOS WIDGETS DE TEXTO */
                QCheckBox, QLabel, QRadioButton, QgsMapLayerComboBox, QComboBox {
                    font-size: 9pt; 
                }
            """
            self.dlg.setStyleSheet(estilo_consolidado)
            self.dlg.previewProgressBar.hide()
            self.dlg.progressBar.hide()

            # --- ISOLAMENTO DOS RADIO BUTTONS (v0.2.8) ---
            self.grupo_orientacao = QButtonGroup(self.dlg)
            self.grupo_orientacao.addButton(self.dlg.rb_retrato)
            self.grupo_orientacao.addButton(self.dlg.rb_paisagem)

            self.grupo_escala = QButtonGroup(self.dlg)
            self.grupo_escala.addButton(self.dlg.rb_escala_auto)
            self.grupo_escala.addButton(self.dlg.rb_escala_fixa)

            # --- SETUP TAMANHOS DE PÁGINA (v0.2.8) ---
            self.dlg.combo_tamanho_pagina.clear()
            tamanhos_disponiveis = [
                ("A5 (148 x 210 mm)", (148.0, 210.0)),
                ("A4 (210 x 297 mm)", (210.0, 297.0)),
                ("A3 (297 x 420 mm)", (297.0, 420.0)),
                ("A2 (420 x 594 mm)", (420.0, 594.0)),
                ("A1 (594 x 841 mm)", (594.0, 841.0)),
                ("A0 (841 x 1189 mm)", (841.0, 1189.0))
            ]
            for texto, dimensoes in tamanhos_disponiveis:
                self.dlg.combo_tamanho_pagina.addItem(texto, dimensoes)
            
            # Força o A4 (índice 1) a ser o padrão selecionado ao abrir a janela
            self.dlg.combo_tamanho_pagina.setCurrentIndex(1)

            # --- SETUP ESCALA (v0.2.8) ---
            self.dlg.combo_escala_fixa.clear()
            escalas = [
                ("1:1.000", 1000), ("1:5.000", 5000), ("1:10.000", 10000),
                ("1:50.000", 50000), ("1:100.000", 100000), 
                ("1:250.000", 250000), ("1:500.000", 500000), ("1:1.000.000", 1000000),
                ("1:2.500.000", 2500000), ("1:5.000.000", 5000000), ("1:7.500.000", 7500000),
                ("1:10.000.000", 10000000)
            ]
            for texto, valor in escalas:
                self.dlg.combo_escala_fixa.addItem(texto, valor)
            self.dlg.combo_escala_fixa.setCurrentIndex(2)

            self.dlg.mMapLayerComboBox.layerChanged.connect(self.configurar_escala_ao_mudar_camada)
            self.configurar_escala_ao_mudar_camada() 

            self.dlg.rb_escala_auto.toggled.connect(self.validar_geometria_escala)
            self.dlg.rb_escala_fixa.toggled.connect(self.validar_geometria_escala)
            self.dlg.rb_escala_auto.toggled.connect(self.disparar_preview_se_autorizado)
            self.dlg.rb_escala_fixa.toggled.connect(self.disparar_preview_se_autorizado)
            self.dlg.combo_escala_fixa.currentIndexChanged.connect(self.disparar_preview_se_autorizado)
            
            # --- SETUP DE BOTÕES COM ORDEM FORÇADA ---
            
            # 1. Limpa qualquer botão que tenha vindo do Qt Designer
            self.dlg.button_box.clear()
            
            # 2. Criamos TODOS os botões manualmente (inclusive OK e Cancelar)
            self.btn_export = QPushButton(self.tr("Exportar"))
            self.btn_render = QPushButton(self.tr("Preview"))
            self.btn_ok = QPushButton(self.tr("OK"))
            self.btn_cancel = QPushButton(self.tr("Cancelar"))

            # 3. Adicionamos todos com a 'ActionRole' para obrigar o Qt a enfileirá-los da esquerda para a direita
            self.dlg.button_box.addButton(self.btn_export, QDialogButtonBox.ButtonRole.ActionRole)
            self.dlg.button_box.addButton(self.btn_render, QDialogButtonBox.ButtonRole.ActionRole)
            self.dlg.button_box.addButton(self.btn_ok, QDialogButtonBox.ButtonRole.ActionRole)
            self.dlg.button_box.addButton(self.btn_cancel, QDialogButtonBox.ButtonRole.ActionRole)

            # 4. Conexões dos botões extras
            self.btn_export.clicked.connect(self.preparar_exportacao)
            self.btn_render.clicked.connect(self.atualizar_preview)
            self.export_info = None

            self.dlg.combo_presets.setItemData(0, "quadrado")
            self.dlg.combo_presets.setItemData(1, "75_altura")
            # Adiciona a nova opção via código de forma invisível
            self.dlg.combo_presets.addItem(self.tr("Mapa Vertical"), "vertical")

            self.dlg.setWindowIcon(QIcon(':/plugins/vector_to_map/icon.png'))
            self.dlg.setWindowTitle("VectorToMap")
            # Garante que o aviso de feições comece escondido
            if hasattr(self.dlg, 'lbl_aviso_feicoes'):
                self.dlg.lbl_aviso_feicoes.hide()

            # --- CONFIGURAÇÃO DE JANELA PROFISSIONAL ---
            self.dlg.setWindowFlags(
                Qt.WindowType.Window | 
                Qt.WindowType.WindowMinimizeButtonHint | 
                Qt.WindowType.WindowMaximizeButtonHint | 
                Qt.WindowType.WindowCloseButtonHint
            )
            
            self.is_rendering = False
            self.timer_preview = QTimer()
            self.timer_preview.setSingleShot(True)
            self.timer_preview.timeout.connect(self.atualizar_preview)
            
            if hasattr(self.dlg, 'chk_preview_auto'):
                self.dlg.chk_preview_auto.setChecked(False)
                self.dlg.chk_preview_auto.stateChanged.connect(
                    lambda state: self.timer_preview.start(250) if state == Qt.CheckState.Checked else None
                )

            # Restore window geometry
            settings = QgsSettings()
            geometria = settings.value("/VectorToMap/geometry")
            if geometria: 
                self.dlg.restoreGeometry(geometria)
            else: 
                self.dlg.resize(1050, 750) 
            
            if hasattr(self.dlg, 'splitter'): 
                self.dlg.splitter.setSizes([550, 500]) 
            
            self.dlg.lbl_preview.setFrameShape(QFrame.Shape.StyledPanel)
            self.dlg.mMapLayerComboBox.setFilters(QgsMapLayerProxyModel.VectorLayer)

            self.dlg.mMapLayerComboBox.layerChanged.connect(self.disparar_preview_se_autorizado)
            self.dlg.chk_selecionar_todos.stateChanged.connect(self.marcar_desmarcar_todos)
            self.dlg.chk_numeracao.toggled.connect(self.disparar_preview_se_autorizado)

            # --- CONEXÕES DA HIERARQUIA DE CAMADAS ---
            self.dlg.chk_filtrar_feicoes.stateChanged.connect(self.atualizar_hierarquia_camadas)
            self.dlg.chk_exibir_so_camada_atual.stateChanged.connect(self.atualizar_hierarquia_camadas)
            
            # Conecta a dependência do estilo ao travar camadas
            self.dlg.chk_travar_camadas.stateChanged.connect(self.atualizar_estado_travar_estilos)
            
            # Atualiza a Preview APENAS se o utilizador interagir com filtros ou visibilidade
            self.dlg.chk_filtrar_feicoes.stateChanged.connect(self.disparar_preview_se_autorizado)
            self.dlg.chk_exibir_so_camada_atual.stateChanged.connect(self.disparar_preview_se_autorizado)
            
            # Força a verificação inicial da hierarquia ao abrir a janela
            self.atualizar_hierarquia_camadas()

            # ==========================================================
            # --- SETUP DA COR DE FUNDO (NOVA FUNCIONALIDADE) ---
            if hasattr(self.dlg, 'btn_cor_fundo'):
                self.dlg.btn_cor_fundo.setColor(QColor(255, 255, 255, 255)) # Padrão: Branco Opaco
                self.dlg.btn_cor_fundo.setAllowOpacity(True)
                self.dlg.btn_cor_fundo.colorChanged.connect(self.disparar_preview_se_autorizado)
                self.dlg.btn_cor_fundo.setToolTip(self.tr("Escolha a cor de fundo do mapa. Reduza a Opacidade para 0% para exportar transparente."))

            if hasattr(self.dlg, 'chk_apenas_mapa'):
                self.dlg.chk_apenas_mapa.stateChanged.connect(self.disparar_preview_se_autorizado)
            # ==========================================================

            # --- MONITORING WIDGETS ---
            widgets = [
                self.dlg.combo_tamanho_pagina, self.dlg.combo_presets, 
                self.dlg.rb_retrato, self.dlg.rb_paisagem, 
                self.dlg.chk_modo_formulario, self.dlg.chk_modo_individual,
                self.dlg.combo_atlas, self.dlg.chk_exibir_atributos
            ]

            if hasattr(self.dlg, 'chk_apenas_mapa'): widgets.append(self.dlg.chk_apenas_mapa)
            if hasattr(self.dlg, 'btn_cor_fundo'): widgets.append(self.dlg.btn_cor_fundo)

            for w in widgets:
                if hasattr(w, 'currentIndexChanged'): w.currentIndexChanged.connect(self.disparar_preview_se_autorizado)
                elif hasattr(w, 'stateChanged'): w.stateChanged.connect(self.disparar_preview_se_autorizado)
                elif hasattr(w, 'toggled'): w.toggled.connect(self.disparar_preview_se_autorizado)

            self.dlg.resizeEvent = lambda event: self.disparar_preview_se_autorizado()
            self.btn_ok.clicked.connect(self.processar_clique_ok)
            self.abort_processing = False
            self.btn_cancel.clicked.connect(self.cancelar_e_fechar)
            self.configurar_escala_ao_mudar_camada()

        # --- CENTRALIZAR NA TELA ---        
        screen_geometry = QApplication.primaryScreen().availableGeometry()
        centro_tela = screen_geometry.center()
        
        geometria_janela = self.dlg.frameGeometry()
        geometria_janela.moveCenter(centro_tela)
        self.dlg.move(geometria_janela.topLeft())
        
        self.setup_ui_strings()  
        self.dlg.show()
    

    def preparar_exportacao(self):
        """Abre o diálogo, sugere o último diretório e configura a exportação."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return

        # --- RECUPERA O ÚLTIMO DIRETÓRIO ---
        settings = QgsSettings()
        ultimo_dir = settings.value("/VectorToMap/ultimo_diretorio_exportacao", os.path.expanduser("~"))

        # Monta um nome base amigável já no diretório correto
        nome_sugerido = os.path.join(ultimo_dir, f"Mapas_{camada.name()}")
        
        # 1. ADICIONAMOS O SVG AQUI NO FILTRO
        filtros = "PDF (*.pdf);;SVG (*.svg);;PNG (*.png);;JPEG (*.jpg)"

        caminho_arquivo, filtro_selecionado = QFileDialog.getSaveFileName(
            self.dlg, self.tr("Exportar Mapas"), nome_sugerido, filtros
        )

        if caminho_arquivo:
            # --- SALVA O DIRETÓRIO ESCOLHIDO PARA A PRÓXIMA VEZ ---
            diretorio_escolhido = os.path.dirname(caminho_arquivo)
            settings.setValue("/VectorToMap/ultimo_diretorio_exportacao", diretorio_escolhido)

            # 2. ADICIONAMOS A IDENTIFICAÇÃO DO SVG AQUI
            ext = ".pdf"
            if "PNG" in filtro_selecionado: ext = ".png"
            elif "JPEG" in filtro_selecionado: ext = ".jpg"
            elif "SVG" in filtro_selecionado: ext = ".svg" 

            # Adiciona a extensão se o usuário esqueceu de digitar
            if not caminho_arquivo.lower().endswith(ext):
                caminho_arquivo += ext

            # 3. FORÇAMOS INDIVIDUAL PARA SVG TAMBÉM
            is_individual = self.dlg.chk_export_individual.isChecked() or ext in [".png", ".jpg", ".svg"]

            # Armazena as configurações e dispara o gerador
            self.export_info = {
                'path': caminho_arquivo,
                'ext': ext,
                'individual': is_individual
            }
            self.processar_clique_ok()


    def criar_camada_temporaria(self, camada_original, feicoes, nome_pagina, is_preview=False):
        """Cria uma memory layer. Se for preview, faz oculto. Se for final, joga numa pasta acima da original."""
                        
        crs = camada_original.crs().authid()
        geom_type = camada_original.wkbType()
        geom_string = QgsWkbTypes.displayString(geom_type)
        
        # Forma mais robusta de montar a URI para memory layers
        uri = f"{QgsWkbTypes.displayString(camada_original.wkbType())}?crs={camada_original.crs().authid()}"
        camada_temp = QgsVectorLayer(uri, f"Mapa_{nome_pagina}", "memory")

        # --- NOVO: A BLINDAGEM DO CRS ---
        # Força a cópia exata do CRS, mesmo que a camada original não tenha código EPSG
        camada_temp.setCrs(camada_original.crs())
        
        provider = camada_temp.dataProvider()
        provider.addAttributes(camada_original.fields())
        camada_temp.updateFields()
        
        provider.addFeatures(feicoes)
        camada_temp.updateExtents() 
        
        # Copia o Renderer (cores, preenchimentos)
        camada_temp.setRenderer(camada_original.renderer().clone())
        
        # Opcional: Copia os rótulos também, se existirem
        if camada_original.labelsEnabled():
            camada_temp.setLabeling(camada_original.labeling().clone())
            camada_temp.setLabelsEnabled(True)
        
        if is_preview:
            # PREVIEW: O QGIS carrega na memória do projeto, mas esconde da legenda
            QgsProject.instance().addMapLayer(camada_temp, False)
        else:
            # MAPA FINAL: Cria um grupo posicionado imediatamente acima da camada original
            root = QgsProject.instance().layerTreeRoot()
            
            # --- NOVA LÓGICA DE GRUPO DINÂMICO ---
            # Define o nome do grupo específico para esta camada
            nome_grupo_temp = f"Temp - VectorToMap ({camada_original.name()})"
            
            # Procura se o grupo ESPECÍFICO desta camada já existe
            grupo = root.findGroup(nome_grupo_temp)
            
            if not grupo:
                # 1. Encontra o "nó" da camada original na árvore do QGIS
                no_camada_original = root.findLayer(camada_original.id())
                
                if no_camada_original:
                    # 2. Descobre quem é o "pai" (pode ser a raiz ou uma pasta existente)
                    no_pai = no_camada_original.parent()
                    
                    # 3. Descobre a posição (índice) exata da camada original dentro desse pai
                    idx_insercao = 0
                    for i, filho in enumerate(no_pai.children()):
                        if filho == no_camada_original:
                            idx_insercao = i
                            break
                            
                    # 4. Insere o grupo específico nessa mesma posição, empurrando a original para baixo
                    grupo = no_pai.insertGroup(idx_insercao, nome_grupo_temp)
                else:
                    # Fallback de segurança caso a camada original não seja encontrada
                    grupo = root.insertGroup(0, nome_grupo_temp)
            
            # Adiciona a camada de forma invisível à raiz, e depois move para o nosso grupo
            QgsProject.instance().addMapLayer(camada_temp, False) 
            grupo.addLayer(camada_temp)
            
        return camada_temp


    def limpar_clones_preview(self):
        """Remove da memória apenas os clones gerados invisivelmente para a preview."""
        
        if hasattr(self, 'clones_preview') and self.clones_preview:
            QgsProject.instance().removeMapLayers(self.clones_preview)
            self.clones_preview.clear()


    def atualizar_hierarquia_camadas(self):
        """Controla a dependência obrigatória entre as opções de camada."""
        filtrar = self.dlg.chk_filtrar_feicoes.isChecked()
        exibir_atual = self.dlg.chk_exibir_so_camada_atual.isChecked()

        self.dlg.chk_travar_camadas.blockSignals(True)

        if filtrar or exibir_atual:
            self.dlg.chk_travar_camadas.setChecked(True)
            self.dlg.chk_travar_camadas.setEnabled(False)
        else:
            self.dlg.chk_travar_camadas.setEnabled(True)

        self.dlg.chk_travar_camadas.blockSignals(False)

        # NOVO: Garante que a caixa de estilos acompanha a mudança automática do "Travar Camadas"
        self.atualizar_estado_travar_estilos()

        # --- NOVO: Controle do aviso visual de feições ---
        if hasattr(self.dlg, 'lbl_aviso_feicoes'):
            if filtrar:
                self.dlg.lbl_aviso_feicoes.show()
                self.dlg.lbl_aviso_feicoes.setText(self.tr("⚠️ Aviso: O filtro de feições cria camadas temporárias para cada feição ou grupo de feições."))
            else:
                self.dlg.lbl_aviso_feicoes.hide()
    

    def atualizar_estado_travar_estilos(self):
        """Habilita o 'Travar Estilos' apenas se o 'Travar Camadas' estiver marcado."""
        
        travado = self.dlg.chk_travar_camadas.isChecked()
        self.dlg.chk_travar_estilos.setEnabled(travado)
        
        # Se o 'Travar Camadas' for desmarcado, garantimos que o de estilos desmarque também
        if not travado:
            self.dlg.chk_travar_estilos.blockSignals(True)
            self.dlg.chk_travar_estilos.setChecked(False)
            self.dlg.chk_travar_estilos.blockSignals(False)


    def setup_ui_strings(self):
        """Centraliza todos os Tooltips e textos da interface para tradução."""

        self.dlg.chk_modo_formulario.setToolTip(self.tr("Exibe todos os atributos em um bloco único de texto (HTML)."))
        self.dlg.chk_modo_individual.setToolTip(self.tr("Cria uma linha horizontal para cada feição (evita sobreposições)."))
        self.dlg.chk_selecionar_todos.setToolTip(self.tr("Marca ou desmarca todos os campos da lista de uma vez."))
        self.dlg.combo_atlas.setToolTip(self.tr("Campo da tabela usado para agrupar as feições em cada página."))
        self.dlg.combo_presets.setToolTip(self.tr("Define o tamanho do mapa na folha (ex: 75% da página ou Quadrado)."))
        self.btn_render.setToolTip(self.tr("Gera uma prévia do layout com as configurações atuais."))
        self.btn_export.setToolTip(self.tr("Exporta os mapas diretamente como PDF, PNG ou JPG."))


    def cancelar_e_fechar(self):
        """Gerencia se o clique no Cancelar deve parar o motor ou fechar a janela."""

        self.limpar_clones_preview()

        if self.is_rendering:
            # Se estiver processando, apenas levantamos a bandeira de parada
            self.abort_processing = True
            self.iface.messageBar().pushMessage(
                self.tr("Aviso"), 
                self.tr("Cancelando processamento... A janela permanecerá aberta."), 
                level=3, 
                duration=3
            )
        else:
            # Se não houver nada rodando, o comportamento de fechar é seguro
            self.dlg.reject()

    #################################################################################
    # --- 3. GESTÃO DE ATRIBUTOS E WIDGETS ---
    #################################################################################

    def atualizar_lista_atributos(self):
        """Limpa a interface e reconstrói a lista com isolamento total."""

        self.dlg.chk_modo_formulario.blockSignals(True)
        self.dlg.chk_modo_individual.blockSignals(True)
        self.dlg.chk_modo_formulario.setChecked(False)
        self.dlg.chk_modo_individual.setChecked(False)
        self.dlg.chk_modo_formulario.setEnabled(False) 
        self.dlg.chk_modo_individual.setEnabled(False) 

        camada = self.dlg.mMapLayerComboBox.currentLayer()
        self.dlg.tableWidget.setRowCount(0)
        self.dlg.tableWidget.setColumnCount(0)
        self.colunas_atuais = []

        self.dlg.chk_selecionar_todos.blockSignals(True)
        self.dlg.chk_selecionar_todos.setChecked(False)
        self.dlg.chk_selecionar_todos.blockSignals(False)

        self.dlg.combo_atlas.clear()
        self.dlg.combo_atlas.addItem(self.tr("--- Sem Agrupamento (Cada Feição) ---"), None)

        if not camada: 
            self.dlg.chk_modo_formulario.blockSignals(False)
            self.dlg.chk_modo_individual.blockSignals(False)
            return

        self.atualizar_lista_colunas(camada)
        self.dlg.chk_modo_formulario.blockSignals(False)
        self.dlg.chk_modo_individual.blockSignals(False)
        self.atualizar_estado_modos_exibicao()


    def atualizar_lista_colunas(self, camada):
        """Reconstrói a grade de 3 colunas com distribuição balanceada descendente."""

        container = self.dlg.scrollAreaWidgetContents
        layout_atual = container.layout()

        if layout_atual:
            while layout_atual.count():
                item = layout_atual.takeAt(0)
                if item.widget(): item.widget().deleteLater()
            if not isinstance(layout_atual, QGridLayout):
                sip.delete(layout_atual)
                layout_atual = QGridLayout(container)
                container.setLayout(layout_atual)
        else:
            layout_atual = QGridLayout(container)
            container.setLayout(layout_atual)

        layout_atual.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.colunas_atuais = [] 
        
        campos = list(camada.fields())
        N = len(campos)
        if N == 0: return

        if N <= 2:
            distribuicao = [N, 0, 0]
        elif N <= 4:
            c1 = math.ceil(N / 2)
            distribuicao = [c1, N - c1, 0]
        else:
            base = N // 3
            resto = N % 3
            distribuicao = [base + (1 if resto > 0 else 0), base + (1 if resto > 1 else 0), base]

        idx_campo = 0
        for col_index, limite_linhas in enumerate(distribuicao):
            for row_index in range(limite_linhas):
                if idx_campo < N:
                    campo = campos[idx_campo]
                    nome = campo.name()
                    cb = QCheckBox(nome)
                    self.colunas_atuais.append(cb)
                    cb.stateChanged.connect(self.atualizar_tabela_por_selecao)                    
                    cb.stateChanged.connect(self.verificar_estado_selecionar_todos)
                    cb.stateChanged.connect(self.atualizar_estado_modos_exibicao)
                    cb.stateChanged.connect(self.disparar_preview_se_autorizado)
                    layout_atual.addWidget(cb, row_index, col_index)
                    self.dlg.combo_atlas.addItem(nome, nome)
                    idx_campo += 1


    def marcar_desmarcar_todos(self, state):
        """Bulk check/uncheck attributes using the control list."""

        check = (state == Qt.CheckState.Checked)
        for cb in self.colunas_atuais:
            cb.setChecked(check)


    def verificar_estado_selecionar_todos(self):
        """Synchronize the master 'Select All' checkbox state."""

        container = self.dlg.scrollAreaWidgetContents
        cbs = container.findChildren(QCheckBox)
        total = len(cbs)
        marcados = sum(1 for cb in cbs if cb.isChecked())
        self.dlg.chk_selecionar_todos.blockSignals(True)
        self.dlg.chk_selecionar_todos.setChecked(total > 0 and total == marcados)
        self.dlg.chk_selecionar_todos.blockSignals(False)


    def atualizar_estado_modos_exibicao(self):
        """Manage accessibility for display modes using the control list."""

        tem_selecao = any(cb.isChecked() for cb in self.colunas_atuais)
        self.dlg.chk_modo_formulario.setEnabled(tem_selecao)
        self.dlg.chk_modo_individual.setEnabled(tem_selecao)
        if tem_selecao:
            if not self.dlg.chk_modo_formulario.isChecked() and not self.dlg.chk_modo_individual.isChecked():
                self.dlg.chk_modo_formulario.setChecked(True)
        else:
            self.dlg.chk_modo_formulario.setChecked(False)
            self.dlg.chk_modo_individual.setChecked(False)


    def atualizar_tabela_por_selecao(self):
        """Synchronize the sample data table with the user's attribute selection."""

        camada = self.dlg.mMapLayerComboBox.currentLayer()
        tabela = self.dlg.tableWidget
        if not camada: return
        
        colunas = [cb.text() for cb in self.dlg.scrollAreaWidgetContents.findChildren(QCheckBox) if cb.isChecked()]
        tabela.setColumnCount(len(colunas))
        tabela.setHorizontalHeaderLabels(colunas)
        
        if not colunas:
            tabela.setRowCount(0); return
            
        num_linhas = min(10, camada.featureCount())
        tabela.setRowCount(num_linhas)
        for i, feicao in enumerate(camada.getFeatures(QgsFeatureRequest().setLimit(num_linhas))):
            for j, col in enumerate(colunas):
                tabela.setItem(i, j, QTableWidgetItem(str(feicao[col])))
        tabela.resizeColumnsToContents()

    #####################################################################################
    # --- 4. LÓGICA GEOGRÁFICA E ESCALA ---
    #####################################################################################

    def configurar_escala_ao_mudar_camada(self):
        """Define o botão padrão inicial ao trocar de camada."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return
        
        # Agora a Escala Automática é o padrão para qualquer tipo de geometria
        self.dlg.rb_escala_auto.setChecked(True)
        
        self.atualizar_lista_atributos()
        self.validar_geometria_escala()


    def validar_geometria_escala(self):
        """Gerencia a liberação dos botões de escala."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return

        self.dlg.combo_escala_fixa.blockSignals(True)
        
        # Habilita a Escala Automática para todos os casos (Pontos, Linhas, Polígonos)
        self.dlg.rb_escala_auto.setEnabled(True)
        self.dlg.combo_escala_fixa.setEnabled(self.dlg.rb_escala_fixa.isChecked())

        if self.dlg.rb_escala_fixa.isChecked():
            self.dlg.combo_escala_fixa.setCurrentIndex(2) # 1:10.000 como default visual

        self.dlg.combo_escala_fixa.blockSignals(False)
        self.disparar_preview_se_autorizado()

    ####################################################################################
    # --- 5. RENDERIZAÇÃO E LAYOUT ---
    ####################################################################################

    def obter_dados_preview(self):
        """Atualiza o cache de feições apenas se a camada ou o agrupamento mudar."""

        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada:
            self.preview_data_cache = None
            return None

        campo_atlas = self.dlg.combo_atlas.currentData()
        
        # CONDIÇÃO DE INVALIDAÇÃO:
        # Só buscamos as feições no QGIS se:
        # 1. O cache está vazio
        # 2. A camada selecionada mudou
        # 3. O campo de agrupamento (Atlas) mudou
        if (self.preview_data_cache is None or 
            self.ultimo_id_camada != camada.id() or 
            self.cache_val_atlas != campo_atlas):
            
            self.ultimo_id_camada = camada.id()
            self.cache_val_atlas = campo_atlas
            
            # Buscamos a primeira feição disponível (limit 1 para ser instantâneo)
            # Nota: Não usamos Subset aqui para ter a feição completa em cache
            request_amostra = QgsFeatureRequest().setLimit(1)
            sample_f = next(camada.getFeatures(request_amostra), None)
            
            if sample_f:
                if campo_atlas is None:
                    self.preview_data_cache = [sample_f]
                else:
                    # Se houver atlas, buscamos todas as feições do mesmo grupo
                    val_amostra = sample_f.attribute(campo_atlas)
                    # --- NOVA LÓGICA DE FILTRO NULL ---                    
                    if val_amostra is None or val_amostra == NULL:
                        filtro = f'"{campo_atlas}" IS NULL'
                    else:
                        filtro = f'"{campo_atlas}" = {QgsExpression.quotedValue(val_amostra)}'
                    # ----------------------------------
                    request_filtrado = QgsFeatureRequest().setFilterExpression(filtro)
                    self.preview_data_cache = list(camada.getFeatures(request_filtrado))
            else:
                self.preview_data_cache = None
        
        return self.preview_data_cache
    
    
    def atualizar_preview(self):
        """Renders a sample layout preview usando cache de feições."""

        if not self.dlg or sip.isdeleted(self.dlg):
            return        

        if self.is_rendering or not self.dlg.isVisible(): 
            return
        
        self.is_rendering = True
        self.dlg.previewProgressBar.show() 
        self.dlg.previewProgressBar.setRange(0, 0) 
        QCoreApplication.processEvents()

        self.limpar_clones_preview()

        layout = None
        try:
            # 1. Recupera feições da memória (instantâneo se já carregadas)
            feicoes = self.obter_dados_preview()
            camada = self.dlg.mMapLayerComboBox.currentLayer()

            if not feicoes or not camada:
                self.dlg.lbl_preview.clear()
                return

            # 2. Prepara o Layout
            layout = QgsPrintLayout(QgsProject.instance())
            layout.initializeDefaults()
            
            # 3. Monta o design (Lógica estritamente visual)
            nome_sufixo_preview = "Preview"
            campo_atlas = self.dlg.combo_atlas.currentData()
            if campo_atlas and feicoes:
                val_atr = feicoes[0].attribute(campo_atlas)
                val_str = "Null" if val_atr is None else str(val_atr).strip()
                nome_sufixo_preview = re.sub(r'[\\/*?:"<>|]', "", val_str).strip()[:8].strip()

            self.montar_design_da_pagina(
                layout, 
                camada, 
                feicoes, 
                self.dlg.combo_presets.currentData(),
                "Retrato" if self.dlg.rb_retrato.isChecked() else "Paisagem",
                is_preview=True,
                nome_sufixo=nome_sufixo_preview # <-- Repassa o nome
            )

            # 4. Exporta para imagem
            exporter = QgsLayoutExporter(layout)
            image = exporter.renderPageToImage(0)
            if not image.isNull():
                self.dlg.lbl_preview.setPixmap(QPixmap.fromImage(image).scaled(
                    self.dlg.lbl_preview.size(), 
                    Qt.AspectRatioMode.KeepAspectRatio, 
                    Qt.TransformationMode.SmoothTransformation
                ))
        
        finally:
            if layout:
                layout.pageCollection().clear()
                sip.delete(layout)
            
            self.limpar_clones_preview()

            self.is_rendering = False
            self.dlg.previewProgressBar.hide()
            self.dlg.previewProgressBar.setRange(0, 100)
            QCoreApplication.processEvents()


    def disparar_preview_se_autorizado(self):
        """Start the preview timer only if auto-update is checked."""

        if hasattr(self.dlg, 'chk_preview_auto') and self.dlg.chk_preview_auto.isChecked():
            self.timer_preview.start(450)


    def processar_clique_ok(self):
        """Processes configurations and generates the multi-page layout."""
        
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return
        
        self.abort_processing = False
        container = self.dlg.scrollAreaWidgetContents
        colunas_selecionadas = [cb.text() for cb in container.findChildren(QCheckBox) if cb.isChecked()]

        paginas_dados = []
        campo_atlas = self.dlg.combo_atlas.currentData()
        
        # 1. Pegamos os nomes das colunas que o usuário quer VER no mapa
        colunas_para_carregar = list(colunas_selecionadas)
        if campo_atlas and campo_atlas not in colunas_para_carregar:
            colunas_para_carregar.append(campo_atlas)

        if campo_atlas is None:
            # CORREÇÃO: Sem Subset! Puxa a feição inteira para não quebrar a simbologia
            request = QgsFeatureRequest() 
            for f in camada.getFeatures(request): 
                paginas_dados.append({'feicoes': [f]})
        
        else:
            idx_campo = camada.fields().indexOf(campo_atlas)
            valores_unicos = camada.uniqueValues(idx_campo)
            
            # --- NOVA LÓGICA DE ORDENAÇÃO (NULL POR ÚLTIMO) ---
            valores_validos = []
            tem_nulo = False
            
            for val in valores_unicos:
                if val is None or val == NULL:
                    tem_nulo = True
                else:
                    valores_validos.append(val)
            
            # Ordena os valores válidos (usando key=str para evitar erro se houver mistura de números e texto)
            valores_validos.sort(key=lambda x: str(x) if x is not None else "")
            
            # Adiciona o Nulo no final, se existir
            valores_ordenados = valores_validos
            if tem_nulo:
                valores_ordenados.append(NULL)
            # ---------------------------------------------------

            for val in valores_ordenados:
                if val is None or val == NULL:
                    filtro = f'"{campo_atlas}" IS NULL'
                else:
                    filtro = f'"{campo_atlas}" = {QgsExpression.quotedValue(val)}'

                request = QgsFeatureRequest().setFilterExpression(filtro)
                feicoes_do_grupo = list(camada.getFeatures(request))
                if feicoes_do_grupo:
                    paginas_dados.append({'feicoes': feicoes_do_grupo})
        
        if paginas_dados: 
            self.criar_layout_multi_paginas(
                self.dlg.combo_presets.currentData(), camada, 
                "Retrato" if self.dlg.rb_retrato.isChecked() else "Paisagem", 
                self.dlg.combo_tamanho_pagina.currentData(), 
                paginas_dados, colunas_selecionadas, None,
                export_info=getattr(self, 'export_info', None) # Passa o dicionário
            )
            
        self.export_info = None # Reseta a variável para o botão OK padrão


    def criar_layout_multi_paginas(self, preset, camada, orientacao, tamanho_pg, paginas_dados, colunas, modos, export_info=None):
        """Gera o layout definitivo ou exporta os arquivos com otimização de memória."""
        
        self.is_rendering = True
        self.abort_processing = False
        
        try:
            project = QgsProject.instance()
            manager = project.layoutManager()
            nome_camada = re.sub(r'[^a-zA-Z0-9_]', '_', unicodedata.normalize('NFD', camada.name()).encode('ascii', 'ignore').decode('utf-8'))
            layout_name = f"VectorToMap_{nome_camada}"
            
            # --- CORREÇÃO DO LINTER: Inicializar variáveis antes para o Python não reclamar ---
            is_individual = False
            base_path = ""
            ext = ""
            base_sem_ext = ""
            layout = None
            
            if export_info:
                is_individual = export_info.get('individual', False)
                base_path = export_info.get('path', '')
                ext = export_info.get('ext', '')
                if ext:
                    base_sem_ext = base_path[:-len(ext)]

                # ==========================================================
                # --- VERIFICAÇÃO DE SOBRESCRITA (PRE-FLIGHT CHECK) ---
                arquivos_existentes = False
                
                if is_individual:
                    # Prevemos os nomes baseados nas feições antes de rodar o loop principal
                    for i, dados in enumerate(paginas_dados):
                        campo_atlas = self.dlg.combo_atlas.currentData()
                        if campo_atlas and dados['feicoes']:
                            val_atr = dados['feicoes'][0].attribute(campo_atlas)
                            nome_teste = "Null" if val_atr is None or val_atr == NULL else str(val_atr).strip()
                            # Limpa os caracteres e pega os primeiros 8
                            nome_teste = re.sub(r'[\\/*?:"<>|]', "", nome_teste).strip()[:8].strip()
                        else:
                            # Se não for agrupado, o arquivo será nomeado apenas com o número da página
                            nome_teste = str(i + 1)
                        
                        if os.path.exists(f"{base_sem_ext}_{nome_teste}{ext}"):
                            arquivos_existentes = True
                            break # Achou um arquivo conflitante, já é o suficiente para parar e avisar
                else:
                    # Se for arquivo único, checa apenas o nome do arquivo final
                    if os.path.exists(base_path):
                        arquivos_existentes = True
                
                if arquivos_existentes:
                    resposta = QMessageBox.question(
                        self.dlg,
                        self.tr("Confirmar Sobrescrita"),
                        self.tr("Arquivos com os mesmos nomes já existem no diretório de destino.\n\nDeseja sobrescrevê-los?"),
                        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                        QMessageBox.StandardButton.No
                    )
                    if resposta == QMessageBox.StandardButton.No:
                        self.is_rendering = False
                        self.abort_processing = True
                        return # Aborta o processo pacificamente
                # ==========================================================
            
            # Só criamos um Layout "Persistente" (que acumula páginas) se a exportação NÃO for individual
            if not is_individual:
                contador = 1
                while manager.layoutByName(layout_name):
                    layout_name = f"VectorToMap_{nome_camada}_{contador}"
                    contador += 1
                layout = QgsPrintLayout(project)
                layout.initializeDefaults()
                layout.setName(layout_name)

            total_paginas = len(paginas_dados)
            self.dlg.progressBar.show()
            self.dlg.progressBar.setRange(0, total_paginas)
            self.dlg.progressBar.setValue(0)

            for i, dados in enumerate(paginas_dados):
                if self.abort_processing: break

                # --- IDENTIFICAÇÃO DINÂMICA DO NOME DO ARQUIVO/CAMADA ---
                campo_atlas = self.dlg.combo_atlas.currentData()
                if campo_atlas and dados['feicoes']:
                    val_atr = dados['feicoes'][0].attribute(campo_atlas)
                    nome_sufixo = "Null" if val_atr is None or val_atr == NULL else str(val_atr).strip()
                    # Limpa caracteres especiais e limita aos 8 primeiros caracteres
                    nome_sufixo = re.sub(r'[\\/*?:"<>|]', "", nome_sufixo).strip()[:8].strip()
                    nome_arquivo = nome_sufixo # O arquivo ganha o nome do grupo
                else:
                    # Se não tem Atlas, usamos o número sequencial (1, 2, 3...)
                    nome_arquivo = str(i + 1)
                    nome_sufixo = nome_arquivo # Repassa esse número para ser o sufixo da camada também!

                # --- MONTAGEM E EXPORTAÇÃO ---
                if is_individual:
                    # Lógica Otimizada: 1 Layout descartável de página única por arquivo
                    layout_temp = QgsPrintLayout(project)
                    layout_temp.initializeDefaults()
                    
                    # Monta o design na página 0 do layout temporário
                    self.montar_design_da_pagina(layout_temp, camada, dados['feicoes'], preset, orientacao, pagina_index=0, nome_sufixo=nome_sufixo)
                    layout_temp.refresh()
                    
                    # Exporta imediatamente usando a variável nome_arquivo!
                    caminho_final = f"{base_sem_ext}_{nome_arquivo}{ext}"
                    exporter = QgsLayoutExporter(layout_temp)
                    
                    if ext in [".png", ".jpg", ".jpeg"]:
                        # Usa o motor nativo para garantir transparência e 300 DPI
                        img_settings = QgsLayoutExporter.ImageExportSettings()
                        img_settings.dpi = 300 
                        exporter.exportToImage(caminho_final, img_settings)
                        
                    # ========================================================
                    # NOVO BLOCO SVG: Entra aqui se o usuário escolheu .svg
                    elif ext == ".svg":
                        svg_settings = QgsLayoutExporter.SvgExportSettings()
                        svg_settings.exportLabelsToPaths = False # ISSO É O SEGREDO: Garante texto editável!
                        exporter.exportToSvg(caminho_final, svg_settings)
                    # ========================================================
                        
                    else:
                        pdf_settings = QgsLayoutExporter.PdfExportSettings()
                        pdf_settings.dpi = 300
                        exporter.exportToPdf(caminho_final, pdf_settings)
                        
                    # Libera a memória instantaneamente para a próxima volta do loop
                    layout_temp.pageCollection().clear()
                    sip.delete(layout_temp)
                    
                else:
                    # Lógica Tradicional: Acumula as páginas no Layout Persistente
                    if layout is not None:
                        if i > 0: layout.pageCollection().addPage(QgsLayoutItemPage(layout))
                        self.montar_design_da_pagina(layout, camada, dados['feicoes'], preset, orientacao, pagina_index=i, nome_sufixo=nome_sufixo)

                        # Coleta de lixo para layouts grandes
                        if i % 20 == 0:
                            layout.refresh()
                            gc.collect()

                self.dlg.progressBar.setValue(i + 1)
                QCoreApplication.processEvents()

            # --- FINALIZAÇÃO ---
            if not self.abort_processing:
                if export_info:
                    # Se o usuário pediu um ARQUIVO ÚNICO DE PDF, exportamos o layout persistente
                    if not is_individual and ext == ".pdf" and layout is not None:
                        layout.refresh()
                        exporter = QgsLayoutExporter(layout)
                        settings = QgsLayoutExporter.PdfExportSettings()
                        settings.dpi = 300
                        exporter.exportToPdf(base_path, settings)
                        
                        # Limpa o layout da memória do QGIS, já que foi salvo em arquivo
                        layout.pageCollection().clear()
                        sip.delete(layout)

                    self.iface.messageBar().pushMessage(self.tr("Sucesso"), self.tr("Exportação concluída com sucesso!"), level=0)
                else:
                    # Se NÃO tem export_info, o usuário clicou no botão "OK" (Abrir layout)
                    if layout is not None:
                        layout.refresh()
                        manager.addLayout(layout)
                        self.iface.openLayoutDesigner(layout)
            else:
                self.iface.messageBar().pushMessage(self.tr("Cancelado"), self.tr("O processamento foi interrompido."), level=2)

        except Exception as e:
            erro_detalhado = traceback.format_exc()
            self.iface.messageBar().pushMessage(
                self.tr("Erro"), 
                f"{str(e)}. {self.tr('Veja o console para detalhes.')}", 
                level=3
            )
            print(erro_detalhado)

        finally:
            self.is_rendering = False
            self.dlg.progressBar.hide()
            gc.collect()


    # ====================================================================================
    # --- RENDERIZAÇÃO E DESIGN (ARQUITETURA REFATORADA v0.4.3) ---
    # ====================================================================================

    def montar_design_da_pagina(self, layout, camada, feicoes_da_pagina, preset, orientacao, pagina_index=0, is_preview=False, nome_sufixo=""):
        """Maestro: Orquestra a montagem completa da página chamando sub-funções menores."""

        # 1. Prepara o papel (Tamanho, Geometria e Fundo)
        geometria, apenas_mapa, cor_fundo = self._configurar_papel_e_fundo(
            layout, pagina_index, preset, orientacao, is_preview
        )

        # 2. Cria o item de mapa e o posiciona na página
        map_item = self._adicionar_item_mapa(
            layout, geometria, apenas_mapa, cor_fundo
        )

        # 3. Calcula o zoom geográfico exato das feições
        self._aplicar_extensao_e_escala(
            map_item, camada, feicoes_da_pagina, geometria['w_map'], geometria['h_map']
        )

        # 4. Define quais camadas (TOC e clones) aparecem no mapa
        self._gerenciar_visibilidade_camadas(
            map_item, camada, feicoes_da_pagina, is_preview, nome_sufixo, pagina_index
        )

        # 5. Adiciona os textos e tabela de atributos (se houver espaço)
        self._renderizar_textos_e_atributos(
            layout, feicoes_da_pagina, preset, orientacao, geometria, apenas_mapa
        )

        # 6. Adiciona numeração da página no rodapé
        if not apenas_mapa:
            self.adicionar_numeracao_pagina(layout, geometria['w_pg'], geometria['h_pg'], geometria['y_zero'])


    # ------------------------------------------------------------------------------------
    # --- SUB-FUNÇÕES DE MONTAGEM ---
    # ------------------------------------------------------------------------------------

    def _configurar_papel_e_fundo(self, layout, pagina_index, preset, orientacao, is_preview):
        """Calcula dimensões, recortes e o comportamento de fundo (transparência/PNG)."""
        dim = self.dlg.combo_tamanho_pagina.currentData()
        if not dim: dim = (210.0, 297.0)

        w_pg, h_pg = (dim[1], dim[0]) if orientacao == "Paisagem" else (dim[0], dim[1])
        pagina = layout.pageCollection().pages()[pagina_index]
        pagina.setPageSize(QgsLayoutSize(w_pg, h_pg, QgsUnitTypes.LayoutMillimeters))

        y_zero_folha = pagina_index * (h_pg + 10)
        margin_padrao = 10.0

        # Modelos Pré-definidos
        if preset == "75_altura":
            header_h = 35.0
            h_util = h_pg - (2 * margin_padrao) - header_h
            w_map_f, h_map_f = w_pg - (2 * margin_padrao), h_util * 0.75
            x_map_f, y_map_f = margin_padrao, y_zero_folha + margin_padrao + header_h
            
        elif preset == "vertical":
            if orientacao == "Retrato":
                # Mantém a mesma base inferior do Mapa Quadrado, mas "estica" o topo para margem de 1cm
                w_map_f = w_pg - (2 * margin_padrao)
                x_map_f = margin_padrao
                y_map_f = y_zero_folha + margin_padrao
                h_map_f = w_pg + 10.0 # <-- Matemática: Fica cravado na mesma linha de base do mapa quadrado!
            else:
                # Paisagem: 50% da área útil na direita
                largura_util = w_pg - (2 * margin_padrao)
                h_map_f = h_pg - (2 * margin_padrao)
                w_map_f = largura_util * 0.50
                x_map_f = w_pg - margin_padrao - w_map_f
                y_map_f = y_zero_folha + margin_padrao
                
        else: # "quadrado"
            if orientacao == "Retrato":
                w_map_f, h_map_f = w_pg - (2 * margin_padrao), w_pg - (2 * margin_padrao)
                x_map_f, y_map_f = margin_padrao, y_zero_folha + 40.0
            else:
                h_map_f = h_pg - (2 * margin_padrao)
                w_map_f = h_map_f
                x_map_f, y_map_f = w_pg - margin_padrao - w_map_f, y_zero_folha + margin_padrao

        # Checagem de Fundo e Exportação Avulsa
        apenas_mapa = hasattr(self.dlg, 'chk_apenas_mapa') and self.dlg.chk_apenas_mapa.isChecked()
        cor_fundo = QColor(255, 255, 255, 255)
        if hasattr(self.dlg, 'btn_cor_fundo'):
            cor_fundo = self.dlg.btn_cor_fundo.color()

        if apenas_mapa:
            w_pg, h_pg = w_map_f, h_map_f
            x_map_f = 0.0
            y_zero_folha = pagina_index * (h_pg + 10)
            y_map_f = y_zero_folha
            pagina.setPageSize(QgsLayoutSize(w_pg, h_pg, QgsUnitTypes.LayoutMillimeters))

        is_png_export = False
        if not is_preview:
            info = getattr(self, 'export_info', {}) or {}
            if info.get('ext', '').lower() == '.png': is_png_export = True

        if apenas_mapa and is_png_export:
            pagina.setBackgroundEnabled(True)
            if pagina.pageStyleSymbol(): pagina.pageStyleSymbol().setColor(QColor(255, 255, 255, 0))
        else:
            pagina.setBackgroundEnabled(True)
            if pagina.pageStyleSymbol(): pagina.pageStyleSymbol().setColor(QColor(255, 255, 255, 255))

        # Empacota a geometria para facilitar o repasse entre funções
        geometria = {
            'w_pg': w_pg, 'h_pg': h_pg, 'w_map': w_map_f, 'h_map': h_map_f,
            'x_map': x_map_f, 'y_map': y_map_f, 'y_zero': y_zero_folha, 'margin': margin_padrao
        }
        return geometria, apenas_mapa, cor_fundo


    def _adicionar_item_mapa(self, layout, geo, apenas_mapa, cor_fundo):
        """Cria e injeta o quadro do mapa no layout."""
        map_item = QgsLayoutItemMap(layout)
        map_item.setFrameEnabled(not apenas_mapa) 
        map_item.setBackgroundEnabled(True)
        map_item.setBackgroundColor(cor_fundo)
        layout.addLayoutItem(map_item)
        
        map_item.attemptResize(QgsLayoutSize(geo['w_map'], geo['h_map'], QgsUnitTypes.LayoutMillimeters))
        map_item.attemptMove(QgsLayoutPoint(geo['x_map'], geo['y_map'], QgsUnitTypes.LayoutMillimeters))
        return map_item


    def _aplicar_extensao_e_escala(self, map_item, camada, feicoes_da_pagina, w_map, h_map):
        """Gerencia o BoundingBox, CRS e regras de zoom máximo do mapa."""
        if not feicoes_da_pagina: return

        ext = QgsRectangle()
        ext.setMinimal()
        for f in feicoes_da_pagina: ext.combineExtentWith(f.geometry().boundingBox())
        
        project_crs = QgsProject.instance().crs()
        trans = QgsCoordinateTransform(camada.crs(), project_crs, QgsProject.instance().transformContext())
        ext_proj = trans.transformBoundingBox(ext)
        
        is_coordenada_unica = (ext_proj.width() == 0 and ext_proj.height() == 0)
        if is_coordenada_unica: 
            respiro = 0.0001 if project_crs.isGeographic() else 1.0
            ext_proj.grow(respiro)
        
        map_item.setExtent(ext_proj)
        
        if self.dlg.rb_escala_fixa.isChecked():
            escala_val = self.dlg.combo_escala_fixa.currentData()
            map_item.setScale(float(escala_val) if escala_val else 10000.0)
        else:
            if is_coordenada_unica:
                escala_final = 10000.0
            else:
                unit_to_mm = QgsUnitTypes.fromUnitToUnitFactor(project_crs.mapUnits(), QgsUnitTypes.DistanceMillimeters)
                scale_w = (ext_proj.width() * unit_to_mm) / w_map
                scale_h = (ext_proj.height() * unit_to_mm) / h_map
                escala_calculada = max(scale_w, scale_h) * 1.25
                escala_final = 10000.0 if escala_calculada < 500.0 else escala_calculada
        
            map_item.setScale(escala_final)

        map_item.refresh()
        map_item.attemptResize(QgsLayoutSize(w_map, h_map, QgsUnitTypes.LayoutMillimeters))
        # Ajusta posição sem alterar o tamanho consolidado
        map_item.attemptMove(QgsLayoutPoint(map_item.pos().x(), map_item.pos().y(), QgsUnitTypes.LayoutMillimeters))


    def _gerenciar_visibilidade_camadas(self, map_item, camada, feicoes_da_pagina, is_preview, nome_sufixo, pagina_index):
        """Avalia as regras do TOC, cria camadas clone (se necessário) e trava os estilos."""
        camada_alvo = camada

        # 1. Criação do Clone para Filtro
        if self.dlg.chk_filtrar_feicoes.isChecked():
            nome_camada_limpo = re.sub(r'[^a-zA-Z0-9_]', '_', unicodedata.normalize('NFD', camada.name()).encode('ascii', 'ignore').decode('utf-8'))
            nome_temp = f"{nome_camada_limpo}_{nome_sufixo}" if nome_sufixo else f"{nome_camada_limpo}_{pagina_index + 1}"
            camada_alvo = self.criar_camada_temporaria(camada, feicoes_da_pagina, nome_temp, is_preview)
            if is_preview: self.clones_preview.append(camada_alvo.id())

        # 2. Definição do Array de Camadas Exibidas
        if self.dlg.chk_exibir_so_camada_atual.isChecked():
            map_item.setLayers([camada_alvo])
            
        elif self.dlg.chk_filtrar_feicoes.isChecked() or self.dlg.chk_travar_camadas.isChecked():            
            root = QgsProject.instance().layerTreeRoot()
            camadas_finais_para_layout = []
            forcar_clone = self.dlg.chk_filtrar_feicoes.isChecked()
            
            for layer in root.layerOrder():
                is_visible_in_toc = root.findLayer(layer.id()).isVisible()
                if layer.id() == camada.id():
                    if is_visible_in_toc or forcar_clone:
                        if camada_alvo not in camadas_finais_para_layout: camadas_finais_para_layout.append(camada_alvo)
                else:
                    if is_visible_in_toc and layer.id() not in self.clones_preview and layer.id() != camada_alvo.id():
                        camadas_finais_para_layout.append(layer)
                        
            map_item.setLayers(camadas_finais_para_layout)

        # 3. Travar Camadas e Estilos
        if self.dlg.chk_travar_camadas.isChecked():
            map_item.setKeepLayerSet(True)
            if hasattr(self.dlg, 'chk_travar_estilos') and self.dlg.chk_travar_estilos.isChecked():
                map_item.setKeepLayerStyles(True)

        map_item.refresh()

        # Oculta da Legend principal se for exportação final com clone
        if self.dlg.chk_filtrar_feicoes.isChecked() and not is_preview:
            no_da_camada = QgsProject.instance().layerTreeRoot().findLayer(camada_alvo.id())
            if no_da_camada: no_da_camada.setItemVisibilityChecked(False)


    def _renderizar_textos_e_atributos(self, layout, feicoes_da_pagina, preset, orientacao, geo, apenas_mapa):
        """Posiciona e renderiza os labels em formato HTML ou linha a linha."""
        colunas = [cb.text() for cb in self.dlg.scrollAreaWidgetContents.findChildren(QCheckBox) if cb.isChecked()]
        limite_fundo = geo['y_zero'] + geo['h_pg'] - geo['margin']

        if not (colunas and feicoes_da_pagina and hasattr(self.dlg, 'chk_exibir_atributos') and self.dlg.chk_exibir_atributos.isChecked() and not apenas_mapa):
            return

        # Zonas de renderização
        if preset == "75_altura":
            largura_util = geo['w_pg'] - (2 * geo['margin'])
            terco_x = geo['margin'] + (largura_util * 0.33) 
            
            x_form, y_form = geo['margin'], geo['y_map'] + geo['h_map'] + 3.0
            w_form = (largura_util * 0.33) - 2.0
            h_form = limite_fundo - (geo['y_map'] + geo['h_map'] + 3.0)
            
            x_ind_start = terco_x + 2.0
            y_ind_min = geo['y_map'] + geo['h_map'] + 2.0
            
        elif preset == "vertical":
            x_form = geo['margin']
            if orientacao == "Retrato":
                y_form = geo['y_map'] + geo['h_map'] + 5.0
                w_form = geo['w_pg'] - (2 * geo['margin'])

                x_ind_start = x_form
                if self.dlg.chk_modo_individual.isChecked():
                    h_form = (limite_fundo - y_form) * 0.5
                    y_ind_min = y_form + h_form + 2.0
                else:
                    h_form = (limite_fundo - y_form)
                    y_ind_min = y_form
            else:
                # Paisagem: Divide a área à esquerda (50% da folha) em 3 blocos verticais
                h_util = geo['h_map']
                h_top = h_util * 0.34 # 34% Superior em branco
                
                y_form = geo['y_map'] + h_top # Formulário começa na linha dos 34%
                h_form = h_util * 0.33        # Formulário ocupa os 33% do meio
                
                # Largura do texto tem um "respiro" de 5mm para não colar na borda do mapa
                w_form = (geo['x_map'] - geo['margin']) - 5.0 

                x_ind_start = x_form
                y_ind_min = y_form + h_form   # Textos Individuais ocupam os 33% de baixo
                
        else: # quadrado
            x_form = geo['margin']
            if orientacao =="Retrato":
                y_form = geo['y_map'] + geo['h_map'] + 5.0
                w_form = geo['w_pg'] - (2 * geo['margin'])
            else:
                y_form = geo['y_map'] + (geo['h_map'] / 2)
                w_form = geo['x_map'] - (2 * geo['margin'])

            x_ind_start = x_form
            if self.dlg.chk_modo_individual.isChecked():
                h_form = (limite_fundo - y_form) * 0.5
                y_ind_min = y_form + h_form + 2.0
            else:
                h_form = (limite_fundo - y_form)
                y_ind_min = y_form

        # Modo Formulário (Bloco HTML)
        if self.dlg.chk_modo_formulario.isChecked():
            txt_html = ""
            for idx, f in enumerate(feicoes_da_pagina):                    
                for col in colunas:
                    try: txt_html += f"<b>{col}:</b> {str(f.attribute(col) or '').strip()}<br>"
                    except: continue
            
            lbl_f = QgsLayoutItemLabel(layout)
            mode = QgsLayoutItemLabel.ContentMode.ModeHtml if hasattr(QgsLayoutItemLabel, 'ContentMode') else (QgsLayoutItemLabel.ModeHtml if hasattr(QgsLayoutItemLabel, 'ModeHtml') else 1)
            lbl_f.setMode(mode)
            lbl_f.setText(txt_html)
            lbl_f.attemptMove(QgsLayoutPoint(x_form, y_form, QgsUnitTypes.LayoutMillimeters))
            lbl_f.attemptResize(QgsLayoutSize(w_form, h_form, QgsUnitTypes.LayoutMillimeters))
            layout.addLayoutItem(lbl_f)

        # Modo Individual (Linhas isoladas)
        if self.dlg.chk_modo_individual.isChecked():
            campo_atlas = self.dlg.combo_atlas.currentData() 
            altura_linha = 5.5

            if campo_atlas is None:
                f = feicoes_da_pagina[0] 
                xi, yi = x_ind_start, y_ind_min
                limite_colunas = (3 if orientacao == "Retrato" else 5) if preset == "75_altura" else (5 if orientacao == "Retrato" else 3)
                
                for idx, col in enumerate(colunas):
                    if idx > 0 and idx % limite_colunas == 0:
                        yi += altura_linha
                        xi = x_ind_start
                    try:
                        lbl_i = QgsLayoutItemLabel(layout)
                        val = f.attribute(col)
                        lbl_i.setText(f"{col}: {str(val).strip() if val is not None else ''}")
                        lbl_i.setFrameEnabled(True)
                        lbl_i.adjustSizeToText()
                        ancho_folga = lbl_i.rect().width() + 2.0
                        lbl_i.attemptResize(QgsLayoutSize(ancho_folga, lbl_i.rect().height(), QgsUnitTypes.LayoutMillimeters))
                        lbl_i.attemptMove(QgsLayoutPoint(xi, yi, QgsUnitTypes.LayoutMillimeters))
                        layout.addLayoutItem(lbl_i)
                        xi += ancho_folga + 2.0
                    except: continue
            else:
                altura_total = len(feicoes_da_pagina) * altura_linha
                yi = (limite_fundo - altura_total) if (y_ind_min + altura_total) > limite_fundo else y_ind_min

                for f in feicoes_da_pagina:
                    xi = x_ind_start
                    for col in colunas:
                        try:
                            lbl_i = QgsLayoutItemLabel(layout)
                            val = f.attribute(col)
                            lbl_i.setText(f"{col}: {str(val).strip() if val is not None else ''}")
                            lbl_i.setFrameEnabled(True)
                            lbl_i.adjustSizeToText()
                            ancho_folga = lbl_i.rect().width() + 2.0
                            lbl_i.attemptResize(QgsLayoutSize(ancho_folga, lbl_i.rect().height(), QgsUnitTypes.LayoutMillimeters))
                            lbl_i.attemptMove(QgsLayoutPoint(xi, yi, QgsUnitTypes.LayoutMillimeters))
                            layout.addLayoutItem(lbl_i)
                            xi += ancho_folga + 2.0
                        except: continue
                    yi += altura_linha


    def adicionar_numeracao_pagina(self, layout, w_pg, h_pg, y_zero_folha):
        """Adiciona o número da página no canto inferior direito."""
        
        if not self.dlg.chk_numeracao.isChecked(): return
        label_page = QgsLayoutItemLabel(layout)
        label_page.setText("[% @layout_page %]") 
        label_page.adjustSizeToText() 
        label_page.setReferencePoint(QgsLayoutItem.LowerRight)
        x, y = w_pg - 5, y_zero_folha + h_pg - 5
        label_page.attemptMove(QgsLayoutPoint(x, y, QgsUnitTypes.LayoutMillimeters))
        label_page.setZValue(100)
        layout.addLayoutItem(label_page)