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

# /***************************************************************************
#  VectorToMap - QGIS Plugin
#  Automates the generation of print layouts for vector features.
#  Author: Matheus Durso Neves Caetano
# 
#  Copyright (C) 2026 Matheus Durso Neves Caetano
# 
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
# 
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#  GNU General Public License for more details.
#  ***************************************************************************/

import math
import re
import unicodedata
import os
import gc
import traceback
import sys
# --- NOVO: Ajuda o Python a enxergar bibliotecas externas (como o Sentry) ---
plugin_dir = os.path.dirname(__file__)
if plugin_dir not in sys.path:
    sys.path.append(plugin_dir) # Use APPEND em vez de insert(0)
# -------------------------------------------------------------------------------
import sentry_sdk
from qgis.PyQt import sip
from qgis.PyQt.QtCore import QTranslator, QCoreApplication, Qt, QTimer, QUrl, QObject, QEvent
from qgis.PyQt.QtGui import QIcon, QPixmap, QColor, QFont, QDesktopServices
from qgis.PyQt.QtWidgets import (
    QAction, QCheckBox, QTableWidgetItem, QFrame, QPushButton,
    QDialogButtonBox, QButtonGroup, QApplication, QGridLayout,
    QFileDialog, QMessageBox, QDialog, QVBoxLayout, QHBoxLayout, QLabel,
    QFontDialog, QGraphicsDropShadowEffect, QGraphicsOpacityEffect, QWidget,
    QTextBrowser, QComboBox)
from qgis.core import (
    QgsProject, QgsPrintLayout, QgsLayoutSize, QgsUnitTypes, QgsLayoutItemMap, 
    QgsLayoutPoint, QgsMapLayerProxyModel, QgsFeatureRequest, QgsSettings, 
    QgsLayoutExporter, QgsRectangle, QgsCoordinateTransform, QgsLayoutItemPage, 
    QgsLayoutItemLabel, QgsLayoutItem, QgsWkbTypes, QgsExpression, QgsVectorLayer, NULL,
    Qgis, QgsMessageLog
)
# Resources generated by Plugin Builder
from .resources import *
from .vector_to_map_dialog import VectorToMapDialog


# ============================================================================
# --- NOVO: CLASSE BLOQUEADORA DE CLIQUES (TEASER PRO) ---
# ============================================================================
class ProBlocker(QObject):
    """Intercepta cliques em ferramentas Pro e executa o redirecionamento."""
    def __init__(self, callback):
        super().__init__()
        self.callback = callback

    def eventFilter(self, obj, event):
        # Se o usuário tentar clicar com o mouse, matamos o evento e chamamos o aviso
        if event.type() == QEvent.Type.MouseButtonPress:
            self.callback()
            return True # Retorna True para 'consumir' o clique e impedir a ação normal
        return False
# ============================================================================


class BoasVindasDialog(QDialog):
    """Janela de Boas-Vindas limpa, alinhada e profissional."""
    def __init__(self, parent=None):
        super().__init__(parent)
        
        # --- NOVO: Atalho interno para tradução ---
        def tr(texto):
            return QCoreApplication.translate('@default', texto)
        # ------------------------------------------

        self.setWindowTitle("VectorToMap")
        self.resize(550, 350) # Ajustado o tamanho para o novo layout sem o card
        
        self.setWindowIcon(QIcon(':/plugins/vector_to_map/icon.png'))
        self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)

        # --- Layout Principal ---
        layout_principal = QVBoxLayout(self)
        # Margens confortáveis para respirar: Esquerda, Topo, Direita, Baixo.
        layout_principal.setContentsMargins(30, 25, 30, 20) 
        layout_principal.setSpacing(12) # Espaçamento padrão entre linhas

        # --- 1. CABEÇALHO (Ícone + Título alinhados horizontalmente) ---
        layout_cabecalho = QHBoxLayout()
        layout_cabecalho.setSpacing(15)

        # Ícone do Plugin (tamanho controlado 48x48)
        self.lbl_icone = QLabel()
        pixmap = QPixmap(':/plugins/vector_to_map/icon.png').scaled(48, 48, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
        self.lbl_icone.setPixmap(pixmap)
        layout_cabecalho.addWidget(self.lbl_icone)

        # Título
        lbl_titulo = QLabel(tr("Bem-vindo ao VectorToMap!"))
        fonte_titulo = lbl_titulo.font()
        fonte_titulo.setPointSize(fonte_titulo.pointSize() + 6) 
        fonte_titulo.setBold(True)
        lbl_titulo.setFont(fonte_titulo)
        layout_cabecalho.addWidget(lbl_titulo)
        
        layout_cabecalho.addStretch() # Alinha tudo à esquerda

        layout_principal.addLayout(layout_cabecalho)
        layout_principal.addSpacing(10) # Espaço extra após cabeçalho

        # --- 2. TEXTO DE BOAS-VINDAS ---
        texto_welcome = tr("Obrigado por instalar o VectorToMap para automatizar sua cartografia técnica!")
        lbl_welcome = QLabel(texto_welcome)
        lbl_welcome.setWordWrap(True)
        lbl_welcome.setStyleSheet("font-size: 10pt; color: #333;")
        layout_principal.addWidget(lbl_welcome)

        layout_principal.addSpacing(5) # Pequeno espaço antes da seção pro

        # --- 3. SEÇÃO PRO (LIMPA - SEM RETÂNGULO E SEM INCRA/IBAMA) ---

        # 1. Traduz apenas o texto legível
        t_tit_pro = tr("🚀 Quer economizar ainda mais tempo?")
        t_desc_pro = tr("Apoie o desenvolvimento e ganhe acesso à <b>Versão Pro</b>, que inclui suporte prioritário e modelos de layout exclusivos.")
        t_link_pro = tr("👉 Clique aqui para conhecer a Versão Pro")

        # 2. Injeta no HTML via f-string
        texto_pro_html = f"""
            <h3 style='margin: 0; color: #2c3e50;'>{t_tit_pro}</h3>
            <p style='margin: 10px 0 12px 0; color: #333;'>{t_desc_pro}</p>
            <p style='margin: 0;'><a href='https://matheusdurso.gumroad.com/l/vectortomap' style='color: #3498db; text-decoration: none;'><b>{t_link_pro}</b></a></p>
        """
        
        lbl_pro_texto = QLabel(texto_pro_html)
        lbl_pro_texto.setWordWrap(True)
        lbl_pro_texto.setOpenExternalLinks(True) 
        # Garante o alinhamento total à esquerda
        lbl_pro_texto.setAlignment(Qt.AlignmentFlag.AlignLeft) 
        layout_principal.addWidget(lbl_pro_texto)

        layout_principal.addSpacing(5) # Espaço antes da linha divisória

        # --- 4. LINHA DIVISÓRIA ---
        linha = QFrame()
        linha.setFrameShape(QFrame.Shape.HLine)
        linha.setFrameShadow(QFrame.Shadow.Sunken)
        linha.setStyleSheet("border: 1px solid #dcdcdc; background-color: transparent;")
        layout_principal.addWidget(linha)

        # --- 5. TEXTO DE CONSENTIMENTO DO SENTRY ---
        t_tit_sentry = tr("Relatórios de Erro:")
        t_desc_sentry = tr("Você aceita enviar relatórios automáticos e totalmente anônimos caso algum erro ocorra no sistema? Nenhuma informação pessoal ou dado geográfico será enviado.")
        
        texto_sentry = f"<b>{t_tit_sentry}</b><br>{t_desc_sentry}"

        lbl_sentry = QLabel(texto_sentry)
        lbl_sentry.setWordWrap(True)
        lbl_sentry.setStyleSheet("font-size: 9pt; color: #555;")
        layout_principal.addWidget(lbl_sentry)

        # --- 6. ESPAÇADOR VERTICAL (EMPURRA BOTÕES PARA O RODAPÉ) ---
        layout_principal.addStretch()  

        # --- 7. BOTÕES DE AÇÃO ---
        layout_botoes = QHBoxLayout()
        layout_botoes.setContentsMargins(0, 10, 0, 0) # Margem superior
        
        self.btn_recusar = QPushButton(tr("Não aceito, apenas começar"))
        self.btn_recusar.setCursor(Qt.CursorShape.PointingHandCursor)
        self.btn_recusar.setStyleSheet("padding: 8px 15px;") # Conforto no clique
        self.btn_recusar.clicked.connect(self.reject) 

        self.btn_aceitar = QPushButton(tr("Aceitar e Começar"))
        self.btn_aceitar.setCursor(Qt.CursorShape.PointingHandCursor)
        self.btn_aceitar.setDefault(True) 
        # CSS refinado para o botão de aceitar
        self.btn_aceitar.setStyleSheet("""
            QPushButton {
                background-color: #3498db; 
                color: white; 
                font-weight: bold; 
                padding: 8px 25px; 
                border: none; 
                border-radius: 4px;
            }
            QPushButton:hover {
                background-color: #2980b9;
            }
        """)
        self.btn_aceitar.clicked.connect(self.accept) 

        layout_botoes.addWidget(self.btn_recusar)
        layout_botoes.addStretch() # Mola entre os botões
        layout_botoes.addWidget(self.btn_aceitar)

        layout_principal.addLayout(layout_botoes)


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+B")

        # ==========================================================
        # --- NOVO: CÓDIGO PARA COLOCAR ÍCONE NO MENU PAI (RAIZ) ---
        # ==========================================================
        menu_complementos = self.iface.pluginMenu()
        
        # Varre todos os itens do menu "Complementos"
        for acao_menu in menu_complementos.actions():
            # Se encontrar o texto que criamos (self.menu = '&VectorToMap')
            if acao_menu.text() == self.menu:
                # Injeta o ícone na pasta principal
                acao_menu.setIcon(QIcon(icon_path))
                break


    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()

    
    def _verificar_consentimento_sentry(self):
        """Pergunta ao usuário apenas na primeira vez que ele abre o plugin."""
        settings = QgsSettings()
        
        # Verifica se já fizemos essa pergunta no passado
        ja_perguntou = settings.value("/VectorToMap/sentry_ja_perguntou", False, type=bool)
        
        if not ja_perguntou:
            # Chama a NOSSA janela estilizada
            dialog = BoasVindasDialog(self.iface.mainWindow())
            
            # Mostra a janela e congela o QGIS até o usuário clicar em algo
            # Se clicou em Aceitar, exec_() retorna 1. Se clicou em Recusar, retorna 0.
            aceitou = (dialog.exec_() == 1)
            
            # Salva a escolha na memória do QGIS
            settings.setValue("/VectorToMap/enviar_erros_sentry", aceitou)
            settings.setValue("/VectorToMap/sentry_ja_perguntou", True)

        # Retorna a escolha salva
        return settings.value("/VectorToMap/enviar_erros_sentry", False, type=bool)


    def inicializar_sentry(self):
        """Liga o motor do Sentry se o usuário permitiu e filtra erros externos."""
        self.sentry_ativo = False
        
        def filtro_sentry(event, hint):
            """Analisa o rastro do erro. Só envia se vier do VectorToMap."""
            # 1. Verifica se a mensagem de log menciona o seu plugin
            log_msg = event.get('logentry', {}).get('message', '')
            if 'VectorToMap' in log_msg:
                return event
                
            # 2. Verifica a pilha de erros (traceback)
            try:
                frames = event.get('exception', {}).get('values', [])[0].get('stacktrace', {}).get('frames', [])
                for frame in frames:
                    arquivo = frame.get('filename', '').lower()
                    # Se o erro passou por algum arquivo com 'vector_to_map' no nome ou caminho, é nosso!
                    if 'vector_to_map' in arquivo:
                        return event
            except Exception:
                pass
                
            # Se chegou aqui, o erro é de outro plugin do QGIS. Nós descartamos.
            return None

        try:
            sentry_sdk.init(
                dsn="https://3a3fd55bd680f6cc5594929bec0c7609@o4511038786240512.ingest.de.sentry.io/4511038808457296",
                send_default_pii=False, 
                release="vectortomap@1.5.0",
                before_send=filtro_sentry  # <--- INSERIMOS O FILTRO AQUI
            )
            self.sentry_ativo = True
            
        except ImportError:
            # Falha silenciosa se a biblioteca não estiver na pasta
            pass
        except Exception:
            # Qualquer outro erro de inicialização é ignorado para não travar o plugin
            pass

    
    # ##################################################################################
    # --- 2. INTERFACE E DIÁLOGO (REFATORADO v0.4.4) ---
    # ##################################################################################

    def run(self):
        """Maestro da Interface: Inicializa a UI e orquestra a montagem da janela."""
        
        if self.first_start:
            self.first_start = False
            
            # --- Verifica consentimento e liga o Sentry ---
            aceitou_sentry = self._verificar_consentimento_sentry()
            if aceitou_sentry:
                self.inicializar_sentry()
            # ----------------------------------------------------

            self.dlg = VectorToMapDialog()

            # --- Força os botões de Maximizar e Minimizar a aparecerem ---
            self.dlg.setWindowFlags(self.dlg.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint | Qt.WindowType.WindowMinimizeButtonHint)
            # -------------------------------------------------------------------

            # 1. Aplica as cores e estilos visuais
            self._aplicar_estilos()

            # 2. Preenche as listas e cria os botões personalizados
            self._configurar_widgets_iniciais()

            # 3. Conecta os cliques e gatilhos de eventos
            self._conectar_sinais()

            # 4. Ajusta o tamanho, posição e comportamento da janela no Windows/Mac
            self._configurar_janela()

            # 5. Prepara os textos para tradução
            self.setup_ui_strings()  
        
        # --- Sincroniza a ComboBox com a camada selecionada no QGIS ---
        camada_ativa = self.iface.activeLayer()
        # Verifica se existe uma camada selecionada E se ela é do tipo Vetorial
        if camada_ativa and isinstance(camada_ativa, QgsVectorLayer):
            self.dlg.mMapLayerComboBox.setLayer(camada_ativa)
        # --------------------------------------------------------------------

        self.dlg.show()


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

    def _aplicar_estilos(self):
        """Injeta o CSS consolidado (Azul Profissional & Clean) e esconde barras padrão."""

        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; 
            }
            
            /* --- 5. BOTÕES DA ABA DE SUPORTE --- */
            QPushButton#pushButton_apoiar {
                background-color: #3498db;
                color: white;
                font-weight: bold;
                font-size: 13px;
                padding: 8px 20px;
                border: none;
                border-radius: 4px;
            }
            QPushButton#pushButton_apoiar:hover {
                background-color: #2980b9;
            }

            QPushButton#pushButton_bugreport {
                background-color: transparent;
                color: #333333;
                font-size: 12px;
                padding: 8px 15px;
                border: 1px solid #cccccc;
                border-radius: 4px;
            }
            QPushButton#pushButton_bugreport:hover {
                background-color: #e0e0e0;
            }

            /* =================================================== */
            /* NOVO: BOTÃO DO TUTORIAL (VERMELHO YOUTUBE)          */
            /* =================================================== */
            QPushButton#pushButton_tutorial {
                background-color: #e74c3c;
                color: white;
                font-weight: bold;
                font-size: 12px;
                padding: 8px 15px;
                border: 1px solid #e74c3c;
                border-radius: 4px;
            }
            QPushButton#pushButton_tutorial:hover {
                background-color: #c0392b;
            }
            
            /* --- ESTILO DOS RECURSOS PRO (Falso Inativo) --- */
            QCheckBox#chk_export_individual, QCheckBox#chk_abrir_arquivo, QLabel#lbl_cor_pagina,
            QCheckBox#chk_map_fundo_transp, QCheckBox#chk_pag_fundo_transp,
            QCheckBox#chk_legenda, QCheckBox#chk_norte, QCheckBox#chk_escala, QCheckBox#chk_grid,
            QLabel#lblLegenda, QLabel#lblNorte, QLabel#lblEscala, QLabel#lblGrid,
            QComboBox#comboLegenda, QComboBox#comboNorte, QComboBox#comboEscala, QComboBox#comboGrid {
                color: #888888; /* Cinza que parece inativo */
            }
            QCheckBox#chk_export_individual:hover, QCheckBox#chk_abrir_arquivo:hover, QLabel#lbl_cor_pagina:hover,
            QCheckBox#chk_map_fundo_transp:hover, QCheckBox#chk_pag_fundo_transp:hover,
            QCheckBox#chk_legenda:hover, QCheckBox#chk_norte:hover, QCheckBox#chk_escala:hover, QCheckBox#chk_grid:hover,
            QLabel#lblLegenda:hover, QLabel#lblNorte:hover, QLabel#lblEscala:hover, QLabel#lblGrid:hover,
            QComboBox#comboLegenda:hover, QComboBox#comboNorte:hover, QComboBox#comboEscala:hover, QComboBox#comboGrid:hover {
                color: #3498db; /* Fica azul quando passa o mouse, convidando ao clique! */
                cursor: pointer;
            }
        """
        self.dlg.setStyleSheet(estilo_consolidado)
        self.dlg.previewProgressBar.hide()
        self.dlg.progressBar.hide()
        self.dlg.lbl_preview.setFrameShape(QFrame.Shape.StyledPanel)

        # --- NOVO: Efeito de Sombra na Folha de Preview ---
        sombra = QGraphicsDropShadowEffect()
        sombra.setBlurRadius(20) 
        sombra.setXOffset(5)
        sombra.setYOffset(5)
        sombra.setColor(QColor(0, 0, 0, 80)) 

        if hasattr(self.dlg, 'lbl_preview'):
            self.dlg.lbl_preview.setGraphicsEffect(sombra)
        # --------------------------------------------------
    

    def _adicionar_aba_sobre(self):
        """Injeta dinamicamente a aba 'Sobre' sincronizada com a tradução em blocos e Registro INPI."""
        if not hasattr(self.dlg, 'tabWidget'): return

        aba_sobre = QWidget()
        layout = QVBoxLayout(aba_sobre)
        layout.setContentsMargins(20, 20, 20, 20)

        txt_browser = QTextBrowser()
        txt_browser.setOpenExternalLinks(True)
        txt_browser.setStyleSheet("border: none; background-color: transparent;")

        # 1. TRADUZIMOS APENAS OS TEXTOS CURTOS E LIMPOS
        t_sub = self.tr("Motor de Renderização Cartográfica Automatizada")
        t_h1 = self.tr("Visão Geral do Sistema")
        t_p1 = self.tr("O VectorToMap é uma solução avançada de automação para Sistemas de Informação Geográfica (SIG). Projetado para lidar com rotinas de alta demanda cartográfica, o complemento atua diretamente sobre a API de layouts nativa (QgsPrintLayout), permitindo a renderização em lote de feições espaciais com parâmetros de enquadramento escaláveis.")
        
        t_h2 = self.tr("Aplicações Técnicas")
        t_li1 = self.tr("<b>Engenharia e Licenciamento:</b> Geração massiva de plantas de situação, croquis de acesso e anexos cartográficos padronizados para estudos ambientais.")
        t_li2 = self.tr("<b>Gestão Fundiária:</b> Automatização de pranchas para o Cadastro Ambiental Rural (CAR), regularização fundiária e cadastros técnicos multifinalitários urbanos.")
        t_li3 = self.tr("<b>Inspeção e Laudos:</b> Padronização visual e geração rápida de encartes de mapas para relatórios de vistoria em campo.")
        
        t_h3 = self.tr("Arquitetura e Funcionalidades Core")
        t_li4 = self.tr("<b>Integração QGIS Expressions:</b> Suporte nativo a Data-Defined Overrides (DDO). O motor lê funções SQL inseridas pelo usuário para calcular dinamicamente margens e escalas com base no contexto geométrico.")
        t_li5 = self.tr("<b>Isolamento em Memória:</b> Utiliza rotinas de instanciamento de camadas transitórias (Memory Layers) e filtros de visibilidade sem afetar a Layer Tree principal do projeto.")
        t_li6 = self.tr("<b>Engine de Exportação:</b> Suporte multi-thread e multi-formato dinâmico perfeitamente ajustado para lotes de impressão.")
        
        t_dev = self.tr("Desenvolvido por:")
        t_inpi = self.tr("Registro INPI:")
        t_contato = self.tr("Contato:")
        t_licenca = self.tr("Licença:")

        # 2. INJETAMOS NO HTML COM F-STRINGS (As chaves {{ }} protegem o CSS)
        html_content = f"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li {{ white-space: pre-wrap; }}
</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:12pt; font-weight:400; font-style:normal;">

<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt; font-weight:600; color:#2c3e50;">VectorToMap</span></p>
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt; color:#7f8c8d;">{t_sub}</span></p>

<hr style="border: 1px solid #eaeaea; margin-top:12px; margin-bottom:12px;" />

<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:13pt; font-weight:600; color:#2c3e50;">{t_h1}</span></p>
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_p1}</p>

<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>

<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:13pt; font-weight:600; color:#2c3e50;">{t_h2}</span></p>
<ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;">
<li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li1}</li>
<li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li2}</li>
<li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li3}</li>
</ul>

<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>

<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:13pt; font-weight:600; color:#2c3e50;">{t_h3}</span></p>
<ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;">
<li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li4}</li>
<li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li5}</li>
<li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li6}</li>
</ul>

<hr style="border: 1px solid #eaeaea; margin-top:16px; margin-bottom:12px;" />

<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:11pt; color:#555555;">
<span style=" font-weight:600;">{t_dev}</span> Matheus Durso Neves Caetano<br />
<span style=" font-weight:600;">{t_inpi}</span> BR 51 2026 001755-6<br />
<span style=" font-weight:600;">{t_contato}</span> <a href="mailto:matheusdursonc@gmail.com"><span style=" text-decoration: underline; color:#3498db;">matheusdursonc@gmail.com</span></a><br />
<span style=" font-weight:600;">{t_licenca}</span> GNU GPLv2
</span></p>

</body></html>"""

        txt_browser.setHtml(html_content)
        layout.addWidget(txt_browser)

        # Adiciona a aba criada ao tabWidget existente
        self.dlg.tabWidget.addTab(aba_sobre, self.tr("Sobre o Plugin"))


    def _configurar_widgets_iniciais(self):
        """Popula as ComboBoxes, recria os botões na ordem certa e define variáveis de estado."""
        # Isolamento dos Radio Buttons
        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
        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)
        self.dlg.combo_tamanho_pagina.setCurrentIndex(1) # Força o A4

        # Setup Escala
        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)

        # Filtro de Camadas Vectoriais
        self.dlg.mMapLayerComboBox.setFilters(QgsMapLayerProxyModel.VectorLayer)

        # Setup de Presets
        self.dlg.combo_presets.setItemText(0, self.tr("Mapa Quadrado"))
        self.dlg.combo_presets.setItemData(0, "quadrado")
        self.dlg.combo_presets.setItemText(1, self.tr("Mapa Horizontal"))
        self.dlg.combo_presets.setItemData(1, "horizontal")
        self.dlg.combo_presets.addItem(self.tr("Mapa Vertical"), "vertical")

        # Setup das Posições das Decorações (Legenda, Norte, Escala)
        opcoes_posicao = [
            (self.tr("Superior Esquerdo"), "SE"),
            (self.tr("Superior Direito"), "SD"),
            (self.tr("Inferior Esquerdo"), "IE"),
            (self.tr("Inferior Direito"), "ID")
        ]
        
        for combo_name in ['comboLegenda', 'comboEscala', 'comboNorte']:
            if hasattr(self.dlg, combo_name):
                combo = getattr(self.dlg, combo_name)
                combo.clear()
                for texto, dado in opcoes_posicao:
                    combo.addItem(texto, dado)
                    
        # Define os padrões visuais iniciais (Legenda=InfEsq, Escala=InfDir, Norte=SupDir)
        if hasattr(self.dlg, 'comboLegenda'): self.dlg.comboLegenda.setCurrentIndex(2)
        if hasattr(self.dlg, 'comboEscala'): self.dlg.comboEscala.setCurrentIndex(3)
        if hasattr(self.dlg, 'comboNorte'): self.dlg.comboNorte.setCurrentIndex(1)

        # Setup da Grade (Grid)
        if hasattr(self.dlg, 'comboGrid'):
            self.dlg.comboGrid.clear()
            self.dlg.comboGrid.addItem(self.tr("Linhas Sólidas + Zebra"), "solido")
            self.dlg.comboGrid.addItem(self.tr("Apenas Cruzes"), "cruz")

        # Setup de Cor de Fundo
        if hasattr(self.dlg, 'btn_cor_fundo'):
            self.dlg.btn_cor_fundo.setColor(QColor(255, 255, 255, 255))
            self.dlg.btn_cor_fundo.setAllowOpacity(True)
            self.dlg.btn_cor_fundo.setToolTip(self.tr("Escolha a cor de fundo do mapa. Reduza a Opacidade para 0% para exportar transparente."))

        # Reconstrução Segura dos Botões
        try:
            self.dlg.button_box.accepted.disconnect()
            self.dlg.button_box.rejected.disconnect()
        except: pass

        self.dlg.button_box.clear()

        # --- NOVO: Criação do botão de suporte/ajuda (Apenas Ícone 25x25) ---
        self.btn_suporte_ajuda = QPushButton() 
        icon = self.dlg.style().standardIcon(self.dlg.style().StandardPixmap.SP_MessageBoxInformation)
        self.btn_suporte_ajuda.setIcon(icon)
        self.btn_suporte_ajuda.setFixedSize(22, 22) 
        self.btn_suporte_ajuda.setToolTip(self.tr("Ajuda do VectorToMap")) 
        
        # Adiciona o botão de suporte no início como ActionRole para ficar do lado esquerdo
        self.dlg.button_box.addButton(self.btn_suporte_ajuda, QDialogButtonBox.ButtonRole.ActionRole)
        # ----------------------------------------------------------------------

        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"))

        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)

        # --- NOVO: SETUP DO TÍTULO E ALINHAMENTO ---
        self.fonte_titulo = QFont("Arial", 20, QFont.Weight.Bold)

        if hasattr(self.dlg, 'lineEdit_titulo'):
            self.dlg.lineEdit_titulo.textChanged.connect(self.disparar_preview_se_autorizado)
            
        if hasattr(self.dlg, 'btn_fonte_titulo'):
            self.dlg.btn_fonte_titulo.clicked.connect(self.escolher_fonte_titulo)
            
        # Preenche a ComboBox de Alinhamento e conecta ao gerador de Preview
        if hasattr(self.dlg, 'combo_alinhamento_titulo'):
            self.dlg.combo_alinhamento_titulo.clear()
            self.dlg.combo_alinhamento_titulo.addItem(self.tr("Esquerda"), Qt.AlignmentFlag.AlignLeft)
            self.dlg.combo_alinhamento_titulo.addItem(self.tr("Centro"), Qt.AlignmentFlag.AlignHCenter)
            self.dlg.combo_alinhamento_titulo.addItem(self.tr("Direita"), Qt.AlignmentFlag.AlignRight)
            self.dlg.combo_alinhamento_titulo.setCurrentIndex(1) # Força o "Centro" como padrão
            self.dlg.combo_alinhamento_titulo.currentIndexChanged.connect(self.disparar_preview_se_autorizado)
        # -------------------------------------------

        # Variáveis de Estado
        self.export_info = None
        self.abort_processing = False
        self.is_rendering = False
        
        # Setup Timer Preview
        self.timer_preview = QTimer()
        self.timer_preview.setSingleShot(True)

        # --- NOVO: Injeta a aba 'Sobre' dinamicamente ---
        self._adicionar_aba_sobre()
        # ------------------------------------------------

        # --- Força a janela a abrir sempre na primeira aba (Configurações) ---
        if hasattr(self.dlg, 'tabWidget'):
            self.dlg.tabWidget.setCurrentIndex(0)


    def _conectar_sinais(self):
        """Faz todas as ligações entre as ações do usuário (cliques) e as funções lógicas."""
        # Botões Principais
        self.btn_suporte_ajuda.clicked.connect(self.mostrar_tela_sobre)
        self.btn_export.clicked.connect(self.preparar_exportacao)
        self.btn_render.clicked.connect(self.atualizar_preview)
        self.btn_ok.clicked.connect(self.processar_clique_ok)
        self.btn_cancel.clicked.connect(self.cancelar_e_fechar)
        # --- Atalhos de Navegação ---
        if hasattr(self.dlg, 'pushButton_ir_suporte'):
            self.dlg.pushButton_ir_suporte.clicked.connect(self.ir_para_suporte)

        # Timer e Preview Auto
        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
            )

        # Gatilhos de Escala e Camada
        self.dlg.mMapLayerComboBox.layerChanged.connect(self.configurar_escala_ao_mudar_camada)
        self.dlg.mMapLayerComboBox.layerChanged.connect(self.disparar_preview_se_autorizado)
        self.dlg.rb_escala_auto.toggled.connect(self.validar_geometria_escala)
        self.dlg.rb_escala_fixa.toggled.connect(self.validar_geometria_escala)
        
        # Gatilhos de Layout
        self.dlg.chk_selecionar_todos.stateChanged.connect(self.marcar_desmarcar_todos)
        self.dlg.chk_numeracao.toggled.connect(self.disparar_preview_se_autorizado)
        if hasattr(self.dlg, 'chk_apenas_mapa'): self.dlg.chk_apenas_mapa.stateChanged.connect(self.disparar_preview_se_autorizado)
        if hasattr(self.dlg, 'btn_cor_fundo'): self.dlg.btn_cor_fundo.colorChanged.connect(self.disparar_preview_se_autorizado)

        # Gatilhos de Hierarquia e Trava de Estilos
        self.dlg.chk_filtrar_feicoes.stateChanged.connect(self.atualizar_hierarquia_camadas)
        self.dlg.chk_exibir_so_camada_atual.stateChanged.connect(self.atualizar_hierarquia_camadas)
        self.dlg.chk_travar_camadas.stateChanged.connect(self.atualizar_estado_travar_estilos)

        self.dlg.combo_atlas.currentIndexChanged.connect(self.atualizar_estado_filtro_feicoes)

        # --- Conexões da Aba Suporte ---
        if hasattr(self.dlg, 'pushButton_tutorial'):
            self.dlg.pushButton_tutorial.clicked.connect(self.abrir_tutorial_youtube)
        self.dlg.pushButton_apoiar.clicked.connect(self.abrir_gumroad)
        self.dlg.pushButton_bugreport.clicked.connect(self.abrir_github)

        if hasattr(self.dlg, 'chk_export_individual'):
            # O lambda permite passar o próprio checkbox como argumento para a nossa função!
            self.dlg.chk_export_individual.clicked.connect(
                lambda: self._bloquear_recurso_pro(self.dlg.chk_export_individual)
            )
        
        if hasattr(self.dlg, 'chk_abrir_arquivo'):
            # O lambda permite passar o próprio checkbox como argumento para a nossa função!
            self.dlg.chk_abrir_arquivo.clicked.connect(
                lambda: self._bloquear_recurso_pro(self.dlg.chk_abrir_arquivo)
            )
        
        # --- BLOQUEIO DAS NOVAS OPÇÕES DE FUNDO TRANSPARENTE (PRO) ---
        if hasattr(self.dlg, 'chk_map_fundo_transp'):
            self.dlg.chk_map_fundo_transp.clicked.connect(
                lambda: self._bloquear_recurso_pro(self.dlg.chk_map_fundo_transp)
            )

        if hasattr(self.dlg, 'chk_pag_fundo_transp'):
            self.dlg.chk_pag_fundo_transp.clicked.connect(
                lambda: self._bloquear_recurso_pro(self.dlg.chk_pag_fundo_transp)
            )
        
        # Função anônima (lambda) que ignora o clique real e chama nosso segurança
        # O parâmetro 'e' representa o evento do mouse (que nós vamos descartar)
        bloqueio_mouse = lambda e: self._bloquear_recurso_pro(None)

        if hasattr(self.dlg, 'lbl_cor_pagina'):
            self.dlg.lbl_cor_pagina.mousePressEvent = bloqueio_mouse
            
        if hasattr(self.dlg, 'btn_cor_pagina'):
            self.dlg.btn_cor_pagina.mousePressEvent = bloqueio_mouse
        
        # ======================================================================
        # --- BLOQUEIO DAS DECORAÇÕES E GRADE (TEASER PRO) ---
        # ======================================================================[
        
        # Instanciamos o nosso bloqueador mestre
        self.pro_blocker = ProBlocker(lambda: self._bloquear_recurso_pro(None))

        itens_pro_decoracoes = [
            'chk_legenda', 'chk_norte', 'chk_escala', 'chk_grid',
            'lblLegenda', 'lblNorte', 'lblEscala', 'lblGrid',
            'comboLegenda', 'comboNorte', 'comboEscala', 'comboGrid'
        ]

        for item_name in itens_pro_decoracoes:
            if hasattr(self.dlg, item_name):
                widget = getattr(self.dlg, item_name)
                
                # Se for ComboBox, aplicamos uma leve transparência para ficar igual às expressões DDO
                if isinstance(widget, QComboBox):
                    opacidade = QGraphicsOpacityEffect()
                    opacidade.setOpacity(0.6)
                    widget.setGraphicsEffect(opacidade)

                # Instala o nosso bloqueador de eventos para matar cliques diretos
                widget.installEventFilter(self.pro_blocker)
                
                # Garante que os "filhos" do widget (como a setinha do combobox) também sejam bloqueados
                for child in widget.findChildren(QWidget):
                    child.installEventFilter(self.pro_blocker)

                # Adiciona o balãozinho de aviso (Tooltip)
                widget.setToolTip(self.tr("Recurso exclusivo da Versão Pro 💎"))
                
                # Se for CheckBox, conecta o clique para desmarcar caso ele consiga burlar o filtro
                if isinstance(widget, QCheckBox):
                    # Usamos w=widget no lambda para fixar a referência da variável no loop
                    widget.clicked.connect(lambda checked, w=widget: self._bloquear_recurso_pro(w))
        
        # ======================================================================
        # --- BOTÕES DE INFORMAÇÃO FLUTUANTES ---
        # ======================================================================
        if hasattr(self.dlg, 'btn_info_ddo_auto'):
            self.dlg.btn_info_ddo_auto.clicked.connect(
                lambda: self.mostrar_info("ddo_auto", self.dlg.btn_info_ddo_auto)
            )
            
        if hasattr(self.dlg, 'btn_info_ddo_fixa'):
            self.dlg.btn_info_ddo_fixa.clicked.connect(
                lambda: self.mostrar_info("ddo_fixa", self.dlg.btn_info_ddo_fixa)
            )

        # ======================================================================
        # --- LÓGICA DO TEASER (ESCALAS DINÂMICAS MEIO APAGADAS) ---
        # ======================================================================
        
        if hasattr(self.dlg, 'exp_ddo_auto'):
            # 1. Aplica o filtro de 50% de opacidade para dar o visual "meio apagado"
            opacidade_auto = QGraphicsOpacityEffect()
            opacidade_auto.setOpacity(0.5)
            self.dlg.exp_ddo_auto.setGraphicsEffect(opacidade_auto)
            
            # 2. Instala o bloqueador de cliques no widget principal e em TODOS os seus filhos
            self.dlg.exp_ddo_auto.installEventFilter(self.pro_blocker)
            for child in self.dlg.exp_ddo_auto.findChildren(QWidget):
                child.installEventFilter(self.pro_blocker)
                
            # 3. Atualiza o balãozinho de dica
            self.dlg.exp_ddo_auto.setToolTip(self.tr("Recurso exclusivo da Versão Pro 💎"))

        if hasattr(self.dlg, 'exp_ddo_fixa'):
            opacidade_fixa = QGraphicsOpacityEffect()
            opacidade_fixa.setOpacity(0.5)
            self.dlg.exp_ddo_fixa.setGraphicsEffect(opacidade_fixa)
            
            self.dlg.exp_ddo_fixa.installEventFilter(self.pro_blocker)
            for child in self.dlg.exp_ddo_fixa.findChildren(QWidget):
                child.installEventFilter(self.pro_blocker)
                
            self.dlg.exp_ddo_fixa.setToolTip(self.tr("Recurso exclusivo da Versão Pro 💎"))
        # ======================================================================

        # Monitoramento em Massa para Disparo de Preview
        widgets_preview = [
            self.dlg.combo_tamanho_pagina, self.dlg.combo_presets, self.dlg.combo_atlas,
            self.dlg.rb_retrato, self.dlg.rb_paisagem, self.dlg.combo_escala_fixa,
            self.dlg.chk_modo_formulario, self.dlg.chk_modo_individual, self.dlg.chk_exibir_atributos,
            self.dlg.rb_escala_auto, self.dlg.rb_escala_fixa, 
            self.dlg.chk_filtrar_feicoes, self.dlg.chk_exibir_so_camada_atual
        ]
        for w in widgets_preview:
            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)
            
        # Salva o comportamento original de redimensionamento do QGIS
        original_resize = self.dlg.resizeEvent
        
        def novo_resize_event(event):
            self.disparar_preview_se_autorizado()
            original_resize(event) # Repassa a ordem para o Qt organizar os botões
            
        self.dlg.resizeEvent = novo_resize_event

        # Força o estado inicial dos gatilhos
        self.configurar_escala_ao_mudar_camada()
        self.atualizar_hierarquia_camadas()


    def _configurar_janela(self):
        """Define ícones, comportamento do sistema operacional, restaura tamanho e centraliza."""
        self.dlg.setWindowIcon(QIcon(':/plugins/vector_to_map/icon.png'))
        self.dlg.setWindowTitle("VectorToMap")
        
        self.dlg.setWindowFlags(
            Qt.WindowType.Window | 
            Qt.WindowType.WindowMinimizeButtonHint | 
            Qt.WindowType.WindowMaximizeButtonHint | 
            Qt.WindowType.WindowCloseButtonHint
        )

        if hasattr(self.dlg, 'lbl_aviso_feicoes'):
            self.dlg.lbl_aviso_feicoes.hide()

        # Restore window geometry
        settings = QgsSettings()
        geometria = settings.value("/VectorToMap/geometry")
        if geometria: 
            self.dlg.restoreGeometry(geometria)
        else: 
            self.dlg.resize(1100, 750) 
        
        if hasattr(self.dlg, 'splitter'): 
            self.dlg.splitter.setSizes([600, 500]) 

        # 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())
    

    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()}")
        
        # Filtros de arquivo disponíveis na janela do sistema
        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:
            # --- NOVA LÓGICA DE BLOQUEIO: Bloqueia PNG e SVG na versão Free ---
            if "PNG" in filtro_selecionado or "SVG" in filtro_selecionado:
                QMessageBox.warning(
                    self.dlg,
                    self.tr("Recurso Exclusivo Pro 👑"),
                    self.tr("A exportação de imagens PNG (fundo transparente) e vetores SVG "
                            "é exclusiva da Versão Pro.\n\n"
                            "Na versão gratuita, você pode exportar em formato PDF (Múltiplas páginas) ou imagem em JPG.\n\n"
                            "Acesse a aba 'Desenvolvimento e Suporte' para conhecer a Versão Pro!")
                )
                return

            # --- 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)

            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

            # Força a exportação individual para imagens
            is_individual = hasattr(self.dlg, 'chk_export_individual') and 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"{self.tr('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"{self.tr('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 _adicionar_marca_dagua_free(self, layout, y_zero, h_pg):
        """Adiciona marca d'água no rodapé da versão Free."""

        try:
            lbl = QgsLayoutItemLabel(layout)
            lbl.setText(self.tr("Gerado por VectorToMap (Versão Free) - Adquira a Pro"))
            
            # Forma 100% segura de configurar a fonte no PyQt
            fonte = QFont("Arial", 8)
            fonte.setItalic(True)
            lbl.setFont(fonte)
            
            lbl.setFontColor(QColor(120, 120, 120, 180)) # Cinza levemente transparente
            lbl.adjustSizeToText()
            
            # Boa prática do QGIS: adicionar ao layout ANTES de mover
            layout.addLayoutItem(lbl)
            
            # Posiciona no canto inferior esquerdo
            lbl.attemptMove(QgsLayoutPoint(5.0, y_zero + h_pg - 5.0, QgsUnitTypes.LayoutMillimeters))
            lbl.setZValue(100) # Por cima de tudo
            
        except Exception as e:
            # Se a marca d'água falhar (ex: fonte Arial não instalada), 
            # não trava a geração do mapa do usuário. Apenas loga o erro.
            QgsMessageLog.logMessage(f"Erro na marca d'água: {str(e)}", "VectorToMap", Qgis.MessageLevel.Warning)


    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:
            # --- NOVO: Libera a referência da ComboBox antes de deletar a camada ---
            if hasattr(self, 'dlg') and self.dlg and hasattr(self.dlg, 'mMapLayerComboBox'):
                self.dlg.mMapLayerComboBox.setExceptedLayerList([])
            # -----------------------------------------------------------------------

            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_filtro_feicoes(self):
        """Bloqueia o filtro e a aba de atributos quando o modo 'Zoom na Camada' é ativado."""
        if not hasattr(self.dlg, 'combo_atlas') or not hasattr(self.dlg, 'chk_filtrar_feicoes'):
            return
            
        campo_atlas = self.dlg.combo_atlas.currentData()
        
        if campo_atlas == "__ALL_FEATURES__":
            # Desativa o filtro de feições
            self.dlg.chk_filtrar_feicoes.setChecked(False)
            self.dlg.chk_filtrar_feicoes.setEnabled(False)
            
            # --- NOVO: Desativa a caixa inteira de atributos ---
            if hasattr(self.dlg, 'groupAtributos'):
                self.dlg.groupAtributos.setEnabled(False)
        else:
            # Reabilita o filtro
            self.dlg.chk_filtrar_feicoes.setEnabled(True)
            
            # --- NOVO: Reabilita a caixa inteira de atributos ---
            if hasattr(self.dlg, 'groupAtributos'):
                self.dlg.groupAtributos.setEnabled(True)
    

    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()
    



    def abrir_tutorial_youtube(self):
        """Abre o vídeo tutorial do YouTube no navegador padrão do usuário."""
        url = QUrl("https://www.youtube.com/watch?v=AIITugHD-_o")
        QDesktopServices.openUrl(url)
    



    def abrir_gumroad(self):
        """Abre a página do Gumroad para apoiar o projeto."""
        url_gumroad = "https://matheusdurso.gumroad.com/l/vectortomap"
        QDesktopServices.openUrl(QUrl(url_gumroad))




    def abrir_github(self):
        """Abre a página de Issues do GitHub para relatar bugs."""
        url_github = "https://github.com/matheusdurso/VectorToMap/issues"
        QDesktopServices.openUrl(QUrl(url_github))
    



    def ir_para_suporte(self):
        """Muda a aba ativa para a página de Desenvolvimento e Suporte."""
        if hasattr(self.dlg, 'tabWidget'):
            # setCurrentIndex(1) muda para a segunda aba. 
            # Se a aba de suporte for a terceira no seu layout, mude para 2.
            self.dlg.tabWidget.setCurrentIndex(1)
    



    def _bloquear_recurso_pro(self, widget_clicado=None, event=None):
        """Intercepta o clique em recursos Pro, reverte a ação e manda pra aba de compra."""
        
        # Só tenta desmarcar se for um checkbox (se tiver a função setChecked)
        if widget_clicado and hasattr(widget_clicado, 'setChecked'):
            widget_clicado.blockSignals(True)
            widget_clicado.setChecked(False)
            widget_clicado.blockSignals(False)

        # Muda para a aba 2 (Suporte/Pro)
        if hasattr(self.dlg, 'tabWidget'):
            self.dlg.tabWidget.setCurrentIndex(1)

        # Mostra o aviso na barra do QGIS
        self.iface.messageBar().pushMessage(
            self.tr("Recurso Pro 👑"),
            self.tr("Este recurso é exclusivo da versão Pro. Veja como fazer o upgrade abaixo!"),
            level=3, 
            duration=5
        )




    #################################################################################
    # --- 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)

        self.dlg.combo_atlas.addItem(self.tr("--- Sem Agrupamento (Todas as Feições - Zoom na Camada) ---"), "__ALL_FEATURES__")

        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()

        self.atualizar_estado_filtro_feicoes()




    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]
                
                elif campo_atlas == "__ALL_FEATURES__":
                    # ZERO RAM: Não puxa as feições reais. Apenas destrava a Preview.
                    self.preview_data_cache = ["ALL"]
                
                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 == "__ALL_FEATURES__":
                nome_sufixo_preview = "Geral"
            elif campo_atlas and feicoes and not isinstance(feicoes[0], str):
                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 
            )

            # 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
                ))

        # --- NOVO: Captura falhas de renderização da Preview ---
        except Exception as e:
            if getattr(self, 'sentry_ativo', False):
                try:
                    sentry_sdk.capture_exception(e)
                except Exception:
                    pass
            QgsMessageLog.logMessage(f"Erro detalhado: {e}", "VectorToMap", Qgis.MessageLevel.Warning)
        # -------------------------------------------------------

        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
        
        try:
            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 != "__ALL_FEATURES__" 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]})

            elif campo_atlas == "__ALL_FEATURES__":
                # ZERO RAM: Apenas passa uma lista "dummy" (falsa) para sinalizar que a página existe,
                # sem copiar nenhuma feição do banco de dados para a memória!
                paginas_dados.append({'feicoes': ["ALL"]})
            
            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: 
                LIMITE_FREE = 25
                if len(paginas_dados) > LIMITE_FREE:
                    QMessageBox.information(
                        self.dlg,
                        self.tr("Limite da Versão Free 👑"),
                        self.tr(f"A versão gratuita permite exportar até {LIMITE_FREE} mapas por vez.\n\n"
                                f"Apenas os {LIMITE_FREE} primeiros serão gerados nesta rodada.\n\n"
                                "🚀 Atualize para a Versão Pro para remover este limite e exportar centenas de mapas em lote!"),
                    )
                    # Corta a lista para o limite da versão free
                    paginas_dados = paginas_dados[:LIMITE_FREE]

                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
                )
                
        # --- NOVO: Proteção contra quebras de banco de dados/camada corrompida ---
        except Exception as e:
            if getattr(self, 'sentry_ativo', False):
                try:
                    sentry_sdk.capture_exception(e)
                    sentry_sdk.flush(timeout=2.0)
                except Exception:
                    pass
            self.iface.messageBar().pushMessage(self.tr("Erro"), f"{self.tr('Falha ao ler atributos da camada:')} {str(e)}", level=2)
        # -------------------------------------------------------------------------
        
        finally:
            self.export_info = None # Reseta a variável para o botão OK padrão




    # ====================================================================================
    # --- ORQUESTRAÇÃO DE LAYOUTS E EXPORTAÇÃO ---
    # ====================================================================================

    def criar_layout_multi_paginas(self, preset, camada, orientacao, tamanho_pg, paginas_dados, colunas, modos, export_info=None):
        """Maestro: Gera o layout definitivo ou exporta os arquivos delegando as tarefas."""

        self.is_rendering = True
        self.abort_processing = False
        
        try:
            project = QgsProject.instance()
            manager = project.layoutManager()
            
            # 1. Inicializa Variáveis de Exportação
            is_individual = export_info.get('individual', False) if export_info else False
            base_path = export_info.get('path', '') if export_info else ''
            ext = export_info.get('ext', '') if export_info else ''
            base_sem_ext = base_path[:-len(ext)] if ext else base_path

            # 2. Pre-flight Check: Verificação de Sobrescrita Segura
            if export_info:
                pode_continuar = self._verificar_sobrescrita_arquivos(base_sem_ext, ext, paginas_dados, is_individual)
                if not pode_continuar:
                    self.is_rendering = False
                    self.abort_processing = True
                    return # Aborta o processo pacificamente

            # 3. Configuração da UI
            self.dlg.progressBar.show()
            self.dlg.progressBar.setRange(0, len(paginas_dados))
            self.dlg.progressBar.setValue(0)

            # 4. Roteamento de Trabalho: Exportação Avulsa vs Layout Acumulado
            layout_persistente = None
            
            if is_individual:
                self._exportar_paginas_individuais(project, camada, paginas_dados, preset, orientacao, base_sem_ext, ext)
            else:
                layout_persistente = self._montar_layout_persistente(project, manager, camada, paginas_dados, preset, orientacao)

            # 5. Finalização e Fechamento
            if not self.abort_processing:
                if export_info:
                    # Se pediu PDF único, exportamos o layout que acumulou as páginas
                    if not is_individual and ext == ".pdf" and layout_persistente:
                        layout_persistente.refresh()
                        exporter = QgsLayoutExporter(layout_persistente)
                        settings = QgsLayoutExporter.PdfExportSettings()
                        settings.dpi = 300
                        exporter.exportToPdf(base_path, settings)
                        
                        # Limpa da memória pois já virou arquivo
                        layout_persistente.pageCollection().clear()
                        sip.delete(layout_persistente)

                    # --- NOVO: Mensagem de Sucesso Nativa do QGIS ---
                    dir_path = os.path.dirname(base_path)
                    url_pasta = QUrl.fromLocalFile(dir_path).toString()
                    
                    texto_sucesso = self.tr("Layout exportado com sucesso para")
                    
                    # Formatação fiel à barra nativa: link com a cor verde escuro do QGIS e o caminho escrito
                    mensagem_html = f"{texto_sucesso} <a href='{url_pasta}' style='color: #005e00; text-decoration: underline;'>{dir_path}</a>"
                    
                    # level=3 (Fundo Verde de Sucesso) | duration=0 (Fica fixada até o usuário fechar)
                    self.iface.messageBar().pushMessage(self.tr("Exportar layout"), mensagem_html, level=3, duration=0)
                    # ---------------------------------------------------
                else:
                    # Se clicou em "OK" apenas para visualizar no QGIS
                    if layout_persistente:
                        layout_persistente.refresh()
                        manager.addLayout(layout_persistente)
                        self.iface.openLayoutDesigner(layout_persistente)
            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()
            
            # --- ENVIO PARA O SENTRY ---
            if getattr(self, 'sentry_ativo', False):
                try:
                    sentry_sdk.capture_exception(e)                    
                    # O PULO DO GATO: Força o envio imediato esperando até 2 segundos
                    sentry_sdk.flush(timeout=2.0) 
                except Exception:
                    pass
            # ---------------------------

            self.iface.messageBar().pushMessage(
                self.tr("Erro"), 
                f"{str(e)}. {self.tr('Veja o console para detalhes.')}", 
                level=2
            )
            QgsMessageLog.logMessage(erro_detalhado, "VectorToMap", Qgis.MessageLevel.Critical)

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




    # ------------------------------------------------------------------------------------
    # --- SUB-FUNÇÕES DE EXPORTAÇÃO E LAYOUT (ESPECIALISTAS) ---
    # ------------------------------------------------------------------------------------

    def _verificar_sobrescrita_arquivos(self, base_sem_ext, ext, paginas_dados, is_individual):
        """Pre-flight: Avalia o disco antes de rodar o motor pesado."""
        arquivos_existentes = False
        
        if is_individual:
            for i, dados in enumerate(paginas_dados):
                nome_teste = self._gerar_nome_arquivo_pagina(dados, i)
                if os.path.exists(f"{base_sem_ext}_{nome_teste}{ext}"):
                    arquivos_existentes = True
                    break
        else:
            if os.path.exists(f"{base_sem_ext}{ext}"):
                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
            )
            return resposta == QMessageBox.StandardButton.Yes
        return True




    def _gerar_nome_arquivo_pagina(self, dados, index):
        """Gera um sufixo limpo de até 8 caracteres baseado no Atlas ou sequencial."""

        campo_atlas = self.dlg.combo_atlas.currentData()

        # --- ADICIONE ESTAS DUAS LINHAS ---
        if campo_atlas == "__ALL_FEATURES__":
            return self.tr("Zoom_Camada")
        # ----------------------------------

        if campo_atlas and dados.get('feicoes'):
            val_atr = dados['feicoes'][0].attribute(campo_atlas)
            nome = "Null" if val_atr is None or val_atr == NULL else str(val_atr).strip()
            return re.sub(r'[\\/*?:"<>|]', "", nome).strip()[:8].strip()
        
        return str(index + 1)




    def _exportar_paginas_individuais(self, project, camada, paginas_dados, preset, orientacao, base_sem_ext, ext):
        """Modo Descartável: Cria, exporta e destrói um layout para cada página."""
        for i, dados in enumerate(paginas_dados):
            if self.abort_processing: break

            nome_arquivo = self._gerar_nome_arquivo_pagina(dados, i)
            
            layout_temp = QgsPrintLayout(project)
            layout_temp.initializeDefaults()
            
            self.montar_design_da_pagina(layout_temp, camada, dados['feicoes'], preset, orientacao, pagina_index=0, nome_sufixo=nome_arquivo)
            layout_temp.refresh()
            
            caminho_final = f"{base_sem_ext}_{nome_arquivo}{ext}"
            exporter = QgsLayoutExporter(layout_temp)
            
            if ext in [".png", ".jpg", ".jpeg"]:
                img_settings = QgsLayoutExporter.ImageExportSettings()
                img_settings.dpi = 300 
                exporter.exportToImage(caminho_final, img_settings)
            elif ext == ".svg":
                svg_settings = QgsLayoutExporter.SvgExportSettings()
                svg_settings.exportLabelsToPaths = False # Mantém textos editáveis
                exporter.exportToSvg(caminho_final, svg_settings)
            else:
                pdf_settings = QgsLayoutExporter.PdfExportSettings()
                pdf_settings.dpi = 300
                exporter.exportToPdf(caminho_final, pdf_settings)
                
            # Limpa memória instantaneamente
            layout_temp.pageCollection().clear()
            sip.delete(layout_temp)

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




    def _montar_layout_persistente(self, project, manager, camada, paginas_dados, preset, orientacao):
        """Modo Acumulador: Adiciona todas as páginas num único layout do QGIS."""
        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}"
        
        # Garante nome único no QGIS
        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)

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

            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 programada para evitar estouro de memória em dezenas de páginas
            if i > 0 and i % 20 == 0:
                layout.refresh()
                gc.collect()

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




    # ====================================================================================
    # --- 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
        )

        # --- NOVO: 1.5 Desenha o Título no topo ---
        if not apenas_mapa:
            self._adicionar_titulo(layout, geometria)
        # ------------------------------------------

        # 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, camada, 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'])
        
        self._adicionar_marca_dagua_free(layout, geometria['y_zero'], geometria['h_pg'])




    # ------------------------------------------------------------------------------------
    # --- 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 == "horizontal":
            header_h = 35.0
            h_util = h_pg - (2 * margin_padrao) - header_h
            w_map_f = w_pg - (2 * margin_padrao)
            
            if orientacao == "Paisagem":
                # Paisagem: Exatamente como era antes (intocado)
                h_map_f = h_util * 0.75
                x_map_f = margin_padrao
                y_map_f = y_zero_folha + margin_padrao + header_h
            else:
                # Retrato: Nova altura (75% da LARGURA) mantendo o centro vertical intacto
                altura_antiga = h_util * 0.75
                y_antigo = y_zero_folha + margin_padrao + header_h
                centro_y = y_antigo + (altura_antiga / 2.0)
                
                h_map_f = w_map_f * 0.75
                x_map_f = margin_padrao
                y_map_f = centro_y - (h_map_f / 2.0)
                
        elif preset == "vertical":
            if orientacao == "Retrato":
                w_map_f = w_pg - (2 * margin_padrao)
                x_map_f = margin_padrao
                y_map_f = y_zero_folha + margin_padrao + 30.0
                h_map_f = w_pg + 10.0 
            else:
                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))

        # --- A COR DA PÁGINA ---
        # Na versão Free, a folha de papel é SEMPRE branca sólida.
        # A transparência (se o usuário escolher) será aplicada apenas ao quadro do mapa.
        pagina.setBackgroundEnabled(True)
        
        # Correção: Compatibilidade entre versões antigas e novas do QGIS
        if hasattr(pagina, 'pageStyleSymbol'):
            # Para QGIS 3.40 ou superior
            if pagina.pageStyleSymbol(): 
                pagina.pageStyleSymbol().setColor(QColor(255, 255, 255, 255))
        else:
            # Para QGIS anterior a 3.40 (ex: 3.28 LTR, 3.34 LTR)
            if layout.pageCollection().pageStyleSymbol():
                layout.pageCollection().pageStyleSymbol().setColor(QColor(255, 255, 255, 255))

        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 escolher_fonte_titulo(self):
        """Abre a janela nativa do Windows/Mac para escolher fonte, tamanho e estilo."""
        fonte, ok = QFontDialog.getFont(self.fonte_titulo, self.dlg, self.tr("Escolher Fonte do Título"))
        if ok:
            self.fonte_titulo = fonte
            self.disparar_preview_se_autorizado()




    def _adicionar_titulo(self, layout, geo):
        """Renderiza o título a 1cm das margens superiores e laterais."""
        if not hasattr(self.dlg, 'lineEdit_titulo'): return
        
        texto = self.dlg.lineEdit_titulo.text().strip()
        if not texto: return

        lbl_titulo = QgsLayoutItemLabel(layout)
        lbl_titulo.setText(texto)
        lbl_titulo.setFont(self.fonte_titulo)
        
        # --- NOVO: Lê a ComboBox para decidir o alinhamento ---
        alinhamento = Qt.AlignmentFlag.AlignHCenter # Backup caso não ache a ComboBox
        if hasattr(self.dlg, 'combo_alinhamento_titulo'):
            alinhamento = self.dlg.combo_alinhamento_titulo.currentData()
            
        lbl_titulo.setHAlign(alinhamento)
        # ------------------------------------------------------
        
        lbl_titulo.setVAlign(Qt.AlignmentFlag.AlignTop)
        
        x = geo['margin'] 
        y = geo['y_zero'] + geo['margin'] 
        largura = geo['w_pg'] - (2 * geo['margin'])
        altura_livre = 60.0 
        
        lbl_titulo.attemptMove(QgsLayoutPoint(x, y, QgsUnitTypes.LayoutMillimeters))
        lbl_titulo.attemptResize(QgsLayoutSize(largura, altura_livre, QgsUnitTypes.LayoutMillimeters))
        
        # --- NOVO: Garante que o título flutue sempre por cima do mapa ---
        lbl_titulo.setZValue(50) 
        # -----------------------------------------------------------------
        
        layout.addLayoutItem(lbl_titulo)




    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))
        map_item.setZValue(0)
        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

        campo_atlas = self.dlg.combo_atlas.currentData()

        if campo_atlas == "__ALL_FEATURES__":
            # MATEMÁTICA O(1): Pega a extensão máxima direto dos metadados do arquivo
            ext = camada.extent()
        else:
            # MATEMÁTICA O(N): Calcula a extensão somando a geometria das feições filtradas
            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 atualizar_mapa_para_canvas(self, map_item):
        """Força o layout a refletir a tela principal do QGIS sem travar as camadas."""
        # 1. Desliga qualquer trava de camadas ou estilos
        map_item.setKeepLayerSet(False)
        map_item.setKeepLayerStyles(False)
        
        # 2. Invalida o cache atual para forçar o QGIS a redesenhar com base no canvas
        map_item.invalidateCache()
        
        # 3. Atualiza o item no layout
        map_item.refresh()




    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())

                # --- NOVO: Esconde a camada fantasma da ComboBox instantaneamente ---
                camadas_excluidas = self.dlg.mMapLayerComboBox.exceptedLayerList()
                if camada_alvo not in camadas_excluidas:
                    camadas_excluidas.append(camada_alvo)
                    self.dlg.mMapLayerComboBox.setExceptedLayerList(camadas_excluidas)
                # --------------------------------------------------------------------

        # =====================================================================
        # NOVA LÓGICA DE BLINDAGEM: Trava Saudável vs Mapa Livre
        # =====================================================================
        
        is_isolado = self.dlg.chk_exibir_so_camada_atual.isChecked()
        is_filtrado = self.dlg.chk_filtrar_feicoes.isChecked()
        is_travado_manualmente = self.dlg.chk_travar_camadas.isChecked()

        # CENÁRIO A: O usuário ativou qualquer opção que exige alterar/congelar camadas
        if is_isolado or is_filtrado or is_travado_manualmente:
            
            camadas_finais_para_layout = []
            
            if is_isolado:
                camadas_finais_para_layout = [camada_alvo]
            else:
                root = QgsProject.instance().layerTreeRoot()
                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 is_filtrado:
                            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)
            
            # Injeta a lista e OBRIGATORIAMENTE aplica a trava para não bugar o QGIS
            map_item.setLayers(camadas_finais_para_layout)
            map_item.setKeepLayerSet(True)
            
            # Trava estilos se solicitado
            if hasattr(self.dlg, 'chk_travar_estilos') and self.dlg.chk_travar_estilos.isChecked():
                map_item.setKeepLayerStyles(True)

        # CENÁRIO B: Exportação normal sem restrições. O mapa nasce 100% livre.
        else:
            self.atualizar_mapa_para_canvas(map_item)

        # =====================================================================

        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, camada, feicoes_da_pagina, preset, orientacao, geo, apenas_mapa):
        """Maestro: Organiza as dimensões e delega a renderização para os modos selecionados."""
        campo_atlas = self.dlg.combo_atlas.currentData()
        
        if apenas_mapa: return
        
        # Só desenha textos se a caixinha principal (Exibir Atributos) estiver marcada
        if hasattr(self.dlg, 'chk_exibir_atributos') and not self.dlg.chk_exibir_atributos.isChecked():
            return
            
        # 1. Isola a matemática: Descobre as coordenadas exatas das caixas de texto
        geom = self._calcular_geometria_textos(preset, orientacao, geo)

        # --- ROTA A: RESUMO DA CAMADA INTEIRA ---
        if campo_atlas == "__ALL_FEATURES__": 
            self._renderizar_resumo_camada(layout, camada, geom, geo)
            return

        # --- ROTA B: ATRIBUTOS INDIVIDUAIS DAS FEIÇÕES ---
        colunas = [cb.text() for cb in self.dlg.scrollAreaWidgetContents.findChildren(QCheckBox) if cb.isChecked()]
        
        # Fuga rápida se o usuário não escolheu colunas
        if not colunas or not feicoes_da_pagina:
            return

        if self.dlg.chk_modo_formulario.isChecked():
            self._renderizar_modo_formulario(layout, feicoes_da_pagina, colunas, geom)

        if self.dlg.chk_modo_individual.isChecked():
            self._renderizar_modo_individual(layout, feicoes_da_pagina, colunas, geom, preset, orientacao)




    # ------------------------------------------------------------------------------------
    # --- SUB-FUNÇÕES DE RENDERIZAÇÃO DE TEXTOS (ESPECIALISTAS) ---
    # ------------------------------------------------------------------------------------

    def _calcular_geometria_textos(self, preset, orientacao, geo):
        """Calcula a área disponível (X, Y, Largura, Altura) baseando-se no layout."""
        limite_fundo = geo['y_zero'] + geo['h_pg'] - geo['margin']
        geom = {'limite_fundo': limite_fundo}

        # --- NOVA LÓGICA UNIFICADA ---
        # Se for Retrato (qualquer mapa) OU se for Paisagem no "Mapa Horizontal",
        # os textos ficam embaixo do mapa, divididos em 1/3 (esquerda) e 2/3 (direita)
        if orientacao == "Retrato" or preset == "horizontal":
            largura_util = geo['w_pg'] - (2 * geo['margin'])
            terco_x = geo['margin'] + (largura_util * 0.33) 
            
            geom['x_form'] = geo['margin']
            geom['y_form'] = geo['y_map'] + geo['h_map'] + 3.0
            geom['w_form'] = (largura_util * 0.33) - 2.0
            geom['h_form'] = limite_fundo - geom['y_form']
            
            geom['x_ind_start'] = terco_x + 2.0
            geom['y_ind_min'] = geo['y_map'] + geo['h_map'] + 2.0
            
        # --- LÓGICA ESPECÍFICA PARA PAISAGEM (Vertical e Quadrado) ---
        elif preset == "vertical":
            # Textos ficam na lateral esquerda, centralizados
            geom['x_form'] = geo['margin']
            h_util = geo['h_map']
            h_top = h_util * 0.34 
            geom['y_form'] = geo['y_map'] + h_top 
            geom['h_form'] = h_util * 0.33       
            geom['w_form'] = (geo['x_map'] - geo['margin']) - 5.0 
            geom['x_ind_start'] = geom['x_form']
            geom['y_ind_min'] = geom['y_form'] + geom['h_form'] 
            
        else: # "quadrado"
            # Textos ficam na lateral esquerda, usando a metade inferior
            geom['x_form'] = geo['margin']
            geom['y_form'] = geo['y_map'] + (geo['h_map'] / 2)
            geom['w_form'] = geo['x_map'] - (2 * geo['margin'])

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

        return geom




    def _inserir_label_no_layout(self, layout, texto, x, y, w=None, h=None, is_html=False, auto_resize=False):
        """Fábrica de rótulos: Instancia, configura e posiciona um texto sem repetir código da API do QGIS."""
        lbl = QgsLayoutItemLabel(layout)
        
        if is_html:
            mode = QgsLayoutItemLabel.ContentMode.ModeHtml if hasattr(QgsLayoutItemLabel, 'ContentMode') else (QgsLayoutItemLabel.ModeHtml if hasattr(QgsLayoutItemLabel, 'ModeHtml') else 1)
            lbl.setMode(mode)
        else:
            lbl.setFrameEnabled(True)

        lbl.setText(texto)
        
        # Otimiza o tamanho automático com folga para o Modo Individual
        if auto_resize:
            lbl.adjustSizeToText()
            w = lbl.rect().width() + 2.0
            h = lbl.rect().height()
            
        if w is not None and h is not None:
            lbl.attemptResize(QgsLayoutSize(w, h, QgsUnitTypes.LayoutMillimeters))
            
        lbl.attemptMove(QgsLayoutPoint(x, y, QgsUnitTypes.LayoutMillimeters))
        layout.addLayoutItem(lbl)
        
        # Retorna a largura útil (garantindo que seja um float (0.0) para não dar erro na soma)
        return float(w) if w is not None else 0.0




    def _renderizar_modo_formulario(self, layout, feicoes_da_pagina, colunas, geom):
        """Constrói o bloco de texto único em HTML."""
        txt_html = ""
        for f in feicoes_da_pagina:                    
            for col in colunas:
                try: 
                    txt_html += f"<b>{col}:</b> {str(f.attribute(col) or '').strip()}<br>"
                except: continue
        
        self._inserir_label_no_layout(
            layout, txt_html, 
            geom['x_form'], geom['y_form'], 
            geom['w_form'], geom['h_form'], 
            is_html=True
        )




    def _renderizar_modo_individual(self, layout, feicoes_da_pagina, colunas, geom, preset, orientacao):
        """Constrói as caixas de texto individuais em grade ou em formato de lista."""

        campo_atlas = self.dlg.combo_atlas.currentData() 
        altura_linha = 5.5
        xi, yi = geom['x_ind_start'], geom['y_ind_min']

        if campo_atlas is None:
            # Sem Atlas: Distribui as colunas de UMA feição numa "Grade" (Grid)
            f = feicoes_da_pagina[0]
            
            # --- NOVA REGRA DE COLUNAS ---
            if orientacao == "Retrato":
                limite_colunas = 3 # Todos os mapas Retrato usam 3 colunas
            else:
                # No modo Paisagem:
                if preset == "horizontal":
                    limite_colunas = 5 # Espaço largo (embaixo do mapa)
                else:
                    limite_colunas = 3 # Espaço curto (lateral esquerda)
            # -----------------------------

            for idx, col in enumerate(colunas):
                if idx > 0 and idx % limite_colunas == 0:
                    yi += altura_linha
                    xi = geom['x_ind_start']
                try:
                    val = f.attribute(col)
                    texto = f"{col}: {str(val).strip() if val is not None else ''}"
                    largura_ocupada = self._inserir_label_no_layout(layout, texto, xi, yi, auto_resize=True)
                    xi += largura_ocupada + 2.0 
                except: continue
        else:
            # Com Atlas: Cria uma linha em formato de "Lista" para CADA feição do grupo
            altura_total = len(feicoes_da_pagina) * altura_linha
            yi = (geom['limite_fundo'] - altura_total) if (geom['y_ind_min'] + altura_total) > geom['limite_fundo'] else geom['y_ind_min']

            for f in feicoes_da_pagina:
                xi = geom['x_ind_start']
                for col in colunas:
                    try:
                        val = f.attribute(col)
                        texto = f"{col}: {str(val).strip() if val is not None else ''}"
                        largura_ocupada = self._inserir_label_no_layout(layout, texto, xi, yi, auto_resize=True)
                        xi += largura_ocupada + 2.0
                    except: continue
                yi += altura_linha




    def _renderizar_resumo_camada(self, layout, camada, geom, geo):
        """Renderiza um bloco de metadados quando o mapa for da camada inteira, usando toda a largura."""
        
        nome = camada.name()
        total = camada.featureCount()
        geom_tipo = QgsWkbTypes.displayString(camada.wkbType())
        
        # --- LÓGICA DE SRC (Sistemas de Referência de Coordenadas) ---
        crs_camada = camada.crs()
        crs_camada_str = f"{crs_camada.authid()} - {crs_camada.description()}"
        
        crs_projeto = QgsProject.instance().crs()
        crs_projeto_str = f"{crs_projeto.authid()} - {crs_projeto.description()}"
        # -------------------------------------------------------------
        
        # Textos isolados para tradução
        t_tit = self.tr("Resumo do Mapa Geral")
        t_camada = self.tr("Camada Vetorial")
        t_geometria = self.tr("Tipo de Geometria")
        t_total = self.tr("Total de Feições")
        t_crs_camada = self.tr("SRC da Camada")
        t_crs_proj = self.tr("SRC do Projeto")
        
        # Bloco HTML padronizado e unificado
        txt_html = f"""
            <b style='font-size: 14px; color: #2c3e50;'>{t_tit}</b><br><br>
            <b>{t_camada}:</b> {nome}<br>
            <b>{t_geometria}:</b> {geom_tipo}<br>
            <b>{t_total}:</b> {total}<br>
            <b>{t_crs_camada}:</b> {crs_camada_str}<br>
            <b>{t_crs_proj}:</b> {crs_projeto_str}
        """
        
        # --- NOVO: Usa toda a largura da folha, respeitando apenas a margem direita ---
        largura_maxima = geo['w_pg'] - geom['x_form'] - geo['margin']
        
        self._inserir_label_no_layout(
            layout, txt_html, 
            geom['x_form'], geom['y_form'], 
            largura_maxima, geom['h_form'], 
            is_html=True
        )




    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)
    

    def mostrar_info(self, topico, widget_ancora):
        """Central de Ajuda: Cria uma caixa flutuante elegante que some ao clicar fora."""

        texto_ajuda = ""

        # Usamos HTML básico para deixar o texto bonito dentro da caixa
        if topico == "ddo_auto":
            t_tit = self.tr("Escala Automática Dinâmica")
            t_desc1 = self.tr("Use o botão 'ε' para definir a margem de zoom dinamicamente com base na sua tabela de atributos.")
            t_ex_tit = self.tr("Exemplo:")
            t_ex_desc = self.tr("Uma coluna com o número '15' aplicará 15% de margem no enquadramento.")
            t_sql_tit = self.tr("Expressão SQL:")
            t_aviso = self.tr("Se o cálculo falhar ou a célula estiver vazia, o plugin usará a margem padrão (25%).")

            texto_ajuda = f"""
                <b style='color: #2980b9; font-size: 14px;'>{t_tit}</b><br><br>
                {t_desc1}<br><br>
                <b>{t_ex_tit}</b> {t_ex_desc}<br>
                <b>{t_sql_tit}</b> <i>if("Tipo" = 'Urbano', 10, 25)</i><br><br>
                <span style='color: #555;'>{t_aviso}</span>
            """
            
        elif topico == "ddo_fixa":
            t_tit = self.tr("Escala Fixa Dinâmica")
            t_desc1 = self.tr("Use o botão 'ε' para definir a escala absoluta dinamicamente com base na sua tabela de atributos.")
            t_ex_tit = self.tr("Exemplo:")
            t_ex_desc = self.tr("Uma coluna com o número '5000' forçará o mapa para a escala 1:5000.")
            t_sql_tit = self.tr("Expressão SQL:")
            t_aviso = self.tr("Se o cálculo falhar ou a célula for nula, o plugin usará a escala padrão.")

            texto_ajuda = f"""
                <b style='color: #2980b9; font-size: 14px;'>{t_tit}</b><br><br>
                {t_desc1}<br><br>
                <b>{t_ex_tit}</b> {t_ex_desc}<br>
                <b>{t_sql_tit}</b> <i>"Minha_Coluna" * 1000</i><br><br>
                <span style='color: #555;'>{t_aviso}</span>
            """

        if not texto_ajuda: return

        # 1. Cria a janela popup (sem barra superior, que fecha ao perder o foco)
        self.info_popup = QWidget(self.dlg)
        self.info_popup.setWindowFlags(Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint)
        self.info_popup.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) # Fundo transparente para a sombra

        layout_principal = QVBoxLayout(self.info_popup)
        layout_principal.setContentsMargins(10, 10, 10, 10) # Margem externa para a sombra respirar

        # 2. Cria o contêiner branco (o "cartão" em si)
        container = QFrame()
        container.setStyleSheet("""
            QFrame {
                background-color: #ffffff;
                border: 1px solid #dcdcdc;
                border-radius: 6px;
            }
            QLabel {
                color: #333333;
                font-size: 12px;
                border: none;
            }
        """)
        layout_container = QVBoxLayout(container)
        layout_container.setContentsMargins(15, 15, 15, 15)

        # 3. O texto
        lbl = QLabel(texto_ajuda)
        lbl.setWordWrap(True)
        lbl.setFixedWidth(280) # Trava a largura para não esticar demais
        layout_container.addWidget(lbl)
        
        layout_principal.addWidget(container)

        # 4. Sombra Flutuante (Efeito Clean)
        sombra = QGraphicsDropShadowEffect()
        sombra.setBlurRadius(15)
        sombra.setColor(QColor(0, 0, 0, 40))
        sombra.setOffset(2, 4)
        container.setGraphicsEffect(sombra)

        # 5. Posicionamento Dinâmico (Magia!)
        # Pega as coordenadas globais da tela com base no botão clicado
        ponto_ancora = widget_ancora.mapToGlobal(widget_ancora.rect().bottomLeft())
        
        # Move o popup para o exato ponto abaixo do botão
        self.info_popup.move(ponto_ancora.x(), ponto_ancora.y() + 2)

        # Exibe!
        self.info_popup.show()
    

    def mostrar_tela_sobre(self):
        """Exibe a janela de ajuda com texto corrido, no estilo algoritmo e sem ícone grande no corpo."""
        
        msg_box = QMessageBox(self.dlg)
        msg_box.setWindowTitle(self.tr("Ajuda do Algoritmo"))
        msg_box.setIcon(QMessageBox.Icon.NoIcon)

        # Traduz os parágrafos separadamente
        p1 = self.tr("O VectorToMap automatiza a geração de layouts de impressão para cartografia técnica. Este algoritmo transforma os dados da camada vetorial em pranchas prontas, com enquadramento e escala calculados dinamicamente.")
        p2 = self.tr("O parâmetro de agrupamento funciona de forma semelhante a um Atlas, permitindo gerar um mapa individual para cada feição ou agrupar várias feições que compartilham o mesmo valor em uma coluna específica da tabela.")
        p3 = self.tr("O parâmetro de escala controla o nível de zoom do mapa. Pode ser definido para centralização automática ou fixado em valores absolutos. O uso de expressões (ε) permite que a margem de respiro ou a escala exata sejam lidas e aplicadas diretamente a partir dos atributos da feição atual.")
        p4 = self.tr("A opção de filtrar geometrias controla a visibilidade dos elementos no layout final. Quando ativada, ela isola e exibe apenas as feições que pertencem à página que está sendo gerada, ocultando automaticamente o restante dos dados do mapa.")
        p_aviso = self.tr("Utilize o botão de Preview para validar o enquadramento, os estilos e a renderização das informações na tela antes de executar a exportação em lote para arquivos finais.")

        # Constrói o HTML unindo as variáveis
        texto_corrido = f"""
            VectorToMap<br><br>
            {p1}<br><br>
            {p2}<br><br>
            {p3}<br><br>
            {p4}<br><br>
            <ul><li><i>{p_aviso}</i></li></ul>
        """
        
        msg_box.setText(texto_corrido)
        msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
        msg_box.exec()