# -*- coding: utf-8 -*-
"""
/***************************************************************************
RAVIDialog
A QGIS plugin
Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
-------------------
begin                : 2024-10-24
git sha              : $Format:%H$
copyright            : (C) 2024 by Caio Arantes
email                : caiosimplicioarante@gmail.com
***************************************************************************/

/***************************************************************************
*                                                                         *
*   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.                                   *
*                                                                         *
***************************************************************************/


"""

# =============================================================================
# STANDARD LIBRARY IMPORTS
# =============================================================================
import os
import sys
import re
import json
import array
import tempfile
import datetime
import platform
import subprocess
import traceback
import zipfile
import webbrowser
import io
from functools import partial
from datetime import timedelta

# =============================================================================
# THIRD-PARTY IMPORTS
# =============================================================================
import requests
import pandas as pd
import numpy as np
import geopandas as gpd
import plotly.express as px
import plotly.graph_objects as go
import urllib.request

# Earth Engine imports
#from ee_s1_ard import S1ARDImageCollection
import ee

# Additional dependencies
import qgis
# Additional scientific computing imports
import processing
from dateutil.relativedelta import relativedelta
from shapely.geometry import shape
from scipy.signal import savgol_filter
from osgeo import gdal

import os
import json
import tempfile
import zipfile
import geopandas as gpd
from shapely.geometry import shape, Polygon, MultiPolygon, GeometryCollection
from shapely.ops import unary_union
import os
import time
import tempfile
import datetime
import requests
import re
import sys
import importlib
import platform
import subprocess
import traceback
import zipfile
import json
import webbrowser
import io
import array
import qgis
import pandas as pd
import numpy as np
import geopandas as gpd
import processing
import plotly.express as px
import plotly.graph_objects as go
import urllib.request
import ee
import shutil
from functools import partial
from datetime import timedelta
from dateutil.relativedelta import relativedelta
from shapely.geometry import shape
from shapely.ops import unary_union
import json
from scipy.signal import savgol_filter
from osgeo import gdal
from qgis.core import QgsDistanceArea, QgsCoordinateReferenceSystem, QgsCoordinateTransform

from qgis.core import (
    QgsMessageLog,
    Qgis,
    QgsWkbTypes,
    QgsVectorLayer,
    QgsVectorFileWriter,
    QgsFeatureRequest,
    QgsProject,
    QgsRasterLayer,
    QgsRasterShader,
    QgsColorRampShader,
    QgsSingleBandPseudoColorRenderer,
    QgsStyle,
    QgsRasterBandStats,
    QgsMapLayer,
    QgsColorRamp,
    QgsLayerTreeLayer,
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransform,
    QgsMultiBandColorRenderer,
    QgsContrastEnhancement,
    QgsProcessingFeedback,
    QgsApplication,
    QgsRectangle,
    QgsFeature,
    QgsGeometry,
    QgsField,
    QgsMapLayerProxyModel
)
from qgis.PyQt.QtGui import QFont, QColor, QDesktopServices
from qgis.PyQt.QtCore import QDate, Qt, QVariant, QSettings, QTimer, QEvent
from qgis.PyQt.QtWidgets import (
    QApplication,
    QMainWindow,
    QLabel,
    QMessageBox,
    QFileDialog,
    QGridLayout,
    QWidget,
    QDialog,
    QVBoxLayout,
    QCheckBox,
    QDialogButtonBox,
    QDateEdit,
    QScrollArea,
    QPushButton,
    QHBoxLayout,
    QToolButton,
    QLineEdit,
    QTextBrowser,
    QSizePolicy,
)


from qgis.PyQt import uic, QtWidgets
from qgis.PyQt.QtCore import Qt, QEvent # This specific import was duplicated multiple times, consolidating here.
from qgis.gui import (
    QgsMapToolEmitPoint,
    QgsRubberBand,
    QgsMapToolCapture,
    QgsMapToolExtent,
    QgsMapToolPan,
)
from qgis.utils import iface
from qgis.PyQt.QtGui import QDesktopServices
from .modules import (
    map_tools,
    nasa_power,
    vegetation_index_info,
    save_utils,
    authentication,
    coordinate_capture,
    datasets_info,
)

from .modules.coordinate_capture import CoordinateCaptureTool

# =============================================================================
# RAVIDialog Class Definition / Definição da Classe RAVIDialog
# =============================================================================

# Load the .ui file based on the language setting / Carrega o arquivo .ui com
# base na configuração de idioma
language = QSettings().value("locale/userLocale", "en")[0:2]

if language == "pt":
    ui_file = os.path.join("ui", "ravi_dialog_base_pt.ui")
else:
    ui_file = os.path.join("ui", "ravi_dialog_base.ui")

FORM_CLASS, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), ui_file))

class RAVIDialog(QDialog, FORM_CLASS):
    def __init__(self, parent=None, iface=None):
        super(RAVIDialog, self).__init__(parent)
        self.setupUi(self)
        self.iface = iface
        
        self.setWindowFlags(
            Qt.WindowType.Window |
            Qt.WindowType.WindowCloseButtonHint |
            Qt.WindowType.WindowMinimizeButtonHint |
            Qt.WindowType.WindowMaximizeButtonHint
        )
        
        self.setModal(False)
        
        # Initialize timer but don't start it yet
        self.focus_timer = QTimer()
        self.focus_timer.timeout.connect(self.check_focus)

        authentication.loadProjectId(self)

        self.inicialize_variables()

        # UI setup and signal connections / Configuração da UI e conexões de sinal
        self.setup_ui()
        self.connect_signals()

        
        # Set default values / Define valores padrão
        self.last_clicked(3)
        self.index_explain()
        self.load_intro()
        # Show only vector layers with polygon geometry (layers that have area)
        self.mMapLayerComboBox.setFilters(QgsMapLayerProxyModel.VectorLayer)
        self.mMapLayerComboBox_2.setFilters(QgsMapLayerProxyModel.VectorLayer)
        self.mMapLayerComboBox_3.setFilters(QgsMapLayerProxyModel.VectorLayer)

        self.tabWidget.setCurrentIndex(0)
        
        # Lock window size immediately after initialization
        # Use QTimer to ensure the window is fully initialized
        QTimer.singleShot(0, lambda: self.resizeEvent("small"))
        

    def load_intro(self):
        # Load intro.html into QTextBrowser

        if self.language == "pt":
            remote_intro_url = "https://raw.githubusercontent.com/caioarantes/ravi-qgis-plugin/refs/heads/main/ui/intro_pt.html"
            try:
                resp = requests.get(remote_intro_url, timeout=8)
                if resp.status_code == 200 and resp.text.strip():
                    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".html", mode="w", encoding="utf-8")
                    tmp.write(resp.text)
                    tmp.close()
                    intro_path = tmp.name
            except Exception:
                pass
                #intro_path = os.path.join(os.path.dirname(__file__), "ui", "intro_pt.html")
        else:
            remote_intro_url = "https://raw.githubusercontent.com/caioarantes/ravi-qgis-plugin/refs/heads/main/ui/intro.html"
            try:
                resp = requests.get(remote_intro_url, timeout=8)
                if resp.status_code == 200 and resp.text.strip():
                    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".html", mode="w", encoding="utf-8")
                    tmp.write(resp.text)
                    tmp.close()
                    intro_path = tmp.name
            except Exception:
                pass
                #intro_path = os.path.join(os.path.dirname(__file__), "ui", "intro.html")
        with open(intro_path, "r", encoding="utf-8") as f:
            html_content = f.read()
        
        self.QTextBrowser.setHtml(html_content)

    def showEvent(self, event):
        """Start timer when dialog is shown"""
        super().showEvent(event)
        
        # Ensure window is locked when first shown
        if not hasattr(self, '_size_locked'):
            self.resizeEvent("small")
            self._size_locked = True
            
        if hasattr(self, 'focus_timer'):
            self.focus_timer.start(100)

    def hideEvent(self, event):
        """Stop timer when dialog is hidden"""
        super().hideEvent(event)
        if hasattr(self, 'focus_timer'):
            try:
                self.focus_timer.stop()
            except RuntimeError:
                pass

    def check_focus(self):
        """Check if we should bring the plugin to front - less aggressive"""
        if not self.isVisible() or self.isMinimized():
            return
            
        # Only act if clicking on map canvas specifically
        active_window = QApplication.activeWindow()
        if (active_window == self.iface.mainWindow() and 
            not QApplication.activeModalWidget() and
            not self.isActiveWindow()):
            
            # Only raise, don't activate to avoid interfering with internal events
            self.raise_()

    def closeEvent(self, event):
        """Handle close event"""
        if hasattr(self, 'focus_timer'):
            try:
                self.focus_timer.stop()
            except RuntimeError:
                pass
        
        # Hide instead of close to preserve state
        self.hide()
        event.ignore()


    def inicialize_variables(self):
        """Initializes variables."""          

        # Determine language for UI elements / Determina o idioma para os
        # elementos da UI
        self.language = QSettings().value("locale/userLocale", "en")[0:2]

        # Initialize variables / Inicializa variáveis
        self.coordinate_capture_tool = None
        self.plot1 = None
        self.autentication = False
        self.folder_set = False
        self.inicio = None
        self.final = None
        self.nuvem = None
        self.vector_path = None
        self.aoi = None
        self.aoi_checked = False
        self.df = None
        self.recorte_datas = None
        self.df_aux = None
        self.selected_dates = []
        self.output_folder = None
        self.df_nasa = None
        self.df_points = None
        self.daily_data = None
        self.selected_aio_layer_path = None
        self.custom_expression_name = ""
        self._cached_area_m2 = None
        self._cached_area_layer_id = None


    def setup_ui(self):
          # Prevent manual resizing
        """Initial UI setup."""
        """Configuração inicial da UI."""
        self.QTextBrowser.setReadOnly(True)  # Prevent editing / Impede a edição
        self.QTextBrowser.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
        self.QTextBrowser.setOpenLinks(False)
        self.QTextBrowser.setOpenExternalLinks(False)
        self.textBrowser_valid_pixels.setReadOnly(True)
        self.textBrowser_valid_pixels.setTextInteractionFlags(
            Qt.TextInteractionFlag.TextBrowserInteraction
        )
        self.textBrowser_valid_pixels.setOpenLinks(False)
        self.textBrowser_valid_pixels.setOpenExternalLinks(False)
        self.project_QgsPasswordLineEdit.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal)

        vegetation_index = [
            "NDVI",
            "EVI",
            'EVI2',
            "SAVI",
            "GNDVI",
            "MSAVI",
            "SFDVI",
            "CIgreen",
            "NDRE",
            "ARVI",
            "NDMI",
            "NBR",
            "SIPI",
            "NDWI",
            "ReCI",
            "MTCI",
            "MCARI",
            "VARI",
            "TVI",
        ]

        self.imagem_unica_indice.addItems(vegetation_index)
        self.indice_composicao.addItems(vegetation_index)
        self.series_indice_2.addItems(vegetation_index)
        self.series_indice_3.addItems(vegetation_index)
        self.series_indice.addItems(vegetation_index)
        self.combo_year.addItems(
            [str(year) for year in range(2017, datetime.datetime.now().year + 1)]
        )

        self.QComboBox_composition.addItems([
            "Real color (RGB)",
            "Red-NIR-Green",
            "NIR-Red-Green",
            "SWIR2-NIR-Green",
            "SWIR1-NIR-SWIR2",
        ])


    def connect_signals(self):
        """Connect UI signals to their respective slots."""
        """Conecta os sinais da UI aos seus respectivos slots."""
        

        self.autenticacao.clicked.connect(lambda: authentication.auth(self))
        self.desautenticacao.clicked.connect(lambda: authentication.auth_clear(self))
        
        # Connect the textChanged signal to the autoSaveProjectId function
        self.project_QgsPasswordLineEdit.textChanged.connect(
            lambda new_text: authentication.autoSaveProjectId(self, new_text)
        )

        self.clear_button_points.clicked.connect(self.remove_all_dots)
        self.QPushButton_features.clicked.connect(self.QPushButton_features_clicked)
        self.tabWidget.currentChanged.connect(self.on_tab_changed)
        self.load_1index.clicked.connect(self.load_index)
        self.load_1index_batch.clicked.connect(self.index_batch_clicked)
        self.load_1rgb.clicked.connect(self.load_rgb)
        self.rgb_batch.clicked.connect(self.rgb_batch_clicked)
        self.composicao.clicked.connect(self.composite_clicked)
        self.load_1index_preview.clicked.connect(lambda: self.load_index(True))
        self.load_1rgb_preview.clicked.connect(lambda: self.load_rgb(True))
        self.composicao_preview.clicked.connect(lambda: self.composite_clicked(True))
        self.clear_raster.clicked.connect(self.clear_all_raster_layers)
        self.hybrid.clicked.connect(map_tools.hybrid_function)
        self.QPushButton_next.clicked.connect(self.next_clicked)
        self.QPushButton_next_2.clicked.connect(self.next_clicked)
        self.QPushButton_next_3.clicked.connect(self.next_clicked)
        self.QPushButton_next_4.clicked.connect(self.next_clicked)
        self.QPushButton_next_5.clicked.connect(self.next_clicked)
        self.QPushButton_next_6.clicked.connect(self.next_clicked)
        self.learn.clicked.connect(self.learn_clicked)
        self.QPushButton_next_7.clicked.connect(self.next_clicked)
        self.QPushButton_next_8.clicked.connect(self.next_clicked)
        self.QPushButton_back.clicked.connect(self.back_clicked)
        self.QPushButton_back_2.clicked.connect(self.back_clicked)
        self.QPushButton_back_3.clicked.connect(self.back_clicked)
        self.QPushButton_back_4.clicked.connect(self.back_clicked)
        self.QPushButton_back_5.clicked.connect(self.back_clicked)
        self.QPushButton_back_6.clicked.connect(self.back_clicked)
        self.QPushButton_back_7.clicked.connect(self.back_clicked)
        self.QPushButton_back_8.clicked.connect(self.back_clicked)
        self.QPushButton_back_9.clicked.connect(self.back_clicked)
        self.QPushButton_easy.clicked.connect(self.easy_clicked)
        self.loadtimeseries.clicked.connect(self.loadtimeseries_clicked)
        self.QPushButton_fast.clicked.connect(self.fast_clicked)
        self.loadtimeseries_2.clicked.connect(self.loadtimeseries_clicked_2)
        self.navegador.clicked.connect(self.open_browser)
        self.navegador_2.clicked.connect(self.open_browser_2)
        self.navegador_3.clicked.connect(self.open_browser_3)
        self.datasrecorte.clicked.connect(self.datasrecorte_clicked)
        self.datasrecorte_2.clicked.connect(self.datasrecorte_clicked)
        self.datasrecorte_3.clicked.connect(self.datasrecorte_clicked)
        self.add_dot.clicked.connect(self.add_dot_from_coordinates)
        self.QPushButton_skip.clicked.connect(lambda: self.tabWidget.setCurrentIndex(9))
        self.salvar.clicked.connect(self.salvar_clicked)
        self.salvar_2.clicked.connect(self.salvar_clicked_2)
        self.salvar_3.clicked.connect(self.salvar_clicked_3)
        self.salvar_nasa.clicked.connect(self.salvar_nasa_clicked)
        #self.build_vector_layer.clicked.connect(self.build_vector_layer_clicked)
        self.drawing.stateChanged.connect(self.drawing_clicked)
        self.QCheckBox_sav_filter.stateChanged.connect(self.plot_timeseries)
        self.filtro_grau.currentIndexChanged.connect(self.plot_timeseries)
        self.window_len.currentIndexChanged.connect(self.plot_timeseries)
        self.checkBox_captureCoordinates.stateChanged.connect(self.toggle_coordinate_capture_tool)
        self.mQgsFileWidget.fileChanged.connect(self.on_file_changed)
        self.radioButton_all.clicked.connect(self.all_clicked)
        self.radioButton_3months.clicked.connect(lambda: self.last_clicked(3))
        self.radioButton_6months.clicked.connect(lambda: self.last_clicked(6))
        self.radioButton_12months.clicked.connect(lambda: self.last_clicked(12))
        self.radioButton_3years.clicked.connect(lambda: self.last_clicked(3 * 12))
        self.radioButton_5years.clicked.connect(lambda: self.last_clicked(5 * 12))
        self.combo_year.currentIndexChanged.connect(self.selected_year_clicked)
        self.mMapLayerComboBox.currentIndexChanged.connect(self.combobox_update)
        self.mMapLayerComboBox_2.currentIndexChanged.connect(self.combobox_update_2)
        self.mMapLayerComboBox_3.currentIndexChanged.connect(self.combobox_update_3)
        self.horizontalSlider_local_pixel_limit.valueChanged.connect(self.update_labels)
        self.horizontalSlider_local_pixel_limit_2.valueChanged.connect(self.update_labels_2)
        self.horizontalSlider_aio_cover.valueChanged.connect(self.update_labels)
        self.horizontalSlider_aio_cover_2.valueChanged.connect(self.update_labels_2)
        self.horizontalSlider_buffer.valueChanged.connect(self.update_labels)
        self.horizontalSlider_buffer_2.valueChanged.connect(self.update_labels_2)
        self.horizontalSlider_total_pixel_limit.valueChanged.connect(self.update_labels)
        self.horizontalSlider_total_pixel_limit_2.valueChanged.connect(self.update_labels_2)
        self.series_indice.currentIndexChanged.connect(self.reload_update)
        self.series_indice_2.currentIndexChanged.connect(self.reload_update2)
        self.series_indice_3.currentIndexChanged.connect(self.reload_update3)
        self.incioedit.dateChanged.connect(self.reload_update)
        self.incioedit_2.dateChanged.connect(self.reload_update2)
        self.finaledit.dateChanged.connect(self.reload_update)
        self.finaledit_2.dateChanged.connect(self.reload_update2)
        self.proxy.clicked.connect(self.open_proxy_dialog)       
        self.nasapower.clicked.connect(self.nasapower_clicked)
        self.clear_nasa.clicked.connect(self.clear_nasa_clicked)
        self.QTextBrowser.anchorClicked.connect(self.open_link)
        self.textBrowser_valid_pixels.anchorClicked.connect(self.open_link)
        self.series_indice.currentIndexChanged.connect(self.index_explain)
        self.QPushButton_sysi.clicked.connect(self.open_sysi)
        self.setup_custom.clicked.connect(self.setup_custom_clicked)
        self.QPushButton_aglgis.clicked.connect(self.open_aglgis)

        self.label_ravi.setText(
            '<a href="https://www.raviqgis.org">www.raviqgis.org</a>'
        )
        self.label_ravi.setTextFormat(Qt.RichText)
        self.label_ravi.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self.label_ravi.setOpenExternalLinks(True)

        self.label_farm.setText(
            '<a href="https://www.farmanalytica.com.br">www.farmanalytica.com.br</a>'
        )
        self.label_farm.setTextFormat(Qt.RichText)
        self.label_farm.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self.label_farm.setOpenExternalLinks(True)



        # Create a list of primary and secondary checkboxes
        self.primary_masks = [
            self.mask,
            self.mask_class0,
            self.mask_class1,
            self.mask_class2,
            self.mask_class3,
            self.mask_class4,
            self.mask_class5,
            self.mask_class6,
            self.mask_class7,
            self.mask_class8,
            self.mask_class9,
            self.mask_class10,
            self.mask_class11,
        ]

        self.secondary_masks = [
            self.mask_2,
            self.mask_class0_2,
            self.mask_class1_2,
            self.mask_class2_2,
            self.mask_class3_2,
            self.mask_class4_2,
            self.mask_class5_2,
            self.mask_class6_2,
            self.mask_class7_2,
            self.mask_class8_2,
            self.mask_class9_2,
            self.mask_class10_2,
            self.mask_class11_2,
        ]

        # Connect primary checkboxes to update_masks
        for primary in self.primary_masks:
            primary.stateChanged.connect(self.update_masks)

        # Connect secondary checkboxes to update_masks_2
        for secondary in self.secondary_masks:
            secondary.stateChanged.connect(self.update_masks_2)

        self.mask.stateChanged.connect(self.mask_warning)
        self.mask_2.stateChanged.connect(self.mask_warning)

    def mask_warning(self):
        if not hasattr(self, "_mask_warning_shown"):
            if self.mask.isChecked() or self.mask_2.isChecked():
                message = (
                    "SCL mask activated. The effectiveness of this feature is "
                    "uncertain and depends on validation. It is recommended to "
                    "compare images without pixel removal to verify if the "
                    "performance is appropriate for your purposes."
                )
                if language == "pt":
                    message = (
                        "Máscara SCL para remover pixels ativada. A eficácia desde recurso é incerta e "
                        "depende de validação. É recomendado a comparação das imagens "
                        "sem remoção de pixel para verificar se o desempenho obtido é "
                        "apropriado para a sua finalidade."
                    )
                self.pop_warning(message)
            self._mask_warning_shown = True

    def update_masks(self):
        # Synchronize secondary checkboxes based on primary ones
        for primary, secondary in zip(self.primary_masks, self.secondary_masks):
            secondary.setChecked(primary.isChecked())

    def update_masks_2(self):
        # Synchronize primary checkboxes based on secondary ones
        for primary, secondary in zip(self.primary_masks, self.secondary_masks):
            primary.setChecked(secondary.isChecked())

    def learn_clicked(self):
        webbrowser.open("https://www.raviqgis.org")

    def index_batch_clicked(self):
        if self.language == "pt":
            if self.pop_warning_3(
                "Download em lote inclui todas as datas selecionadas e pode demorar um pouco...\n"
                "O tempo de processamento depende do tamanho da área de interesse e da quantidade de imagens selecionadas.\n"
                "O QGIS ficará travado até que o processo seja finalizado."
            ) == QMessageBox.StandardButton.Cancel:
                return
        else:
            if self.pop_warning_3(
                "Batch download includes all selected dates and may take a while...\n"
                "Processing time depends on the size of the area of interest and the number of selected images.\n"
                "QGIS will be unresponsive until the process is complete."
            ) == QMessageBox.StandardButton.Cancel:
                return

        if self.recorte_datas is None:
            dates = self.df_aux.date.tolist()
        else:
            dates = self.recorte_datas

        for data in dates:
            index = self.dataunica.findText(str(data))
            self.dataunica.setCurrentIndex(index)
            print(f"Loading index for date: {data}, index: {index}")
            self.load_index()        

    def rgb_batch_clicked(self):
        if self.language == "pt":
            if self.pop_warning_3(
                "Download em lote inclui todas as datas selecionadas e pode demorar um pouco...\n"
                "O tempo de processamento depende do tamanho da área de interesse e da quantidade de imagens selecionadas.\n"
                "O QGIS ficará travado até que o processo seja finalizado."
            ) == QMessageBox.StandardButton.Cancel:
                return
        else:
            if self.pop_warning_3(
                "Batch download includes all selected dates and may take a while...\n"
                "Processing time depends on the size of the area of interest and the number of selected images.\n"
                "QGIS will be unresponsive until the process is complete."
            ) == QMessageBox.StandardButton.Cancel:
                return

        if self.recorte_datas is None:
            dates = self.df_aux.date.tolist()
        else:
            dates = self.recorte_datas

        for data in dates:
            index = self.dataunica.findText(str(data))
            self.dataunica.setCurrentIndex(index)
            print(f"Loading index for date: {data}, index: {index}")
            self.load_rgb()  


    def nasapower_clicked(self):
        """Handles the event when the "NASA POWER" button is clicked."""
        """Manipula o evento quando o botão "NASA POWER" é clicado."""
        # Get the latitude and longitude from the UI / Obtém a latitude e
        # longitude da UI
        self.df_nasa, self.daily_data = nasa_power.open_nasapower(
            str(self.lat),
            str(self.lon),
            self.df_aux.date.tolist()[0],
            self.df_aux.date.tolist()[-1],
        )
        self.plot_timeseries()

    def setup_custom_clicked(self):
        # Load the custom index UI
        if self.language == "pt":
            custom_index_ui_path = os.path.join(os.path.dirname(__file__), "ui", "custom_index_pt.ui")
        else:
            custom_index_ui_path = os.path.join(os.path.dirname(__file__), "ui", "custom_index.ui")
        custom_index_dialog, _ = uic.loadUiType(custom_index_ui_path)

        class CustomIndexDialog(QDialog, custom_index_dialog):
            def __init__(self, parent=None):
                super(CustomIndexDialog, self).__init__(parent)
                self.setupUi(self)
                self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint)
                self.add_custom_index.clicked.connect(self.add_custom_index_clicked)
                
                # Load previously saved expression and name from settings
                settings = QSettings()
                default_expression = "((B1 + B2 + B3 + B4 + B5 + B6 + B7 + B8 + B8A + B9 + B11 + B12) / 12)"
                default_name = "Average"
                
                # Load and set values, use defaults if not found in settings
                self.expressionTextEdit.setPlainText(settings.value("ravi_plugin/custom_expression", default_expression))
                self.expression_nameTextEdit.setPlainText(settings.value("ravi_plugin/custom_expression_name", default_name))
                
                self.expression = None # add a variable
                self.expression_name = None
            def add_custom_index_clicked(self):
                # Retrieve the custom expression and name from the dialog
                expression = self.expressionTextEdit.toPlainText()
                expression_name = self.expression_nameTextEdit.toPlainText()
                    
                self.expression = expression
                self.expression_name = expression_name
                    
                self.accept() # close dialog
        # Create and show the dialog
        dialog = CustomIndexDialog(self)  # Pass RAVIDialog instance as parent
        dialog.exec() # Run and await

        if dialog.result():
        # Store expression and name in settings
            settings = QSettings()
            settings.setValue("ravi_plugin/custom_expression", dialog.expression)
            settings.setValue("ravi_plugin/custom_expression_name",  dialog.expression_name)

            # Add the custom name, avoid repeat custom indexes
            custom_index_name = dialog.expression_name + " (custom)"
            
            # Check if the name already exists
            vegetation_index = [
                "NDVI",
                "EVI",
                'EVI2',
                "SAVI",
                "GNDVI",
                "MSAVI",
                "SFDVI",
                "CIgreen",
                "NDRE",
                "ARVI",
                "NDMI",
                "NBR",
                "SIPI",
                "NDWI",
                "ReCI",
                "MTCI",
                "MCARI",
                "VARI",
                "TVI",
                custom_index_name
                ]
            self.imagem_unica_indice.clear()
            self.indice_composicao.clear()
            self.series_indice_2.clear()
            self.series_indice_3.clear()
            self.series_indice.clear()

            self.imagem_unica_indice.addItems(vegetation_index)
            self.indice_composicao.addItems(vegetation_index)
            self.series_indice_2.addItems(vegetation_index)
            self.series_indice_3.addItems(vegetation_index)
            self.series_indice.addItems(vegetation_index)
            # Add the custom index to all dropdowns
            self.imagem_unica_indice.setCurrentIndex(self.imagem_unica_indice.count() - 1)
            self.indice_composicao.setCurrentIndex(self.indice_composicao.count() - 1)
            self.series_indice_2.setCurrentIndex(self.series_indice_2.count() - 1)
            self.series_indice_3.setCurrentIndex(self.series_indice_3.count() - 1)
            self.series_indice.setCurrentIndex(self.series_indice.count() - 1)
            self.custom_expression = dialog.expression
            self.custom_expression_name = dialog.expression_name
            if self.language == "pt":
                self.pop_warning("Índice personalizado adicionado com sucesso!")
            else:
                self.pop_warning(f"Custom index added successfully!")

    def toggle_coordinate_capture_tool(self, state):
        print("toggle_coordinate_capture_tool called")
        """Toggles the coordinate capture tool based on the checkbox state."""
        """Ativa/desativa a ferramenta de captura de coordenadas com base no
        estado da checkbox."""
        canvas = iface.mapCanvas()
        if state == Qt.Checked:  # Checkbox is checked (active) / Checkbox
            # está marcada (ativa)
            # Deactivate any existing tool before activating the new one /
            # Desativa qualquer ferramenta existente antes de ativar a nova
            if canvas.mapTool():
                canvas.unsetMapTool(canvas.mapTool())
            if not self.coordinate_capture_tool:  # Only create if it doesn't exist / Cria somente se não existir
                self.coordinate_capture_tool = CoordinateCaptureTool(canvas, self)
                print("CoordinateCaptureTool created")
            canvas.setMapTool(self.coordinate_capture_tool)
            print("Coordinate capture tool activated.")
            print(f"self.coordinate_capture_tool: {self.coordinate_capture_tool}")
            self.sentinel2_selected_dates_update()
        else:  # Checkbox is unchecked (inactive) / Checkbox não está marcada
            # (inativa)
            self.deactivate_coordinate_capture_tool()
            print("Coordinate capture tool deactivated.")
            print(f"self.coordinate_capture_tool: {self.coordinate_capture_tool}")

    def activate_coordinate_capture_tool(self):
        canvas = iface.mapCanvas()
        if (
            self.checkBox_captureCoordinates.isChecked()
        ):  # Button is checked (active)
            # Deactivate any existing tool before activating the new one
            if canvas.mapTool():
                canvas.unsetMapTool(canvas.mapTool())
            self.coordinate_capture_tool = CoordinateCaptureTool(canvas, self)
            canvas.setMapTool(self.coordinate_capture_tool)
            print("Coordinate capture tool activated.")
        else:  # Button is unchecked (inactive)
            self.deactivate_coordinate_capture_tool()

            print("Coordinate capture tool deactivated.")

    def deactivate_coordinate_capture_tool(self):
        """Deactivates the coordinate capture map tool."""
        """Desativa a ferramenta de mapa de captura de coordenadas."""
        print("deactivate_coordinate_capture_tool called")
        if (
            hasattr(self, "coordinate_capture_tool")
            and self.coordinate_capture_tool
        ):
            canvas = iface.mapCanvas()
            canvas.unsetMapTool(self.coordinate_capture_tool)

            # self.coordinate_capture_tool = None  # Deactivate the tool
            print("CoordinateCaptureTool deactivated")
            print(f"self.coordinate_capture_tool: {self.coordinate_capture_tool}")
        else:
            print("No coordinate capture tool to deactivate.")


    def remove_all_dots(self):
        """Removes all dots from the map canvas."""
        print("remove_all_dots called")
        canvas = iface.mapCanvas()

        if not self.coordinate_capture_tool:
            print("No coordinate capture tool active.")
            return  # Exit if no tool is active

        rubberBands = self.coordinate_capture_tool.rubber_bands

        if not rubberBands:
            print("No dots to remove.")
            return

        for rubberBand in list(rubberBands):
            try:
                canvas.scene().removeItem(rubberBand)
            except Exception as e:
                print(f"Error removing rubber band: {e}")

        iface.mapCanvas().refresh()  # Refresh the canvas

        # Clear dataframes and web view
        self.df_points = None
        self.df_aux_points = None
        self.webView_2.setHtml("")

        print("All dots removed.")
        CoordinateCaptureTool.DOT_COLORS = []



        # Clear the list in the tool
        self.coordinate_capture_tool.rubber_bands = []

    # =========================================================================
    # Project ID Management / Gerenciamento do ID do Projeto
    # =========================================================================

    def process_coordinates(self, longitude, latitude):
        """Processes the captured coordinates with Earth Engine."""
        """Processa as coordenadas capturadas com o Earth Engine."""
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
        try:
            # 1. Define the point (longitude, latitude) / Define o ponto
            # (longitude, latitude)
            point = ee.Geometry.Point(longitude, latitude)

            # 2. Buffer the point to create a *very* small polygon (e.g., 2
            # meters) / Cria um buffer no ponto para criar um polígono *muito*
            # pequeno (ex: 2 metros)
            aoi = point.buffer(2)

            # Do something with the aoi (e.g., print its information) / Faz
            # algo com a aoi (ex: imprime suas informações)
            print(f"AOI (point) defined")
            name = f"({round(latitude,5)},{round(longitude,5)})"
            print("point:", name)
            # print(self.point_calculate_timeseries(aoi, name))

            if self.df_points is None:
                self.df_points = self.df_aux[["date", "AOI_average"]]
                self.df_points["date"] = pd.to_datetime(self.df_points["date"])

            new_df = self.point_calculate_timeseries(aoi, name)
            new_df["date"] = pd.to_datetime(new_df["date"])
            print(new_df)
            self.df_points = pd.merge(
                self.df_points, new_df, on="date", how="outer"
            )

            self.df_ajust_points()
            self.plot_timeseries_points()
        except Exception as e:
            QApplication.restoreOverrideCursor()
            print(f"Error processing with Earth Engine: {e}")
            QMessageBox.critical(
                self,
                "Earth Engine Error",
                f"An error occurred: {e}",
            )
        QApplication.restoreOverrideCursor()

    def plot_timeseries_points(self):
        print("Plotting time series for points...")

        df = self.df_aux_points
        print(df.shape)

        # Melt the dataframe to have a long format
        df_melted = df.melt(
            id_vars="date",
            var_name="Points (lat, long)",
            value_name=self.series_indice.currentText(),
        )

        # Get unique point labels
        unique_points = df_melted["Points (lat, long)"].unique()
        
        # Create color mapping
        color_map = {}
        
        # Use blue for the first point
        if len(unique_points) > 0:
            color_map[unique_points[0]] = "blue"
        
        # Use the captured colors for the remaining points
        for i, point in enumerate(unique_points[1:], 1):
            if i-1 < len(CoordinateCaptureTool.DOT_COLORS):
                # Convert QColor to hex string
                qcolor = CoordinateCaptureTool.DOT_COLORS[i-1]
                hex_color = f"#{qcolor.red():02x}{qcolor.green():02x}{qcolor.blue():02x}"
                color_map[point] = hex_color
        
        print("Color mapping:")
        print(color_map)

        # Create the line plot with custom colors
        fig = px.line(
            df_melted,
            x="date",
            y=self.series_indice.currentText(),
            color="Points (lat, long)",
            line_dash="Points (lat, long)",
            title=f"Time Series - {self.series_indice.currentText()} - Points",
            color_discrete_map=color_map
        )
        
        fig.update_layout(
            yaxis_title=self.series_indice.currentText(),
            title=f"Time Series - {self.series_indice.currentText()} - Points",
            xaxis_title=None,  # Remove x-axis label
        )
        
        self.fig_3 = fig
        #fig.show()

        self.webView_2.setHtml(
            fig.to_html(include_plotlyjs="cdn", config=self.config)
        )
        
        print("Feature info calculated and plotted.")
        
        # print('colors:')
        # print(CoordinateCaptureTool.DOT_COLORS)

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

    def load_fields(self, id_column=None):
        # Get the system's temporary directory / Obtém o diretório temporário do
        # sistema
        temp_folder = tempfile.gettempdir()
        print(f"Temporary folder: {temp_folder}")

        # Input shapefile path / Caminho do shapefile de entrada
        input_path = self.mMapLayerComboBox.currentLayer().source()

        # Open the layer / Abre a camada
        layer = QgsVectorLayer(input_path, "Polygons", "ogr")
        if not layer.isValid():
            print("Failed to load the layer.")
            return

        # Populate the attributes_id combobox with unique field names /
        # Popula a combobox attributes_id com nomes de campos únicos
        unique_fields = [field.name() for field in layer.fields()]

        self.attributes_id.clear()
        self.attributes_id.addItems(sorted(unique_fields))

    def QPushButton_features_clicked(self):
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
        self.sentinel2_selected_dates_update()
        feature_info = self.split_features(self.attributes_id.currentText())
        self.feature_info(feature_info)
        self.df_ajust_features()
        self.plot_timeseries_features()
        QApplication.restoreOverrideCursor()

    def split_features(self, id_column=None):
        """
        Splits each feature from the selected vector layer into individual
        shapefiles using memory layers and avoids creating multiple memory layers
        inside the loop.
        """
        # Get the system's temporary directory
        temp_folder = tempfile.gettempdir()
        print(f"Temporary folder: {temp_folder}")

        # Input shapefile path
        input_path = self.mMapLayerComboBox.currentLayer().source()

        # Open the layer
        layer = QgsVectorLayer(input_path, "Polygons", "ogr")

        # Validate id_column if provided
        if id_column and id_column not in [field.name() for field in layer.fields()]:
            raise ValueError(f"Column '{id_column}' not found in layer attributes")

        feature_count = layer.featureCount()
        print(f"Total features to process: {feature_count}")

        feature_info = {}

        # Create a single memory layer outside the loop
        memory_layer = QgsVectorLayer(
            f"Polygon?crs={layer.crs().authid()}", "split_features_memory_layer", "memory"
        )
        memory_layer.startEditing()
        memory_provider = memory_layer.dataProvider()
        memory_provider.addAttributes(layer.fields())
        memory_layer.updateFields()

        try:
            for feature in layer.getFeatures():
                # Get feature identifier
                if id_column:
                    feature_identifier = str(feature[id_column])
                    feature_identifier = "".join(
                        c for c in feature_identifier if c.isalnum() or c in ("-", "_")
                    )
                else:
                    feature_identifier = str(feature.id())

                output_path = os.path.join(
                    temp_folder, f"feature_{feature_identifier}.shp"
                )

                # Clear existing features from the memory layer
                memory_provider.truncate()
                memory_layer.commitChanges()
                memory_layer.startEditing()

                # Add the current feature to the memory layer
                memory_provider.addFeature(feature)
                memory_layer.commitChanges()

                try:
                    # Save the memory layer as a new shapefile
                    save_options = QgsVectorFileWriter.SaveVectorOptions()
                    save_options.driverName = "ESRI Shapefile"

                    error = QgsVectorFileWriter.writeAsVectorFormat(
                        memory_layer,
                        output_path,
                        "UTF-8",
                        layer.crs(),
                        "ESRI Shapefile",
                    )

                    if error[0] != QgsVectorFileWriter.NoError:
                        print(f"Error saving feature {feature_identifier}: {error}")
                    else:
                        print(
                            f"Feature {feature_identifier} saved to: {output_path}"
                        )
                        feature_info[feature_identifier] = {
                            "path": output_path,
                            "attributes": {
                                field.name(): feature[field.name()]
                                for field in layer.fields()
                            },
                        }

                except Exception as e:
                    print(f"Error saving feature {feature_identifier}: {str(e)}")
                finally:
                    pass # No need to clean memory layer inside the loop

        finally:
            memory_layer.commitChanges()
            del memory_layer  # Clean up memory layer

        print("\nFinished processing all features.")
        return feature_info

    def feature_info(self, feature_info):
        features = []

        for feature_id, info in feature_info.items():
            # Get the name from attributes, handle cases where 'NAME' might not
            # exist / Obtém o nome dos atributos, lida com casos onde 'NAME'
            # pode não existir
            print(f"Feature ID: {self.attributes_id.currentText()}")
            name = info["attributes"].get(
                self.attributes_id.currentText(), "N/A"
            )

            path = info["path"]

            self.aoi_feature = self.load_vector_feature(path)
            df_feature = self.feature_calculate_timeseries(name)
            df_feature["date"] = pd.to_datetime(df_feature["date"])
            features.append(df_feature)

        # Merge all dataframes in the features list horizontally on the 'date'
        # key / Mescla todos os dataframes na lista de feições horizontalmente
        # na chave 'date'

        df_aux = self.df_aux[["date", "AOI_average"]]
        df_aux.date = pd.to_datetime(df_aux.date)

        features.append(df_aux)
        merged_df = features[0]
        for df in features[1:]:
            merged_df = pd.merge(merged_df, df, on="date", how="outer")

        print(f"Merged features calculated for {len(features)} features.")
        df = merged_df.copy()

        df["date"] = pd.to_datetime(df["date"])

        self.df_features = df

    def plot_timeseries_features(self):
        df = self.df_aux_features

        # Melt the dataframe to have a long format / Transforma o dataframe para
        # um formato longo
        df_melted = df.melt(
            id_vars="date",
            var_name="Polygon",
            value_name=self.series_indice.currentText(),
        )

        # Create the line plot with varied color and line dash / Cria o gráfico
        # de linha com cor e traço variados
        fig = px.line(
            df_melted,
            x="date",
            y=self.series_indice.currentText(),
            color="Polygon",
            line_dash="Polygon",
            title=f"Time Series - {self.series_indice.currentText()} - {self.mMapLayerComboBox.currentText()}",
        )
        fig.update_layout(
            yaxis_title=self.series_indice.currentText(),
            title=f"Time Series - {self.series_indice.currentText()} - {self.mMapLayerComboBox.currentText()}",
            xaxis_title=None,  # Remove x-axis label
        )
        self.fig_2 = fig

        self.webView_3.setHtml(
            fig.to_html(include_plotlyjs="cdn", config=self.config)
        )
        print("Feature info calculated and plotted.")

    def reload_update2(self):
        self.finaledit.setDate(self.finaledit_2.date())
        self.incioedit.setDate(self.incioedit_2.date())
        self.series_indice.setCurrentIndex(self.series_indice_2.currentIndex())

    def reload_update3(self):
        self.finaledit.setDate(self.finaledit_2.date())
        self.incioedit.setDate(self.incioedit_2.date())
        self.series_indice.setCurrentIndex(self.series_indice_3.currentIndex())

    def reload_update(self):
        self.finaledit_2.setDate(self.finaledit.date())
        self.incioedit_2.setDate(self.incioedit.date())
        self.series_indice_2.setCurrentIndex(self.series_indice.currentIndex())
        self.series_indice_3.setCurrentIndex(self.series_indice.currentIndex())

    def clear_nasa_clicked(self):
        """Clears the NASA data and updates the timeseries plot."""
        """Limpa os dados da NASA e atualiza o gráfico de séries temporais."""
        self.df_nasa = None
        self.daily_data = None
        self.plot_timeseries()

    def update_labels(self):
        """Updates the text of several labels based on the values of horizontal
        sliders."""
        """Atualiza o texto de vários rótulos com base nos valores dos
        sliders horizontais."""
        self.label_cloud_aoi.setText(f"{self.horizontalSlider_local_pixel_limit.value()}%")
        self.label_cloud_aoi_2.setText(f"{self.horizontalSlider_local_pixel_limit.value()}%")
        self.horizontalSlider_local_pixel_limit_2.setValue(self.horizontalSlider_local_pixel_limit.value())

        self.label_coverage.setText(f"{self.horizontalSlider_aio_cover.value()}%")
        self.label_coverage_2.setText(f"{self.horizontalSlider_aio_cover.value()}%")
        self.horizontalSlider_aio_cover_2.setValue(self.horizontalSlider_aio_cover.value())

        self.label_cloud.setText(f"{self.horizontalSlider_total_pixel_limit.value()}%")
        self.label_cloud_2.setText(f"{self.horizontalSlider_total_pixel_limit.value()}%")
        self.horizontalSlider_total_pixel_limit_2.setValue(self.horizontalSlider_total_pixel_limit.value())

        self.label_buffer.setText(f"{self.horizontalSlider_buffer.value()}m")
        self.label_buffer_2.setText(f"{self.horizontalSlider_buffer.value()}m")
        self.horizontalSlider_buffer_2.setValue(self.horizontalSlider_buffer.value())

    def update_labels_2(self):
        """Updates the text of several labels based on the values of horizontal
        sliders."""
        """Atualiza o texto de vários rótulos com base nos valores dos
        sliders horizontais."""
        self.label_cloud_aoi.setText(f"{self.horizontalSlider_local_pixel_limit_2.value()}%")
        self.label_cloud_aoi_2.setText(f"{self.horizontalSlider_local_pixel_limit_2.value()}%")
        self.horizontalSlider_local_pixel_limit.setValue(self.horizontalSlider_local_pixel_limit_2.value())

        self.label_coverage.setText(f"{self.horizontalSlider_aio_cover_2.value()}%")
        self.label_coverage_2.setText(f"{self.horizontalSlider_aio_cover_2.value()}%")
        self.horizontalSlider_aio_cover.setValue(self.horizontalSlider_aio_cover_2.value())

        self.label_cloud.setText(f"{self.horizontalSlider_total_pixel_limit_2.value()}%")
        self.label_cloud_2.setText(f"{self.horizontalSlider_total_pixel_limit_2.value()}%")
        self.horizontalSlider_total_pixel_limit.setValue(self.horizontalSlider_total_pixel_limit_2.value())

        self.label_buffer.setText(f"{self.horizontalSlider_buffer_2.value()}m")
        self.label_buffer_2.setText(f"{self.horizontalSlider_buffer_2.value()}m")
        self.horizontalSlider_buffer.setValue(self.horizontalSlider_buffer_2.value())

    def custom_filter_clicked(self):
        """Slot method to handle the custom filter checkbox click event."""
        """Método slot para lidar com o evento de clique da checkbox de filtro
        personalizado."""
        if self.customfilter.isChecked():
            self.horizontalSlider.setEnabled(True)
        else:
            self.horizontalSlider.setEnabled(False)

    def open_link(self, url):
        """Open the clicked link in the default web browser."""
        """Abre o link clicado no navegador padrão."""
        print(f"Opening URL: {url.toString()}")
        if not url or not url.isValid():
            return
        if not QDesktopServices.openUrl(url):
            webbrowser.open(url.toString())

    def last_clicked(self, months):
        today = datetime.datetime.today().strftime("%Y-%m-%d")
        one_month_ago = (datetime.datetime.today() - relativedelta(months=months)).strftime(
            "%Y-%m-%d"
        )
        self.finaledit.setDate(QDate.fromString(today, "yyyy-MM-dd"))
        self.incioedit.setDate(QDate.fromString(one_month_ago, "yyyy-MM-dd"))

    def all_clicked(self):
        today = datetime.datetime.today().strftime("%Y-%m-%d")
        since = "2017-03-28"
        self.finaledit.setDate(QDate.fromString(today, "yyyy-MM-dd"))
        self.incioedit.setDate(QDate.fromString(since, "yyyy-MM-dd"))

    def selected_year_clicked(self):
        """Sets the date range in the UI to the selected year from the combo
        box."""
        """Define o intervalo de datas na UI para o ano selecionado na combo
        box."""
        year = self.combo_year.currentText()
        start = f"{year}-01-01"
        end = f"{year}-12-31"
        self.incioedit.setDate(QDate.fromString(start, "yyyy-MM-dd"))
        self.finaledit.setDate(QDate.fromString(end, "yyyy-MM-dd"))

    def drawing_clicked(self):
        print("Drawing clicked")
        if self.drawing.isChecked():
            self.vector_builder()
        else:
            # Deactivate the extent tool if the checkbox is unchecked
            iface.mapCanvas().setMapTool(QgsMapToolPan(iface.mapCanvas()))

    def vector_builder(self):
        """Handles the event when the "Build Vector Layer" button is clicked."""
        if self.output_folder is None:
            self.pop_warning("Please select an output folder first.")
            return

        # existing_layers = QgsProject.instance().mapLayers().values()
        # layer_names = [layer.name() for layer in existing_layers]
        # if "Google Hybrid" not in layer_names:
        #     if self.language == "pt":
        #         self.pop_warning(
        #             "Por favor, carregue a camada do Google Maps primeiro."
        #         )
        #     else:
        #         # Show a warning message in English if the layer is not loaded
        #         self.pop_warning("Please load the Google Maps layer first.")
        #     self.drawing.setChecked(False)
        #     return

        # Activate the extent drawing tool
        self.extent_tool = QgsMapToolExtent(iface.mapCanvas())
        self.extent_tool.extentChanged.connect(self.process_extent)
        iface.mapCanvas().setMapTool(self.extent_tool)
        print("Extent tool activated.")

    def process_extent(self, extent: QgsRectangle):
        """Process the drawn extent and save it as a vector layer."""
        # Transform the extent to EPSG:4326
        canvas_crs = iface.mapCanvas().mapSettings().destinationCrs()
        target_crs = QgsCoordinateReferenceSystem("EPSG:4326")
        transform = QgsCoordinateTransform(canvas_crs, target_crs, QgsProject.instance())
        extent_4326 = transform.transformBoundingBox(extent)

        # Create a vector layer in EPSG:4326
        layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "drawn_extent", "memory")
        pr = layer.dataProvider()

        # Add fields
        pr.addAttributes([QgsField("id", QVariant.Int)])
        layer.updateFields()

        # Create a feature with the transformed extent
        feature = QgsFeature()
        geometry = QgsGeometry.fromRect(extent_4326)
        feature.setGeometry(geometry)
        feature.setAttributes([1])
        pr.addFeature(feature)

        # Update layer extents
        layer.updateExtents()

        # Generate unique filename and layer name with subdirectory
        shp_path, shp_name = self.get_subdirectory_filename("drawn_extent")
        print(f"Shapefile path: {shp_path}")
        print(f"Shapefile name: {shp_name}")

        # Write the layer to disk
        save_options = QgsVectorFileWriter.SaveVectorOptions()
        save_options.driverName = "ESRI Shapefile"
        save_options.fileEncoding = "UTF-8"

        QgsVectorFileWriter.writeAsVectorFormat(
            layer, shp_path, save_options
        )

        # Load the shapefile
        loaded_layer = QgsVectorLayer(shp_path, shp_name, "ogr")
        if loaded_layer.isValid():
            # Verify CRS
            if loaded_layer.crs().authid() != "EPSG:4326":
                print("Warning: Layer CRS is not EPSG:4326, reprojecting...")
                # Reproject if necessary
                params = {
                    'INPUT': loaded_layer,
                    'TARGET_CRS': target_crs,
                    'OUTPUT': 'memory:'
                }
                reprojected = processing.run("native:reprojectlayer", params)['OUTPUT']
                loaded_layer = reprojected

            QgsProject.instance().addMapLayer(loaded_layer)
            print(f"Layer added successfully with CRS: {loaded_layer.crs().authid()}")

    
    def salvar_clicked(self):
        """Handles the event when the save button is clicked."""
        """Manipula o evento quando o botão salvar é clicado."""
        df = self.df_aux
        try:
            df = df[["date", "AOI_average", "savitzky_golay_filtered", "image_id"]]
        except:
            df = df[["date", "AOI_average", "image_id"]]

        name = (
            f"{self.series_indice.currentText()}_{self.mMapLayerComboBox.currentText()}_time_series.csv"
        )
        save_utils.save(df, name, self)

    def salvar_clicked_2(self):
        """Handles the event when the save button is clicked."""
        """Manipula o evento quando o botão salvar é clicado."""
        df = self.df_features
        name = (
            f"{self.series_indice.currentText()}_{self.mMapLayerComboBox.currentText()}_time_series_features.csv"
        )
        save_utils.save(df, name, self)

    def salvar_clicked_3(self):
        """Handles the event when the save button is clicked."""
        """Manipula o evento quando o botão salvar é clicado."""
        df = self.df_points
        name = (
            f"{self.series_indice.currentText()}_{self.mMapLayerComboBox.currentText()}_time_series_points.csv"
        )
        save_utils.save(df, name, self)

    def salvar_nasa_clicked(self):
        if self.df_nasa is None:
            self.pop_warning("No NASA data to save.")
            return
        name = (
            f"nasa_power_climate_{self.mMapLayerComboBox.currentText()}.csv"
        )

        save_utils.save(self.daily_data, name, self)

    def datasrecorte_clicked(self):
        """Opens a dialog for selecting specific dates for the time series."""
        """Abre um diálogo para selecionar datas específicas para a série
        temporal."""
        dialog = QDialog(self)
        dialog.setWindowTitle("Date Selection for Time Series")
        dialog.setGeometry(100, 100, 400, 500)

        layout = QVBoxLayout(dialog)

        # Scroll Area for Checkboxes / Área de Scroll para Checkboxes
        scroll_area = QScrollArea(dialog)
        scroll_area.setWidgetResizable(True)
        scroll_content = QWidget()
        scroll_layout = QVBoxLayout(scroll_content)

        self.checkboxes = []
        self.group_checkboxes = {}  # Store month group checkboxes
        self.year_checkboxes = {}  # Store year checkboxes
        self.group_widgets = {}  # Store month group content widgets

        # Group Dates by Year and Month / Agrupa Datas por Ano e Mês
        self.df["date"] = pd.to_datetime(
            self.df["date"]
        )  # Ensure dates are datetime objects
        grouped = self.df.groupby([self.df["date"].dt.year, self.df["date"].dt.month])

        # Organize by Year / Organiza por Ano
        years = self.df["date"].dt.year.unique()
        for year in sorted(years):
            # Create a year-level widget / Cria um widget de nível de ano
            year_widget = QWidget(dialog)
            year_layout = QVBoxLayout(year_widget)

            # Year-level checkbox (above all content for the year) / Checkbox
            # de nível de ano (acima de todo o conteúdo para o ano)
            year_checkbox = QCheckBox(f"Select All in {year}", dialog)
            year_checkbox.setChecked(
                True
                if self.recorte_datas is None
                else all(
                    str(date.date()) in self.recorte_datas
                    for date in self.df[self.df["date"].dt.year == year]["date"]
                )
            )
            year_checkbox.stateChanged.connect(
                lambda state, yr=year: self.toggle_year_checkboxes(yr, state)
            )
            scroll_layout.addWidget(year_checkbox)
            self.year_checkboxes[year] = year_checkbox

            # Indented content for the year / Conteúdo indentado para o ano
            year_content_widget = QWidget(dialog)
            year_content_layout = QVBoxLayout(year_content_widget)
            year_content_layout.setContentsMargins(
                20, 0, 0, 0
            )  # Add indentation for year content
            scroll_layout.addWidget(year_content_widget)

            # Add months under each year / Adiciona meses sob cada ano
            for (group_year, month), group in grouped:
                if group_year != year:
                    continue

                # Create a month-level widget / Cria um widget de nível de mês
                group_label = f"{group_year}-{month:02d}"
                month_widget = QWidget(dialog)
                month_layout = QVBoxLayout(month_widget)

                # Month toggle button / Botão de alternância do mês
                month_toggle_button = QToolButton(dialog)
                month_toggle_button.setText(f"▶ {group_label}")
                month_toggle_button.setCheckable(True)
                month_toggle_button.setChecked(True)
                month_toggle_button.setStyleSheet("text-align: left;")
                month_toggle_button.toggled.connect(
                    lambda checked, grp=month_widget, btn=month_toggle_button, lbl=group_label: self.toggle_group_visibility(
                        grp, btn, lbl
                    )
                )

                # Month-level checkbox / Checkbox de nível de mês
                group_checkbox = QCheckBox(f"Select All in {group_label}", dialog)
                group_checkbox.setChecked(
                    True
                    if self.recorte_datas is None
                    else all(
                        str(date.date()) in self.recorte_datas for date in group["date"]
                    )
                )
                group_checkbox.stateChanged.connect(
                    lambda state, grp=group_label: self.toggle_group_checkboxes(
                        grp, state
                    )
                )
                month_layout.addWidget(group_checkbox)
                self.group_checkboxes[group_label] = group_checkbox

                # Add individual checkboxes with further indentation / Adiciona
                # checkboxes individuais com mais indentação
                for date in group["date"]:
                    date_str = str(date.date())
                    checkbox = QCheckBox(date_str, dialog)
                    checkbox.setChecked(
                        True
                        if self.recorte_datas is None
                        else date_str in self.recorte_datas
                    )
                    month_layout.addWidget(checkbox)
                    checkbox.setContentsMargins(
                        20, 0, 0, 0
                    )  # Further indentation for dates
                    self.checkboxes.append((checkbox, group_label, group_year))

                # Add month layout to the year content layout / Adiciona o layout
                # do mês ao layout do conteúdo do ano
                year_content_layout.addWidget(month_toggle_button)
                year_content_layout.addWidget(month_widget)
                self.group_widgets[group_label] = month_widget

        scroll_content.setLayout(scroll_layout)
        scroll_area.setWidget(scroll_content)
        layout.addWidget(scroll_area)

        # Buttons / Botões
        # Use QDialogButtonBox.StandardButton for Qt6 compatibility
        button_box = QDialogButtonBox(
            QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, dialog
        )
        button_layout = QHBoxLayout()
        apply_button = QPushButton("Apply", dialog)
        select_button = QPushButton("Select All", dialog)
        deselect_button = QPushButton("Deselect All", dialog)
        button_layout.addWidget(apply_button)
        button_layout.addWidget(select_button)
        button_layout.addWidget(deselect_button)
        layout.addLayout(button_layout)
        layout.addWidget(button_box)

        # Signal Connections / Conexões de Sinal
        button_box.accepted.connect(dialog.accept)
        button_box.rejected.connect(dialog.reject)
        apply_button.clicked.connect(
            self.apply_changes
        )  # Apply without closing / Aplica sem fechar
        select_button.clicked.connect(self.select_all_checkboxes)
        deselect_button.clicked.connect(self.deselect_all_checkboxes)

        if dialog.exec() == QDialog.DialogCode.Accepted:
            self.apply_changes()  # Ensure changes are applied before closing
        else:
            print("Time series dialog canceled. No changes made.")

    def apply_changes(self):
        """
        Apply changes to the selected dates without closing the dialog.

        This method updates the selected dates based on the checked checkboxes
        and adjusts the time series plot accordingly.
        """
        """
        Aplica as mudanças nas datas selecionadas sem fechar o diálogo.

        Este método atualiza as datas selecionadas com base nas checkboxes
        marcadas e ajusta o gráfico de séries temporais de acordo.
        """
        self.selected_dates = [
            cb.text() for cb, _, _ in self.checkboxes if cb.isChecked()
        ]
        self.recorte_datas = self.selected_dates
        print(f"Selected dates for time series (applied): {self.recorte_datas}")
        self.df_ajust()
        self.plot_timeseries()

        try:
            self.df_ajust_features()
            self.plot_timeseries_features()
            print("Feature info updated and ploted).")
        except:
            pass

        try:
            self.df_ajust_points()
            self.plot_timeseries_points()
            print("Points info updated and ploted).")
        except:
            pass

    def toggle_group_visibility(self, group_widget, toggle_button, group_label):
        """
        Toggle the visibility of a group widget.

        Args:
            group_widget (QWidget): The widget representing the group.
            toggle_button (QToolButton): The button used to toggle the
                visibility.
            group_label (str): The label of the group.
        """
        """
        Alterna a visibilidade de um widget de grupo.

        Args:
            group_widget (QWidget): O widget representando o grupo.
            toggle_button (QToolButton): O botão usado para alternar a
                visibilidade.
            group_label (str): O rótulo do grupo.
        """
        group_widget.setVisible(toggle_button.isChecked())
        toggle_button.setText(
            f"▶ {group_label}"
            if not toggle_button.isChecked()
            else f"▼ {group_label}"
        )

    def toggle_group_checkboxes(self, group_label, state):
        """
        Toggle all checkboxes in a month group.

        Args:
            group_label (str): The label of the group.
            state (int): The state of the checkbox (checked or unchecked).
        """
        """
        Alterna todas as checkboxes em um grupo de mês.

        Args:
            group_label (str): O rótulo do grupo.
            state (int): O estado da checkbox (marcado ou desmarcado).
        """
        for checkbox, group, _ in self.checkboxes:
            if group == group_label:
                checkbox.setChecked(state == Qt.Checked)

    def toggle_year_checkboxes(self, year, state):
        """
        Toggle all checkboxes in a year group.

        Args:
            year (int): The year to toggle.
            state (int): The state of the checkbox (checked or unchecked).
        """
        """
        Alterna todas as checkboxes em um grupo de ano.

        Args:
            year (int): O ano a ser alternado.
            state (int): O estado da checkbox (marcado ou desmarcado).
        """
        for checkbox, _, group_year in self.checkboxes:
            if group_year == year:
                checkbox.setChecked(state == Qt.Checked)
        for group_label, group_checkbox in self.group_checkboxes.items():
            if group_label.startswith(str(year)):
                group_checkbox.setChecked(state == Qt.Checked)

    def select_all_checkboxes(self):
        """Select all checkboxes."""
        """Seleciona todas as checkboxes."""
        for checkbox, _, _ in self.checkboxes:
            checkbox.setChecked(True)
        for group_checkbox in self.group_checkboxes.values():
            group_checkbox.setChecked(True)
        for year_checkbox in self.year_checkboxes.values():
            year_checkbox.setChecked(True)

    def deselect_all_checkboxes(self):
        """Deselect all checkboxes."""
        """Desmarca todas as checkboxes."""
        for checkbox, _, _ in self.checkboxes:
            checkbox.setChecked(False)
        for group_checkbox in self.group_checkboxes.values():
            group_checkbox.setChecked(False)
        for year_checkbox in self.year_checkboxes.values():
            year_checkbox.setChecked(False)

    def centralizar(self):
        """
        Centers the window on the screen without changing the screen the dialog
        is on.

        This method calculates the geometry of the window frame and moves it to
        the center of the available screen space on the current screen.
        """
        """
        Centraliza a janela na tela sem alterar a tela em que o diálogo está.

        Este método calcula a geometria do quadro da janela e a move para o
        centro do espaço de tela disponível na tela atual.
        """
        # 1. Get the current geometry of the window frame.
        qtRectangle = self.frameGeometry()
        # 2. Determine the center point of the available screen space on the
        # current screen.
        # screen = QDesktopWidget().screenNumber(self)
        # centerPoint = QDesktopWidget().availableGeometry(screen).center()
        # Replacement for QDesktopWidget using QApplication.primaryScreen()
        screen = QApplication.screenAt(self.frameGeometry().center())
        if screen is None:
            screen = QApplication.primaryScreen()
        centerPoint = screen.availableGeometry().center()
        # 3. Move the center of the window frame to the center point of the
        # screen.
        qtRectangle.moveCenter(centerPoint)
        # 4. Move the window to the new top-left position.
        self.move(qtRectangle.topLeft())

    def resizeEvent(self, size):
        # Remove size constraints first
        self.setMinimumSize(0, 0)
        self.setMaximumSize(16777215, 16777215)

        if size == "small":
            self.resize(765, 371)
            # Lock to small size - this prevents resizing
            self.setFixedSize(self.width(), self.height())
        elif size == "big":
            self.resize(1145, 620)
            # Lock to big size - this prevents resizing
            self.setFixedSize(self.width(), self.height())

    def on_tab_changed(self, index):
        # CRITICAL: Get the recursion guard flag safely.
        # If '_programmatic_tab_change' hasn't been set, default to False.
        _programmatic_tab_change = getattr(self, '_programmatic_tab_change', False)

        if _programmatic_tab_change:
            return

        if index == 2:
            self.combobox_update()
        # Helper function for safe programmatic tab changes
        # This also needs to set the attribute on 'self' if it's missing,
        # then manage it for the duration of the set.
        def _safe_set_current_index(target_index):
            # Ensure self.tabWidget exists before proceeding
            _tabWidget = getattr(self, 'tabWidget', None)
            if not _tabWidget:
                print("Warning: tabWidget not found, cannot set current index safely.")
                return False

            if _tabWidget.currentIndex() != target_index:
                # Set the flag on 'self'
                setattr(self, '_programmatic_tab_change', True)
                try:
                    _tabWidget.setCurrentIndex(target_index)
                finally:
                    # Reset the flag on 'self'
                    setattr(self, '_programmatic_tab_change', False)
                return True # Indicates tab was changed
            return False # Indicates no change was necessary or possible

        # --- Authentication Check ---
        # Get 'autentication' safely; if not set, default to False
        autentication = getattr(self, 'autentication', False)
        if not autentication:
            if index != 0:
                if self.language == "pt":
                    QMessageBox.warning(self, "Acesso Negado", "Por favor, autentique-se primeiro!")
                else:
                    QMessageBox.warning(self, "Access Denied", "Please authenticate first!")
                if _safe_set_current_index(0):
                    return # Stop processing this event as tab was reverted

        # --- UI Resizing Logic ---
        # Get 'resizeEvent' safely
        _resizeEvent = getattr(self, 'resizeEvent', None)
        if index < 9:
            if _resizeEvent and callable(_resizeEvent):
                _resizeEvent("small")
            # else: print("Warning: resizeEvent method not found or not callable for 'small'.")
        elif index >= 9:
            if _resizeEvent and callable(_resizeEvent):
                _resizeEvent("big")
            # else: print("Warning: resizeEvent method not found or not callable for 'big'.")

            # Centralization logic
            _centralized = getattr(self, '_centralized', False)
            if not _centralized:
                _centralizar = getattr(self, 'centralizar', None)
                if _centralizar and callable(_centralizar):
                    _centralizar()
                    setattr(self, '_centralized', True) # Update the attribute on 'self'
                else:
                    print("Warning: centralizar method not found or not callable.")

        # --- Tab 10: Data Frame (DF) Check ---
        # Get 'df' safely; if not set, default to None
        _df = getattr(self, 'df', None)
        if index >= 10:
            if _df is None:
                if self.language == "pt":
                    QMessageBox.warning(self, "Dados Ausentes", "Por favor, processe os dados nas abas anteriores primeiro.")
                else:
                    QMessageBox.warning(self, "Data Missing", "Please process data on previous tabs first.")
                if _safe_set_current_index(9):
                    return

        # --- Tab 1: Load Path Suggestion ---
        # Get 'path_suggestion_loaded' safely; if not set, default to False
        _path_suggestion_loaded = getattr(self, 'path_suggestion_loaded', False)
        if index == 1 and not _path_suggestion_loaded:
            try:
                _load_last_output_folder = getattr(self, 'load_last_output_folder', None)
                if _load_last_output_folder and callable(_load_last_output_folder):
                    _load_last_output_folder()
                    setattr(self, 'path_suggestion_loaded', True)
                else:
                    print("Warning: load_last_output_folder method not found or not callable.")
                    raise AttributeError("Method not found for fallback") # Force fallback if method doesn't exist
            except Exception as e:
                print(f"Error loading last output folder: {e}. Falling back to suggestion.")
                _load_path_sugestion = getattr(self, 'load_path_sugestion', None)
                if _load_path_sugestion and callable(_load_path_sugestion):
                    _load_path_sugestion()
                    setattr(self, 'path_suggestion_loaded', True) # Still set to True to prevent re-running on next visit
                else:
                    print("Warning: load_path_sugestion method not found or not callable.")


        # --- Progression Checks (using QPushButton_next states) ---
        # Retrieve QPushButton objects safely. Assumes they might be assigned to 'self'
        # or you'd use self.findChild() in __init__ (preferred).
        # If they are not found in self.__dict__, getattr will return None.
        _QPushButton_next_4 = getattr(self, 'QPushButton_next_4', None)
        _QPushButton_next = getattr(self, 'QPushButton_next', None)

        if index > 1:
            if _QPushButton_next_4 and not _QPushButton_next_4.isEnabled():
                if self.language == 'pt':
                    QMessageBox.warning(self, "Proceed Step-by-Step", "Por favor, complete a Passo 2 antes de prosseguir.")
                else:
                    QMessageBox.warning(self, "Proceed Step-by-Step", "Please complete Step 2 before proceeding.")
                if _safe_set_current_index(1):
                    return

        if index > 2:
            if _QPushButton_next and not _QPushButton_next.isEnabled():
                if self.language == 'pt':
                    QMessageBox.warning(self, "Proceed Step-by-Step", "Por favor, complete a Passo 3 antes de prosseguir.")
                else:
                    QMessageBox.warning(self, "Proceed Step-by-Step", "Please complete Step 3 before proceeding.")
                if _safe_set_current_index(2):
                    _resizeEvent("small")
                    return

        # --- Tab 12: Load Fields ---
        # Get 'plot1' safely; if not set, default to None
        _plot1 = getattr(self, 'plot1', None)
        if index == 12:
            if _plot1 is not None:
                print('load_fields')
                _load_fields = getattr(self, 'load_fields', None)
                if _load_fields and callable(_load_fields):
                    _load_fields()
                else:
                    print("Warning: load_fields method not found or not callable.")

        # --- Checkbox Capture Coordinates ---
        # Get 'checkBox_captureCoordinates' safely
        _checkBox_captureCoordinates = getattr(self, 'checkBox_captureCoordinates', None)
        if index != 11:
            if _checkBox_captureCoordinates:
                try:
                    _checkBox_captureCoordinates.setChecked(False)
                except Exception as e:
                    print(f"Error unchecking coordinate capture checkbox: {e}")
            else:
                print("Warning: checkBox_captureCoordinates not found or accessible.")

        _drawing = getattr(self, 'drawing', None)
        if index != 2:
            if _drawing:
                try:
                    _drawing.setChecked(False)
                except Exception as e:
                    print(f"Error unchecking coordinate capture checkbox: {e}")
            else:
                print("Warning: checkBox_captureCoordinates not found or accessible.")

    def next_clicked(self):
        self.tabWidget.setCurrentIndex(
            (self.tabWidget.currentIndex() + 1) % self.tabWidget.count()
        )

    def back_clicked(self):
        self.tabWidget.setCurrentIndex(
            (self.tabWidget.currentIndex() - 1) % self.tabWidget.count()
        )

    def load_path_sugestion(self):
        """
        Load the path suggestion based on the user's operating system.
        """
        """
        Carrega a sugestão de caminho com base no sistema operacional do
        usuário.
        """
        system = platform.system()
        if system == "Windows":
            self.output_folder = os.path.join(os.environ["USERPROFILE"], "Downloads")
        elif system == "Linux":
            self.output_folder = os.path.join(os.environ["HOME"], "Downloads")
        elif system == "Darwin":  # MacOS
            self.output_folder = os.path.join(os.environ["HOME"], "Downloads")

        # Pre-configure with a suggested directory / Pré-configura com um
        # diretório sugerido
        self.mQgsFileWidget.setFilePath(self.output_folder)

    def pop_warning(self, aviso):
        QApplication.restoreOverrideCursor()
        msg = QMessageBox(self)
        if self.language == "pt":
            msg.setWindowTitle("Aviso!")  
        else:
            msg.setWindowTitle("Warning!")

        msg.setIcon(QMessageBox.Icon.Warning)
        msg.setText(aviso)
        msg.setStandardButtons(QMessageBox.StandardButton.Ok)
        msg.button(QMessageBox.StandardButton.Ok).setText("OK")
        msg.setStyleSheet("font-size: 10pt;")
        msg.exec()

    def pop_warning_2(self, aviso):
        QApplication.restoreOverrideCursor()  # Restore the cursor if it was overridden

        # Create a custom dialog
        dialog = QDialog(self)
        if self.language == "pt":
            dialog.setWindowTitle("Resultados da busca")
        else:
            dialog.setWindowTitle("Search Results")
        
        # Set up the main layout
        layout = QVBoxLayout(dialog)
        
        # Add the warning message
        message_label = QLabel(aviso)
        layout.addWidget(message_label)
        
        # Create a horizontal layout for the buttons
        button_layout = QHBoxLayout()
        
        # Create buttons
        if self.language == "pt":
            cancel_button = QPushButton("Cancelar (permanecer nesta aba)")
            ok_button = QPushButton("OK (ir para a próxima aba)")
        else:
            cancel_button = QPushButton("Cancel (stay on this tab)")
            ok_button = QPushButton("OK (go to the next tab)")
        
        # Add buttons to the horizontal layout
        button_layout.addWidget(cancel_button)
        button_layout.addStretch()  # This will push the OK button to the right
        button_layout.addWidget(ok_button)
        
        # Add the button layout to the main layout
        layout.addLayout(button_layout)
        
        # Connect button signals
        ok_button.clicked.connect(dialog.accept)
        cancel_button.clicked.connect(dialog.reject)
        
        # Set the stylesheet for the dialog
        dialog.setStyleSheet("font-size: 10pt;")
        
        # Execute the dialog and return the result
        result = dialog.exec()
        
        # Return which button was pressed
        if result == QDialog.DialogCode.Accepted:
            return QMessageBox.StandardButton.Ok
        else:
            return QMessageBox.StandardButton.Cancel
        
    def pop_warning_3(self, aviso):

        # Create a custom dialog
        dialog = QDialog(self)
        if self.language == "pt":
            dialog.setWindowTitle("Aviso!")
        else:
            dialog.setWindowTitle("Warning!")

        # Set up the main layout
        layout = QVBoxLayout(dialog)
        
        # Add the warning message
        message_label = QLabel(aviso)
        layout.addWidget(message_label)
        
        # Create a horizontal layout for the buttons
        button_layout = QHBoxLayout()
        
        # Create buttons
        if self.language == "pt":
            cancel_button = QPushButton("Cancelar")
            ok_button = QPushButton("OK (proceder com download em lote)")
        else:
            cancel_button = QPushButton("Cancel")
            ok_button = QPushButton("OK (proceed with batch download)")
        
        # Add buttons to the horizontal layout
        button_layout.addWidget(cancel_button)
        button_layout.addStretch()  # This will push the OK button to the right
        button_layout.addWidget(ok_button)
        
        # Add the button layout to the main layout
        layout.addLayout(button_layout)
        
        # Connect button signals
        ok_button.clicked.connect(dialog.accept)
        cancel_button.clicked.connect(dialog.reject)
        
        # Set the stylesheet for the dialog
        dialog.setStyleSheet("font-size: 10pt;")
        
        # Execute the dialog and return the result
        result = dialog.exec()
        
        # Return which button was pressed
        if result == QDialog.DialogCode.Accepted:
            return QMessageBox.StandardButton.Ok
        else:
            return QMessageBox.StandardButton.Cancel
        
    def combobox_update_3(self):

        print("Combobox selection changed.")  
        
        # self.mMapLayerComboBox.blockSignals(True)# Debug

        self.mMapLayerComboBox.setCurrentIndex(
            self.mMapLayerComboBox_3.currentIndex()
        )

        self.mMapLayerComboBox_2.setCurrentIndex(
            self.mMapLayerComboBox_3.currentIndex()
        )

    def combobox_update_2(self):
        print("Combobox selection changed.")  # Debu
        
        # self.mMapLayerComboBox_3.blockSignals(True)

        self.mMapLayerComboBox.setCurrentIndex(
            self.mMapLayerComboBox_2.currentIndex()
        )

        self.mMapLayerComboBox_3.setCurrentIndex(
            self.mMapLayerComboBox_2.currentIndex()
        )

    def combobox_update(self):
        print("Combobox selection changed.")

        # self.mMapLayerComboBox.blockSignals(True)  # Debug

        self.mMapLayerComboBox_2.setCurrentIndex(
            self.mMapLayerComboBox.currentIndex()
        )

        self.mMapLayerComboBox_3.setCurrentIndex(
            self.mMapLayerComboBox.currentIndex()
        )

        if self.tabWidget.currentIndex() > 1:
            print("AOI definition triggered.")
            self.zoom_to_layer()
            area = self.find_area()
            if area is not None and 0.01 < area <= 120:
                if self.aoi_definition():
                    print("AOI defined successfully.")
                    self.aoi_checked = True
                    self.aoi_checked_function()
                else:
                    print("AOI definition failed.")

            else:
                self.aoi = None
                self.aoi_checked = False
                self.aoi_checked_function()

    def calculate_vegetation_index(self, image, index_name):
        """
        Calculates the specified vegetation index for the given image using
        Earth Engine functions directly.

        Args:
            image (ee.Image): The input Earth Engine image.
            index_name (str): The name of the vegetation index to calculate
                (e.g., "NDVI", "EVI").

                
        Returns:
            ee.Image: The image containing the calculated vegetation index, renamed
                to "index".

        Raises:
            ValueError: If an unsupported vegetation index is specified.
        """

        #EVI 

        def evi(image):
            nir = image.select("B8").divide(10000)
            red = image.select("B4").divide(10000)
            blue = image.select("B2").divide(10000)
            return image.expression(
                "2.5 * ((NIR - RED) / (NIR + 6 * RED - 7.5 * BLUE + 1))",
                {"NIR": nir, "RED": red, "BLUE": blue},
            ).rename("index")
        
        def evi2(image):
            nir = image.select("B8").divide(10000)
            red = image.select("B4").divide(10000)
            return image.expression(
            "2.5 * ((NIR - RED) / (NIR + RED + 1))",
            {"NIR": nir, "RED": red},
            ).rename("index")

        def savi(image):
            nir = image.select("B8").divide(10000)
            red = image.select("B4").divide(10000)
            L = 0.5
            return image.expression(
                "(1 + L) * ((NIR - RED) / (NIR + RED + L))",
                {"NIR": nir, "RED": red, "L": L},
            ).rename("index")

        def msavi(image):
            nir = image.select("B8").divide(10000)
            red = image.select("B4").divide(10000)
            return image.expression(
                "((2 * NIR + 1) - sqrt((2 * NIR + 1) ** 2 - 8 * (NIR - RED))) / 2",
                {"NIR": nir, "RED": red},
            ).rename("index")

        def sfdvi(image):
            return image.expression(
                "((NIR + GREEN)/2 - (RED + REDEDGE)/2)",
                {
                    "NIR": image.select("B8").divide(10000),  # Near-Infrared
                    "GREEN": image.select("B3").divide(10000),  # Green
                    "RED": image.select("B4").divide(10000),  # Red
                    "REDEDGE": image.select("B5").divide(10000),  # Red Edge
                },
            ).rename("index")

        def cigreen(image):
            nir = image.select("B8")
            green = image.select("B3")
            return image.expression(
                "(NIR / GREEN) - 1", {"NIR": nir, "GREEN": green}
            ).rename("index")
        
        def arvi(image):
            nir = image.select("B8").divide(10000)
            red = image.select("B4").divide(10000)
            blue = image.select("B2").divide(10000)
            return image.expression(
                "(NIR - (2 * RED - BLUE)) / (NIR + (2 * RED - BLUE))",
                {"NIR": nir, "RED": red, "BLUE": blue},
            ).rename("index")
        
        def ndmi(image):
            return image.normalizedDifference(["B8", "B11"]).rename("index")
        
        def nbr(image):
            return image.normalizedDifference(["B8", "B12"]).rename("index")
        
        def sipi(image):
            nir = image.select("B8").divide(10000)
            red = image.select("B4").divide(10000)
            blue = image.select("B2").divide(10000)
            return image.expression(
                "(NIR - BLUE) / (NIR - RED)",
                {"NIR": nir, "RED": red, "BLUE": blue},
            ).rename("index")
        
        def ndwi(image):
            return image.normalizedDifference(["B3", "B8"]).rename("index")
        
        def reci(image):
            nir = image.select("B8")
            rededge = image.select("B5")
            return image.expression(
                "(NIR / REDEDGE) - 1",
                {"NIR": nir, "REDEDGE": rededge},
            ).rename("index")
        
        def mtci(image):
            nir = image.select("B8")
            rededge = image.select("B5")
            red = image.select("B4")
            return image.expression(
                "(NIR - REDEDGE) / (REDEDGE - RED)",
                {"NIR": nir, "REDEDGE": rededge, "RED": red},
            ).rename("index")
        
        def mcari(image):
            nir = image.select("B8").divide(10000)
            red = image.select("B4").divide(10000)
            green = image.select("B3").divide(10000)
            return image.expression(
                "((REDEDGE - RED) - 0.2 * (REDEDGE - GREEN)) * (REDEDGE / RED)",
                {"REDEDGE": nir, "RED": red, "GREEN": green},
            ).rename("index")
        
        def vari(image):
            red = image.select("B4").divide(10000)
            green = image.select("B3").divide(10000)
            blue = image.select("B2").divide(10000)
            return image.expression(
                "(GREEN - RED) / (GREEN + RED - BLUE)",
                {"GREEN": green, "RED": red, "BLUE": blue},
            ).rename("index")
         
        def tvi(image):
            nir = image.select("B8").divide(10000)
            red = image.select("B4").divide(10000)
            green = image.select("B3").divide(10000)
            return image.expression(
                "0.5 * (120 * (NIR - GREEN) - 200 * (RED - GREEN))",
                {"NIR": nir, "RED": red, "GREEN": green},
            ).rename("index")
        
        def custom(image):
            # Add all bands to the custom index calculation
            band1 = image.select("B1").divide(10000)  # Coastal aerosol
            band2 = image.select("B2").divide(10000)  # Blue
            band3 = image.select("B3").divide(10000)  # Green
            band4 = image.select("B4").divide(10000)  # Red
            band5 = image.select("B5").divide(10000)  # Red Edge 1
            band6 = image.select("B6").divide(10000)  # Red Edge 2
            band7 = image.select("B7").divide(10000)  # Red Edge 3
            band8 = image.select("B8").divide(10000)  # NIR
            band8a = image.select("B8A").divide(10000)  # Narrow NIR
            band9 = image.select("B9").divide(10000)  # Water vapor
            band11 = image.select("B11").divide(10000)  # SWIR 1
            band12 = image.select("B12").divide(10000)  # SWIR 2

            return image.expression(
                self.custom_expression,
                {
                    "B1": band1,
                    "B2": band2,
                    "B3": band3,
                    "B4": band4,
                    "B5": band5,
                    "B6": band6,
                    "B7": band7,
                    "B8": band8,
                    "B8A": band8a,
                    "B9": band9,
                    "B11": band11,
                    "B12": band12,
                },
            ).rename("index")

        index_functions = {
            "NDVI": lambda image: image.normalizedDifference(["B8", "B4"]).rename("index"),
            "EVI": evi,
            "EVI2": evi2,
            "SAVI": savi,
            "GNDVI": lambda image: image.normalizedDifference(["B8", "B3"]).rename("index"),
            "MSAVI": msavi,
            "SFDVI": sfdvi,
            "CIgreen": cigreen,
            "NDRE": lambda image: image.normalizedDifference(["B8", "B5"]).rename("index"),
            "ARVI": arvi,
            "NDMI": ndmi,
            "NBR": nbr,
            "SIPI": sipi,
            "NDWI": ndwi,
            "ReCI": reci,
            "MTCI": mtci,
            "MCARI": mcari,
            "VARI": vari,
            "TVI": tvi,
            self.custom_expression_name + " (custom)": custom,
        }

        if index_name in index_functions:
            index_image = index_functions[index_name](image)
            return index_image
        else:
            raise ValueError(f"Unsupported vegetation index: {index_name}")

    def load_index(self, temporary=False):
        """Calculates a vegetation index, downloads the GeoTIFF, and adds it to
        the QGIS project as a styled raster layer, ensuring unique names for
        each layer."""
        aoi = self.apply_buffer(self.aoi)
        try:
            print("First index clicked")
            QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)

            vegetation_index = self.imagem_unica_indice.currentText()
            date = [self.dataunica.currentText()]

            first_image = self.sentinel2.filter(ee.Filter.inList("date", date)).first()
            first_image = first_image.clip(aoi)

            index_image = self.calculate_vegetation_index(first_image, vegetation_index)

            # Prepare download URL and output filename
            url = index_image.getDownloadURL(
                {
                    "scale": 10,
                    "region": aoi.geometry().bounds().getInfo(),
                    "format": "GeoTIFF",
                    "crs": "EPSG:4326",  # Use WGS84 for compatibility
                }
            )
            base_output_file = f"{vegetation_index}_{date[0]}.tiff"
            output_file = self.get_unique_filename(base_output_file, temporary)

            # Download the image
            response = requests.get(url)
            if response.status_code == 200:
                with open(output_file, "wb") as f:
                    f.write(response.content)
                print(f"{vegetation_index} image downloaded as {output_file}")
            else:
                print(f"Failed to download image. HTTP Status: {response.status_code}")
                return

            # Prepare unique layer name
            layer_name = f"{vegetation_index} {date[0]}"
            base_name = layer_name
            i = 1
            while QgsProject.instance().mapLayersByName(layer_name):
                layer_name = f"{base_name}_{i}"
                i += 1
            print(f"Layer name adjusted to '{layer_name}' to ensure uniqueness.")

            # Add raster layer with styling
            map_tools.load_raster_layer_colorful(
                output_file, layer_name, vegetation_index, None
            )

        except Exception as e:
            print(f"An error occurred: {e}")
        finally:
            QApplication.restoreOverrideCursor()

    def aggregate_index_collection(self, index_collection, metrica):
        """Aggregates the index collection based on the specified metric."""
        # Obter a primeira imagem como referência espacial
        first_image = index_collection.first()

        # Dicionário para mapear métricas às funções correspondentes
        metric_functions = {
            "Mean": lambda: index_collection.mean(),
            "Média": lambda: index_collection.mean(),
            "Max": lambda: index_collection.max(),
            "Máximo": lambda: index_collection.max(),
            "Min": lambda: index_collection.min(),
            "Mínimo": lambda: index_collection.min(),
            "Median": lambda: index_collection.median(),
            "Mediana": lambda: index_collection.median(),
            "Amplitude": lambda: index_collection.max().subtract(index_collection.min()),
            "Standard Deviation": lambda: index_collection.reduce(ee.Reducer.stdDev()),
            "Desvio Padrão": lambda: index_collection.reduce(ee.Reducer.stdDev()),
            "Sum": lambda: index_collection.sum(),
            "Soma": lambda: index_collection.sum(),
            "Area Under Curve (AUC)": lambda: self.calculate_auc(index_collection),
        }

        # Verificar se a métrica é válida
        if metrica not in metric_functions:
            valid_metrics = ", ".join(metric_functions.keys())
            raise ValueError(f"Invalid metric: {metrica}. Valid metrics are: {valid_metrics}")

        # Calcular a métrica
        result_image = metric_functions[metrica]()

        # Garantir que a imagem resultante tenha o mesmo alinhamento espacial da primeira imagem
        aligned_image = result_image.setDefaultProjection(
            first_image.projection()
        ).clip(first_image.geometry())

        return aligned_image

    def calculate_auc(self, index_collection):
        """
        Calculates the Area Under Curve (AUC) with proper spatial alignment.
        """
        print("Calculating AUC...")
        count = index_collection.size().getInfo()
        if count < 2:
            raise ValueError("Insufficient number of images to calculate AUC.")
        
        # Get the first image to use as a spatial reference
        first_image = index_collection.first()
        
        # Convert collection to multi-band image while preserving projection
        index_stack = index_collection.toBands()
        
        # Define a valid mask (minimum mask value of all bands)
        valid_mask = index_stack.mask().reduce(ee.Reducer.min())
        
        # Get the band names (each band corresponds to a date)
        bands = index_stack.bandNames()
        
        # Define the start date
        start_date = ee.Date(self.inicio)
        
        # Create a list of timestamps in days relative to the start date
        timestamps = index_collection.aggregate_array("system:time_start").map(
            lambda date: ee.Date(date).difference(start_date, "day")
        )
        
        # Ensure timestamps is a valid list and calculate time differences
        time_diffs = ee.List(timestamps).slice(0, -1).zip(ee.List(timestamps).slice(1)).map(
            lambda pair: ee.Number(ee.List(pair).get(1)).subtract(ee.Number(ee.List(pair).get(0)))
        )
        
        # Convert the index stack to an array
        index_array = index_stack.toArray()
        
        # Calculate the sums of index values for consecutive images
        sums = index_array.arraySlice(0, 1).add(index_array.arraySlice(0, 0, -1))
        
        # Calculate the AUC using the trapezoidal rule
        auc = ee.Image.constant(time_diffs).toArray().multiply(sums).divide(2).arrayReduce(
            ee.Reducer.sum(), [0]
        )
        
        # Extract result and apply mask
        auc_image = auc.arrayGet([0]).updateMask(valid_mask)
        
        # Create a new image with the same footprint as the first image
        final_image = first_image.select(0).multiply(0).add(auc_image)
        
        return final_image

    def calculate_timeseries(self):
        """Calculates the time series of the selected vegetation index for the AOI."""
        vegetation_index = self.series_indice.currentText()
        aoi = self.apply_buffer(self.aoi)

        result = self.sentinel2.map(lambda image: self.calculate_index_with_mean(image, vegetation_index, aoi))
        result = result.filter(ee.Filter.notNull(["mean_index"]))

        # Retrieve dates and mean index values separately using aggregate_array
        dates = result.aggregate_array("date").getInfo()
        mean_indices = result.aggregate_array("mean_index").getInfo()
        image_ids = result.aggregate_array("system:index").getInfo()

        # Combine the dates, mean indices, and image IDs into a DataFrame
        df = pd.DataFrame({"date": dates, "AOI_average": mean_indices, "image_id": image_ids})

        # Optional: Smoothing or further processing
        self.df = df.copy()
        self.df_aux = df.copy()
        self.load_dates()
        self.plot_timeseries()

    def apply_buffer(self, aoi):
        """Applies a buffer to the AOI geometry."""
        buffer_distance = self.horizontalSlider_buffer.value()
        if buffer_distance != 0:
            print(f"Buffer distance: {buffer_distance} meters")
            aoi = aoi.map(lambda feature: feature.buffer(buffer_distance))
            return aoi
        else:
            print("No buffer applied")
            return aoi

    def calculate_index_with_mean(self, image, index_name, aoi):
        """Calculates the mean value for the specified index over the AOI."""
        index_image = self.calculate_vegetation_index(image, index_name)
        mean_index = (
            index_image.reduceRegion(
                reducer=ee.Reducer.mean(), geometry=aoi, scale=10, bestEffort=True
            ).get("index")
        )
        return image.set({"mean_index": mean_index})

    def feature_calculate_timeseries(self, name):
        """Calculates the time series of the selected vegetation index for a specific feature."""
        vegetation_index = self.series_indice.currentText()
        aoi = self.apply_buffer(self.aoi_feature)

        result = self.sentinel2_selected_dates.map(lambda image: self.calculate_index_with_mean(image, vegetation_index, aoi))
        result = result.filter(ee.Filter.notNull(["mean_index"]))

        # Retrieve dates and mean index values separately using aggregate_array
        dates = result.aggregate_array("date").getInfo()
        mean_indices = result.aggregate_array("mean_index").getInfo()

        print(f"Creating DataFrame for {name}")
        return pd.DataFrame({"date": dates, name: mean_indices})

    def point_calculate_timeseries(self, aoi, name):
        """Calculates the time series of the selected vegetation index for a specific point."""
        vegetation_index = self.series_indice.currentText()

        result = self.sentinel2_selected_dates.map(lambda image: self.calculate_index_with_mean(image, vegetation_index, aoi))
        result = result.filter(ee.Filter.notNull(["mean_index"]))

        # Retrieve dates and mean index values separately using aggregate_array
        dates = result.aggregate_array("date").getInfo()
        mean_indices = result.aggregate_array("mean_index").getInfo()

        print(f"Creating DataFrame for {name}")
        return pd.DataFrame({"date": dates, name: mean_indices})

    def load_rgb(self, temporary=False, min_val=200, max_val=2300):
        """
        Loads Sentinel-2 RGB image into QGIS with proper band names.

        
        """
        aoi = self.apply_buffer(self.aoi)

            
        # Set the cursor to indicate processing
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)

        try:
            date = [self.dataunica.currentText()]
            print(f"Selected date: {date}")

            first_image = self.sentinel2.filter(
                ee.Filter.inList("date", date)
            ).first()

            # Clip image to AOI
            

            # Select the bands we want
            bands = [
                "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B8A", "B9", "B11", "B12"
            ]
            
            first_image = first_image.select(bands)
            first_image = first_image.clip(aoi)

            # Get the acquisition date and define download region
            region = aoi.geometry().bounds().getInfo()["coordinates"]

            # Generate download URL
            try:
                url = first_image.getDownloadURL(
                    {
                        "scale": 10,
                        "region": region,
                        "format": "GeoTIFF",
                        "crs": "EPSG:4326"# Use WGS84 for compatibility
                    }
                )
            except Exception as e:
                self.pop_warning(f"Failed to generate download URL: {e}")
                return

            # Define output file
            base_output_file = f"Sentinel2_AllBands_{date[0]}.tiff"
            output_file = self.get_unique_filename(base_output_file, temporary)
            temp_file = output_file + "_temp.tiff"

            # Download the image
            try:
                response = requests.get(url, stream=True)
                response.raise_for_status()
                with open(output_file, "wb") as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        if chunk:
                            f.write(chunk)
                print(f"Image downloaded to {output_file}")
            except requests.exceptions.RequestException as e:
                self.pop_warning(f"Error downloading image: {e}")
                return

            # Define descriptive band names
            band_names = [
                "Coastal aerosol (B1)",
                "Blue (B2)",
                "Green (B3)",
                "Red (B4)",
                "Red Edge 1 (B5)",
                "Red Edge 2 (B6)",
                "Red Edge 3 (B7)",
                "NIR (Broad) (B8)",
                "NIR (Narrow) (B8A)",
                "Water Vapour (B9)",
                "SWIR 1 (B11)",
                "SWIR 2 (B12)",
            ]

            # Modify the GeoTIFF to include band names
            try:
                # Open the source dataset
                src_ds = gdal.Open(output_file)
                if not src_ds:
                    self.pop_warning(f"Failed to open the downloaded file: {output_file}")
                    return

                # Get band count and GeoTIFF properties
                band_count = src_ds.RasterCount
                print(f"Downloaded image has {band_count} bands")
                
                # Create a copy with the same properties
                driver = gdal.GetDriverByName("GTiff")
                dst_ds = driver.CreateCopy(temp_file, src_ds, strict=1)
                
                # Set band names in the new file
                for i in range(min(band_count, len(band_names))):
                    band = dst_ds.GetRasterBand(i + 1)  # 1-based indexing
                    band.SetDescription(band_names[i])
                
                # Close datasets to flush to disk
                dst_ds = None
                src_ds = None
                
                # Replace original file with the modified one
                os.remove(output_file)
                os.rename(temp_file, output_file)
                
                print(f"Created GeoTIFF with named bands: {output_file}")
            except Exception as e:
                self.pop_warning(f"Error modifying GeoTIFF: {e}")
                print(traceback.format_exc())
                # If an error occurred during modification, we might still have the original file
                if os.path.exists(temp_file):
                    os.remove(temp_file)

            # Add the image as a raster layer in QGIS
            layer_name = f"Sentinel-2 {date[0]}"
            layer = QgsRasterLayer(output_file, layer_name)
            if not layer.isValid():
                self.pop_warning(f"Failed to load the layer: {output_file}")
                return

            render_channels = self.rgb_rendering()
            # Create a new MultiBandColorRenderer for RGB
            renderer = QgsMultiBandColorRenderer(
                layer.dataProvider(),
                render_channels[0],  # Red channel
                render_channels[1],  # Green channel
                render_channels[2],  # Blue channel
            )

            # Apply contrast enhancement using cumulative cut (2% - 98%)
            # similar to the SYSI rendering strategy
            try:
                provider = layer.dataProvider()
                canvas = iface.mapCanvas()
                extent = canvas.extent()
                
                # Safety Check: If zoomed out or panning, use the layer's full extent
                if not extent.intersects(layer.extent()):
                    extent = layer.extent()
                
                # Define band indices for RGB rendering
                red_band = render_channels[0]
                green_band = render_channels[1]
                blue_band = render_channels[2]
                
                # Loop through bands to apply Cumulative Cut (2% - 98%)
                bands_config = [
                    (red_band, renderer.setRedContrastEnhancement),
                    (green_band, renderer.setGreenContrastEnhancement),
                    (blue_band, renderer.setBlueContrastEnhancement)
                ]
                
                for band_index, set_enhancement_func in bands_config:
                    # Calculate 2% and 98% cumulative cut stats
                    min_max_tuple = provider.cumulativeCut(band_index, 0.02, 0.98, extent, 250000)
                    
                    val_min = min_max_tuple[0]
                    val_max = min_max_tuple[1]
                    
                    # Create Contrast Enhancement Object
                    ce = QgsContrastEnhancement(provider.dataType(band_index))
                    ce.setContrastEnhancementAlgorithm(QgsContrastEnhancement.StretchToMinimumMaximum)
                    ce.setMinimumValue(val_min)
                    ce.setMaximumValue(val_max)
                    
                    # Apply to renderer
                    set_enhancement_func(ce)
                
            except Exception as e:
                print(f"Error configuring contrast enhancement: {e}")
                return

            # Set the renderer to the layer
            layer.setRenderer(renderer)

            # Add the raster layer to the QGIS project
            QgsProject.instance().addMapLayer(layer, addToLegend=False)
            root = QgsProject.instance().layerTreeRoot()
            root.insertChildNode(0, QgsLayerTreeLayer(layer))
            iface.setActiveLayer(layer)
            layer.triggerRepaint()

        except Exception as e:
            self.pop_warning(f"An error occurred: {e}")
            print(traceback.format_exc())
        finally:
            QApplication.restoreOverrideCursor()

    def rgb_rendering(self):
        """Returns the band numbers for RGB rendering."""
        # Sentinel-2 band numbers for RGB

        pallet = self.QComboBox_composition.currentText()
        if pallet == "Real color (RGB)":
            red_band = 4  # Red
            green_band = 3  # Green
            blue_band = 2  # Blue
        elif pallet == "Red-NIR-Green":
            red_band = 4  # Red
            green_band = 8  # NIR
            blue_band = 3  # Green
        elif pallet == "NIR-Red-Green":
            red_band = 8  # NIR
            green_band = 4  # Red
            blue_band = 3  # Green
        elif pallet == "SWIR2-NIR-Green":
            red_band = 12  # SWIR2
            green_band = 8  # NIR
            blue_band = 3  # Green
        elif pallet == "SWIR1-NIR-SWIR2":
            red_band = 11  # SWIR1
            green_band = 8  # NIR
            blue_band = 12  # SWIR2

        return (red_band, green_band, blue_band)

    def zoom_to_layer(self, margin_ratio=0.3):
        """
        Zoom to the layer currently selected in self.mMapLayerComboBox,
        adding an optional margin.

        :param margin_ratio: Fraction of the extent to add as margin (default 0.3).
        """
        # Try to obtain the layer directly from the combobox (preferred)
        layer = None
        try:
            if hasattr(self, "mMapLayerComboBox") and self.mMapLayerComboBox is not None:
                layer = self.mMapLayerComboBox.currentLayer()
        except Exception:
            layer = None

        # Fallback: try to resolve by combobox text (name)
        if layer is None or not getattr(layer, "isValid", lambda: False)():
            try:
                layer_name = None
                if hasattr(self, "mMapLayerComboBox"):
                    try:
                        layer_name = self.mMapLayerComboBox.currentText().strip()
                    except Exception:
                        layer_name = None

                if layer_name:
                    layers = QgsProject.instance().mapLayersByName(layer_name)
                    if layers:
                        layer = layers[0]
            except Exception:
                layer = None

        if layer is None or not getattr(layer, "isValid", lambda: False)():
            print("No valid layer selected in mMapLayerComboBox to zoom to.")
            return

        # Use iface canvas to set extent
        canvas = qgis.utils.iface.mapCanvas()

        # Ensure canvas destination CRS matches the layer CRS
        try:
            canvas.setDestinationCrs(layer.crs())
        except Exception:
            # If setting CRS fails, continue anyway
            pass

        # Compute expanded extent with margin
        layer_extent = layer.extent()
        x_margin = layer_extent.width() * margin_ratio
        y_margin = layer_extent.height() * margin_ratio

        expanded_extent = QgsRectangle(
            layer_extent.xMinimum() - x_margin,
            layer_extent.yMinimum() - y_margin,
            layer_extent.xMaximum() + x_margin,
            layer_extent.yMaximum() + y_margin,
        )

        canvas.setExtent(expanded_extent)
        canvas.refresh()

        try:
            print(f"Zoomed to layer '{layer.name()}' extent with margin: {expanded_extent.toString()}")
        except Exception:
            print("Zoomed to selected layer extent.")

    def get_subdirectory_filename(self, base_name, temporary=False):
        """
        Creates a unique layer name with incrementing number and 
        matching subdirectory, then returns the unique filename path.
        """
        if temporary:
            base_output_folder = tempfile.gettempdir()
        else:
            base_output_folder = self.output_folder
        
        # Find a unique layer name with incrementing number
        counter = 1
        layer_name = f"{base_name}{counter}"
        subdirectory_path = os.path.join(base_output_folder, layer_name)
        
        # Keep incrementing until we find an unused name
        while os.path.exists(subdirectory_path):
            counter += 1
            layer_name = f"{base_name}{counter}"
            subdirectory_path = os.path.join(base_output_folder, layer_name)
        
        # Create the directory
        os.makedirs(subdirectory_path)
        
        # Create the filename with the same base name
        file_name = f"{layer_name}.shp"
        file_path = os.path.join(subdirectory_path, file_name)
        
        print(f"Unique layer name: {layer_name}")
        print(f"Unique filename: {file_path}")
        
        return file_path, layer_name

    def get_unique_filename(self, base_file_name, temporary=False):
        name, extension = os.path.splitext(base_file_name)
        if temporary:
            output_folder = tempfile.gettempdir()  # Get system's temp directory
            output_file = os.path.join(output_folder, base_file_name)
        else:
            output_file = os.path.join(self.output_folder, base_file_name)
            output_folder = self.output_folder
        counter = 1

        while os.path.exists(output_file):
            output_file = os.path.join(
                output_folder, f"{name}_{counter}{extension}"
            )
            counter += 1

        print(f"Unique filename: {output_file}")
        return output_file

    def sentinel2_selected_dates_update(self):
        Date_list_selection = (
            [
                date.strftime("%Y-%m-%d")
                for date in pd.to_datetime(self.df_aux["date"]).tolist()
            ]
            if "date" in self.df_aux.columns
            else []
        )
        print(f"Selected dates for time series: {Date_list_selection}")

        print("Final number of images before:", self.sentinel2.size().getInfo())
        dates_in_collection = self.sentinel2.aggregate_array("date").getInfo()
        print(f"Dates in the collection: {dates_in_collection}")
        print(f"Selected dates (timestamps): {Date_list_selection}")

        # Filtra a coleção Sentinel-2 pelas datas selecionadas
        sentinel2_selected_dates = self.sentinel2.filter(
            ee.Filter.inList("date", ee.List(Date_list_selection))
        )
        print(
            "Final number of images after:", sentinel2_selected_dates.size().getInfo()
        )

        self.sentinel2_selected_dates = sentinel2_selected_dates

    def composite_clicked(self, temporary=False):
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
        self.sentinel2_selected_dates_update()
        self.composite(temporary)
        QApplication.restoreOverrideCursor()

    def composite(self, temporary):
        """
        Calculates a composite of a vegetation index, downloads it clipped to the
        precise AOI using an explicit mask, and loads it into QGIS.
        NOTE: For a real plugin, this entire logic should be moved into a QgsTask
        to avoid freezing the QGIS UI.
        """
        try:
            QgsMessageLog.logMessage("Starting vegetation index composite process.", "MyPlugin", Qgis.Info)

            # Get the selected vegetation index and metric for aggregation
            indice_vegetacao = self.indice_composicao.currentText()
            metrica = self.metrica.currentText()

            # Function to calculate the desired vegetation index and preserve the date
            def calculate_index(image):
                # Ensure the image has the necessary bands before calculating the index.
                # It's good practice to add checks or handle missing bands gracefully.
                index_image = self.calculate_vegetation_index(image, indice_vegetacao)
                # copyProperties is good for retaining metadata like system:time_start
                return index_image.copyProperties(image, ["system:time_start"])

            # Apply the index calculation to the filtered collection
            index_collection = self.sentinel2_selected_dates.map(calculate_index)

            # Aggregate the index collection based on the selected metric
            final_image = self.aggregate_index_collection(index_collection, metrica)

            # --- IMPORTANT: Cast to float before masking for robust NoData handling ---
            # This ensures that the image data type can correctly represent the NoData value.
            final_image = final_image.toFloat()

            # --- KEY CHANGE: Use updateMask for more reliable clipping ---
            # Apply buffer if needed. Ensure self.aoi is an ee.Geometry object.
            aoi = self.apply_buffer(self.aoi)

            # 1. Create an explicit mask from the AOI geometry.
            # ee.Image(1).clip(aoi) creates an image where the AOI is 1 and outside is 0.
            # .mask() converts this to a mask where 1 is valid data and 0 is NoData.
            mask = ee.Image(1).clip(aoi).mask()

            # 2. Apply this mask to the final composite image.
            # This operation sets pixels outside the mask to NoData (transparent).
            final_image_masked = final_image.updateMask(mask)

            # 3. Define the download region using the BOUNDING BOX of the AOI.
            # This ensures the downloaded GeoTIFF is a rectangle that fully covers the AOI.
            # The actual clipping to the irregular shape is handled by updateMask.
            download_region = aoi.geometry().bounds().getInfo()

            url = final_image_masked.getDownloadURL(
                {
                    "scale": 10,
                    "region": download_region,
                    "format": "GeoTIFF",
                    'crs': 'EPSG:4326',  # Optional: specify CRS if needed
                }
            )

            base_output_file = f"{metrica}_{indice_vegetacao}.tiff"
            output_file = self.get_unique_filename(base_output_file, temporary)
            response = requests.get(url)
            with open(output_file, "wb") as f:
                f.write(response.content)
            print(f"{indice_vegetacao} image downloaded as {output_file}")

            layer_name = f"{indice_vegetacao} {metrica}"
            # Carrega a camada raster com estilo colorido no QGIS
            map_tools.load_raster_layer_colorful(
                output_file, layer_name, indice_vegetacao, self.metrica.currentText()
            )


        except requests.exceptions.RequestException as e:
            # Catches network-related errors (e.g., connection refused, DNS error, timeout)
            # and HTTP errors (4xx, 5xx) caught by response.raise_for_status().
            QgsMessageLog.logMessage(f"Network error or error from GEE server: {e}", "MyPlugin", Qgis.Critical)
        except Exception as e:
            # It's good practice to log the full traceback for debugging complex errors from GEE
            import traceback
            QgsMessageLog.logMessage(f"An unexpected error occurred: {e}\n{traceback.format_exc()}", "MyPlugin", Qgis.Critical)

    def on_file_changed(self, file_path):
        """Slot called when the selected file changes."""
        """Slot chamado quando o arquivo selecionado muda."""
        
        if self.mQgsFileWidget.filePath():
            print(f"File selected: {file_path}")
            self.output_folder = file_path
            self.folder_set = True
            self.QPushButton_next_4.setEnabled(True)
            
            # Save the selected file path to QGIS settings for persistence
            QSettings().setValue("ravi_plugin/last_output_folder", file_path)
            print(f"Last output folder saved: {file_path}")
        else:
            print("No file selected.")
            self.folder_set = False
            self.QPushButton_next_4.setEnabled(False)

    def load_last_output_folder(self):
        """Loads the last selected output folder from QGIS settings."""
        """Carrega a última pasta de saída selecionada das configurações do QGIS."""
        
        last_folder = QSettings().value("ravi_plugin/last_output_folder", "")
        if last_folder:
            self.mQgsFileWidget.setFilePath(last_folder)
            self.output_folder = last_folder
            self.folder_set = True
            self.QPushButton_next_4.setEnabled(True)
            print(f"Last output folder loaded: {last_folder}")
        else:
            print("No previously selected output folder found.")

    def index_explain(self):
        if self.language == "pt":
            explanation = vegetation_index_info.vegetation_indices_pt.get(
                self.series_indice.currentText()
            )
        else:
            explanation = vegetation_index_info.vegetation_indices.get(
                self.series_indice.currentText()
            )
        self.textBrowser_index_explain.setHtml(explanation)

    def load_vector_feature(self, shapefile_path=None):
        """
        Loads a vector layer (memory layer or file path), reprojects to EPSG:4326,
        dissolves to a single polygonal geometry, strips Z, and returns an
        Earth Engine FeatureCollection representing the AOI.
        """
        def transform_ok(ret):
            # Normalize transform() return: in many PyQGIS builds, 0 = success.
            # In some, it may return True/False.
            if isinstance(ret, bool):
                return ret
            try:
                return int(ret) == 0
            except Exception:
                # Fallback: consider truthy as success
                return bool(ret)

        def polygonal_union(geoms):
            if not geoms:
                return None
            merged = unary_union(geoms)
            if merged.is_empty:
                return None
            if isinstance(merged, (Polygon, MultiPolygon)):
                return merged
            if isinstance(merged, GeometryCollection):
                polys = []
                for g in merged.geoms:
                    if isinstance(g, (Polygon, MultiPolygon)):
                        polys.append(g)
                if not polys:
                    return None
                if len(polys) == 1:
                    return polys[0]
                return unary_union(polys)
            return None

        def transform_to_wgs84_shapely(qgs_geom, src_crs):
            # Copy via WKT to avoid clone() and fromWkb() binding issues
            try:
                copied = QgsGeometry.fromWkt(qgs_geom.asWkt())
            except Exception:
                # As a last resort, try constructing a new geometry from JSON
                try:
                    gj = qgs_geom.asJson()
                    # Note: QgsGeometry has fromGeoJSON in newer versions; avoid reliance
                    # We'll go straight to shapely if transform below fails.
                    copied = QgsGeometry.fromWkt(qgs_geom.asWkt())
                except Exception:
                    return None

            target_crs = QgsCoordinateReferenceSystem("EPSG:4326")
            xform = QgsCoordinateTransform(src_crs, target_crs, QgsProject.instance())
            ret = copied.transform(xform)
            if not transform_ok(ret):
                return None

            try:
                gj = copied.asJson()
                geo = json.loads(gj)
                return shape(geo)
            except Exception:
                return None

        try:
            # Resolve layer object and/or path
            if shapefile_path is None:
                shapefile_path = getattr(self, "selected_aio_layer_path", None)
                layer_obj = getattr(self, "selected_aio_layer", None)
            else:
                layer_obj = None

            geojson = None

            # Branch A: memory layer preferred
            if layer_obj is not None and hasattr(layer_obj, "getFeatures"):
                feats = [f for f in layer_obj.getFeatures()]
                if len(feats) == 0:
                    # Fallback: export to GeoJSON and read
                    try:
                        tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".geojson")
                        tmp_path = tmp.name
                        tmp.close()
                        err = QgsVectorFileWriter.writeAsVectorFormat(
                            layer_obj, tmp_path, "utf-8", layer_obj.crs(), "GeoJSON"
                        )
                        err_code = err[0] if isinstance(err, (tuple, list)) else err
                        if err_code != QgsVectorFileWriter.NoError:
                            print(f"Failed to export layer to GeoJSON (error: {err})")
                            return
                        aoi_gdf = gpd.read_file(tmp_path)
                        os.remove(tmp_path)

                        if aoi_gdf is None or aoi_gdf.empty:
                            print("Exported GeoJSON contains no geometries.")
                            return

                        if len(aoi_gdf) > 1:
                            aoi_gdf = aoi_gdf.dissolve()

                        geometry = aoi_gdf.geometry.iloc[0]
                        if geometry is None or geometry.is_empty:
                            print("Exported GeoJSON geometry is empty.")
                            return

                        try:
                            if aoi_gdf.crs and aoi_gdf.crs.to_epsg() != 4326:
                                aoi_gdf = aoi_gdf.to_crs(epsg=4326)
                                geometry = aoi_gdf.geometry.iloc[0]
                        except Exception:
                            pass

                        geojson = geometry.__geo_interface__
                    except Exception as e:
                        print(f"Fallback export failed: {e}")
                        return
                else:
                    src_crs = layer_obj.crs()
                    if not src_crs.isValid():
                        print("Source layer has no valid CRS; cannot reproject to EPSG:4326.")
                        return

                    geoms = []
                    for feat in feats:
                        g = feat.geometry()
                        if not g or g.isEmpty():
                            continue
                        shp = transform_to_wgs84_shapely(g, src_crs)
                        if shp is not None and not shp.is_empty:
                            geoms.append(shp)

                    merged_poly = polygonal_union(geoms)
                    if merged_poly is None or merged_poly.is_empty:
                        print("No polygonal geometry found after union/dissolve.")
                        return

                    geojson = merged_poly.__geo_interface__

            # Branch B: load from path
            else:
                if shapefile_path is None:
                    print("No path or layer provided to load_vector_feature.")
                    return

                if shapefile_path.lower().endswith(".zip"):
                    try:
                        tmp_dir = tempfile.mkdtemp()
                        with zipfile.ZipFile(shapefile_path, "r") as zip_ref:
                            zip_ref.extractall(tmp_dir)
                        shp_files = [
                            f for f in os.listdir(tmp_dir) if f.lower().endswith(".shp")
                        ]
                        if not shp_files:
                            print("Zip file does not contain a .shp.")
                            return
                        shp_path = os.path.join(tmp_dir, shp_files[0])
                        layer = QgsVectorLayer(shp_path, "temp", "ogr")
                    except Exception as e:
                        print(f"Failed to read zip file: {e}")
                        return
                else:
                    layer = QgsVectorLayer(shapefile_path, "temp", "ogr")

                if not layer or not layer.isValid():
                    print(f"Failed to load layer from {shapefile_path}")
                    return

                feats = [f for f in layer.getFeatures()]
                if len(feats) == 0:
                    print("The selected layer does not contain any geometries.")
                    return

                src_crs = layer.crs()
                if not src_crs.isValid():
                    print("Source layer has no valid CRS; cannot reproject to EPSG:4326.")
                    return

                geoms = []
                for feat in feats:
                    g = feat.geometry()
                    if not g or g.isEmpty():
                        continue
                    shp = transform_to_wgs84_shapely(g, src_crs)
                    if shp is not None and not shp.is_empty:
                        geoms.append(shp)

                merged_poly = polygonal_union(geoms)
                if merged_poly is None or merged_poly.is_empty:
                    print("No polygonal geometry found after union/dissolve.")
                    return

                geojson = merged_poly.__geo_interface__

            if geojson is None:
                print("Failed to build GeoJSON from input geometries.")
                return

            # Debug info
            try:
                print(f"GeoJSON type: {geojson.get('type')}")
                if geojson.get("type") == "Polygon" and "coordinates" in geojson:
                    print(f"First ring first coord: {geojson['coordinates'][0][0]}")
                elif geojson.get("type") == "MultiPolygon" and "coordinates" in geojson:
                    print(f"First polygon first ring first coord: {geojson['coordinates'][0][0][0]}")
            except Exception:
                pass

            # Strip Z
            def strip_z_coords(gj):
                t = gj.get("type")
                if t == "Polygon":
                    gj["coordinates"] = [
                        [coord[:2] for coord in ring] for ring in gj["coordinates"]
                    ]
                elif t == "MultiPolygon":
                    gj["coordinates"] = [
                        [[coord[:2] for coord in ring] for ring in poly]
                        for poly in gj["coordinates"]
                    ]
                return gj

            geojson = strip_z_coords(geojson)

            # Build Earth Engine AOI
            ee_geometry = ee.Geometry(geojson)
            feature = ee.Feature(ee_geometry)
            aoi = ee.FeatureCollection([feature])

            print("AOI defined successfully.")

            return aoi

        except Exception as e:
            print(f"Error in load_vector_feature: {e}")
            return

    def aoi_definition(self):
        """
        Uses the currently selected layer in self.mMapLayerComboBox as the AOI source.
        Reprojects to EPSG:4326, dissolves polygonal geometries to a single polygonal
        geometry, strips Z, and returns an Earth Engine FeatureCollection (AOI).
        Returns None if preconditions are not met or no polygonal result is produced.
        """

        def transform_ok(ret):
            if isinstance(ret, bool):
                return ret
            try:
                return int(ret) == 0
            except Exception:
                return bool(ret)

        def polygonal_union(geoms):
            if not geoms:
                return None
            merged = unary_union(geoms)
            if merged.is_empty:
                return None
            if isinstance(merged, (Polygon, MultiPolygon)):
                return merged
            if isinstance(merged, GeometryCollection):
                polys = [g for g in merged.geoms if isinstance(g, (Polygon, MultiPolygon))]
                if not polys:
                    return None
                if len(polys) == 1:
                    return polys[0]
                return unary_union(polys)
            return None

        def transform_to_wgs84_shapely(qgs_geom, src_crs):
            copied = QgsGeometry.fromWkt(qgs_geom.asWkt())
            target_crs = QgsCoordinateReferenceSystem("EPSG:4326")
            xform = QgsCoordinateTransform(src_crs, target_crs, QgsProject.instance())
            ret = copied.transform(xform)
            if not transform_ok(ret):
                return None
            gj = copied.asJson()
            geo = json.loads(gj)
            return shape(geo)

        # Get the selected layer from the combo box
        layer = getattr(self, "mMapLayerComboBox", None)
        if layer is None:
            return None

        layer = self.mMapLayerComboBox.currentLayer()
        if not layer or not layer.isValid():
            return None

        src_crs = layer.crs()
        if not src_crs or not src_crs.isValid():
            return None

        # Iterate features without fetching attributes
        feats = layer.getFeatures(QgsFeatureRequest().setNoAttributes())

        geoms = []
        for feat in feats:
            g = feat.geometry()
            if not g or g.isEmpty():
                continue
            if g.type() != QgsWkbTypes.GeometryType.PolygonGeometry:
                continue
            shp = transform_to_wgs84_shapely(g, src_crs)
            if shp is None or shp.is_empty:
                continue
            if shp.geom_type not in ("Polygon", "MultiPolygon"):
                continue
            geoms.append(shp)

        merged_poly = polygonal_union(geoms)
        if merged_poly is None or merged_poly.is_empty:
            return None

        geojson = merged_poly.__geo_interface__
        if not geojson or "type" not in geojson:
            return None

        def strip_z_coords(gj):
            t = gj.get("type")
            if t == "Polygon":
                gj["coordinates"] = [
                    [coord[:2] for coord in ring] for ring in gj["coordinates"]
                ]
            elif t == "MultiPolygon":
                gj["coordinates"] = [
                    [[coord[:2] for coord in ring] for ring in poly]
                    for poly in gj["coordinates"]
                ]
            return gj

        geojson = strip_z_coords(geojson)

        ee_geometry = ee.Geometry(geojson)
        feature = ee.Feature(ee_geometry)
        aoi = ee.FeatureCollection([feature])
        self.aoi = aoi
        return True

    def find_area(self):
        try:
            da = QgsDistanceArea()
            da.setEllipsoid("WGS84")

            # Resolve layer (same as before)
            layer = None
            try:
                if getattr(self, "mMapLayerComboBox", None) is not None:
                    layer = self.mMapLayerComboBox.currentLayer()
            except Exception:
                layer = None

            if layer is None or not getattr(layer, "isValid", lambda: False)():
                try:
                    layer_name = (
                        self.mMapLayerComboBox.currentText().strip()
                        if hasattr(self, "mMapLayerComboBox")
                        else None
                    )
                    if layer_name:
                        layers = QgsProject.instance().mapLayersByName(layer_name)
                        if layers:
                            layer = layers[0]
                except Exception:
                    layer = None

            if layer is None or not getattr(layer, "isValid", lambda: False)():
                raise ValueError("No valid layer selected in mMapLayerComboBox for area calculation")

            self.selected_aio_layer = layer
            try:
                self.selected_aio_layer_path = layer.dataProvider().dataSourceUri().split("|")[0]
            except Exception:
                self.selected_aio_layer_path = None

            layer_crs = layer.crs()
            if not layer_crs or not layer_crs.isValid():
                raise ValueError("Selected layer has invalid CRS")

            da.setSourceCrs(layer_crs, QgsProject.instance().transformContext())

            # Collect polygon geometries
            poly_geoms = []
            for f in layer.getFeatures():
                g = f.geometry()
                if not g or g.isEmpty():
                    continue
                try:
                    g = g.makeValid()
                except Exception:
                    pass
                # keep only polygonal geometries
                if QgsWkbTypes.geometryType(g.wkbType()) != QgsWkbTypes.PolygonGeometry:
                    try:
                        g2 = g.convertToType(QgsWkbTypes.PolygonGeometry, False)
                        if not g2 or g2.isEmpty():
                            continue
                        g = g2
                    except Exception:
                        continue
                # Optional: segmentize curves
                try:
                    if QgsWkbTypes.isCurvedType(g.wkbType()):
                        g = QgsGeometry(g.constGet().segmentize(5.0))
                except Exception:
                    pass
                poly_geoms.append(g)

            if not poly_geoms:
                print("No polygon geometries to measure.")
                if hasattr(self, "aoi_area") and self.aoi_area is not None:
                    self.aoi_area.setText("Total Area: 0.00 km² (0.00 hectares)")
                return 0.0

            # Union all polygons to remove overlaps
            try:
                union_geom = QgsGeometry.unaryUnion(poly_geoms)
            except Exception:
                # Fallback: progressive union if unaryUnion fails
                union_geom = poly_geoms[0]
                for g in poly_geoms[1:]:
                    try:
                        union_geom = union_geom.union(g)
                    except Exception:
                        pass

            if not union_geom or union_geom.isEmpty():
                print("Union resulted in empty geometry.")
                if hasattr(self, "aoi_area") and self.aoi_area is not None:
                    self.aoi_area.setText("Total Area: 0.00 km² (0.00 hectares)")
                return 0.0

            # Measure area of the union (geodesic)
            total_area = da.measureArea(union_geom)
            if total_area is None or total_area != total_area or total_area < 0:
                total_area = 0.0

            # Centroid from union for consistency
            try:
                centroid_geom = union_geom.centroid()
                if centroid_geom and not centroid_geom.isEmpty():
                    to_wgs84 = QgsCoordinateTransform(
                        layer_crs,
                        QgsCoordinateReferenceSystem("EPSG:4326"),
                        QgsProject.instance().transformContext(),
                    )
                    pt_wgs84 = to_wgs84.transform(centroid_geom.asPoint())
                    self.lat = pt_wgs84.y()
                    self.lon = pt_wgs84.x()
                    print(f"Centroid set to: {round(self.lat, 4)},{round(self.lon, 4)}")
            except Exception:
                pass

            area_km2 = total_area / 1e6
            area_ha = area_km2 * 100.0
            print(f"Area (no overlaps): {area_km2:.2f} km² ({area_ha:.2f} ha)")
            if hasattr(self, "aoi_area") and self.aoi_area is not None:
                self.aoi_area.setText(
                    f"Total Area: {area_km2:.2f} km² ({area_ha:.2f} hectares)"
                )
            return area_km2

        except Exception as e:
            print(f"Error in find_area: {e}")
            return None


    def aoi_checked_function(self):
        print(f"AOI checked: {self.aoi_checked}, Folder set: {self.folder_set}")
        if self.aoi_checked and self.folder_set:
            self.QPushButton_next.setEnabled(True)
            self.QPushButton_skip.setEnabled(True)
            self.QPushButton_fast.setEnabled(True)
            self.QPushButton_easy.setEnabled(True)
            self.QPushButton_sysi.setEnabled(True)
            self.loadtimeseries.setEnabled(True)
            self.loadtimeseries_2.setEnabled(True)
            self.QPushButton_features.setEnabled(True)

        else:
            self.QPushButton_next.setEnabled(False)
            self.QPushButton_skip.setEnabled(False)
            self.QPushButton_fast.setEnabled(False)
            self.QPushButton_easy.setEnabled(False)
            self.QPushButton_sysi.setEnabled(False)
            self.loadtimeseries.setEnabled(False)
            self.loadtimeseries_2.setEnabled(False)
            self.QPushButton_features.setEnabled(False)

    def resetting(self):
        self.recorte_datas = None
        self.inicio = self.incioedit.date().toString("yyyy-MM-dd")
        self.final = self.finaledit.date().toString("yyyy-MM-dd")
        self.nuvem = self.horizontalSlider_total_pixel_limit.value()
        self.QCheckBox_sav_filter.setChecked(False)
        self.filtro_grau.setCurrentIndex(0)
        self.window_len.setCurrentIndex(0)
        self.df_nasa = None
        self.df_aux = None
        self.df_points = None

    def loadtimeseries_clicked(self):
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
        try:
            self.resetting()
            self.ee_load_collection()
            self.calculate_timeseries()
            self.plot_timeseries()
            
            self.centralizar()
            self.webView_3.setHtml("")
            self.webView_2.setHtml("")
            print("Time series loaded successfully.")
            
            if self.language == "pt":
                response = self.pop_warning_2("\n".join(self.collection_info_pt))
            else:
                response = self.pop_warning_2("\n".join(self.collection_info))

            if response == QMessageBox.StandardButton.Ok:
                print("User clicked OK")
                self.tabWidget.setCurrentIndex(10)
            else:
                print("User clicked Cancel")

        except Exception as e:
            print(f"An error occurred: {e}")
            QApplication.restoreOverrideCursor()
            self.pop_warning(f"An error occurred: {e}")
        QApplication.restoreOverrideCursor()

    def loadtimeseries_clicked_2(self):
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
        try:
            self.calculate_timeseries()

            if self.df is None or self.df.empty:

                if self.language == "pt":
                    self.pop_warning(
                        "Nenhum dado disponível para os critérios selecionados. "
                        "O foco na região dentro da área de interesse (AOI) deve ser usado com um vetor "
                        "que esteja realmente contido pela AOI original, ou altere o índice de vegetação "
                        "selecionado para a série temporal mantendo a seleção atual de datas "
                        "(mesma coleção de imagens)."
                    )
                else:
                    self.pop_warning(
                        "No data available for the selected criteria. "
                        "Focus on region within area of interest (AOI) is to be used with a vector "
                        "that is actually contained by the original AOI, or change the selected "
                        "vegetation index for the time series while maintaining the current date "
                        "selection (same image collection)."
                    )
                QApplication.restoreOverrideCursor()
                return
            
            
            self.df_ajust()
            self.plot_timeseries()

            self.centralizar()
            self.webView_3.setHtml("")
            self.webView_2.setHtml("")
            print("Time series loaded successfully.")
            
        except Exception as e:
            print(f"An error occurred: {e}")
            QApplication.restoreOverrideCursor()
            self.pop_warning(f"An error occurred: {e}")
        QApplication.restoreOverrideCursor()

    def ee_load_collection(self):

        self.collection_info = []
        self.collection_info_pt = []
        """Loads the Earth Engine image collection based on user-defined
        criteria."""
        """Carrega a coleção de imagens do Earth Engine com base nos critérios
        definidos pelo usuário."""

        # Find the centroid of the AOI and check if the area is within the limit

        # Reset settings and set the cursor to indicate processing

        # Retrieve user inputs for date range, cloud percentage, and AOI
        inicio = self.inicio
        final = self.final
        nuvem = self.nuvem
        aoi = self.apply_buffer(self.aoi)
        coverage_threshold = self.horizontalSlider_aio_cover.value() / 100
        local_pixel_limit = self.horizontalSlider_local_pixel_limit.value()
        print(f"Coverage threshold: {coverage_threshold}")

        # Define the Sentinel-2 image collection with filtering by date, bounds,
        # and cloud percentage
        sentinel2 = (
            ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
            .filterDate(inicio, final)
            .filterBounds(aoi)
            #.filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", nuvem))
            .map(lambda image: image.set("date", image.date().format("YYYY-MM-dd")))
        )

        # Check if the collection is empty
        original_count = sentinel2.size().getInfo()
        print(f"Collection size before any filtering: {original_count}")
        self.collection_info.append(f"Collection size before any filtering: {original_count}")
        self.collection_info_pt.append(f"Tamanho da coleção antes de qualquer filtro: {original_count}")

        if original_count == 0:
            QApplication.restoreOverrideCursor()
            self.pop_warning(
                "No images found for the selected criteria. Please select a larger date range or less strick filtering and try again."
            )
            return

        # Apply cloud percentage filter
        sentinel2 = sentinel2.filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", nuvem))

        # Check if the collection is empty after cloud filtering
        cloud_filtered_count = sentinel2.size().getInfo()
        print(f"Collection size after cloud filtering: {cloud_filtered_count}")
        self.collection_info.append(f"Collection size after tile cloud percentage filtering (Step 8): {cloud_filtered_count}")
        self.collection_info_pt.append(f"Tamanho da coleção após filtro de limite de nuvem na cena (Passo 8): {cloud_filtered_count}")

        if cloud_filtered_count == 0:
            QApplication.restoreOverrideCursor()
            self.pop_warning(
                "No images found for the selected criteria. Please select a larger date range or less strick filtering and try again."
            )
            return


        # Apply AOI coverage filter to the image collection
        if coverage_threshold > 0:
            sentinel2 = self.AOI_coverage_filter(sentinel2, aoi, coverage_threshold)
            if sentinel2.size().getInfo() == 0:
                QApplication.restoreOverrideCursor()
                self.pop_warning(
                    "No images found for the selected criteria. Please select a larger date range or less strick filtering and try again."
                )
                return

        if local_pixel_limit > 0:
            # Apply local pixel limit filter to the image collection

            #aoi_SCL = aoi.map(lambda feature: feature.buffer(300))
            sentinel2 = self.SCL_filter(sentinel2, aoi, local_pixel_limit)
            if sentinel2.size().getInfo() == 0:
                QApplication.restoreOverrideCursor()
                self.pop_warning(
                    "No images found for the selected criteria. Please select a larger date range or less strick filtering and try again."
                )
                return

        # Apply cloud and shadow mask if the checkbox is checked
        if self.mask.isChecked():
            sentinel2 = self.SCL_mask(sentinel2, aoi)
            if sentinel2.size().getInfo() == 0:
                QApplication.restoreOverrideCursor()
                self.pop_warning(
                    "No images found for the selected criteria. Please select a larger date range or less strick filtering and try again."
                )
                return

        sentinel2 = self.uniqueday_collection(sentinel2)

        # Store the filtered image collection in the instance variable
        self.sentinel2 = sentinel2

    def uniqueday_collection(self, sentinel2):
        """
        Filters the image collection to include only one image per day,
        prioritizing images with higher AOI coverage ratio, using Earth Engine
        operations.
        """
        print("Filtering to unique days using Earth Engine...")
        print("Original collection size:", sentinel2.size().getInfo())

        def process_date(date):
            """Finds the image with the highest coverage for a given date."""
            date_start = ee.Date(date)
            date_end = date_start.advance(1, 'day')

            # Filter for images within the day
            daily_images = sentinel2.filterDate(date_start, date_end)

            # Sort by coverage_ratio (descending) and get the first image
            best_image = daily_images.sort('coverage_ratio', False).first()
            return best_image

        # Get list of unique dates
        dates = sentinel2.aggregate_array("date").distinct()

        # Map the process_date function over the list of unique dates
        unique_images = ee.List(dates.map(process_date))

        # Filter out any null values (in case a date has no images)
        unique_images = unique_images.removeAll([None])

        # Convert back to an ImageCollection
        filtered_collection = ee.ImageCollection(unique_images)

        print(f"Collection size after keeping only unique dates: {filtered_collection.size().getInfo()}")
        self.collection_info.append(f"Collection size after keeping only unique dates: {filtered_collection.size().getInfo()}")
        self.collection_info_pt.append(f"Tamanho da coleção após manter apenas datas únicas: {filtered_collection.size().getInfo()}")

        return filtered_collection

    def AOI_coverage_filter(self, sentinel2, aoi, coverage_threshold):
        """Filters the image collection based on the coverage of the Area of
        Interest (AOI)."""
        """Filtra a coleção de imagens com base na cobertura da Área de
        Interesse (AOI)."""
        print("Applying AOI coverage filter...")
        if coverage_threshold == 1:
            coverage_threshold = 0.9999  # Avoid floating-point comparison issues

        # Coverage Ratio Function
        aoi_geometry = aoi.first().geometry()
        aoi_area = aoi_geometry.area()

        def calculate_coverage_ratio(image):
            """
            Calculates the ratio of the AOI area covered by the image.

            Args:
                image (ee.Image): The Sentinel-2 image.

            Returns:
                ee.Image: The original image with an added 'coverage_ratio'
                    property.
            """
            # Compute the intersection geometry between AOI and image footprint
            intersection = aoi_geometry.intersection(image.geometry(), ee.ErrorMargin(1))

            # Calculate the area of the intersection
            intersection_area = intersection.area()

            # Calculate the coverage ratio (intersection area / AOI area)
            coverage_ratio = intersection_area.divide(aoi_area)

            # Set the coverage ratio as a property of the image
            return image.set("coverage_ratio", coverage_ratio)

        # -------------------------------
        # Step 6: Apply Coverage Ratio Calculation
        # -------------------------------

        # Map the coverage ratio function over the Sentinel-2 collection
        sentinel2_with_ratio = sentinel2.map(calculate_coverage_ratio)

        # -------------------------------
        # Step 7: Filter Based on Coverage Ratio
        # -------------------------------

        # Define a filter to keep images with coverage_ratio >=
        # coverage_threshold
        coverage_filter = ee.Filter.gte("coverage_ratio", coverage_threshold)

        # Apply the filter to get the final collection
        covering_colection = sentinel2_with_ratio.filter(coverage_filter)


        # Get the number of images after coverage filtering
        filtered_count = covering_colection.size().getInfo()

        print(
            f"Collection size after filtro >= {coverage_threshold*100}% AOI coverage: {filtered_count}"
        )
        self.collection_info.append(f"Collection size after AOI overlap filter (Step 6): {filtered_count}")
        self.collection_info_pt.append(f"Tamanho da coleção após filtro de sobreposição com a AOI (Passo 6): {filtered_count}")
        
        return covering_colection

    def SCL_filter(self, sentinel2, aoi, valid_pixel_threshold):
        """Filters the image collection based on the percentage of valid pixels
        within the AOI."""
        """Filtra a coleção de imagens com base na porcentagem de pixels
        válidos dentro da AOI."""

        print("Applying SCL filter...")
        #print("Original collection size:", sentinel2.size().getInfo())

        scl_classes_behavior = {
            0: self.mask_class0.isChecked(),  # No data
            1: self.mask_class1.isChecked(),  # Saturated/defective
            2: self.mask_class2.isChecked(),  # Dark features
            3: self.mask_class3.isChecked(),  # Cloud shadows
            4: self.mask_class4.isChecked(),  # Vegetation
            5: self.mask_class5.isChecked(),  # Bare soils
            6: self.mask_class6.isChecked(),  # Water
            7: self.mask_class7.isChecked(),  # Cloud low probability
            8: self.mask_class8.isChecked(),  # Cloud medium probability
            9: self.mask_class9.isChecked(),  # Cloud high probability
            10: self.mask_class10.isChecked(),  # Thin cirrus
            11: self.mask_class11.isChecked(),  # Snow or ice
        }

        def mask_cloud_and_shadows(image):
            scl = image.select("SCL")
            # Start with an all-inclusive mask
            mask = ee.Image.constant(1)
            # Apply exclusions
            for class_value, include in scl_classes_behavior.items():
                if include:
                    mask = mask.And(scl.neq(class_value))

            masked_image = image.updateMask(mask)

            # Calculate the percentage of valid pixels
            total_pixels = (
                image.select(0)
                .reduceRegion(reducer=ee.Reducer.count(), geometry=aoi, scale=10)
                .get("B1")
            )

            valid_pixels = (
                masked_image.select(0)
                .reduceRegion(reducer=ee.Reducer.count(), geometry=aoi, scale=10)
                .get("B1")
            )

            percentage_valid = (
                ee.Number(valid_pixels).divide(total_pixels).multiply(100)
            )

            # Add the percentage of valid pixels as a property
            return masked_image.set("percentage_valid_pixels", percentage_valid)

            # Apply the cloud and shadow mask function

        # Apply the cloud and shadow mask function to the image collection
        sentinel2_masked = sentinel2.map(mask_cloud_and_shadows)

        # Filter the collection based on the valid pixel threshold
        filtered_collection = sentinel2_masked.filter(
            ee.Filter.gte("percentage_valid_pixels", valid_pixel_threshold)
        )

        masked_timestamps = filtered_collection.aggregate_array("system:time_start").getInfo()

        sentinel2 =  sentinel2.filter(
            ee.Filter.inList("system:time_start", ee.List(masked_timestamps))
        )

        print("Collection size after SCL filter:", sentinel2.size().getInfo())
        self.collection_info.append(f"Collection size after SCL filter (Step 9): {sentinel2.size().getInfo()}")
        self.collection_info_pt.append(f"Tamanho da coleção após filtro SCL (Passo 9): {sentinel2.size().getInfo()}")
        return sentinel2

    def SCL_mask(self, sentinel2, aoi):
        """Applies a Scene Classification Layer (SCL) mask based on user selections."""
        print("Applying SCL MASK...")

        # Get user-selected classes to mask
        selected_classes = [class_value for class_value, include in {
            0: self.mask_class0.isChecked(),   # No data
            1: self.mask_class1.isChecked(),   # Saturated/defective
            2: self.mask_class2.isChecked(),   # Dark features
            3: self.mask_class3.isChecked(),   # Cloud shadows
            4: self.mask_class4.isChecked(),   # Vegetation
            5: self.mask_class5.isChecked(),   # Bare soils
            6: self.mask_class6.isChecked(),   # Water
            7: self.mask_class7.isChecked(),   # Cloud low probability
            8: self.mask_class8.isChecked(),   # Cloud medium probability
            9: self.mask_class9.isChecked(),   # Cloud high probability
            10: self.mask_class10.isChecked(), # Thin cirrus
            11: self.mask_class11.isChecked()  # Snow or ice
        }.items() if include]

        def mask_selected_classes(image):
            scl = image.select("SCL")
            # Build mask efficiently using logical OR across selected classes
            mask = scl.neq(selected_classes[0]) if selected_classes else ee.Image.constant(1)
            for class_value in selected_classes[1:]:
                mask = mask.And(scl.neq(class_value))  # Keep only allowed pixels

            return image.updateMask(mask)

        return sentinel2.map(mask_selected_classes)

    def clear_all_raster_layers(self):
        """Removes all raster layers from the QGIS project, except for the
        Google Hybrid layer."""
        """Remove todas as camadas raster do projeto QGIS, exceto a camada
        Google Hybrid."""
        # Get the current project instance
        project = QgsProject.instance()

        # Create a copy of the layer list to avoid issues with removing during
        # iteration
        layers_to_remove = list(project.mapLayers().values())

        # Iterate over the copied list
        for layer in layers_to_remove:
            if (
                layer.type() == QgsMapLayer.RasterLayer
                and layer.name() != "Google Hybrid"
            ):
                layer_name = layer.name()  # Store the layer name before removing it
                project.removeMapLayer(layer.id())  # Use layer.id() for removal
                print(f"Removed raster layer: {layer_name}")
                iface.mapCanvas().refresh()

    def df_ajust(self):
        """Adjusts the main DataFrame based on the selected dates."""
        """Ajusta o DataFrame principal com base nas datas selecionadas."""
        df = self.df.copy()
        if self.recorte_datas:
            # Ensure both sides are strings for comparison
            df = df[df["date"].astype(str).isin([str(d) for d in self.recorte_datas])]
            self.df_aux = df.copy()
        else:
            self.df_aux = df.copy()

    def df_ajust_features(self):
        """Adjusts the features DataFrame based on the selected dates."""
        """Ajusta o DataFrame de feições com base nas datas selecionadas."""
        df = self.df_features.copy()
        if self.recorte_datas:
            df = df[df["date"].astype(str).isin([str(d) for d in self.recorte_datas])]
            self.df_aux_features = df.copy()
        else:
            self.df_aux_features = df.copy()

    def df_ajust_points(self):
        """Adjusts the points DataFrame based on the selected dates."""
        """Ajusta o DataFrame de pontos com base nas datas selecionadas."""
        df = self.df_points.copy()
        if self.recorte_datas:
            df = df[df["date"].astype(str).isin([str(d) for d in self.recorte_datas])]
            self.df_aux_points = df.copy()
        else:
            self.df_aux_points = df.copy()

    def df_run_filter(self):
        """Applies the Savitzky-Golay filter to smooth the time series data."""
        """Aplica o filtro Savitzky-Golay para suavizar os dados da série
        temporal."""
        df = self.df_aux.copy()
        try:
            if self.window_len.count() == 0:
                self.window_len.clear()
                self.window_len.addItems(
                    list(map(str, list(range(7, len(df) + 1))))
                )
                self.window_len.setCurrentIndex(0)

            window_length = int(self.window_len.currentText())
            polyorder = int(self.filtro_grau.currentText().split("%")[0])
            print(f"Window length: {window_length}, Polyorder: {polyorder}")

            if window_length > len(df):
                window_length = len(df)
                self.window_len.setCurrentIndex(len(df) - 5)
                print(f"Window length too large. Using maximum value: {window_length}")

            # Apply Savitzky-Golay filter to smooth the time series

            df["savitzky_golay_filtered"] = savgol_filter(
                df["AOI_average"], window_length=window_length, polyorder=polyorder
            )
            self.df_aux = df.copy()
            return True
        except Exception as e:
            self.pop_warning(
                f"Not enough images to apply the Savitzky-Golay filter. Please select a larger date range or less strick filtering."
            )
            self.QCheckBox_sav_filter.setChecked(False)
            self.plot_timeseries()
            return False


    def plot_timeseries(self):
        """Plots the time series data, optionally applying the Savitzky-Golay
        filter."""
        """Plota os dados da série temporal, opcionalmente aplicando o filtro
        Savitzky-Golay."""
        print("plot1 started")

        # Prepare to plot
        myFile = io.StringIO()
        if self.QCheckBox_sav_filter.isChecked() and self.df_run_filter():
            df = self.df_aux
            try:
                self.fig = go.Figure()
                self.fig.add_trace(
                    go.Scatter(
                        x=df["date"],
                        y=df["AOI_average"],
                        mode="lines",
                        name=self.series_indice.currentText(),
                        line=dict(color="green"),
                    )
                )
                self.fig.add_trace(
                    go.Scatter(
                        x=df["date"],
                        y=df["savitzky_golay_filtered"],
                        mode="lines",
                        name=f"{self.series_indice.currentText()} filtered",
                        line=dict(color="purple"),
                    )
                )
            except Exception as e:
                self.pop_warning(f"An error occurred while plotting: {e}")
                self.QCheckBox_sav_filter.setChecked(False)
        else:
            df = self.df_aux
            self.fig = go.Figure()
            self.fig.add_trace(
                go.Scatter(
                    x=df["date"],
                    y=df["AOI_average"],
                    mode="lines",
                    name=self.series_indice.currentText(),
                    line=dict(color="green"),
                )
            )

        self.fig.update_layout(
            # xaxis_title='Date',
            yaxis_title=self.series_indice.currentText(),
            title=f"Time Series - {self.series_indice.currentText()} - {self.mMapLayerComboBox.currentText()}               Image count: {len(df)}",
        )

        self.fig.update_traces(
            hovertemplate="date = %{x|%Y-%m-%d}<br>average_ndvi = %{y:.2f}<extra></extra>"
        )

        # Configurations for the Plotly plot
        self.config = {
            "displaylogo": False,
            "modeBarButtonsToRemove": [
                "toImage",
                "sendDataToCloud",
                "zoom2d",
                "pan2d",
                "select2d",
                "lasso2d",
                "zoomIn2d",
                "zoomOut2d",
                "autoScale2d",
                "resetScale2d",
                "hoverClosestCartesian",
                "hoverCompareCartesian",
                "zoom3d",
                "pan3d",
                "orbitRotation",
                "tableRotation",
                "resetCameraLastSave",
                "resetCameraDefault3d",
                "hoverClosest3d",
                "zoomInGeo",
                "zoomOutGeo",
                "resetGeo",
                "hoverClosestGeo",
                "hoverClosestGl2d",
                "hoverClosestPie",
                "toggleHover",
                "toggleSpikelines",
                "resetViews",
            ],
        }

        if isinstance(self.df_nasa, pd.DataFrame):
            # Add bar plot (set below the line explicitly)
            self.fig.add_trace(
                go.Bar(
                    x=self.df_nasa.index,
                    y=self.df_nasa["Precipitation"],
                    name="Monthly Precipitation",
                    yaxis="y2",
                    marker_color="blue",
                    opacity=0.4,
                )
            )

            # Ensure correct layout and layering
            self.fig.update_layout(
                yaxis=dict(
                    title=self.series_indice.currentText(),
                ),
                yaxis2=dict(
                    title="Precipitation (mm)",
                    overlaying="y",
                    side="right",
                ),
                xaxis=None,
            )

        # Update layout and render the plot
        self.webView.setHtml(
            self.fig.to_html(include_plotlyjs="cdn", config=self.config)
        )

        self.plot1 = True


    def open_browser(self):
        """Opens the plot in a web browser."""
        """Abre o gráfico em um navegador da web."""
        self.fig.show()

    def open_browser_2(self):
        """Opens the feature plot in a web browser."""
        """Abre o gráfico de feições em um navegador da web."""
        try:
            self.fig_2.show()
        except:
            self.pop_warning("No data to plot")

    def open_browser_3(self):
        """Opens the points plot in a web browser."""
        """Abre o gráfico de pontos em um navegador da web."""
        
        try:
            self.fig_3.show()
        except:
            self.pop_warning("No data to plot")

    def load_dates(self):
        """Loads the unique dates from the DataFrame into the date selection
        combobox."""
        """Carrega as datas exclusivas do DataFrame na combobox de seleção de
        data."""
        datas = self.df.date.unique().astype(str).tolist()
        self.dataunica.clear()
        self.dataunica.addItems(datas)
        self.dataunica.setCurrentIndex(self.dataunica.count() - 1)

    def add_dot_from_coordinates(self):

        print("Adding dot from coordinates...")

        """Adds a dot to the map from latitude and longitude values entered in QLineEdit widgets.
        Handles commas as decimal separators and validates input with regex.
        """
        longitude_text = self.mLineEdit_longitude.text()
        latitude_text = self.mLineEdit_latitude.text()

        # Replace commas with periods
        longitude_text = longitude_text.replace(",", ".")
        latitude_text = latitude_text.replace(",", ".")

        # Regex to validate the format
        regex = r"[-+]?\d+(\.\d*)?"
        if not (re.match(regex, longitude_text) and re.match(regex, latitude_text)):
            self.pop_warning("Invalid longitude/latitude format. Please enter numeric values.")
            return

        try:
            longitude = float(longitude_text)
            latitude = float(latitude_text)
        except ValueError:
            self.pop_warning("Invalid longitude/latitude format. Please enter numeric values.")
            return

        # Check if the coordinate capture tool is active
        if self.coordinate_capture_tool is None:
            print("Coordinate capture tool is not active.")
            self.checkBox_captureCoordinates.setChecked(True)
            self.activate_coordinate_capture_tool()
        
        # Call the method to add the point
        self.coordinate_capture_tool.add_dot_from_coordinates(longitude, latitude)
        self.process_coordinates(longitude, latitude)

    def fast_clicked(self):
        """Handles the 'Fast' button click event to quickly load time series data and load first image."""
        print("Fast button clicked.")
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
        try:
            self.resetting()
            self.ee_load_collection()
            self.calculate_timeseries()
            self.plot_timeseries()
            
            self.centralizar()
            self.webView_3.setHtml("")
            self.webView_2.setHtml("")
            print("Time series loaded successfully.")
            self.load_index(True)
            self.load_rgb(True)
            self.tabWidget.setCurrentIndex(10)


        except Exception as e:
            print(f"An error occurred: {e}")
            QApplication.restoreOverrideCursor()
            self.pop_warning(f"An error occurred: {e}")
        QApplication.restoreOverrideCursor()

    def open_proxy_dialog(self):
        """Open a dialog to configure HTTP/HTTPS proxy and persist it in QSettings.

        Behavior:
        - On first use (no saved value), preload the field with the example
          'http://user:pass@host:port'.
        - When confirming, save only if the value is not empty and not equal to
          the example placeholder. Otherwise, clear the saved proxy.
        """
        dialog = QDialog(self)
        dialog.setWindowTitle("Proxy Settings")
        dialog.setModal(True)

        layout = QVBoxLayout(dialog)

        label = QLabel(
            "Enter proxy settings (only if required by your network provider)\n"
            "Example: http://user:pass@host:port"
        )
        layout.addWidget(label)

        proxy_edit = QLineEdit(dialog)
        # Load existing proxy from QSettings if available
        example = "http://user:pass@host:port"
        existing = QSettings().value("ravi/proxy", None)
        if existing:
            proxy_edit.setText(str(existing))
        else:
            # First use: preload with example
            proxy_edit.setText(example)
        layout.addWidget(proxy_edit)

        button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, dialog)
        layout.addWidget(button_box)

        def accept():
            value = proxy_edit.text().strip()
            # Save only real values (not empty and not placeholder example)
            if value and value != example:
                QSettings().setValue("ravi/proxy", value)
            else:
                # Clear saved proxy if user left it empty or as example
                QSettings().setValue("ravi/proxy", "")
            dialog.accept()

        button_box.accepted.connect(accept)
        button_box.rejected.connect(dialog.reject)

        dialog.exec_()

    def easy_clicked(self):
        """Open the 'easy' UI dialog (localized)."""
        try:
            if self.language == "pt":
                ui_path = os.path.join(os.path.dirname(__file__), "ui", "easy_pt.ui")
            else:
                ui_path = os.path.join(os.path.dirname(__file__), "ui", "easy.ui")

            EasyForm, _ = uic.loadUiType(ui_path)

            class EasyDialog(QDialog, EasyForm):
                def __init__(self, parent=None):
                    super(EasyDialog, self).__init__(parent)

                      # Assuming parent has an 'aoi' attribute

                    self.language = QSettings().value("locale/userLocale", "en")[0:2]
                    if self.language == "pt":
                        self.dem_datasets = datasets_info.dem_datasets_pt
                    else:
                        self.dem_datasets = datasets_info.dem_datasets_en
                    self.setupUi(self)
                    # Make it non-modal and allow interaction with QGIS
                    self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint)
                    self.setModal(False)
                    # If the UI has a close/ok button, try to connect it
                    for name in ("closeButton", "btnClose", "okButton", "buttonBox"):
                        if hasattr(self, name):
                            widget = getattr(self, name)
                            try:
                                # buttonBox is a QDialogButtonBox
                                if isinstance(widget, QDialogButtonBox):
                                    widget.accepted.connect(self.accept)
                                    widget.rejected.connect(self.reject)
                                else:
                                    widget.clicked.connect(self.close)
                            except Exception:
                                pass
                    self.update_dem_datasets()
                    self.dem_dataset_combobox.currentIndexChanged.connect(self.update_dem_info)
                    self.elevacao.clicked.connect(self.elevacao_clicked)
                    self.horizontalSlider_buffer.valueChanged.connect(self.update_labels)
                    self.dem_info_textbox.setReadOnly(True)  # Prevent editing# Make it interactive
                    self.dem_info_textbox.setOpenExternalLinks(True)
                    self.dem_info_textbox.anchorClicked.connect(self.open_link)

                def open_link(self, url):
                    """Open the clicked link in the default web browser."""
                    print(f"Opening URL: {url.toString()}")
                    webbrowser.open(url.toString())

                def update_labels(self):
                    """Updates the text of several labels based on the values of horizontal
                    sliders."""
                    self.label_buffer.setText(f"{self.horizontalSlider_buffer.value()}m")

                def apply_buffer(self, aoi):
                    """Applies a buffer to the AOI geometry."""
                    buffer_distance = self.horizontalSlider_buffer.value()
                    print(f"Applying buffer of {buffer_distance} meters to AOI.")
                    if buffer_distance != 0:
                        print(f"Buffer distance: {buffer_distance} meters")
                        aoi = aoi.map(lambda feature: feature.buffer(buffer_distance))
                        #self.aoi = aoi
                        return aoi
                    else:
                        print("No buffer applied")
                        return aoi

                def update_dem_datasets(self):
                    print(list(self.dem_datasets.keys()))
                    self.dem_dataset_combobox.addItems(list(self.dem_datasets.keys()))
                    self.update_dem_info()

                def update_dem_info(self):
                    dem_name = self.dem_dataset_combobox.currentText()
                    dem_info = self.dem_datasets[dem_name]["Info"]
                    self.dem_info_textbox.setHtml(dem_info)
                    self.dem_resolution_combobox.clear()
                    self.dem_resolution_combobox.addItems([str(res) for res in self.dem_datasets[dem_name]["Resolution"]])

                def elevacao_clicked(self):
                    self.aoi = self.parent().aoi

                    # Validate that AOI is defined before proceeding
                    if self.aoi is None:
                        self.parent().pop_aviso("Error: No AOI defined. Please select a vector layer first.")
                        return

                    QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)  

                    try: 
                        self.elevacao_workflow()
                        QApplication.restoreOverrideCursor()
                    except Exception as e:
                        print(f"Error in elevacao_workflow: {e}")
                        QApplication.restoreOverrideCursor()
                        self.parent().pop_aviso(f"Error in elevation data processing: {e}")
                        return


                def elevacao_workflow(self):

                    # Assuming 'self.aoi' holds the Earth Engine FeatureCollection
                    # --- KEY CHANGE: Use updateMask for more reliable clipping ---
                    # Apply buffer if needed. Ensure self.aoi is an ee.Geometry object.
                    aoi = self.apply_buffer(self.aoi)

                    DEM_source_key = self.dem_dataset_combobox.currentText()
                    DEM_source_id = self.dem_datasets[DEM_source_key]["ID"]
                    DEM_resolution = int(self.dem_resolution_combobox.currentText())
                    print(f"Selected DEM source: {DEM_source_key} ({DEM_source_id})", DEM_resolution)

                    # Replace invalid characters in DEM source ID for filenames
                    safe_dem_source_id = DEM_source_id.replace("/", "_").replace("\\", "_")

                    # Fetch DEM image based on selected source
                    if DEM_source_id == 'COPERNICUS/DEM/GLO30':
                        dem = ee.ImageCollection(DEM_source_id).select('DEM').mosaic().clip(aoi)
                    elif DEM_source_id == 'JAXA/ALOS/AW3D30/V3_2':
                        dem = ee.ImageCollection(DEM_source_id).select('DSM').mosaic().clip(aoi)
                    elif DEM_source_id == 'NASA/NASADEM_HGT/001':
                        dem = ee.Image(DEM_source_id).select('elevation').clip(aoi)
                    elif DEM_source_id == 'USGS/GMTED2010_FULL':
                        dem = ee.Image(DEM_source_id).select('min').clip(aoi)
                    elif DEM_source_id == 'ASTER/ASTGTM':
                        dem = ee.Image(DEM_source_id).select('elevation').clip(aoi)
                    else:
                        dem = ee.Image(DEM_source_id).clip(aoi).select('elevation')

                    # Create a temporary file to store the downloaded DEM
                    with tempfile.NamedTemporaryFile(suffix=".tif", delete=False) as tmp_file:
                        temp_output_file = tmp_file.name

                    final_image = dem.toFloat()


                    # 1. Create an explicit mask from the AOI geometry.
                    # ee.Image(1).clip(aoi) creates an image where the AOI is 1 and outside is 0.
                    # .mask() converts this to a mask where 1 is valid data and 0 is NoData.
                    mask = ee.Image(1).clip(aoi).mask()

                    # 2. Apply this mask to the final composite image.
                    # This operation sets pixels outside the mask to NoData (transparent).
                    final_image_masked = final_image.updateMask(mask)

                    # 3. Define the download region using the BOUNDING BOX of the AOI.
                    # This ensures the downloaded GeoTIFF is a rectangle that fully covers the AOI.
                    # The actual clipping to the irregular shape is handled by updateMask.
                    download_region = aoi.first().geometry().bounds().getInfo()

                    try:
                        url = final_image_masked.getDownloadUrl({
                            'scale': DEM_resolution,
                            'region': download_region,
                            'format': 'GeoTIFF'
                        })

                        response = requests.get(url)
                        # Ensure the downloaded DEM has a .tif extension
                        output_path = self.parent().get_unique_filename(f"{safe_dem_source_id}.tif")
                        with open(output_path, 'wb') as file:
                            file.write(response.content)
                        print(f"DEM image downloaded to: {output_path}")

                        self._load_raster_to_canvas(output_path, f'{safe_dem_source_id}')

                    except Exception as e:
                        print(f"Error downloading DEM image: {e}")
                        self.parent().pop_aviso(f"Error downloading DEM image: {e}")
                        return

                def _load_raster_to_canvas(self, raster_path, layer_name):
                    """Loads a raster with single band pseudocolor rendering (Magma style) to the QGIS canvas,
                    dynamically determining the data range. This version does not perform any clipping."""
                    raster_layer = QgsRasterLayer(raster_path, layer_name)
                    if not raster_layer.isValid():
                        print(f"Failed to load raster layer from '{raster_path}'.")
                        return

                    # Get min and max values from the raster
                    provider = raster_layer.dataProvider()
                    stats = provider.bandStatistics(1)
                    min_val = stats.minimumValue
                    max_val = stats.maximumValue

                    print(f"Using data range {min_val} to {max_val} for rendering.")

                    QgsProject.instance().addMapLayer(raster_layer, False)
                    root = QgsProject.instance().layerTreeRoot()
                    root.insertChildNode(0, QgsLayerTreeLayer(raster_layer))
                    print("Raster layer loaded successfully!")

                    # Create a color ramp shader
                    color_ramp_shader = QgsColorRampShader()
                    color_ramp_shader.setColorRampType(QgsColorRampShader.Interpolated)

                    # Load the predefined "Magma" color ramp from the QGIS style manager
                    style = QgsStyle().defaultStyle()
                    color_ramp = style.colorRamp('Magma')

                    # Check if the color ramp is successfully loaded
                    if color_ramp:
                        # Define the number of color stops (adjust as needed)
                        num_stops = 5
                        step = (max_val - min_val) / (num_stops - 1)

                        # Create color ramp items using the actual data range
                        color_ramp_items = []
                        for i in range(num_stops):
                            value = min_val + i * step
                            color = color_ramp.color(i / (num_stops - 1))  # Interpolates color along the ramp
                            color_ramp_items.append(QgsColorRampShader.ColorRampItem(value, color))

                        # Set the color ramp items to the color ramp shader
                        color_ramp_shader.setColorRampItemList(color_ramp_items)
                    else:
                        print("Color ramp 'Magma' not found in the QGIS style library.")
                        return  # Exit if the color ramp is not found

                    # Create a raster shader and set it to use the color ramp shader
                    raster_shader = QgsRasterShader()
                    raster_shader.setRasterShaderFunction(color_ramp_shader)

                    # Apply the raster shader to the raster layer renderer
                    renderer = QgsSingleBandPseudoColorRenderer(raster_layer.dataProvider(), 1, raster_shader)

                    # Set the classification range to match the data range
                    renderer.setClassificationMin(min_val)
                    renderer.setClassificationMax(max_val)

                    raster_layer.setRenderer(renderer)

                    # Refresh the layer to update the visualization
                    raster_layer.triggerRepaint()


            dialog = EasyDialog(self)
            dialog.exec()

        except Exception as e:
            print(f"Failed to open easy UI: {e}")
            try:
                self.pop_warning("Unable to open help dialog.")
            except Exception:
                pass
    
    def open_sysi(self):
        print(self.mMapLayerComboBox.currentLayer())

        # if self.language == "pt":
        #     ui_path = os.path.join(os.path.dirname(__file__), "ui", "sysi_pt.ui")
        # else:
        #     ui_path = os.path.join(os.path.dirname(__file__), "ui", "sysi.ui")

        ui_path = os.path.join(os.path.dirname(__file__), "ui", "sysi.ui")

        EasyForm, _ = uic.loadUiType(ui_path)

        class SysiDialog(QDialog, EasyForm):
            def __init__(self, parent=None):
                super(SysiDialog, self).__init__(parent)

                    # Assuming parent has an 'aoi' attribute

                #self.language = QSettings().value("locale/userLocale", "en")[0:2]

                self.setupUi(self)
                # Make it non-modal and allow interaction with QGIS
                self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint)
                self.setModal(False)
                # If the UI has a close/ok button, try to connect it
                for name in ("closeButton", "btnClose", "okButton", "buttonBox"):
                    if hasattr(self, name):
                        widget = getattr(self, name)
                        try:
                            # buttonBox is a QDialogButtonBox
                            if isinstance(widget, QDialogButtonBox):
                                widget.accepted.connect(self.accept)
                                widget.rejected.connect(self.reject)
                            else:
                                widget.clicked.connect(self.close)
                        except Exception:
                            pass
                self.QPushButton_sysi.clicked.connect(self.sysi_clicked)
                self.horizontalSlider_buffer.valueChanged.connect(self.update_labels)
                self.dem_info_textbox.setReadOnly(True)  # Prevent editing# Make it interactive
                self.dem_info_textbox.setOpenExternalLinks(True)
                self.dem_info_textbox.anchorClicked.connect(self.open_link)
                today = datetime.datetime.today().strftime("%Y-%m-%d")
                since = "2017-03-28"
                self.dateEdit_final.setDate(QDate.fromString(today, "yyyy-MM-dd"))
                self.dateEdit_inicio.setDate(QDate.fromString(since, "yyyy-MM-dd"))

                self.horizontalSlider_ndvi_inf.valueChanged.connect(self.update_labels)
                self.horizontalSlider_ndvi_sup.valueChanged.connect(self.update_labels)
                self.horizontalSlider_nbr_inf.valueChanged.connect(self.update_labels)
                self.horizontalSlider_nbr_sup.valueChanged.connect(self.update_labels)
                self.horizontalSlider_clouds.valueChanged.connect(self.update_labels)



            def open_link(self, url):
                """Open the clicked link in the default web browser."""
                print(f"Opening URL: {url.toString()}")
                webbrowser.open(url.toString())

            def update_labels(self):
                """Updates the text of several labels based on the values of horizontal
                sliders."""
                self.label_buffer.setText(f"{self.horizontalSlider_buffer.value()}m")
                self.Qlabel_ndvi_inf.setText(f"{-self.horizontalSlider_ndvi_inf.value()/100:.2f}")
                self.Qlabel_ndvi_sup.setText(f"{self.horizontalSlider_ndvi_sup.value()/100:.2f}")
                self.Qlabel_nbr_inf.setText(f"{-self.horizontalSlider_nbr_inf.value()/100:.2f}")
                self.Qlabel_nbr_sup.setText(f"{self.horizontalSlider_nbr_sup.value()/100:.2f}")
                self.Qlabel_clouds.setText(f"{self.horizontalSlider_clouds.value()}%")

            def pop_aviso(self, aviso):
                QApplication.restoreOverrideCursor()
                msg = QMessageBox(parent=self)
                msg.setWindowTitle("Alerta!")
                msg.setIcon(QMessageBox.Icon.Warning)
                msg.setText(aviso)
                msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel)
                ret = msg.exec()  # Get the result of the dialog

            def apply_buffer(self, aoi):
                """Applies a buffer to the AOI geometry."""
                buffer_distance = self.horizontalSlider_buffer.value()
                print(f"Applying buffer of {buffer_distance} meters to AOI.")
                if buffer_distance != 0:
                    print(f"Buffer distance: {buffer_distance} meters")
                    aoi = aoi.map(lambda feature: feature.buffer(buffer_distance))
                    #self.aoi = aoi
                    return aoi
                else:
                    print("No buffer applied")
                    return aoi

            def sysi_clicked(self):
                self.aoi = self.parent().aoi

                # Validate that AOI is defined before proceeding
                if self.aoi is None:
                    self.pop_aviso("Error: No AOI defined. Please select a vector layer first.")
                    return

                QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)  

                try: 
                    self.soil_image()
                    QApplication.restoreOverrideCursor()
                except Exception as e:
                    print(f"Error in soil_image: {e}")
                    QApplication.restoreOverrideCursor()
                    self.pop_aviso(f"Error in soil image processing: {e}")
                    return

            def soil_image(self):
                print("Starting SYSI soil image processing...")

                # --- Definição da Área de Interesse ---
                aoi2 = self.aoi.geometry().bounds()

                # --- Definição de Variáveis e Listas ---
                s2_names = ['B2', 'B3', 'B4', 'B6', 'B8','B11','B12','QA60']
                b_names = ['Blue', 'Green', 'Red','Rededge', 'NIR', 'SWIR1', 'SWIR2', 'QA60']

                # --- Funções de Processamento ---

                def maskS2clouds(image):
                    qa = image.select('QA60')
                    # Bits 10 e 11 são nuvens e cirrus
                    cloudBitMask = 1 << 10
                    cirrusBitMask = 1 << 11
                    # Ambos devem ser zero
                    mask = qa.bitwiseAnd(cloudBitMask).eq(0).And(qa.bitwiseAnd(cirrusBitMask).eq(0))
                    return image.updateMask(mask)

                def add_indexes(img):
                    ndvi = img.normalizedDifference(['NIR', 'Red']) 
                    nbr = img.normalizedDifference(['SWIR1', 'SWIR2'])
                    grbl = img.select('Green').subtract(img.select('Blue'))
                    regr = img.select('Red').subtract(img.select('Green'))
                    
                    img = img.addBands(ndvi.rename('NDVI')) \
                            .addBands(nbr.rename('NBR2')) \
                            .addBands(grbl.rename('GRBL')) \
                            .addBands(regr.rename('REGR'))
                    return img

                def applyScaleFactors(image):
                    opticalBands = image.select(b_names).divide(10000)
                    return image.addBands(opticalBands, None, True)

                def addGEOS3Mask(img, options):
                    # Extração de opções do dicionário
                    rescale_flag = options.get('rescale_flag', 0)
                    ndvi_thres = options.get('ndvi_thres', [-0.25, 0.25])
                    nbr_thres = options.get('nbr_thres', [-0.3, 0.1])
                    vnsir_thres = options.get('vnsir_thres', 0.9)

                    if rescale_flag == 1:
                        img = img.divide(10000)

                    # Índice de tendência Visible-to-shortwave-infrared
                    vnsir = ee.Image(1).subtract(
                        ee.Image(2).multiply(img.select('Red'))
                        .subtract(img.select('Green')).subtract(img.select('Blue'))
                        .add(ee.Image(3).multiply(img.select('NIR').subtract(img.select('Red'))))
                    )
                            
                    # Lógica GEOS3
                    geos3 = img.select('NDVI').gte(ndvi_thres[0]).And(img.select('NDVI').lte(ndvi_thres[1])) \
                        .And(img.select('NBR2').gte(nbr_thres[0]).And(img.select('NBR2').lte(nbr_thres[1]))) \
                        .And(vnsir.lte(vnsir_thres)) \
                        .And(img.select('GRBL').gt(0)).And(img.select('REGR').gt(0))
                            
                    img = img.addBands(geos3.rename('GEOS3'))
                    return img

                def maskByGEOS3(image):
                    # Cria máscara onde GEOS3 é verdadeiro (1)
                    mask_geos3 = image.select('GEOS3').eq(1)
                    # Removendo linhas pretas/bordas
                    mask_swir = image.select('SWIR2').gte(0)
                    mask_green = image.select('Green').gte(0)
                    mask_red = image.select('Red').gte(0)
                    mask_blue = image.select('Blue').gte(0)
                    
                    mask = mask_geos3.And(mask_swir).And(mask_green).And(mask_red).And(mask_blue)
                    return image.updateMask(mask)
                
                # def maskByndvi(image):
                #     mask_ndvi_v1 = image.select('NDVI').gte(0.00)
                #     mask_ndvi_v2 = image.select('NDVI').lte(0.20)
                #     mask = mask_ndvi_v1.And(mask_ndvi_v2)
                #     return image.updateMask(mask)

                def calculateMean(image, region):
                    return image.reduceRegion(
                        reducer=ee.Reducer.mean(),
                        geometry=region,
                        scale=10,
                        maxPixels=1e8
                    )

                # --- Fluxo Principal de Execução ---
                start = self.dateEdit_inicio.date().toString("yyyy-MM-dd")
                end = self.dateEdit_final.date().toString("yyyy-MM-dd")
                cloud_threshold = self.horizontalSlider_clouds.value()

                # 1. Carregar Coleção e Filtragem Inicial
                dataset_s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED') \
                    .filterDate(start, end) \
                    .filterBounds(aoi2) \
                    .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', cloud_threshold)) \
                    .map(maskS2clouds) \
                    .select(s2_names, b_names)
                
                jan = self.checkBox_1.isChecked()
                fev = self.checkBox_2.isChecked()
                mar = self.checkBox_3.isChecked()
                abr = self.checkBox_4.isChecked()
                mai = self.checkBox_5.isChecked()
                jun = self.checkBox_6.isChecked()
                jul = self.checkBox_7.isChecked()
                ago = self.checkBox_8.isChecked()
                set = self.checkBox_9.isChecked()
                out = self.checkBox_10.isChecked()
                nov = self.checkBox_11.isChecked()
                dez = self.checkBox_12.isChecked()
                # Filter collection by selected months (1 = Jan, 12 = Dec)
                selected_months = []
                if jan: selected_months.append(1)
                if fev: selected_months.append(2)
                if mar: selected_months.append(3)
                if abr: selected_months.append(4)
                if mai: selected_months.append(5)
                if jun: selected_months.append(6)
                if jul: selected_months.append(7)
                if ago: selected_months.append(8)
                if set: selected_months.append(9)
                if out: selected_months.append(10)
                if nov: selected_months.append(11)
                if dez: selected_months.append(12)

                print(f"Selected months for filtering: {selected_months}")
                if len(selected_months) == 0:
                    self.pop_aviso("Please select at least one month.")
                    return

                # Add month property and filter the collection
                def _add_month(image):
                    month = ee.Date(image.get('system:time_start')).get('month')
                    return image.set('month', month)

                dataset_s2 = dataset_s2.map(_add_month)
                dataset_s2 = dataset_s2.filter(ee.Filter.inList('month', ee.List(selected_months)))

                # 2. Adicionar Índices e Fatores de Escala
                s2_indexes = dataset_s2.map(add_indexes)
                s2_indexes = s2_indexes.map(applyScaleFactors)

                # 4. Aplicação da Máscara GEOS3 na coleção
                # Opções fixas conforme o script original
                geos3_options = {
                    'rescale_flag': 0, 
                    'ndvi_thres': [-self.horizontalSlider_ndvi_inf.value()/100, self.horizontalSlider_ndvi_sup.value()/100], #-1 - 1
                    'nbr_thres': [-self.horizontalSlider_nbr_inf.value()/100, self.horizontalSlider_nbr_sup.value()/100], #-0.5 - 1.3
                    'vnsir_thres': 0.9
                }

                sent_collection_geos3 = s2_indexes.map(lambda img: addGEOS3Mask(img, geos3_options))

                # 5. Obter a mediana dos pixels (TESS)
                tess_v1_collection = sent_collection_geos3.map(maskByGEOS3)
                tess_v1 = tess_v1_collection.median()

                #tess_v2 = maskByndvi(tess_v1)


                # Etapa 6: Seleciona as bandas finais para o pós-processamento
                bands_to_export = ['Blue', 'Green', 'Red', 'Rededge', 'NIR', 'SWIR1', 'SWIR2', 'NDVI', 'NBR2']
                # Etapa 7: Chama o método de pós-processamento com a imagem final
                # self.sysi_processing(tess_v2.select(bands_to_export), "final v2")
                self.sysi_processing(tess_v1.select(bands_to_export))

            def sysi_processing(self, imageToDownload):
                print("Starting SYSI soil image download and load...")
                        
                final_image = imageToDownload.toFloat()

                # --- KEY CHANGE: Use updateMask for more reliable clipping ---
                # Apply buffer if needed. Ensure self.aoi is an ee.Geometry object.
                aoi = self.apply_buffer(self.aoi)

                # 1. Create an explicit mask from the AOI geometry.
                # ee.Image(1).clip(aoi) creates an image where the AOI is 1 and outside is 0.
                # .mask() converts this to a mask where 1 is valid data and 0 is NoData.
                mask = ee.Image(1).clip(aoi).mask()

                # 2. Apply this mask to the final composite image.
                # This operation sets pixels outside the mask to NoData (transparent).
                final_image_masked = final_image.updateMask(mask)

                # 3. Define the download region using the BOUNDING BOX of the AOI.
                # This ensures the downloaded GeoTIFF is a rectangle that fully covers the AOI.
                # The actual clipping to the irregular shape is handled by updateMask.
                download_region = aoi.geometry().bounds().getInfo()["coordinates"]


                # Download the selected composite (all selected bands) and load as a raster layer
                try:
                    try:
                        url = final_image_masked.getDownloadURL(
                            {
                                "scale": 10,
                                "region": download_region,
                                "format": "GeoTIFF",
                                "crs": "EPSG:4326",
                            }
                        )
                    except Exception as e:
                        self.parent().pop_warning(f"Failed to generate download URL: {e}")
                        return

                    base_output_file = f"Sentinel2_SYSI.tiff"
                    output_file = self.parent().get_unique_filename(base_output_file, temporary=False)

                    try:
                        response = requests.get(url, stream=True)
                        response.raise_for_status()
                        with open(output_file, "wb") as f:
                            for chunk in response.iter_content(chunk_size=8192):
                                if chunk:
                                    f.write(chunk)
                        print(f"Image downloaded to {output_file}")
                    except requests.exceptions.RequestException as e:
                        self.parent().pop_warning(f"Error downloading image: {e}")
                        return

                    try:
                        # Add descriptive band names to the downloaded GeoTIFF (same technique as load_rgb)
                        src_ds = gdal.Open(output_file)
                        if not src_ds:
                            self.parent().pop_warning(f"Failed to open the downloaded file: {output_file}")
                            return

                        band_count = src_ds.RasterCount
                        driver = gdal.GetDriverByName("GTiff")
                        temp_file = output_file + "_temp.tiff"
                        dst_ds = driver.CreateCopy(temp_file, src_ds, strict=1)

                        # Expected SYSI band order used earlier in the workflow
                        band_names = ["Blue", "Green", "Red", "Rededge", "NIR", "SWIR1", "SWIR2", "NDVI", "NBR2"]

                        for i in range(min(band_count, len(band_names))):
                            band = dst_ds.GetRasterBand(i + 1)  # 1-based indexing
                            try:
                                band.SetDescription(band_names[i])
                            except Exception:
                                # ignore per-band failures but continue
                                pass

                        # Close datasets to flush changes
                        dst_ds = None
                        src_ds = None

                        # Replace original file with the modified one
                        os.remove(output_file)
                        os.rename(temp_file, output_file)
                        print(f"Created GeoTIFF with named bands: {output_file}")

                    except Exception as e:
                        self.parent().pop_warning(f"Error modifying GeoTIFF: {e}")
                        print(traceback.format_exc())
                        try:
                            if os.path.exists(temp_file):
                                os.remove(temp_file)
                        except Exception:
                            pass

                    self.sysi_render_image(output_file)

                except Exception as e:
                    print(f"Error in soil_image download: {e}")
                    self.parent().pop_warning(f"An error occurred while exporting the soil image: {e}")

            def sysi_render_image(self, output_file):
                try:
                    """Loads the SYSI soil image with appropriate band rendering and contrast stretch."""
                    
                    # 1. Load the layer
                    layer_name = f"Sentinel-2 SYSI"
                    layer = QgsRasterLayer(output_file, layer_name)
                    if not layer.isValid():
                        self.parent().pop_warning(f"Failed to load the layer: {output_file}")
                        return

                    # 2. Add to QGIS Project (Essential for it to be visible)
                    QgsProject.instance().addMapLayer(layer, addToLegend=False)
                    root = QgsProject.instance().layerTreeRoot()
                    root.insertChildNode(0, QgsLayerTreeLayer(layer))
                    iface.setActiveLayer(layer)

                    # 3. Configure Contrast Stretch
                    provider = layer.dataProvider()
                    canvas = iface.mapCanvas()
                    extent = canvas.extent()
                    
                    # Safety Check: If zoomed out or panning, the canvas extent might not 
                    # cover the new layer. If they don't intersect, use the layer's full extent.
                    if not extent.intersects(layer.extent()):
                        extent = layer.extent()

                    # Define Bands (1=Blue, 2=Green, 3=Red)
                    red_band = 3
                    green_band = 2
                    blue_band = 1

                    # Initialize Renderer
                    renderer = QgsMultiBandColorRenderer(provider, red_band, green_band, blue_band)

                    # Loop through bands to apply Cumulative Cut (2% - 98%)
                    bands_config = [
                        (red_band, renderer.setRedContrastEnhancement),
                        (green_band, renderer.setGreenContrastEnhancement),
                        (blue_band, renderer.setBlueContrastEnhancement)
                    ]

                    for band_index, set_enhancement_func in bands_config:
                        # Calculate 2% and 98% stats. 
                        # FIX: cumulativeCut returns a simple tuple (min, max), not an object.
                        min_max_tuple = provider.cumulativeCut(band_index, 0.02, 0.98, extent, 250000)
                        
                        val_min = min_max_tuple[0]
                        val_max = min_max_tuple[1]
                        
                        # Create Contrast Enhancement Object
                        ce = QgsContrastEnhancement(provider.dataType(band_index))
                        
                        # Set Algorithm to "StretchToMinimumMaximum" using our calculated limits
                        ce.setContrastEnhancementAlgorithm(QgsContrastEnhancement.StretchToMinimumMaximum)
                        ce.setMinimumValue(val_min)
                        ce.setMaximumValue(val_max)
                        
                        # Apply to renderer
                        set_enhancement_func(ce)

                    # 4. Finalize and Refresh
                    layer.setRenderer(renderer)
                    layer.triggerRepaint() 
                    iface.layerTreeView().refreshLayerSymbology(layer.id())

                except Exception as e:
                    print(f"Error in soil_image render: {e}")
                    import traceback
                    print(traceback.format_exc())
                    self.parent().pop_warning(f"An error occurred while rendering the soil image: {e}")

        dialog = SysiDialog(self)
        dialog.exec()

    def open_aglgis(self):

        ui_path = os.path.join(os.path.dirname(__file__), "ui", "AGLgis_dialog_base.ui")

        EasyForm, _ = uic.loadUiType(ui_path)

        class AglgisDialog(QDialog, EasyForm):
            def __init__(self, parent=None):
                super(AglgisDialog, self).__init__(parent)

                self.setupUi(self)
                # Make it non-modal and allow interaction with QGIS
                self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint)
                self.setModal(False)
                # If the UI has a close/ok button, try to connect it
                for name in ("closeButton", "btnClose", "okButton", "buttonBox"):
                    if hasattr(self, name):
                        widget = getattr(self, name)
                        try:
                            # buttonBox is a QDialogButtonBox
                            if isinstance(widget, QDialogButtonBox):
                                widget.accepted.connect(self.accept)
                                widget.rejected.connect(self.reject)
                            else:
                                widget.clicked.connect(self.close)
                        except Exception:
                            pass

                # Initialize instance variables
                self.inicialize_variables()

                # Setup UI components and signal connections
                self.setup_ui()
                self.connect_signals()

                # Set default values and initial state
                self.last_clicked(3)  # Default to 3 months
                self.load_intro()
                self.tabWidget.setCurrentIndex(0)

                self.language = QSettings().value("locale/userLocale", "en")[0:2]
                
                # Initialize window size
                QTimer.singleShot(0, lambda: self.resizeEvent("small"))

            def load_intro(self):
                """Load the introduction HTML content into the text browser."""
                if QSettings().value("locale/userLocale", "en")[0:2] == "pt":
                    intro_path = os.path.join(os.path.dirname(__file__), "ui", "intro_pt.html")
                else:
                    intro_path = os.path.join(os.path.dirname(__file__), "ui", "intro.html")
                with open(intro_path, "r", encoding="utf-8") as f:
                    html_content = f.read()
                self.QTextBrowser.setHtml(html_content)

            # =========================================================================
            # INITIALIZATION METHODS
            # =========================================================================

            def inicialize_variables(self):
                """
                Initialize all instance variables to their default values.
                
                This method sets up all the necessary attributes for data processing,
                UI state management, and Earth Engine operations.
                """
                # Plotting and visualization
                self.plot1 = None
                
                # Authentication and settings
                self.autentication = False
                self.folder_set = False
                
                # Date range variables
                self.inicio = None
                self.final = None
                self.nuvem = None
                
                # Vector and geometry data
                self.vector_path = None
                self.aoi = None  # Area of Interest (Earth Engine geometry)
                self.aoi_checked = False
                self.selected_aoi_layer_path = None
                
                # Data processing variables
                self.df = None  # Main dataframe for time series
                self.df_aux = None  # Auxiliary dataframe for filtered data
                self.recorte_datas = None  # Date filtering
                self.selected_dates = []
                self.collection = None  # Earth Engine image collection
                
                # File system
                self.output_folder = None

            def setup_ui(self):
                """
                Perform initial UI component configuration.
                
                This method configures UI elements with appropriate settings,
                permissions, and default values.
                """
                # Configure text browser properties
                self.QTextBrowser.setReadOnly(True)
                self.QTextBrowser.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
                
                # Configure project ID input field
                self.project_QgsPasswordLineEdit.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal)
                
                # Populate year combo box with available years (Sentinel-1 data starts from 2017)
                current_year = datetime.datetime.now().year
                available_years = [str(year) for year in range(2017, current_year + 1)]
                self.combo_year.addItems(available_years)

            def connect_signals(self):
                """
                Connect UI signals to their respective handler methods.
                
                This method establishes all the signal-slot connections for the dialog,
                organizing them by functional areas for better maintainability.
                """
                # Authentication signals
                self.autenticacao.clicked.connect(lambda: authentication.auth(self))
                self.desautenticacao.clicked.connect(lambda: authentication.auth_clear(self))
                self.project_QgsPasswordLineEdit.textChanged.connect(
                    lambda new_text: authentication.autoSaveProjectId(self, new_text)
                )
                
                # Vector layer management signals
                self.update_vector.clicked.connect(self.update_vector_clicked)
                self.update_vector_2.clicked.connect(self.update_vector_clicked)
                self.vector_layer_combobox.currentIndexChanged.connect(self.get_selected_layer_path)
                self.vector_layer_combobox_2.currentIndexChanged.connect(self.combobox_2_update)
                
                # Tab navigation signals
                self.tabWidget.currentChanged.connect(self.on_tab_changed)
                
                # Image loading signals
                self.load_1index.clicked.connect(self.load_image)
                self.load_1index_preview.clicked.connect(lambda: self.load_image(True))
                self.load_batch.clicked.connect(self.load_batch_clicked)
                
                # Navigation button signals
                self.QPushButton_next.clicked.connect(self.next_clicked)
                self.QPushButton_next_2.clicked.connect(self.next_clicked)
                self.QPushButton_next_4.clicked.connect(self.next_clicked)
                self.QPushButton_back.clicked.connect(self.back_clicked)
                self.QPushButton_back_2.clicked.connect(self.back_clicked)
                self.QPushButton_back_4.clicked.connect(self.back_clicked)
                self.QPushButton_back_7.clicked.connect(self.back_clicked)
                self.QPushButton_skip.clicked.connect(lambda: self.tabWidget.setCurrentIndex(4))
                
                # Data processing signals
                self.loadtimeseries.clicked.connect(self.loadtimeseries_clicked)
                self.datasrecorte.clicked.connect(self.datasrecorte_clicked)
                self.salvar.clicked.connect(self.salvar_clicked)
                self.QTextBrowser.anchorClicked.connect(self.open_link)
                self.QTextBrowser.setOpenExternalLinks(False)
                self.setOpenLinks(False)
                
                # UI interaction signals
                self.navegador.clicked.connect(self.open_browser)
                self.drawing.stateChanged.connect(self.drawing_clicked)
                self.mQgsFileWidget.fileChanged.connect(self.on_file_changed)
                self.hybrid.clicked.connect(map_tools.hybrid_function)

                # Date range selection signals
                self.radioButton_all.clicked.connect(self.all_clicked)
                self.radioButton_3months.clicked.connect(lambda: self.last_clicked(3))
                self.radioButton_6months.clicked.connect(lambda: self.last_clicked(6))
                self.radioButton_12months.clicked.connect(lambda: self.last_clicked(12))
                self.radioButton_3years.clicked.connect(lambda: self.last_clicked(3 * 12))
                self.radioButton_5years.clicked.connect(lambda: self.last_clicked(5 * 12))
                self.combo_year.currentIndexChanged.connect(self.selected_year_clicked)
                
                # Date input synchronization signals
                self.incioedit.dateChanged.connect(self.reload_update)
                self.finaledit.dateChanged.connect(self.reload_update)

            def load_batch_clicked (self):
                length = self.dataunica.count()
                print(f"Number of dates in the collection: {length}")
                for i in range(length):
                    self.dataunica.setCurrentIndex(i)
                    print(f"Loading image for date: {self.dataunica.currentText()}")
                    self.load_image(preview=False)  # Load each image in download mode

            # =========================================================================
            # IMAGE LOADING AND PROCESSING
            # =========================================================================

            def load_image(self, preview=False):
                """
                Load a Sentinel-1 image based on the selected date.
                
                This method downloads and loads a single Sentinel-1 image from the
                processed collection for the selected date, allowing users to visualize
                individual images from their time series.
                
                Args:
                    preview (bool): If True, saves to temporary directory. 
                                If False, saves to user-specified output folder.
                """
                print("Loading image...")
                
                # Get selected date and prepare date range
                date = self.dataunica.currentText()
                next_date = (datetime.datetime.strptime(date, "%Y-%m-%d") + 
                            datetime.timedelta(days=1)).strftime("%Y-%m-%d")
                
                # Extract single image from collection
                selected_image = (self.collection
                                .filterDate(date, next_date)
                                .first()
                                .select(['VV', 'VH', 'VVVH_ratio'])
                                .clip(self.aoi))

                mask = ee.Image(1).clip(self.aoi).mask()

                # 2. Apply this mask to the final composite image.
                # This operation sets pixels outside the mask to NoData (transparent).
                final_image_masked = selected_image.updateMask(mask)

                # Prepare download URL for the clipped image
                url = final_image_masked.getDownloadURL({
                    "scale": 10,
                    "region": self.aoi.geometry().bounds().getInfo(),
                    "format": "GeoTIFF",
                    "crs": "EPSG:4326",
                })
                
                # Generate unique filename
                base_output_file = f"Sentinel1_{date}.tiff"
                output_file = self.get_unique_filename(base_output_file, temporary=preview)

                # Download the image
                response = requests.get(url)
                if response.status_code == 200:
                    with open(output_file, "wb") as f:
                        f.write(response.content)
                    print(f"Sentinel1 image downloaded as {output_file}")
                else:
                    print(f"Failed to download image. HTTP Status: {response.status_code}")
                    return

                # Generate unique layer name
                layer_name = f"Sentinel1 {date}"
                base_name = layer_name
                counter = 1
                while QgsProject.instance().mapLayersByName(layer_name):
                    layer_name = f"{base_name}_{counter}"
                    counter += 1
                print(f"Layer name adjusted to '{layer_name}' to ensure uniqueness.")

                # Load the downloaded image as a raster layer
                raster_layer = QgsRasterLayer(output_file, layer_name, "gdal")
                if not raster_layer.isValid():
                    print(f"Failed to load raster layer from {output_file}")
                    return
                
                # Configure raster layer properties
                raster_layer.setCrs(QgsCoordinateReferenceSystem("EPSG:4326"))
                QgsProject.instance().addMapLayer(raster_layer)
                print(f"Raster layer '{layer_name}' added to the project.")

            # =========================================================================
            # UI UPDATE AND SYNCHRONIZATION METHODS
            # =========================================================================

            def reload_update(self):
                """Synchronize date inputs between different tabs."""
                self.finaledit_2.setDate(self.finaledit.date())
                self.incioedit_2.setDate(self.incioedit.date())


            # =========================================================================
            # DATE RANGE SELECTION METHODS
            # =========================================================================

            def last_clicked(self, months):
                """
                Set date range to the last N months from today.
                
                Args:
                    months (int): Number of months to go back from today
                """
                today = datetime.datetime.today().strftime("%Y-%m-%d")
                start_date = (datetime.datetime.today() - 
                            relativedelta(months=months)).strftime("%Y-%m-%d")
                
                self.finaledit.setDate(QDate.fromString(today, "yyyy-MM-dd"))
                self.incioedit.setDate(QDate.fromString(start_date, "yyyy-MM-dd"))

            def all_clicked(self):
                """Set date range to cover all available Sentinel data (since 2014)."""
                today = datetime.datetime.today().strftime("%Y-%m-%d")
                since = "2014-04-13"  # Sentinel-1A launch date
                self.finaledit.setDate(QDate.fromString(today, "yyyy-MM-dd"))
                self.incioedit.setDate(QDate.fromString(since, "yyyy-MM-dd"))

            def selected_year_clicked(self):
                """
                Set the date range to the selected year from the combo box.
                
                This method updates both start and end dates to cover the entire
                selected year (January 1st to December 31st).
                """
                year = self.combo_year.currentText()
                start_date = f"{year}-01-01"
                end_date = f"{year}-12-31"
                self.incioedit.setDate(QDate.fromString(start_date, "yyyy-MM-dd"))
                self.finaledit.setDate(QDate.fromString(end_date, "yyyy-MM-dd"))

            # =========================================================================
            # VECTOR DRAWING AND GEOMETRY METHODS
            # =========================================================================

            def drawing_clicked(self):
                """
                Handle the drawing checkbox state change.
                
                When the checkbox is checked, activates the vector builder tool.
                When unchecked, deactivates the drawing tool and returns to pan mode.
                """
                print("Drawing clicked")
                if self.drawing.isChecked():
                    self.vector_builder()
                else:
                    # Deactivate the extent tool and return to pan mode
                    iface.mapCanvas().setMapTool(QgsMapToolPan(iface.mapCanvas()))

            def vector_builder(self):
                """
                Initialize the vector building process for drawing AOI.
                
                This method checks if an output folder is selected and then
                activates the extent drawing tool for creating vector layers.
                """
                if self.output_folder is None:
                    self.pop_warning("Please select an output folder first.")
                    return

                # Activate the extent drawing tool
                self.extent_tool = QgsMapToolExtent(iface.mapCanvas())
                self.extent_tool.extentChanged.connect(self.process_extent)
                iface.mapCanvas().setMapTool(self.extent_tool)
                print("Extent tool activated.")

            def process_extent(self, extent: QgsRectangle):
                """
                Process the drawn extent and save it as a vector layer.
                
                This method takes a drawn rectangle extent, transforms it to EPSG:4326,
                creates a vector layer, saves it to disk, and loads it into the project.
                
                Args:
                    extent (QgsRectangle): The drawn extent rectangle
                """
                # Transform the extent to EPSG:4326 for Earth Engine compatibility
                canvas_crs = iface.mapCanvas().mapSettings().destinationCrs()
                target_crs = QgsCoordinateReferenceSystem("EPSG:4326")
                transform = QgsCoordinateTransform(canvas_crs, target_crs, QgsProject.instance())
                extent_4326 = transform.transformBoundingBox(extent)

                # Create a temporary vector layer in memory
                layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "drawn_extent", "memory")
                pr = layer.dataProvider()

                # Add ID field to the layer
                pr.addAttributes([QgsField("id", QVariant.Int)])
                layer.updateFields()

                # Create a feature with the transformed extent geometry
                feature = QgsFeature()
                geometry = QgsGeometry.fromRect(extent_4326)
                feature.setGeometry(geometry)
                feature.setAttributes([1])
                pr.addFeature(feature)

                # Update layer extents
                layer.updateExtents()

                # Generate unique filename and directory structure
                shp_path, shp_name = self.get_subdirectory_filename("drawn_extent")
                print(f"Shapefile path: {shp_path}")
                print(f"Shapefile name: {shp_name}")

                # Configure save options for ESRI Shapefile
                save_options = QgsVectorFileWriter.SaveVectorOptions()
                save_options.driverName = "ESRI Shapefile"
                save_options.fileEncoding = "UTF-8"

                # Write the layer to disk
                QgsVectorFileWriter.writeAsVectorFormat(layer, shp_path, save_options)

                # Load the saved shapefile back into the project
                loaded_layer = QgsVectorLayer(shp_path, shp_name, "ogr")
                if loaded_layer.isValid():
                    # Verify and handle CRS if needed
                    if loaded_layer.crs().authid() != "EPSG:4326":
                        print("Warning: Layer CRS is not EPSG:4326, reprojecting...")
                        # Reproject to ensure EPSG:4326
                        params = {
                            'INPUT': loaded_layer,
                            'TARGET_CRS': target_crs,
                            'OUTPUT': 'memory:'
                        }
                        reprojected = processing.run("native:reprojectlayer", params)['OUTPUT']
                        loaded_layer = reprojected

                    # Add the layer to the project
                    QgsProject.instance().addMapLayer(loaded_layer)
                    print(f"Layer added successfully with CRS: {loaded_layer.crs().authid()}")
                    
                    # Update UI components and process the new layer
                    self.load_vector_layers()
                    self.get_selected_layer_path()
                    self.load_vector_function()
                    self.find_area()

            # =========================================================================
            # DATA EXPORT AND SAVING METHODS
            # =========================================================================

            def salvar_clicked(self):
                """
                Handle the save button click event.
                
                This method exports the processed time series data to a CSV file
                using the configured output folder and file naming conventions.
                """
                df = self.df_aux
                name = "Sentinel1_time_series.csv"
                save_utils.save(df, name, self)

            # =========================================================================
            # DATE SELECTION DIALOG METHODS
            # =========================================================================

            def datasrecorte_clicked(self):
                """
                Open a dialog for selecting specific dates for the time series.
                
                This method creates a hierarchical date selection dialog organized
                by years and months, allowing users to filter which dates to include
                in their time series analysis.
                """
                dialog = QDialog(self)
                dialog.setWindowTitle("Date Selection for Time Series")
                dialog.setGeometry(100, 100, 400, 500)

                layout = QVBoxLayout(dialog)

                # Create scrollable area for date checkboxes
                scroll_area = QScrollArea(dialog)
                scroll_area.setWidgetResizable(True)
                scroll_content = QWidget()
                scroll_layout = QVBoxLayout(scroll_content)

                # Initialize checkbox storage containers
                self.checkboxes = []
                self.group_checkboxes = {}  # Month group checkboxes
                self.year_checkboxes = {}   # Year group checkboxes
                self.group_widgets = {}     # Month group content widgets

                # Prepare date data for grouping
                self.df["dates"] = pd.to_datetime(self.df["dates"])
                grouped = self.df.groupby([self.df["dates"].dt.year, self.df["dates"].dt.month])

                # Process each year
                years = self.df["dates"].dt.year.unique()
                for year in sorted(years):
                    # Create a year-level widget / Cria um widget de nível de ano
                    year_widget = QWidget(dialog)
                    year_layout = QVBoxLayout(year_widget)

                    # Year-level checkbox (above all content for the year) / Checkbox
                    # de nível de ano (acima de todo o conteúdo para o ano)
                    year_checkbox = QCheckBox(f"Select All in {year}", dialog)
                    year_checkbox.setChecked(
                        True
                        if self.recorte_datas is None
                        else all(
                            str(date.date()) in self.recorte_datas
                            for date in self.df[self.df["dates"].dt.year == year]["dates"]
                        )
                    )
                    year_checkbox.stateChanged.connect(
                        lambda state, yr=year: self.toggle_year_checkboxes(yr, state)
                    )
                    scroll_layout.addWidget(year_checkbox)
                    self.year_checkboxes[year] = year_checkbox

                    # Indented content for the year / Conteúdo indentado para o ano
                    year_content_widget = QWidget(dialog)
                    year_content_layout = QVBoxLayout(year_content_widget)
                    year_content_layout.setContentsMargins(
                        20, 0, 0, 0
                    )  # Add indentation for year content
                    scroll_layout.addWidget(year_content_widget)

                    # Add months under each year / Adiciona meses sob cada ano
                    for (group_year, month), group in grouped:
                        if group_year != year:
                            continue

                        # Create a month-level widget / Cria um widget de nível de mês
                        group_label = f"{group_year}-{month:02d}"
                        month_widget = QWidget(dialog)
                        month_layout = QVBoxLayout(month_widget)

                        # Month toggle button / Botão de alternância do mês
                        month_toggle_button = QToolButton(dialog)
                        month_toggle_button.setText(f"▶ {group_label}")
                        month_toggle_button.setCheckable(True)
                        month_toggle_button.setChecked(True)
                        month_toggle_button.setStyleSheet("text-align: left;")
                        month_toggle_button.toggled.connect(
                            lambda checked, grp=month_widget, btn=month_toggle_button, lbl=group_label: self.toggle_group_visibility(
                                grp, btn, lbl
                            )
                        )

                        # Month-level checkbox / Checkbox de nível de mês
                        group_checkbox = QCheckBox(f"Select All in {group_label}", dialog)
                        group_checkbox.setChecked(
                            True
                            if self.recorte_datas is None
                            else all(
                                str(date.date()) in self.recorte_datas for date in group["dates"]
                            )
                        )
                        group_checkbox.stateChanged.connect(
                            lambda state, grp=group_label: self.toggle_group_checkboxes(
                                grp, state
                            )
                        )
                        month_layout.addWidget(group_checkbox)
                        self.group_checkboxes[group_label] = group_checkbox

                        # Add individual checkboxes with further indentation / Adiciona
                        # checkboxes individuais com mais indentação
                        for date in group["dates"]:
                            date_str = str(date.date())
                            checkbox = QCheckBox(date_str, dialog)
                            checkbox.setChecked(
                                True
                                if self.recorte_datas is None
                                else date_str in self.recorte_datas
                            )
                            month_layout.addWidget(checkbox)
                            checkbox.setContentsMargins(
                                20, 0, 0, 0
                            )  # Further indentation for dates
                            self.checkboxes.append((checkbox, group_label, group_year))

                        # Add month layout to the year content layout / Adiciona o layout
                        # do mês ao layout do conteúdo do ano
                        year_content_layout.addWidget(month_toggle_button)
                        year_content_layout.addWidget(month_widget)
                        self.group_widgets[group_label] = month_widget

                scroll_content.setLayout(scroll_layout)
                scroll_area.setWidget(scroll_content)
                layout.addWidget(scroll_area)

                # Buttons / Botões
                # Use QDialogButtonBox.StandardButton for Qt6 compatibility
                button_box = QDialogButtonBox(
                    QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, dialog
                )
                button_layout = QHBoxLayout()
                apply_button = QPushButton("Apply", dialog)
                select_button = QPushButton("Select All", dialog)
                deselect_button = QPushButton("Deselect All", dialog)
                button_layout.addWidget(apply_button)
                button_layout.addWidget(select_button)
                button_layout.addWidget(deselect_button)
                layout.addLayout(button_layout)
                layout.addWidget(button_box)

                # Signal Connections / Conexões de Sinal
                button_box.accepted.connect(dialog.accept)
                button_box.rejected.connect(dialog.reject)
                apply_button.clicked.connect(
                    self.apply_changes
                )  # Apply without closing / Aplica sem fechar
                select_button.clicked.connect(self.select_all_checkboxes)
                deselect_button.clicked.connect(self.deselect_all_checkboxes)

                if dialog.exec() == QDialog.DialogCode.Accepted:
                    self.apply_changes()  # Ensure changes are applied before closing
                else:
                    print("Time series dialog canceled. No changes made.")

            def apply_changes(self):
                """
                Apply changes to the selected dates for time series analysis.

                This method updates the selected dates based on the checked checkboxes
                in the date selection dialog and refreshes the time series plot accordingly.
                It does not close the dialog, allowing users to see the immediate effects
                of their selections.
                """
                self.selected_dates = [
                    cb.text() for cb, _, _ in self.checkboxes if cb.isChecked()
                ]
                self.recorte_datas = self.selected_dates
                print(f"Selected dates for time series (applied): {self.recorte_datas}")
                
                # Update the auxiliary dataframe and refresh the plot
                self.df_ajust()
                self.plot_timeseries()

            def df_ajust(self):
                """
                Adjust the main DataFrame based on the selected dates.
                
                This method filters the main dataframe to include only the dates
                selected by the user in the date selection dialog. The filtered
                data is stored in df_aux for further processing and visualization.
                """
                df = self.df.copy()
                if self.recorte_datas:
                    # Filter dataframe to include only selected dates
                    df = df[df["dates"].astype(str).isin([str(d) for d in self.recorte_datas])]
                    self.df_aux = df.copy()
                else:
                    # If no date filtering, use all data
                    self.df_aux = df.copy()

            def toggle_group_visibility(self, group_widget, toggle_button, group_label):
                """
                Toggle the visibility of a month group widget.

                This method shows or hides a month group's content and updates
                the toggle button's appearance accordingly.

                Args:
                    group_widget (QWidget): The widget representing the month group
                    toggle_button (QToolButton): The button used to toggle visibility
                    group_label (str): The label of the group (YYYY-MM format)
                """
                group_widget.setVisible(toggle_button.isChecked())
                toggle_button.setText(
                    f"▶ {group_label}" if not toggle_button.isChecked() 
                    else f"▼ {group_label}"
                )

            def toggle_group_checkboxes(self, group_label, state):
                """
                Toggle all checkboxes within a specific month group.

                Args:
                    group_label (str): The label of the month group (YYYY-MM format)
                    state (int): The checkbox state (Qt.Checked or Qt.Unchecked)
                """
                for checkbox, group, _ in self.checkboxes:
                    if group == group_label:
                        checkbox.setChecked(state == Qt.Checked)

            def toggle_year_checkboxes(self, year, state):
                """
                Toggle all checkboxes within a specific year group.

                This method affects all individual date checkboxes and month group
                checkboxes for the specified year.

                Args:
                    year (int): The year to toggle
                    state (int): The checkbox state (Qt.Checked or Qt.Unchecked)
                """
                # Toggle individual date checkboxes for the specified year
                for checkbox, _, group_year in self.checkboxes:
                    if group_year == year:
                        checkbox.setChecked(state == Qt.Checked)
                
                # Toggle month group checkboxes for the specified year
                for group_label, group_checkbox in self.group_checkboxes.items():
                    if group_label.startswith(str(year)):
                        group_checkbox.setChecked(state == Qt.Checked)

            def select_all_checkboxes(self):
                """Select all checkboxes in the date selection dialog."""
                for checkbox, _, _ in self.checkboxes:
                    checkbox.setChecked(True)
                for group_checkbox in self.group_checkboxes.values():
                    group_checkbox.setChecked(True)
                for year_checkbox in self.year_checkboxes.values():
                    year_checkbox.setChecked(True)

            def deselect_all_checkboxes(self):
                """Deselect all checkboxes in the date selection dialog."""
                for checkbox, _, _ in self.checkboxes:
                    checkbox.setChecked(False)
                for group_checkbox in self.group_checkboxes.values():
                    group_checkbox.setChecked(False)
                for year_checkbox in self.year_checkboxes.values():
                    year_checkbox.setChecked(False)

            # =========================================================================
            # WINDOW POSITIONING AND RESIZING METHODS
            # =========================================================================

            def centralizar(self):
                """
                Center the window on the current screen.

                This method calculates the geometry of the window frame and moves it to
                the center of the available screen space on the current screen without
                changing which screen the dialog is displayed on.
                """
                # Get the current geometry of the window frame
                qtRectangle = self.frameGeometry()
                
                # Determine the center point of the available screen space
                # Uses QApplication.primaryScreen() as replacement for deprecated QDesktopWidget
                screen = QApplication.screenAt(self.frameGeometry().center())
                if screen is None:
                    screen = QApplication.primaryScreen()
                centerPoint = screen.availableGeometry().center()
                
                # Move the center of the window frame to the center point of the screen
                qtRectangle.moveCenter(centerPoint)
                # Move the window to the new position
                self.move(qtRectangle.topLeft())

            def resizeEvent(self, size):
                """
                Handle window resize events with predefined size configurations.
                
                This method manages two window size modes: "small" for basic operations
                and "big" for data visualization and analysis. It locks the window
                size to prevent manual resizing.
                
                Args:
                    size (str): Either "small" or "big" to set the window size
                """
                # Remove any existing size constraints
                self.setMinimumSize(0, 0)
                self.setMaximumSize(16777215, 16777215)

                if size == "small":
                    # Small window for basic operations (tabs 0-4)
                    self.resize(765, 371)
                    self.setFixedSize(self.width(), self.height())
                elif size == "big":
                    # Large window for data visualization (tabs 5+)
                    self.resize(1145, 620)
                    self.setFixedSize(self.width(), self.height())

            # =========================================================================
            # TAB NAVIGATION AND CONTROL METHODS
            # =========================================================================

            def on_tab_changed(self, index):
                """
                Handle tab change events with comprehensive validation and setup.
                
                This method manages tab transitions, enforces step-by-step workflow,
                handles authentication checks, and performs tab-specific initialization.
                
                Args:
                    index (int): The index of the newly selected tab
                """
                # Prevent recursion during programmatic tab changes
                if getattr(self, '_programmatic_tab_change', False):
                    return

                def _safe_set_current_index(target_index):
                    """Safely change tab index without triggering recursive events."""
                    if not hasattr(self, 'tabWidget'):
                        return False

                    if self.tabWidget.currentIndex() != target_index:
                        self._programmatic_tab_change = True
                        try:
                            self.tabWidget.setCurrentIndex(target_index)
                        finally:
                            self._programmatic_tab_change = False
                        return True
                    return False

                # Authentication check - must be authenticated to access tabs beyond 0
                if not getattr(self, 'autentication', False) and index != 0:
                    QMessageBox.warning(self, "Access Denied", "Please authenticate first!")
                    _safe_set_current_index(0)
                    return

                # Handle window resizing based on tab
                if index < 5:
                    self.resizeEvent("small")
                else:
                    self.resizeEvent("big")
                    # Center window only once for large tabs
                    if not getattr(self, '_centralized', False):
                        self.centralizar()
                        self._centralized = True

                # Tab-specific initialization
                if index == 1 and not getattr(self, 'path_suggestion_loaded', False):
                    # Tab 1: Load output folder suggestion
                    try:
                        self.load_last_output_folder()
                    except Exception as e:
                        print(f"Error loading last output folder: {e}. Using suggestion.")
                        self.load_path_sugestion()
                    finally:
                        self.path_suggestion_loaded = True

                elif index == 2 and not getattr(self, 'vector_layers_loaded', False):
                    # Tab 2: Load vector layers
                    try:
                        self.load_vector_layers()
                        self.get_selected_layer_path()
                        self.vector_layers_loaded = True
                    except Exception as e:
                        print(f"Error loading vector layers: {e}")
                        # Keep vector_layers_loaded as False so it retries

                # Progression checks - enforce step-by-step workflow
                if index > 1:
                    next_button_4 = getattr(self, 'QPushButton_next_4', None)
                    if next_button_4 and not next_button_4.isEnabled():
                        QMessageBox.warning(self, "Proceed Step-by-Step", "Please complete Step 2 before proceeding.")
                        _safe_set_current_index(1)
                        return

                if index > 2:
                    next_button = getattr(self, 'QPushButton_next', None)
                    if next_button and not next_button.isEnabled():
                        QMessageBox.warning(self, "Proceed Step-by-Step", "Please complete Step 3 before proceeding.")
                        _safe_set_current_index(2)
                        self.resizeEvent("small")
                        return

                # Disable coordinate capture when not on tab 2
                if index != 2:
                    drawing_checkbox = getattr(self, 'drawing', None)
                    if drawing_checkbox:
                        try:
                            drawing_checkbox.setChecked(False)
                        except Exception as e:
                            print(f"Error unchecking coordinate capture checkbox: {e}")

            # =========================================================================
            # NAVIGATION BUTTON METHODS
            # =========================================================================

            def next_clicked(self):
                """Navigate to the next tab in the tab widget."""
                current_index = self.tabWidget.currentIndex()
                next_index = (current_index + 1) % self.tabWidget.count()
                self.tabWidget.setCurrentIndex(next_index)

            def back_clicked(self):
                """Navigate to the previous tab in the tab widget."""
                current_index = self.tabWidget.currentIndex()
                previous_index = (current_index - 1) % self.tabWidget.count()
                self.tabWidget.setCurrentIndex(previous_index)

            # =========================================================================
            # FILE AND FOLDER MANAGEMENT METHODS
            # =========================================================================

            def load_path_sugestion(self):
                """
                Load output folder suggestion based on the user's operating system.
                
                Sets the default output folder to the user's Downloads directory
                across different operating systems (Windows, Linux, macOS).
                """
                system = platform.system()
                if system == "Windows":
                    self.output_folder = os.path.join(os.environ["USERPROFILE"], "Downloads")
                elif system == "Linux":
                    self.output_folder = os.path.join(os.environ["HOME"], "Downloads")
                elif system == "Darwin":  # macOS
                    self.output_folder = os.path.join(os.environ["HOME"], "Downloads")
                else:
                    # Fallback for unknown systems
                    self.output_folder = os.path.expanduser("~/Downloads")

                # Set the suggested directory in the file widget
                self.mQgsFileWidget.setFilePath(self.output_folder)

            def pop_warning(self, message):
                """
                Display a warning dialog with the specified message.
                
                Args:
                    message (str): The warning message to display
                """
                QApplication.restoreOverrideCursor()
                msg = QMessageBox(self)
                msg.setWindowTitle("Warning!")
                msg.setIcon(QMessageBox.Icon.Warning)
                msg.setText(message)
                msg.setStandardButtons(QMessageBox.StandardButton.Ok)
                msg.button(QMessageBox.StandardButton.Ok).setText("OK")
                msg.setStyleSheet("font-size: 10pt;")
                msg.exec()

            # =========================================================================
            # VECTOR LAYER MANAGEMENT METHODS
            # =========================================================================

            def update_vector_clicked(self):
                """
                Update vector layers and reload related functions.
                
                This method refreshes the vector layer list, updates the selected
                layer path, and reloads vector-related functionality.
                """
                self.load_vector_layers()
                self.get_selected_layer_path()
                self.load_vector_function()

            def load_vector_layers(self):
                """
                Load and populate the combo box with available vector layers from the current QGIS project.
                
                This method retrieves all vector layers from the current QGIS project,
                filters them by geometry type, and populates the layer selection combo box.
                """
                # Get all layers from the current QGIS project
                layers = list(QgsProject.instance().mapLayers().values())

                # Filter for polygon and multipolygon vector layers only
                vector_layers = [
                    layer
                    for layer in layers
                    if layer.type() == QgsMapLayer.VectorLayer
                    and layer.geometryType() == QgsWkbTypes.PolygonGeometry
                ]

                # Get current layer names for comparison
                current_layer_names = set(
                    self.vector_layer_combobox.itemText(i)
                    for i in range(self.vector_layer_combobox.count())
                )

                # Clear combo boxes and layer ID dictionary
                self.vector_layer_combobox.clear()
                self.vector_layer_ids = {}

                # Populate combo box and track new layers
                new_layer_name = None
                for layer in vector_layers:
                    layer_name = layer.name()
                    self.vector_layer_combobox.addItem(layer_name)
                    self.vector_layer_ids[layer_name] = layer.id()

                    # Check if this is a newly added layer
                    if layer_name not in current_layer_names:
                        new_layer_name = layer_name

                # Synchronize the second combo box
                self.vector_layer_combobox_2.clear()
                for i in range(self.vector_layer_combobox.count()):
                    self.vector_layer_combobox_2.addItem(
                        self.vector_layer_combobox.itemText(i)
                    )
                self.vector_layer_combobox_2.setCurrentIndex(
                    self.vector_layer_combobox.currentIndex()
                )

                # If a new layer was found, select it automatically
                if new_layer_name:
                    index = self.vector_layer_combobox.findText(new_layer_name)
                    if index >= 0:
                        self.vector_layer_combobox.setCurrentIndex(index)

                # Handle case when no vector layers are available
                if self.vector_layer_combobox.count() == 0:
                    self.aoi = None
                    self.tabWidget.setCurrentIndex(2)
                    self.pop_warning(
                        "No vector layers found in the project. Please add a polygon or multipolygon layer to continue."
                    )

            def get_selected_layer_path(self):
                """
                Retrieve the path of the currently selected layer and trigger processing.
                
                This method gets the selected layer from the combo box, validates it,
                and initiates further processing including zooming to the layer and
                calculating its area.
                
                Returns:
                    str or None: The layer path if successful, None if no layer selected
                """
                # Get the currently selected layer name from the combo box
                layer_name = self.vector_layer_combobox.currentText().strip()
                
                if not layer_name:
                    print("No layer selected.")
                    self.aoi_area.setText("Total Area:")
                    self.QPushButton_next.setEnabled(False)
                    self.QPushButton_skip.setEnabled(False)
                    return None
                    
                print(f"Processing layer: '{layer_name}'")
                self.zoom_to_layer(layer_name)

                # Get the corresponding layer ID
                layer_id = self.vector_layer_ids.get(layer_name)
                print(f"Layer ID: {layer_id}")

                if layer_id is None:
                    print(f"Error: Layer ID not found for '{layer_name}'")
                    print(f"Available layers: {list(self.vector_layer_ids.keys())}")
                    return None

                # Get the layer using its ID
                layer = QgsProject.instance().mapLayer(layer_id)
                if layer:
                    print(f"Layer found: {layer.name()}, ID: {layer_id}")
                    # Extract the data source path from the layer
                    self.selected_aoi_layer_path = (
                        layer.dataProvider().dataSourceUri().split("|")[0]
                    )
                    print(f"Selected layer path: {self.selected_aoi_layer_path}")

                    # Process the layer and update UI
                    self.aoi = self.load_vector_function()
                    area = self.find_area()
                    self.QPushButton_next.setEnabled(True)
                    self.QPushButton_skip.setEnabled(True)
                    self.loadtimeseries.setEnabled(True)

                    return self.selected_aoi_layer_path
                else:
                    print(f"Layer '{layer_name}' with ID '{layer_id}' not found in project")
                    return None

            def zoom_to_layer(self, layer_name, margin_ratio=0.3):
                """
                Zoom the map canvas to the extent of the specified layer with optional margin.
                
                Args:
                    layer_name (str): Name of the layer to zoom to
                    margin_ratio (float): Fraction of the extent to add as margin (default 0.3 or 30%)
                """
                project = QgsProject.instance()
                layers = project.mapLayersByName(layer_name)

                if not layers:
                    print(f"Layer '{layer_name}' not found.")
                    return

                layer = layers[0]  # Use the first matching layer
                iface = qgis.utils.iface
                canvas = iface.mapCanvas()

                # Ensure the canvas CRS matches the layer CRS
                canvas.setDestinationCrs(layer.crs())

                # Get the layer's extent and add margin
                layer_extent = layer.extent()
                x_margin = layer_extent.width() * margin_ratio
                y_margin = layer_extent.height() * margin_ratio

                expanded_extent = QgsRectangle(
                    layer_extent.xMinimum() - x_margin,
                    layer_extent.yMinimum() - y_margin,
                    layer_extent.xMaximum() + x_margin,
                    layer_extent.yMaximum() + y_margin,
                )

                # Set the expanded extent and refresh canvas
                canvas.setExtent(expanded_extent)
                canvas.refresh()

                print(f"Zoomed to layer with margin: {expanded_extent.toString()}")

            # =========================================================================
            # FILE MANAGEMENT UTILITY METHODS
            # =========================================================================

            def get_subdirectory_filename(self, base_name, temporary=False):
                """
                Create a unique layer name with incrementing number and matching subdirectory.
                
                Args:
                    base_name (str): Base name for the layer and directory
                    temporary (bool): If True, use temporary directory instead of output folder
                    
                Returns:
                    str: Path to the unique filename
                """
                if temporary:
                    base_output_folder = tempfile.gettempdir()
                else:
                    base_output_folder = self.output_folder
                
                # Find a unique layer name with incrementing number
                counter = 1
                layer_name = f"{base_name}{counter}"
                subdirectory_path = os.path.join(base_output_folder, layer_name)
                
                # Keep incrementing until we find an unused name
                while os.path.exists(subdirectory_path):
                    counter += 1
                    layer_name = f"{base_name}{counter}"
                    subdirectory_path = os.path.join(base_output_folder, layer_name)
                
                # Create the directory if it doesn't exist
                while os.path.exists(subdirectory_path):
                    counter += 1
                    layer_name = f"{base_name}{counter}"
                    subdirectory_path = os.path.join(base_output_folder, layer_name)
                
                os.makedirs(subdirectory_path)
                
                # Create the filename with the same base name
                file_name = f"{layer_name}.shp"
                file_path = os.path.join(subdirectory_path, file_name)
                
                print(f"Unique layer name: {layer_name}")
                print(f"Unique filename: {file_path}")
                
                return file_path, layer_name

            def get_unique_filename(self, base_file_name, temporary=False):
                """
                Generate a unique filename by appending a counter if the file already exists.
                
                Args:
                    base_file_name (str): The base filename to make unique
                    temporary (bool): If True, use temporary directory instead of output folder
                    
                Returns:
                    str: Path to a unique filename
                """
                name, extension = os.path.splitext(base_file_name)
                
                if temporary:
                    output_folder = tempfile.gettempdir()
                    output_file = os.path.join(output_folder, base_file_name)
                else:
                    output_folder = self.output_folder
                    output_file = os.path.join(output_folder, base_file_name)
                    
                counter = 1
                while os.path.exists(output_file):
                    output_file = os.path.join(
                        output_folder, f"{name}_{counter}{extension}"
                    )
                    counter += 1

                print(f"Unique filename: {output_file}")
                return output_file

            def on_file_changed(self, file_path):
                """
                Handle file selection changes in the file widget.
                
                This method is called when the user selects a new file/folder path
                and updates the output folder accordingly.
                
                Args:
                    file_path (str): The selected file path
                """
                if self.mQgsFileWidget.filePath():
                    print(f"File selected: {file_path}")
                    self.output_folder = file_path
                    self.folder_set = True
                    self.QPushButton_next_4.setEnabled(True)
                    
                    # Save the selected file path to QGIS settings for persistence
                    QSettings().setValue("aglgis_plugin/last_output_folder", file_path)
                    print(f"Last output folder saved: {file_path}")
                else:
                    print("No file selected.")
                    self.folder_set = False
                    self.QPushButton_next_4.setEnabled(False)

            def load_last_output_folder(self):
                """
                Load the last selected output folder from QGIS settings.
                
                This method restores the previously used output folder to provide
                continuity between plugin sessions.
                """
                last_folder = QSettings().value("aglgis_plugin/last_output_folder", "")
                if last_folder:
                    self.mQgsFileWidget.setFilePath(last_folder)
                    self.output_folder = last_folder
                    self.folder_set = True
                    self.QPushButton_next_4.setEnabled(True)
                    print(f"Last output folder loaded: {last_folder}")
                else:
                    print("No previously selected output folder found.")

            # =========================================================================
            # EARTH ENGINE INTEGRATION METHODS
            # =========================================================================

            def load_vector_function(self, shapefile_path=None):
                print("Loading vector function...")


                shapefile_path = self.selected_aoi_layer_path
                print(f"Shapefile path: {shapefile_path}")

                if shapefile_path is None:
                    return None

                gpd_aoi = None # Use a distinct name for the geopandas DataFrame

                if shapefile_path.endswith(".zip"):
                    with zipfile.ZipFile(shapefile_path, "r") as zip_ref:
                        shapefile_within_zip = None
                        for file in zip_ref.namelist():
                            if file.lower().endswith(".shp"): # Use .lower() for case-insensitivity
                                shapefile_within_zip = file
                                break
                        if not shapefile_within_zip:
                            return None

                        # Read shapefile directly from the zip archive.
                        gpd_aoi = gpd.read_file(f"zip://{shapefile_path}/{shapefile_within_zip}")
                else:
                    gpd_aoi = gpd.read_file(shapefile_path)

                # Reproject the GeoDataFrame to EPSG:4326 to ensure correct
                # coordinates for Earth Engine.
                gpd_aoi = gpd_aoi.to_crs(epsg=4326)

                if gpd_aoi.empty:
                    raise ValueError(f"The shapefile at {shapefile_path} does not contain any geometries.")

                # Dissolve multiple features into a single geometry if necessary.
                if len(gpd_aoi) > 1:
                    gpd_aoi = gpd_aoi.dissolve()

                # Extract the first geometry.
                # It's safer to ensure there's at least one geometry before iloc[0]
                if gpd_aoi.empty: # Re-check after dissolve if it became empty (unlikely but robust)
                    raise ValueError(f"The shapefile at {shapefile_path} became empty after dissolve.")

                geometry = gpd_aoi.geometry.iloc[0]

                # Convert the geometry to GeoJSON format.
                geojson = geometry.__geo_interface__

                # Remove any third dimension from the coordinates.
                # Note: GeoJSON can support 3D, but Earth Engine often expects 2D.
                # This part of the logic is robust.
                if geojson["type"] == "Polygon":
                    geojson["coordinates"] = [
                        list(map(lambda coord: coord[:2], ring)) for ring in geojson["coordinates"]
                    ]
                elif geojson["type"] == "MultiPolygon":
                    geojson["coordinates"] = [
                        [list(map(lambda coord: coord[:2], ring)) for ring in polygon]
                        for polygon in geojson["coordinates"]
                    ]

                # Create an Earth Engine geometry object.
                ee_geometry = ee.Geometry(geojson)
                feature = ee.Feature(ee_geometry)
                ee_feature_collection = ee.FeatureCollection([feature])

                print("AOI defined successfully.")
                
                # Update UI elements
                self.QPushButton_next.setEnabled(True)
                self.QPushButton_skip.setEnabled(True)
                self.aoi_set = True
                self.vector_layer_combobox_2.setCurrentIndex(
                    self.vector_layer_combobox.currentIndex()
                )
                self.aoi_checked = True

                return ee_feature_collection


            def find_centroid(self):
                """
                Calculate and return the centroid coordinates and area of the AOI.
                
                Returns:
                    float: Area of the AOI in square kilometers
                """
                centroid = self.aoi.geometry().centroid()
                coordinates = centroid.getInfo().get("coordinates")
                self.lat = coordinates[1]
                self.lon = coordinates[0]
                print(f"Centroid: {round(self.lat, 4)}, {round(self.lon, 4)}")
                
                # Calculate area in square kilometers
                area = self.aoi.geometry().area().getInfo() / 1e6
                print(f"Area: {area:.2f} km²")
                return area

            def find_area(self):
                """
                Calculate and display the area of the AOI in multiple units.
                
                This method calculates the area in square kilometers and hectares,
                updates the UI label, and returns the area value.
                
                Returns:
                    float: Area of the AOI in square kilometers
                """
                try:
                    # Convert from square meters to square kilometers
                    area_km2 = self.aoi.geometry().area().getInfo() / 1e6
                    # Convert from square kilometers to hectares
                    area_ha = area_km2 * 100
                    
                    print(f"Area: {area_km2:.2f} km² ({area_ha:.2f} ha)")
                    self.aoi_area.setText(
                        f"Total Area: {area_km2:.2f} km² ({area_ha:.2f} hectares)"
                    )
                    return area_km2
                except Exception as e:
                    print(f"Error in find_area: {e}")
                    self.aoi_area.setText("Total Area:")
                    return 0

            def aoi_checked_function(self):
                """
                Check if both AOI and folder are set to enable progression buttons.
                
                This method validates that both the area of interest is checked and
                the output folder is set before enabling the next/skip buttons.
                """
                if self.aoi_checked and self.folder_set:
                    self.QPushButton_next.setEnabled(True)
                    self.QPushButton_skip.setEnabled(True)
                else:
                    self.QPushButton_next.setEnabled(False)
                    self.QPushButton_skip.setEnabled(False)

            def resetting(self):
                """
                Reset processing variables to their initial state.
                
                This method clears previously processed data and reloads the AOI
                to prepare for a new analysis run.
                """
                self.recorte_datas = None
                self.aoi = self.load_vector_function()
                self.inicio = self.incioedit.date().toString("yyyy-MM-dd")
                self.final = self.finaledit.date().toString("yyyy-MM-dd")
                self.df = None
                self.collection = None
                self.fig = None
                self.df_aux = None

            # =========================================================================
            # TIME SERIES PROCESSING METHODS
            # =========================================================================

            def loadtimeseries_clicked(self):
                """
                Handle the load time series button click event.
                
                This method initiates the complete time series processing workflow,
                including Earth Engine data processing, date loading, and visualization.
                """
                try:
                    QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
                    self.resetting()
                    self.collection = self.ee_process()
                    self.load_dates()
                    self.plot_timeseries()
                    self.next_clicked()
                except Exception as e:
                    print(f"Error during timeseries loading: {e}")
                    self.pop_warning(f"Error loading time series: {str(e)}")
                finally:
                    QApplication.restoreOverrideCursor()

            def ee_process(self):

                #self.cheching()
                """
                Process Earth Engine data to create a time series collection.
                
                This method configures and executes the Sentinel-1 ARD processing
                based on user-selected parameters and returns the processed collection.
                
                Returns:
                    ee.ImageCollection: Processed Sentinel-1 image collection with time series data
                """
                # Determine orbit direction from user selection
                orbit = self.QComboBox_orbit.currentText() != "DESCENDING"

                # Configure the S1ARD processor with user parameters

                processor = S1ARDImageCollection(
                    geometry=self.aoi,
                    start_date=self.inicio,
                    stop_date=self.final,
                    polarization=self.QComboBox_polarization.currentText(),
                    apply_border_noise_correction=self.QCheckBox_apply_border_noise_correction.isChecked(),
                    apply_terrain_flattening=self.QCheckBox_apply_terrain_flattening.isChecked(),
                    apply_speckle_filtering=self.QCheckBox_apply_speckle_filtering.isChecked(),
                    output_format=self.QComboBox_output.currentText(),
                    ascending=orbit  # False for descending orbit, True for ascending orbit
                )
                # print("AOI:", self.aoi.getInfo())
                # print("start date:", self.inicio)
                # print("end date:", self.final)
                # print("Polarization:", self.QComboBox_polarization.currentText())
                # print("Orbit ascending:", orbit)
                # print("Border noise correction:", self.QCheckBox_apply_border_noise_correction.isChecked())
                # print("Terrain flattening:", self.QCheckBox_apply_terrain_flattening.isChecked())
                # print("Speckle filtering:", self.QCheckBox_apply_speckle_filtering.isChecked())
                # print("Output format:", self.QComboBox_output.currentText())

                collection = processor.get_collection().sort('system:time_start')

                size = collection.size().getInfo()
                print(f"Number of images in collection: {size}")

                if size == 0:
                    self.pop_warning("No images found for the selected criteria.")
                    return None

                # Add VV/VH ratio band to each image
                def add_vvvh_ratio_band(image):
                    ratio = image.select("VV").divide(image.select("VH")).rename("VVVH_ratio")
                    return image.addBands(ratio)

                # Map the function to add the VV/VH ratio band
                try:
                    collection = collection.map(add_vvvh_ratio_band)
                except Exception as e:
                    print(f"Error adding VV/VH ratio band: {e}")
                    self.pop_warning(f"Error processing images: {str(e)}")
                    return None

                # Extract the VVVH_ratio band and calculate mean values for each image
                def get_vvvh_ratio_mean(image):
                    stats = image.select("VVVH_ratio").reduceRegion(
                        reducer=ee.Reducer.mean(),
                        geometry=self.aoi,
                        scale=10,
                        maxPixels=1e9
                    )
                    # Get image date
                    date = image.date().format('YYYY-MM-dd')
                    return ee.Feature(None, {
                        'date': date,
                        'VVVH_ratio_mean': stats.get('VVVH_ratio')
                    })

                # Map the function over the collection and get the results
                vvvh_ratio_ts = collection.map(get_vvvh_ratio_mean).getInfo()

                # Extract data for DataFrame creation
                data = [
                    {'dates': f['properties']['date'], 'AOI_average': f['properties']['VVVH_ratio_mean']}
                    for f in vvvh_ratio_ts['features']
                ]

                # Create and store DataFrames
                df = pd.DataFrame(data)
                #print(df)
                self.df = df.copy()
                self.df_aux = df.copy()

                return collection

            def df_adjust(self):
                """
                Adjust the main DataFrame based on selected dates.
                
                This method filters the DataFrame to include only the dates
                that have been selected by the user for analysis.
                """
                df = self.df.copy()
                if self.recorte_datas:
                    # Filter DataFrame to include only selected dates
                    df = df[df["dates"].astype(str).isin([str(d) for d in self.recorte_datas])]
                    self.df_aux = df.copy()
                else:
                    self.df_aux = df.copy()

            # =========================================================================
            # VISUALIZATION AND PLOTTING METHODS
            # =========================================================================

            def plot_timeseries(self):
                """
                Plot the time series data using Plotly.
                
                This method creates an interactive time series plot with customized
                configuration and styling for the VV/VH ratio data.
                """
                # Configure Plotly display options
                self.config = {
                    "displaylogo": False,
                    "modeBarButtonsToRemove": [
                        "toImage",
                        "sendDataToCloud",
                        "zoom2d",
                        "pan2d",
                        "select2d",
                        "lasso2d",
                        "zoomIn2d",
                        "zoomOut2d",
                        "autoScale2d",
                        "resetScale2d",
                        "hoverClosestCartesian",
                        "hoverCompareCartesian",
                        "zoom3d",
                        "pan3d",
                        "orbitRotation",
                        "tableRotation",
                        "resetCameraLastSave",
                        "resetCameraDefault3d",
                        "hoverClosest3d",
                        "zoomInGeo",
                        "zoomOutGeo",
                        "resetGeo",
                        "hoverClosestGeo",
                        "hoverClosestGl2d",
                        "hoverClosestPie",
                        "toggleHover",
                        "toggleSpikelines",
                        "resetViews",
                    ],
                }

                # Create the time series plot
                self.fig = px.line(
                    self.df_aux, 
                    x='dates', 
                    y='AOI_average', 
                    markers=True, 
                    title='VV/VH Time Series'
                )
                self.fig.update_layout(
                    xaxis_title='Date', 
                    yaxis_title='VV/VH Ratio'
                )

                # Render the plot in the web view
                self.QWebView.setHtml(
                    self.fig.to_html(include_plotlyjs="cdn", config=self.config)
                )

            def open_browser(self):
                """
                Open the time series plot in the default web browser.
                
                This method exports the current plot to an HTML file and opens it
                in the system's default web browser for better viewing.
                """
                if self.fig:
                    temp_file = tempfile.NamedTemporaryFile(
                        mode='w', suffix='.html', delete=False
                    )
                    temp_file.write(
                        self.fig.to_html(include_plotlyjs="cdn", config=self.config)
                    )
                    temp_file.close()
                    
                    # Open in default browser
                    webbrowser.open('file://' + temp_file.name)
                else:
                    self.pop_warning("No plot available to open in browser.")

            def load_dates(self):
                """
                Load and populate the date selection interface.
                
                This method extracts unique dates from the processed DataFrame
                and populates the date selection widgets for user interaction.
                """
                if self.df is not None and not self.df.empty:
                    # Get unique dates and sort them
                    dates = self.df['dates'].unique().astype(str).tolist()
                    self.available_dates = sorted(dates)
                    
                    # Populate the date selection combo box
                    self.dataunica.clear()
                    self.dataunica.addItems(dates)
                    # Set to the most recent date by default
                    self.dataunica.setCurrentIndex(self.dataunica.count() - 1)
                    
                    print(f"Available dates loaded: {len(self.available_dates)} dates")
                else:
                    print("No data available for date loading")
                    self.available_dates = []


                

        dialog = AglgisDialog(self)
        dialog.exec()
