# -*- coding: utf-8 -*-
"""
/***************************************************************************
 MapBiomas Fogo - GeoPDF Export Module
                                 
 Este módulo contém toda a lógica de exportação para GeoPDF
 
 ***************************************************************************/
                   contato: newton.monteiro@ipam.org.br
/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

from qgis.core import (
    QgsCoordinateTransform,
    QgsLayoutExporter,
    QgsLayoutItemMap,
    QgsLayoutPoint,
    QgsLayoutSize,
    QgsMapRendererCustomPainterJob,
    QgsMapSettings,
    QgsPrintLayout,
    QgsProject,
    QgsReadWriteContext,
    Qgis,
)
from qgis.PyQt.QtCore import QFile, QIODevice, QObject, pyqtSignal, Qt, QSize
from qgis.PyQt.QtGui import (
    QPainter, QColor, QImage, QTransform,
    QPixmap
)
from qgis.PyQt.QtWidgets import QFrame
from qgis.PyQt.QtXml import QDomDocument
import os
from pathlib import Path

_IMAGE_FORMAT_ENUM = getattr(QImage, "Format", QImage)
_QIMAGE_ARGB32 = getattr(_IMAGE_FORMAT_ENUM, "Format_ARGB32")

_RENDER_HINT_ENUM = getattr(QPainter, "RenderHint", QPainter)
_PAINTER_HINT_ANTIALIAS = getattr(_RENDER_HINT_ENUM, "Antialiasing")
_PAINTER_HINT_SMOOTH = getattr(_RENDER_HINT_ENUM, "SmoothPixmapTransform")

_TRANSFORMATION_MODE_ENUM = getattr(Qt, "TransformationMode", Qt)
_SMOOTH_TRANSFORMATION = getattr(_TRANSFORMATION_MODE_ENUM, "SmoothTransformation")

_OPEN_MODE_ENUM = getattr(QIODevice, "OpenModeFlag", QIODevice)
_QIO_READ_TEXT = getattr(_OPEN_MODE_ENUM, "ReadOnly") | getattr(_OPEN_MODE_ENUM, "Text")


class GeoPDFExporter(QObject):
    """Classe responsável por gerenciar a exportação de mapas para GeoPDF.
    
    Attributes:
        progress_updated: Signal emitido quando progresso é atualizado
        export_completed: Signal emitido quando exportação é concluída
        export_failed: Signal emitido quando ocorre erro na exportação
    """
    
    # Signals
    progress_updated = pyqtSignal(str)  # Mensagem de progresso
    export_completed = pyqtSignal(str)   # Caminho do arquivo exportado
    export_failed = pyqtSignal(str)     # Mensagem de erro
    
    def __init__(self, iface=None):
        """Inicializa o exportador GeoPDF.
        
        Args:
            iface: Interface QGIS (QgsInterface)
        """
        super().__init__()
        self.iface = iface
        self.canvas = iface.mapCanvas() if iface else None
        self.project = QgsProject.instance()
        
        # Armazenar última extensão e CRS renderizados
        self.last_rendered_extent = None
        self.last_rendered_crs = None
        self.last_rendered_size = None  # (width, height) em pixels

    def _log(self, message):
        """Log padronizado de uma linha para depuração."""
        print(f"[MapBiomas Fogo] {message}")

    def _layout_template_path(self, orientation):
        """Resolve o arquivo .qpt correto para retrato ou paisagem."""
        orientation_value = str(orientation or "portrait").strip().lower()
        if orientation_value.startswith("land"):
            candidates = [
                os.path.join(os.path.dirname(__file__), "layout_ladscapeqpt.qpt"),
                os.path.join(os.path.dirname(__file__), "layout_landscape.qpt"),
                os.path.join(os.path.dirname(__file__), "layout_ladscape.qpt"),
            ]
        else:
            candidates = [
                os.path.join(os.path.dirname(__file__), "layout_portrait.qpt"),
            ]

        for candidate in candidates:
            if os.path.exists(candidate):
                return candidate
        return None

    def _load_layout_from_template(self, template_path):
        """Carrega um QgsPrintLayout a partir de um template QPT."""
        self._log(f"Carregando template: {template_path}")
        template_file = QFile(template_path)
        if not template_file.open(_QIO_READ_TEXT):
            raise IOError(f"Nao foi possivel abrir o template: {template_path}")

        try:
            document = QDomDocument()
            if not document.setContent(template_file):
                raise ValueError(f"Template QPT invalido: {template_path}")

            layout = QgsPrintLayout(self.project)
            layout.loadFromTemplate(document, QgsReadWriteContext(), True)

            if layout.pageCollection().pageCount() == 0:
                layout.initializeDefaults()

            layout_name = layout.name().strip() if layout.name() else ""
            if not layout_name:
                layout_name = Path(template_path).stem
                layout.setName(layout_name)

            layout_manager = self.project.layoutManager()
            existing_layout = layout_manager.layoutByName(layout_name)
            if existing_layout is not None:
                layout_manager.removeLayout(existing_layout)
            layout_manager.addLayout(layout)

            self._log(
                f"Layout pronto: nome={layout_name}, paginas={layout.pageCollection().pageCount()}"
            )

            return layout
        finally:
            template_file.close()

    def _validate_output_path(self, output_path):
        """Valida e normaliza o caminho de saida do PDF."""
        if not output_path:
            return None

        normalized_path = os.path.abspath(os.path.normpath(str(output_path).strip()))
        if not normalized_path:
            return None

        parent_dir = os.path.dirname(normalized_path)
        if parent_dir and not os.path.isdir(parent_dir):
            return None

        if not normalized_path.lower().endswith(".pdf"):
            normalized_path = f"{normalized_path}.pdf"

        if parent_dir and not os.access(parent_dir, os.W_OK):
            return None

        if os.path.exists(normalized_path) and not os.access(normalized_path, os.W_OK):
            return None

        return normalized_path

    def _layout_map_items(self, layout):
        """Retorna todos os itens de mapa do layout."""
        return [item for item in layout.items() if isinstance(item, QgsLayoutItemMap)]

    def _first_layout_map_item(self, layout):
        """Retorna o primeiro item de mapa do layout, usado como referencia para georreferenciamento."""
        map_items = self._layout_map_items(layout)
        return map_items[0] if map_items else None

    def _resolve_export_layers(self, include_layers):
        """Seleciona camadas do projeto por nome ou fonte, quando solicitado."""
        if not include_layers:
            return None

        project_layers = list(self.project.mapLayers().values())
        resolved_layers = []

        for identifier in include_layers:
            identifier_text = str(identifier or "").strip()
            if not identifier_text:
                continue

            match = next(
                (
                    layer
                    for layer in project_layers
                    if layer.name() == identifier_text
                    or layer.source() == identifier_text
                    or identifier_text in layer.source()
                ),
                None,
            )
            if match is not None and match not in resolved_layers:
                resolved_layers.append(match)

        return resolved_layers or None

    def _pixel_to_mm(self, pixels, dpi):
        """Converte pixels para milimetros considerando o DPI de exportacao."""
        dpi_value = float(dpi or 300)
        return (float(pixels) / dpi_value) * 25.4

    def _apply_page_pixel_size(self, layout, orientation, dpi):
        """Define o tamanho fisico da pagina para bater o alvo em pixels na exportacao."""
        orientation_value = str(orientation or "portrait").strip().lower()
        if orientation_value.startswith("land"):
            width_px, height_px = 3500, 2485
        else:
            width_px, height_px = 2500, 4500

        width_mm = self._pixel_to_mm(width_px, dpi)
        height_mm = self._pixel_to_mm(height_px, dpi)

        page_collection = layout.pageCollection()
        if page_collection.pageCount() == 0:
            layout.initializeDefaults()

        page_size = QgsLayoutSize(width_mm, height_mm, Qgis.LayoutUnit.Millimeters)
        for page in page_collection.pages():
            page.setPageSize(page_size)

        page_collection.reflow()
        self._log(
            f"Pagina configurada: {width_px}x{height_px}px @ {dpi or 300} DPI ({width_mm:.2f}x{height_mm:.2f} mm)"
        )

    def _apply_map_item_geometry(self, layout, orientation):
        """Ajusta apenas o mapa para ficar recuado dentro da página, sem alterar as dimensões da página."""
        map_item = self._first_layout_map_item(layout)
        if map_item is None:
            return

        orientation_value = str(orientation or "portrait").strip().lower()
        if orientation_value.startswith("land"):
            page_width_px, page_height_px = 3500, 2485
        else:
            page_width_px, page_height_px = 2500, 4500

        margin_px = 23
        map_x = margin_px
        map_y = margin_px
        map_w = max(10, page_width_px - (margin_px * 2))
        map_h = max(10, page_height_px - (margin_px * 2))

        position = QgsLayoutPoint(map_x, map_y, Qgis.LayoutUnit.Pixels)
        size = QgsLayoutSize(map_w, map_h, Qgis.LayoutUnit.Pixels)

        if hasattr(map_item, "setFrameEnabled"):
            map_item.setFrameEnabled(False)
        if hasattr(map_item, "setBackgroundEnabled"):
            map_item.setBackgroundEnabled(False)

        map_item.attemptMove(position)
        map_item.attemptResize(size)
        map_item.refresh()
        self._log(
            f"Mapa ajustado internamente na página: pos=({map_x},{map_y}) px, "
            f"size=({map_w}x{map_h}) px, pagina={page_width_px}x{page_height_px} px"
        )

    def _prepare_map_item_extent(self, map_item, extent, extent_crs):
        """Prepara a extensao no CRS do item de mapa para evitar pagina em branco."""
        if extent is None or extent_crs is None or not extent_crs.isValid():
            return extent

        item_crs = map_item.crs() if hasattr(map_item, "crs") else None
        if item_crs is None or not item_crs.isValid():
            if hasattr(map_item, "setCrs"):
                map_item.setCrs(extent_crs)
            return extent

        if item_crs == extent_crs:
            return extent

        transform = QgsCoordinateTransform(extent_crs, item_crs, self.project)
        return transform.transformBoundingBox(extent)

    def _match_extent_to_frame(self, extent, frame_width, frame_height):
        """Expande a extensao para a mesma proporcao do frame de preview."""
        if extent is None or frame_width <= 0 or frame_height <= 0:
            return extent

        current_width = extent.width()
        current_height = extent.height()
        if current_width <= 0 or current_height <= 0:
            return extent

        frame_ratio = float(frame_width) / float(frame_height)
        extent_ratio = float(current_width) / float(current_height)
        center = extent.center()

        if abs(extent_ratio - frame_ratio) < 1e-9:
            return extent

        if extent_ratio > frame_ratio:
            matched_width = current_width
            matched_height = current_width / frame_ratio
        else:
            matched_height = current_height
            matched_width = current_height * frame_ratio

        from qgis.core import QgsRectangle
        return QgsRectangle(
            center.x() - (matched_width / 2.0),
            center.y() - (matched_height / 2.0),
            center.x() + (matched_width / 2.0),
            center.y() + (matched_height / 2.0),
        )

    def _configure_layout_map(self, layout, extent, include_layers=None, extent_crs=None):
        """Aplica extensão e camadas ao(s) item(ns) de mapa do layout sem distorcer a geometria do zoom."""
        map_items = self._layout_map_items(layout)
        if not map_items:
            raise ValueError("Template nao possui item de mapa.")

        selected_layers = self._resolve_export_layers(include_layers)

        for map_item in map_items:
            target_extent = self._prepare_map_item_extent(map_item, extent, extent_crs)
            map_item.setExtent(target_extent)
            map_item.refresh()
            if selected_layers:
                map_item.setKeepLayerSet(True)
                map_item.setLayers(selected_layers)
            else:
                map_item.setKeepLayerSet(False)

        self._log(
            f"Extensao aplicada em {len(map_items)} mapa(s): xmin={extent.xMinimum():.6f}, ymin={extent.yMinimum():.6f}, xmax={extent.xMaximum():.6f}, ymax={extent.yMaximum():.6f}, crs={extent_crs.authid() if extent_crs else 'n/a'}"
        )

    def _export_layout_to_pdf(self, layout, output_path, dpi=300, include_metadata=True):
        """Exporta o layout carregado para PDF usando QGIS LayoutExporter."""
        exporter = QgsLayoutExporter(layout)

        settings = QgsLayoutExporter.PdfExportSettings()
        settings.dpi = dpi or 300
        settings.forceVectorOutput = False
        settings.rasterizeWholeImage = False
        if hasattr(settings, "exportMetadata"):
            settings.exportMetadata = bool(include_metadata)

        if hasattr(settings, "appendGeoreference"):
            settings.appendGeoreference = True
        if hasattr(settings, "writeGeoPdf"):
            settings.writeGeoPdf = False
        if hasattr(settings, "includeGeoPdfFeatures"):
            settings.includeGeoPdfFeatures = False
        if hasattr(settings, "useIso32000ExtensionFormatGeoreferencing"):
            settings.useIso32000ExtensionFormatGeoreferencing = True
        if hasattr(settings, "useLayerTreeConfig"):
            settings.useLayerTreeConfig = False
        if hasattr(settings, "exportLayersAsSeperateFiles"):
            settings.exportLayersAsSeperateFiles = False

        if hasattr(settings, "georeference"):
            settings.georeference = True
        if hasattr(settings, "imageCompression"):
            settings.imageCompression = 0
        if hasattr(settings, "simplifyGeometries"):
            settings.simplifyGeometries = True
        if (
            hasattr(settings, "textRenderFormat")
            and hasattr(Qgis, "TextRenderFormat")
            and hasattr(Qgis.TextRenderFormat, "AlwaysOutlines")
        ):
            settings.textRenderFormat = Qgis.TextRenderFormat.AlwaysOutlines
        if hasattr(settings, "disableTiledRasterLayerRendering"):
            settings.disableTiledRasterLayerRendering = False

        if os.path.exists(output_path):
            try:
                os.remove(output_path)
            except OSError as exc:
                raise RuntimeError(
                    f"Nao foi possivel sobrescrever o arquivo existente: {output_path} ({exc})"
                )

        result = exporter.exportToPdf(output_path, settings)
        if isinstance(result, tuple):
            result = result[0]

        success_value = getattr(QgsLayoutExporter, "Success", None)
        if success_value is None:
            success_value = 0

        if result != success_value and str(result).lower() not in {"0", "success"}:
            error_name = getattr(result, "name", str(result))
            exporter_error = ""
            exporter_file = ""
            try:
                exporter_error = exporter.errorMessage()
            except Exception:
                exporter_error = ""
            try:
                exporter_file = exporter.errorFile()
            except Exception:
                exporter_file = ""
            if exporter_error:
                if exporter_file:
                    raise RuntimeError(f"Falha ao exportar PDF: {error_name} - {exporter_error} ({exporter_file})")
                raise RuntimeError(f"Falha ao exportar PDF: {error_name} - {exporter_error}")
            if exporter_file:
                raise RuntimeError(f"Falha ao exportar PDF: {error_name} ({exporter_file})")
            raise RuntimeError(f"Falha ao exportar PDF: {error_name}")

        if not os.path.exists(output_path):
            raise RuntimeError(f"Exportacao informada como concluida, mas o arquivo nao foi criado: {output_path}")

        if os.path.getsize(output_path) == 0:
            raise RuntimeError(f"Exportacao gerou arquivo vazio: {output_path}")

        self._log(f"Exportacao PDF concluida: {output_path}")

        return True

    def _export_layout_to_pdf_with_fallback(self, layout, output_path, dpi=300, include_metadata=True):
        """Exporta primeiro como GeoPDF e, se falhar, cai para PDF + georreferenciamento."""
        self._log(f"Iniciando exportacao: {output_path}")
        try:
            return self._export_layout_to_pdf(layout, output_path, dpi=dpi, include_metadata=include_metadata)
        except Exception as primary_error:
            self._log(f"Falha na exportacao direta, tentando fallback: {primary_error}")

            reference_map = self._first_layout_map_item(layout)

            if os.path.exists(output_path):
                try:
                    os.remove(output_path)
                except OSError as exc:
                    self._log(f"Nao foi possivel remover arquivo de fallback anterior: {output_path} ({exc})")

            fallback_settings = QgsLayoutExporter.PdfExportSettings()
            fallback_settings.dpi = dpi or 300
            fallback_settings.forceVectorOutput = False
            fallback_settings.rasterizeWholeImage = True
            if hasattr(fallback_settings, "exportMetadata"):
                fallback_settings.exportMetadata = bool(include_metadata)

            exporter = QgsLayoutExporter(layout)
            result = exporter.exportToPdf(output_path, fallback_settings)
            if isinstance(result, tuple):
                result = result[0]

            success_value = getattr(QgsLayoutExporter, "Success", None)
            if success_value is None:
                success_value = 0

            if result != success_value and str(result).lower() not in {"0", "success"}:
                error_name = getattr(result, "name", str(result))
                exporter_error = ""
                exporter_file = ""
                try:
                    exporter_error = exporter.errorMessage()
                except Exception:
                    exporter_error = ""
                try:
                    exporter_file = exporter.errorFile()
                except Exception:
                    exporter_file = ""
                if exporter_error:
                    if exporter_file:
                        raise RuntimeError(f"Falha ao exportar PDF no fallback: {error_name} - {exporter_error} ({exporter_file})")
                    raise RuntimeError(f"Falha ao exportar PDF no fallback: {error_name} - {exporter_error}")
                raise RuntimeError(f"Falha ao exportar PDF no fallback: {error_name}")

            if reference_map is not None:
                georef_ok = exporter.georeferenceOutput(output_path, reference_map, dpi=dpi or 300)
                if not georef_ok:
                    raise RuntimeError("PDF gerado, mas a georreferenciacao do arquivo falhou no fallback.")

            self._log(f"Fallback concluido com georreferenciamento: {output_path}")

        return True
        
    def export_to_geopdf(self, output_path, include_layers=None, dpi=300,
                         orientation="portrait",
                         include_scale=True, include_north_arrow=True, 
                         include_metadata=True):
        """Exporta o mapa atual para GeoPDF.
        
        Args:
            output_path (str): Caminho completo do arquivo PDF de saída
            include_layers (list): Lista de nomes de camadas a incluir (None = todas)
            dpi (int): Resolução em DPI (padrão: 300)
            include_scale (bool): Incluir barra de escala
            include_north_arrow (bool): Incluir seta norte
            include_metadata (bool): Incluir metadados do mapa
            
        Returns:
            bool: True se exportação foi bem-sucedida, False caso contrário
        """
        return self.export_frame_to_geopdf(
            output_path=output_path,
            frame=None,
            orientation=orientation,
            dpi=dpi,
            include_scale=include_scale,
            include_north_arrow=include_north_arrow,
            include_metadata=include_metadata,
            include_layers=include_layers,
        )
    
    def get_available_layers(self):
        """Retorna lista de camadas disponíveis para exportação.
        
        Returns:
            list: Lista de nomes de camadas
        """
        try:
            layers = []
            for layer in self.project.mapLayers().values():
                layers.append({
                    'name': layer.name(),
                    'type': layer.__class__.__name__,
                    'visible': layer.isVisible()
                })
            return layers
        except Exception as e:
            print(f"[MapBiomas Fogo] Erro ao obter camadas: {e}")
            return []
    
    def get_canvas_extent(self):
        """Retorna a extensão atual do canvas.
        
        Returns:
            dict: Dicionário com coordenadas da extensão
        """
        try:
            if not self.canvas:
                return None
            
            extent = self.canvas.extent()
            return {
                'xmin': extent.xMinimum(),
                'ymin': extent.yMinimum(),
                'xmax': extent.xMaximum(),
                'ymax': extent.yMaximum(),
                'crs': self.canvas.mapSettings().destinationCrs().authid()
            }
        except Exception as e:
            print(f"[MapBiomas Fogo] Erro ao obter extensão: {e}")
            return None

    def render_to_frame(self, frame, zoom_percent=100, rotation_degrees=0, 
                       opacity_percent=100, pan_x=0, pan_y=0, include_layers=None):
        """Renderiza o mapa em um QLabel específico com transformações.
        
        Args:
            frame (QLabel): QLabel onde renderizar
            zoom_percent (int): Zoom em percentual (100 = normal, 50 = zoom out 2x)
            rotation_degrees (int): Ângulo de rotação em graus
            opacity_percent (int): Opacidade em percentual (0-100)
            pan_x (int): Deslocamento X em pixels
            pan_y (int): Deslocamento Y em pixels
            include_layers (list): Camadas a incluir (None = todas visíveis)
            
        Returns:
            QPixmap: Imagem renderizada ou None se falha
        """
        try:
            if not self.canvas or not frame:
                print("[MapBiomas Fogo] Canvas ou Label não disponível")
                return None
            
            # Obter dimensões do label
            label_width = frame.width()
            label_height = frame.height()
            
            if label_width <= 0 or label_height <= 0:
                print(f"[MapBiomas Fogo] Dimensões inválidas: {label_width}x{label_height}")
                return None
            
            print(f"[MapBiomas Fogo] Renderizando label: {label_width}x{label_height}, "
                  f"zoom={zoom_percent}%, pan_x={pan_x}px, pan_y={pan_y}px")
            
            # Criar imagem para renderização
            image = QImage(label_width, label_height, _QIMAGE_ARGB32)
            image.fill(QColor(255, 255, 255, 255))  # Fundo branco
            
            # Configurar painter
            painter = QPainter(image)
            painter.setRenderHint(_PAINTER_HINT_ANTIALIAS)
            painter.setRenderHint(_PAINTER_HINT_SMOOTH)
            
            # Obter extensão original do canvas
            original_extent = self.canvas.extent()
            original_settings = self.canvas.mapSettings()
            
            # Criar configurações de renderização ajustadas para zoom
            render_settings = QgsMapSettings()
            render_settings.setOutputSize(QSize(label_width, label_height))
            render_settings.setLayers(original_settings.layers())
            render_settings.setDestinationCrs(original_settings.destinationCrs())
            render_settings.setBackgroundColor(QColor(255, 255, 255))
            
            # Ajusta a extensão-base à proporção do preview antes de converter
            # deslocamentos de pixels em unidades do mapa.
            extent = self._match_extent_to_frame(
                original_extent,
                label_width,
                label_height,
            )

            # Aplica zoom antes do pan para que cada pixel arrastado mantenha
            # a mesma sensibilidade visual em qualquer nível de zoom.
            if zoom_percent != 100:
                extent = self._apply_zoom_to_extent(
                    extent,
                    zoom_percent / 100.0
                )

            if pan_x != 0 or pan_y != 0:
                extent = self._apply_pan_to_extent(
                    extent,
                    pan_x, pan_y,
                    label_width, label_height
                )

            render_settings.setExtent(extent)
            
            # Armazenar extent e CRS para uso posterior na exportação
            self.last_rendered_extent = extent
            self.last_rendered_crs = original_settings.destinationCrs()
            self.last_rendered_size = (label_width, label_height)  # Armazenar dimensões
            
            # DEBUG: Imprimir coordenadas armazenadas
            if self.last_rendered_extent and self.last_rendered_crs:
                ext = self.last_rendered_extent
                print(f"[MapBiomas Fogo] 📍 Coordenadas armazenadas para exportação:")
                print(f"    XMin: {ext.xMinimum():.6f} | XMax: {ext.xMaximum():.6f}")
                print(f"    YMin: {ext.yMinimum():.6f} | YMax: {ext.yMaximum():.6f}")
                print(f"    Preview: {label_width}x{label_height} px")
                print(f"    CRS: {self.last_rendered_crs.authid()}")
            
            # Filtrar camadas se solicitado
            if include_layers:
                all_layers = original_settings.layers()
                filtered_layers = [
                    layer for layer in all_layers 
                    if layer.name() in include_layers
                ]
                render_settings.setLayers(filtered_layers)
            
            # Renderizar mapa
            job = QgsMapRendererCustomPainterJob(render_settings, painter)
            job.start()
            job.waitForFinished()
            
            painter.end()
            
            # Aplicar transformações (rotação, opacidade)
            pixmap = QPixmap.fromImage(image)
            
            # Aplicar rotação
            if rotation_degrees != 0:
                pixmap = self._rotate_pixmap(pixmap, rotation_degrees)
            
            # Aplicar opacidade
            if opacity_percent != 100:
                pixmap = self._apply_opacity_to_pixmap(pixmap, opacity_percent / 100.0)
            
            # Exibir no QLabel preservando o tamanho real do preview.
            frame.setPixmap(pixmap)
            
            return pixmap
            
        except Exception as e:
            print(f"[MapBiomas Fogo] Erro ao renderizar label: {e}")
            import traceback
            traceback.print_exc()
            return None

    def _apply_zoom_to_extent(self, extent, zoom_factor):
        """Aplica zoom à extensão do mapa.
        
        Args:
            extent: QgsRectangle original
            zoom_factor (float): Fator de zoom (0.5 = zoom out 2x, 2.0 = zoom in 2x)
            
        Returns:
            QgsRectangle: Extensão com zoom aplicado
        """
        center_x = extent.center().x()
        center_y = extent.center().y()
        
        width = extent.width() / zoom_factor
        height = extent.height() / zoom_factor
        
        xmin = center_x - width / 2
        xmax = center_x + width / 2
        ymin = center_y - height / 2
        ymax = center_y + height / 2
        
        from qgis.core import QgsRectangle
        return QgsRectangle(xmin, ymin, xmax, ymax)

    def _apply_pan_to_extent(self, extent, pan_x, pan_y, label_width, label_height):
        """Aplica pan (deslocamento) à extensão do mapa.
        
        Args:
            extent: QgsRectangle original
            pan_x (int): Deslocamento X em pixels (positivo = direita)
            pan_y (int): Deslocamento Y em pixels (positivo = para baixo em tela = para norte em mapa)
            label_width (int): Largura do label em pixels
            label_height (int): Altura do label em pixels
            
        Returns:
            QgsRectangle: Extensão com pan aplicado
        """
        # Converter pixels para unidades de mapa
        pixel_to_unit_x = extent.width() / label_width if label_width > 0 else 1
        pixel_to_unit_y = extent.height() / label_height if label_height > 0 else 1
        
        # Pan já vem com Y negado de mouseMoveEvent, então não negar novamente
        pan_units_x = -pan_x * pixel_to_unit_x  # Direita em tela = esquerda em mapa
        pan_units_y = pan_y * pixel_to_unit_y   # Cima em tela = norte em mapa (já negado em mouseMoveEvent)
        
        xmin = extent.xMinimum() + pan_units_x
        xmax = extent.xMaximum() + pan_units_x
        ymin = extent.yMinimum() + pan_units_y
        ymax = extent.yMaximum() + pan_units_y
        
        from qgis.core import QgsRectangle
        return QgsRectangle(xmin, ymin, xmax, ymax)

    def _rotate_pixmap(self, pixmap, angle_degrees):
        """Rotaciona um QPixmap.
        
        Args:
            pixmap (QPixmap): Imagem a rotacionar
            angle_degrees (float): Ângulo em graus (positivo = sentido anti-horário)
            
        Returns:
            QPixmap: Imagem rotacionada
        """
        try:
            # Criar transformação
            transform = QTransform()
            transform.translate(pixmap.width() / 2, pixmap.height() / 2)
            transform.rotate(-angle_degrees)  # Negativo porque QGIS usa convenção oposta
            transform.translate(-pixmap.width() / 2, -pixmap.height() / 2)
            
            # Aplicar transformação
            rotated = pixmap.transformed(transform, _SMOOTH_TRANSFORMATION)
            
            return rotated
        except Exception as e:
            print(f"[MapBiomas Fogo] Erro ao rotacionar pixmap: {e}")
            return pixmap

    def _apply_opacity_to_pixmap(self, pixmap, opacity):
        """Aplica opacidade a um QPixmap.
        
        Args:
            pixmap (QPixmap): Imagem
            opacity (float): Opacidade (0.0 = transparente, 1.0 = opaco)
            
        Returns:
            QPixmap: Imagem com opacidade aplicada
        """
        try:
            # Converter para imagem ARGB32
            image = pixmap.toImage()
            image = image.convertToFormat(_QIMAGE_ARGB32)
            
            # Aplicar opacidade pixel por pixel
            for y in range(image.height()):
                for x in range(image.width()):
                    pixel = image.pixel(x, y)
                    color = QColor(pixel)
                    
                    # Ajustar alpha
                    alpha = int(color.alpha() * opacity)
                    color.setAlpha(alpha)
                    
                    image.setPixel(x, y, color.rgba())
            
            return QPixmap.fromImage(image)
        except Exception as e:
            print(f"[MapBiomas Fogo] Erro ao aplicar opacidade: {e}")
            return pixmap

    def export_frame_to_geopdf(self, output_path, frame, orientation="portrait",
                              dpi=300, include_scale=True, 
                              include_north_arrow=True, include_metadata=True,
                              include_layers=None):
        """Exporta o layout de impressão do QGIS como GeoPDF.
        
        Args:
            output_path (str): Caminho do PDF de saída
            frame (QFrame): QFrame contendo o mapa renderizado (não usado, apenas para compatibilidade)
            orientation (str): "portrait" ou "landscape" (não usado, o layout já define)
            dpi (int): Resolução em DPI (não usado, o layout já define)
            include_scale (bool): Incluir barra de escala (não usado)
            include_north_arrow (bool): Incluir seta norte (não usado)
            include_metadata (bool): Incluir metadados (não usado)
            
        Returns:
            bool: True se sucesso, False se falha
        """
        try:
            if not output_path:
                self.export_failed.emit("Caminho de saida nao informado")
                return False

            self._log("Preparando exportacao GeoPDF")

            output_path = self._validate_output_path(output_path)
            if not output_path:
                self.export_failed.emit("Caminho de saida invalido")
                return False

            extent = self.last_rendered_extent or (self.canvas.extent() if self.canvas else None)
            crs = self.last_rendered_crs or (
                self.canvas.mapSettings().destinationCrs() if self.canvas else None
            )

            if extent is None or crs is None:
                self.export_failed.emit("Extent renderizado nao encontrado")
                return False

            template_path = self._layout_template_path(orientation)
            if not template_path:
                raise FileNotFoundError(
                    f"Template .qpt nao encontrado para orientacao '{orientation}'."
                )

            layout = self._load_layout_from_template(template_path)
            self._apply_page_pixel_size(layout, orientation, dpi)

            # Primeiro aplica a extensao do mapa para centralizar e aproximar no local desejado.
            # Depois redimensiona o item para preencher toda a área da página do PDF,
            # preservando o zoom e evitando que Xmin/Xmax/Ymin/Ymax "estiquem" o mapa.
            self._configure_layout_map(
                layout,
                extent,
                include_layers=include_layers,
                extent_crs=crs,
            )
            self._apply_map_item_geometry(layout, orientation)

            layout_manager = self.project.layoutManager()
            managed_layout = layout_manager.layoutByName(layout.name())
            if managed_layout is not None:
                layout = managed_layout

            self._export_layout_to_pdf_with_fallback(
                layout=layout,
                output_path=output_path,
                dpi=dpi,
                include_metadata=include_metadata,
            )

            self.export_completed.emit(output_path)
            self.progress_updated.emit(f"Coordenadas capturadas. Layout pronto para exportar.")
            return True
            
        except Exception as e:
            error_msg = f"Erro ao preparar exportação: {str(e)}"
            print(f"[MapBiomas Fogo] {error_msg}")
            import traceback
            traceback.print_exc()
            self.export_failed.emit(error_msg)
            return False

