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


class VectorToMap:

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

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

        self.iface = iface
        self.plugin_dir = os.path.dirname(__file__)
        self.actions = []
        self.menu = self.tr(u'&VectorToMap')
        self.first_start = True
        self.is_rendering = False # Flag to prevent infinite rendering loops
        self.colunas_atuais = []
        
        # 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("Render 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
        )


    def unload(self):
        """Remove o menu e a barra de ferramentas da interface do QGIS ao desativar o plugin."""
        
        for action in self.actions:
            self.iface.removePluginMenu(self.menu, action)
            self.iface.removeToolBarIcon(action)
        
        if hasattr(self, 'toolbar'):
            del self.toolbar

        if hasattr(self, 'dlg'):
            # Deleta o objeto C++ do diálogo explicitamente
            if self.dlg:
                sip.delete(self.dlg)
            del self.dlg

        gc.collect()

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

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

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

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

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

                /* 3. BARRAS DE ROLAGEM MODERNAS (SCROLLBARS) */
                QScrollBar:vertical, QScrollBar:horizontal {
                    border: none;
                    background: #f8f8f8;
                    width: 8px;
                    height: 8px;
                    margin: 0px;
                }
                QScrollBar::handle {
                    background: #cccccc;
                    border-radius: 4px;
                    min-height: 20px;
                    min-width: 20px;
                }
                QScrollBar::handle:hover {
                    background: #707070; /* cinza */
                }
                QScrollBar::add-line, QScrollBar::sub-line {
                    border: none;
                    background: none;
                    height: 0px;
                    width: 0px;
                }
            """
            self.dlg.setStyleSheet(estilo_consolidado)
            self.dlg.previewProgressBar.hide()
            self.dlg.progressBar.hide()

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

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

            # --- SETUP TAMANHOS DE PÁGINA (v0.2.8) ---
            self.dlg.combo_tamanho_pagina.clear()
            tamanhos_disponiveis = [
                ("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)

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

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

            self.dlg.rb_escala_auto.toggled.connect(self.validar_geometria_escala)
            self.dlg.rb_escala_fixa.toggled.connect(self.validar_geometria_escala)
            self.dlg.rb_escala_auto.toggled.connect(self.disparar_preview_se_autorizado)
            self.dlg.rb_escala_fixa.toggled.connect(self.disparar_preview_se_autorizado)
            self.dlg.combo_escala_fixa.currentIndexChanged.connect(self.disparar_preview_se_autorizado)
            
            # --- SETUP 'Render Preview' BUTTON ---
            self.dlg.button_box.addButton(self.btn_render, QDialogButtonBox.ActionRole)
            self.btn_render.clicked.connect(self.atualizar_preview)

            self.dlg.combo_presets.setItemData(0, "quadrado")
            self.dlg.combo_presets.setItemData(1, "75_altura")

            self.dlg.setWindowIcon(QIcon(':/plugins/vector_to_map/icon.png'))
            self.dlg.setWindowTitle("VectorToMap")
            self.dlg.lbl_aviso_escala.hide()

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

            # 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([550, 550]) 
            
            self.dlg.lbl_preview.setFrameShape(QFrame.StyledPanel)
            self.dlg.mMapLayerComboBox.setFilters(QgsMapLayerProxyModel.VectorLayer)

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

            # --- MONITORING WIDGETS ---
            widgets = [
                self.dlg.combo_tamanho_pagina, self.dlg.combo_presets, 
                self.dlg.rb_retrato, self.dlg.rb_paisagem, 
                self.dlg.chk_modo_formulario, self.dlg.chk_modo_individual,
                self.dlg.combo_atlas
            ]
            for w in widgets:
                if hasattr(w, 'currentIndexChanged'): w.currentIndexChanged.connect(self.disparar_preview_se_autorizado)
                elif hasattr(w, 'stateChanged'): w.stateChanged.connect(self.disparar_preview_se_autorizado)
                elif hasattr(w, 'toggled'): w.toggled.connect(self.disparar_preview_se_autorizado)

            self.dlg.resizeEvent = lambda event: self.disparar_preview_se_autorizado()
            self.dlg.button_box.button(QDialogButtonBox.Ok).clicked.connect(self.processar_clique_ok)
            self.abort_processing = False
            self.dlg.button_box.rejected.connect(self.cancelar_e_fechar)
            self.configurar_escala_ao_mudar_camada()

        # --- CENTRALIZAR NA TELA ---        
        geometria_janela = self.dlg.frameGeometry()
        centro_tela = QDesktopWidget().availableGeometry().center()
        geometria_janela.moveCenter(centro_tela)
        self.dlg.move(geometria_janela.topLeft())

        self.setup_ui_strings()  
        self.dlg.show()
    

    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_preview_auto.setToolTip(self.tr("Atualização automática da Preview. Pode ser lento. Alternativamente use o botão Render Preview."))
        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."))


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

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

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

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

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

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

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

        self.dlg.combo_atlas.clear()
        self.dlg.combo_atlas.addItem("--- No Grouping (Each Feature) ---", None)

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

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


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

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

        layout_atual.setAlignment(Qt.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.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
        tipo_geom = camada.geometryType()
        if tipo_geom == 0:
            self.dlg.rb_escala_fixa.setChecked(True)
        else:
            self.dlg.rb_escala_auto.setChecked(True)
        self.atualizar_lista_atributos()
        self.validar_geometria_escala()


    def validar_geometria_escala(self):
        """Gerencia habilitar/desabilitar, avisos e trava em 1:10.000."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return
        tipo_geom = camada.geometryType() 
        wkb_type = camada.wkbType()
        is_multi = QgsWkbTypes.isMultiType(wkb_type)

        self.dlg.combo_escala_fixa.blockSignals(True)
        self.dlg.rb_escala_auto.setEnabled(not (tipo_geom == 0 and not is_multi))
        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)

        perigo = (tipo_geom == 0 and is_multi and self.dlg.rb_escala_auto.isChecked())
        if perigo:
            self.dlg.lbl_aviso_escala.show()
            self.dlg.lbl_aviso_escala.setText(self.tr("⚠️ Atenção: Feições de ponto único podem quebrar a escala automática."))
        else:
            self.dlg.lbl_aviso_escala.hide()

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

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

    def atualizar_preview(self):
        """Renders a sample layout preview with an indeterminate progress bar."""
        
        layout = None

        if self.is_rendering or not self.dlg.isVisible(): 
            return
        
        self.dlg.lbl_preview.clear()

        self.is_rendering = True
        self.dlg.previewProgressBar.show() 
        self.dlg.previewProgressBar.setRange(0, 0) 
        QCoreApplication.processEvents()

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

            layout = QgsPrintLayout(QgsProject.instance())
            layout.initializeDefaults()
            
            campo_atlas = self.dlg.combo_atlas.currentData()
            feicoes = []
            sample_f = next(camada.getFeatures(QgsFeatureRequest().setLimit(1)), None)
            
            if sample_f:
                if campo_atlas is None:
                    feicoes = [sample_f]
                else:
                    # 1. Pega o valor do atributo da feição de amostra
                    val_amostra = sample_f.attribute(campo_atlas)
                    
                    # 2. Cria uma expressão de filtro (ex: "Bairro" = 'Centro')
                    # Usamos o QgsExpression.quotedValue para garantir que textos com aspas não quebrem o código
                    filtro = f'"{campo_atlas}" = {QgsExpression.quotedValue(val_amostra)}'
                    
                    # 3. Faz a busca filtrada diretamente na camada
                    request = QgsFeatureRequest().setFilterExpression(filtro)
                    feicoes = list(camada.getFeatures(request))

            self.montar_design_da_pagina(
                layout, camada, feicoes, self.dlg.combo_presets.currentData(),
                "Retrato" if self.dlg.rb_retrato.isChecked() else "Paisagem",
            )

            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.KeepAspectRatio, Qt.SmoothTransformation))
        
        finally:
            if layout:
                layout.pageCollection().clear() # Esvazia as páginas
                sip.delete(layout)              # Deleta o objeto C++

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


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


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

        paginas_dados = []
        campo_atlas = self.dlg.combo_atlas.currentData()
        
        if campo_atlas is None:
            for f in camada.getFeatures(): 
                paginas_dados.append({'feicoes': [f]})
        else:
            # 1. Obtém todos os valores únicos do campo de agrupamento (MUITO mais rápido)
            idx_campo = camada.fields().indexOf(campo_atlas)
            valores_unicos = camada.uniqueValues(idx_campo)
            
            for val in valores_unicos:
                # 2. Para cada valor único, fazemos uma requisição filtrada
                filtro = f'"{campo_atlas}" = {QgsExpression.quotedValue(val)}'
                request = QgsFeatureRequest().setFilterExpression(filtro)
                
                # 3. Pegamos apenas as feições daquele grupo
                feicoes_do_grupo = list(camada.getFeatures(request))
                if feicoes_do_grupo:
                    paginas_dados.append({'feicoes': feicoes_do_grupo})
        
        if paginas_dados: 
            self.criar_layout_multi_paginas(
                self.dlg.combo_presets.currentData(), camada, 
                "Retrato" if self.dlg.rb_retrato.isChecked() else "Paisagem", 
                self.dlg.combo_tamanho_pagina.currentData(), 
                paginas_dados, colunas_selecionadas, None
            )
            if not self.abort_processing:
                self.dlg.accept()


    def criar_layout_multi_paginas(self, preset, camada, orientacao, tamanho_pg, paginas_dados, colunas, modos):
        """Generate the definitive layout with a real-time progress bar."""
        
        # --- INÍCIO: Liga o motor e reseta o aborto ---
        self.is_rendering = True
        self.abort_processing = False
        
        try:
            project = QgsProject.instance()
            manager = project.layoutManager()
            nome_camada = re.sub(r'[^a-zA-Z0-9_]', '_', unicodedata.normalize('NFD', camada.name()).encode('ascii', 'ignore').decode('utf-8'))
            layout_name = f"VectorToMap_{nome_camada}"
            
            contador = 1
            while manager.layoutByName(layout_name):
                layout_name = f"VectorToMap_{nome_camada}_{contador}"; contador += 1

            layout = QgsPrintLayout(project)
            layout.initializeDefaults()
            layout.setName(layout_name)

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

            for i, dados in enumerate(paginas_dados):
                # Verifica se o usuário clicou no novo botão cancelar
                if self.abort_processing: 
                    break
                    
                if i > 0: layout.pageCollection().addPage(QgsLayoutItemPage(layout))
                
                self.montar_design_da_pagina(layout, camada, dados['feicoes'], preset, orientacao, pagina_index=i)
                
                if i % 10 == 0:
                    layout.refresh()
                    gc.collect()                    

                self.dlg.progressBar.setValue(i + 1)
                # Essencial para que o QGIS "ouça" o clique no botão cancelar durante o loop
                QCoreApplication.processEvents() 

            # --- FINALIZAÇÃO DO LAYOUT ---
            if not self.abort_processing:
                layout.refresh()
                manager.addLayout(layout)
                self.iface.openLayoutDesigner(layout)
            else:
                # Opcional: remover o layout inacabado se foi abortado
                self.iface.messageBar().pushMessage("Cancelado", "O processamento foi interrompido pelo usuário.", level=2)

        except Exception as e:
            # Se der qualquer erro no código, avisa o usuário
            self.iface.messageBar().pushMessage("Erro", f"Falha ao gerar layout: {str(e)}", level=3)

        finally:
            # --- FIM: Desliga o motor SEMPRE, mesmo se houver erro ou break ---
            self.is_rendering = False
            self.dlg.progressBar.hide()
            gc.collect()


    def montar_design_da_pagina(self, layout, camada, feicoes_da_pagina, preset, orientacao, pagina_index=0):
        """Factory Core v0.2.8: Usa dimensões dinâmicas do ComboBox."""

        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
        
        if preset == "75_altura":
            header_h = 35.0
            h_util = h_pg - (2 * margin_padrao) - header_h
            w_map_f, h_map_f = w_pg - (2 * margin_padrao), h_util * 0.75
            x_map_f, y_map_f = margin_padrao, y_zero_folha + margin_padrao + header_h
        else:
            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

        map_item = QgsLayoutItemMap(layout)
        map_item.setFrameEnabled(True)
        layout.addLayoutItem(map_item)
        map_item.attemptResize(QgsLayoutSize(w_map_f, h_map_f, QgsUnitTypes.LayoutMillimeters))
        map_item.attemptMove(QgsLayoutPoint(x_map_f, y_map_f, QgsUnitTypes.LayoutMillimeters))
        
        if feicoes_da_pagina:
            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)
            
            if ext_proj.width() == 0 or ext_proj.height() == 0: ext_proj.grow(1.0)
            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:
                unit_to_mm = QgsUnitTypes.fromUnitToUnitFactor(project_crs.mapUnits(), QgsUnitTypes.DistanceMillimeters)
                scale_w = (ext_proj.width() * unit_to_mm) / w_map_f
                scale_h = (ext_proj.height() * unit_to_mm) / h_map_f
                map_item.setScale(max(scale_w, scale_h) * 1.25)

            map_item.refresh()
            map_item.attemptResize(QgsLayoutSize(w_map_f, h_map_f, QgsUnitTypes.LayoutMillimeters))
            map_item.attemptMove(QgsLayoutPoint(x_map_f, y_map_f, QgsUnitTypes.LayoutMillimeters))

        colunas = [cb.text() for cb in self.dlg.scrollAreaWidgetContents.findChildren(QCheckBox) if cb.isChecked()]
        limite_fundo = y_zero_folha + h_pg - margin_padrao

        if colunas and feicoes_da_pagina:
            # --- ÁREAS DE POSICIONAMENTO ---
            if preset == "75_altura":
                # Layout de 1/3 lateral para o Preset 75 (Independente da orientação)
                largura_util = w_pg - (2 * margin_padrao)
                terco_x = margin_padrao + (largura_util * 0.33) 
                
                x_form, y_form = margin_padrao, y_map_f + h_map_f + 3.0
                w_form = (largura_util * 0.33) - 2.0
                h_form = limite_fundo - (y_map_f + h_map_f + 3.0)
                
                x_ind_start = terco_x + 2.0
                y_ind_min = y_map_f + h_map_f + 2.0
            else:
                # Layout original para Mapas Quadrados
                x_form = margin_padrao
                y_form = y_map_f + h_map_f + 5.0 if orientacao == "Retrato" else y_map_f + (h_map_f / 2)
                w_form = w_pg - (2 * margin_padrao) if orientacao == "Retrato" else x_map_f - (2 * margin_padrao)
                h_form = (limite_fundo - y_form) * 0.5 if self.dlg.chk_modo_individual.isChecked() else (limite_fundo - y_form)
                x_ind_start, y_ind_min = x_form, y_form + h_form + 2.0 if self.dlg.chk_modo_individual.isChecked() else y_form

            # --- RENDERIZAÇÃO DO FORMULÁRIO ---
            if self.dlg.chk_modo_formulario.isChecked():
                txt_html = ""
                for idx, f in enumerate(feicoes_da_pagina):
                    if len(feicoes_da_pagina) > 1: txt_html += f"<b>ITEM {idx+1}</b><br>"
                    for col in colunas:
                        try: txt_html += f"<b>{col}:</b> {str(f.attribute(col) or '').strip()}<br>"
                        except: continue
                lbl_f = QgsLayoutItemLabel(layout)
                lbl_f.setMode(QgsLayoutItemLabel.ModeHtml)
                lbl_f.setText(txt_html)
                lbl_f.attemptMove(QgsLayoutPoint(x_form, y_form, QgsUnitTypes.LayoutMillimeters))
                lbl_f.attemptResize(QgsLayoutSize(w_form, h_form, QgsUnitTypes.LayoutMillimeters))
                layout.addLayoutItem(lbl_f)

            # --- RENDERIZAÇÃO DOS RÓTULOS INDIVIDUAIS ---
            if self.dlg.chk_modo_individual.isChecked():
                campo_atlas = self.dlg.combo_atlas.currentData() 

                if campo_atlas is None:
                    altura_linha = 5.5
                    f = feicoes_da_pagina[0] 
                    xi = x_ind_start
                    yi = y_ind_min
                    
                    # --- DEFINIÇÃO RIGOROSA DOS LIMITES ---
                    if preset == "75_altura":
                        limite_colunas = 3 if orientacao == "Retrato" else 5
                    else:
                        # Casos de Mapa Quadrado
                        limite_colunas = 5 if orientacao == "Retrato" else 3
                    
                    for idx, col in enumerate(colunas):
                        if idx > 0 and idx % limite_colunas == 0:
                            yi += altura_linha
                            xi = x_ind_start
                            
                        try:
                            lbl_i = QgsLayoutItemLabel(layout)

                            val = f.attribute(col)
                            display_val = str(val).strip() if val is not None else ""
                            lbl_i.setText(f"{col}: {display_val}")

                            lbl_i.setFrameEnabled(True)
                            lbl_i.adjustSizeToText()
                            ancho_folga = lbl_i.rect().width() + 2.0
                            lbl_i.attemptResize(QgsLayoutSize(ancho_folga, lbl_i.rect().height(), QgsUnitTypes.LayoutMillimeters))
                            lbl_i.attemptMove(QgsLayoutPoint(xi, yi, QgsUnitTypes.LayoutMillimeters))
                            layout.addLayoutItem(lbl_i)
                            xi += ancho_folga + 2.0
                        except Exception as e:
                            print(f"Erro ao processar campo {col}: {e}")
                            continue
                else:
                    # Comportamento Agrupado (Uma linha por feição)
                    altura_linha = 5.5
                    altura_total = len(feicoes_da_pagina) * altura_linha
                    yi = (limite_fundo - altura_total) if (y_ind_min + altura_total) > limite_fundo else y_ind_min

                    for f in feicoes_da_pagina:
                        xi = x_ind_start
                        for col in colunas:
                            try:
                                lbl_i = QgsLayoutItemLabel(layout)

                                val = f.attribute(col)
                                display_val = str(val).strip() if val is not None else ""
                                lbl_i.setText(f"{col}: {display_val}")

                                lbl_i.setFrameEnabled(True)
                                lbl_i.adjustSizeToText()
                                ancho_folga = lbl_i.rect().width() + 2.0
                                lbl_i.attemptResize(QgsLayoutSize(ancho_folga, lbl_i.rect().height(), QgsUnitTypes.LayoutMillimeters))
                                lbl_i.attemptMove(QgsLayoutPoint(xi, yi, QgsUnitTypes.LayoutMillimeters))
                                layout.addLayoutItem(lbl_i)
                                xi += ancho_folga + 2.0
                            except Exception as e:
                                print(f"Erro ao processar campo {col}: {e}")
                                continue
                        yi += altura_linha
        
        self.adicionar_numeracao_pagina(layout, w_pg, h_pg, y_zero_folha)


    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)