"""
/***************************************************************************
 Class CDB4LoaderDialog

        This is a QGIS plugin for the CityGML 3D City Database.
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2021-09-30
        git sha              : $Format:%H$
        author(s)            : Giorgio Agugiaro
                               Konstantinos Pantelios
        email                : g.agugiaro@tudelft.nl                               
                               konstantinospantelios@yahoo.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
   Copyright 2021 Giorgio Agugiaro, Konstantinos Pantelios

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 *                                                                         *
 ***************************************************************************/
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from ...cdb_tools_main import CDBToolsMain
    from ..shared.dataTypes import CDBSchemaPrivs
    from ..gui_db_connector.other_classes import DBConnectionInfo
    from .other_classes import CDBLayer, CDBDetailView, FeatureType, EnumConfig, CodeListConfig

import os
from psycopg2.extensions import connection as pyconn

from qgis.core import Qgis, QgsMessageLog, QgsProject, QgsRectangle, QgsGeometry, QgsWkbTypes, QgsCoordinateReferenceSystem
from qgis.gui import QgsRubberBand, QgsMapCanvas, QgsMessageBar 
from qgis.PyQt import uic
from qgis.PyQt.QtCore import Qt, QThread
from qgis.PyQt.QtWidgets import QDialog, QMessageBox, QProgressBar, QVBoxLayout

from ..shared.dataTypes import BBoxType
from ..gui_db_connector.db_connector_dialog import DBConnectorDialog
from ..gui_geocoder.geocoder_dialog import GeoCoderDialog
from ..gui_db_connector.functions import conn_functions as conn_f
from ..shared.functions import general_functions as gen_f
from ..shared.functions import sql as sh_sql
from .functions import tab_conn_widget_functions as tc_wf
from .functions import tab_conn_functions as tc_f
from .functions import tab_layers_widget_functions as tl_wf
from .functions import tab_layers_functions as tl_f
from .functions import tab_settings_widget_functions as ts_wf
from .functions import canvas, sql, threads as thr
from .other_classes import DialogChecks, DefaultSettings
from . import loader_constants as c

# This loads the .ui file so that PyQt can populate the plugin with the elements from Qt Designer
FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), "ui", "cdb4_loader_dialog.ui"))

class CDB4LoaderDialog(QDialog, FORM_CLASS):
    """User Dialog class of the plugin.
    The GUI is imported from an external .ui xml
    """

    def __init__(self, cdbMain: CDBToolsMain, parent=None):
        """Constructor."""
        super(CDB4LoaderDialog, self).__init__(parent)
        # super().__init__()
        # Set up the user interface from Designer through FORM_CLASS.
        # After self.setupUi() you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots
        self.setupUi(self)

        ############################################################
        ## "Standard" variables or constants
        ############################################################

        self.PLUGIN_NAME: str = cdbMain.PLUGIN_NAME
        # Variable to store the qgis_pkg
        self.QGIS_PKG_SCHEMA: str = cdbMain.QGIS_PKG_SCHEMA

        # Variable to store the label of this dialog
        self.DLG_NAME_LABEL: str = cdbMain.MENU_LABEL_LOADER
        # Variable to store the variable name (in cdbMain) of this dialog
        self.DLG_NAME: str = cdbMain.DLG_NAME_LOADER

        # Variable to store the qgis_pkg_usrgroup_* associated to the current database.
        self.GROUP_NAME: str = None
        # Variable to store the selected cdb_schema name.
        self.CDB_SCHEMA: str = None
        # Variable to store the ADE prefix of the selected cdb_schema name.
        self.ADE_PREFIX: str = None
        # Variable to store the selected usr_schema name.
        self.USR_SCHEMA: str = None

        # Variable to store the current open connection of a database.
        self.conn: pyconn = None
        # Variable to store the existing connection parameters.
        self.DB: DBConnectionInfo = None

        self.msg_bar: QgsMessageBar
        self.bar: QProgressBar
        self.thread: QThread

        self.settings = DefaultSettings()
        self.checks = DialogChecks()

        ############################################################
        ## From here you can add your variables or constants
        ############################################################

        # Metadata Registries (dictionaries)
        # Variable to store metadata about the Feature Types (i.e. CityGML modules/packages) 
        # The availability is defined by the existence of at least one Feature of that Feature Type inside the current extents.
        self.FeatureTypesRegistry: dict[str, FeatureType] = {}
        # Variable to store metadata about the DetailViews (i.e. children tables in the forms) 
        self.DetailViewsRegistry: dict[str, CDBDetailView] = {}
        # Dictionary containing config data to set up enumeration combo boxes in the attribute forms
        self.EnumConfigRegistry: dict[str, EnumConfig] = {}
        # Dictionary containing config data to set up codelist combo boxes in the attribute forms
        self.CodeListConfigRegistry: dict[str, CodeListConfig] = {}
        # Variable to store the selected CodeListSet
        self.selectedCityGMLCodeListSet: str = None

        self.CDBSchemaPrivileges: str = None

        # QGIS current version
        self.QGIS_VERSION_STR: str = Qgis.version() 
        self.QGIS_VERSION_MAJOR: int = int(self.QGIS_VERSION_STR.split(".")[0])
        self.QGIS_VERSION_MINOR: int = int(self.QGIS_VERSION_STR.split(".")[1])

        # Variable to store the selected crs.
        self.CRS: QgsCoordinateReferenceSystem = None
        # self.CRS: QgsCoordinateReferenceSystem = cdbMain.iface.mapCanvas().mapSettings().destinationCrs()
        self.CRS_is_geographic: bool = None    # Will be True if we are using lon lat in the database, False if we use projected coordinates
 
        # Variable to store the selected extents.
        self.CURRENT_EXTENTS = cdbMain.iface.mapCanvas().extent()
        # Variable to store the extents of the selected cdb_schema
        self.CDB_SCHEMA_EXTENTS = QgsRectangle()
        # Variable to store the extents of the Layers
        self.LAYER_EXTENTS = QgsRectangle()
        # Variable to store the extents of the QGIS layers.
        self.QGIS_EXTENTS = QgsRectangle()

        # Variable to store an additional canvas (to show the extents in the CONNECTION TAB).
        self.CANVAS = QgsMapCanvas()
        self.CANVAS.enableAntiAliasing(True)
        self.CANVAS.setMinimumWidth(300)
        self.CANVAS.setMaximumHeight(350)

        # Variable to store a rubberband formed by the current extents.
        self.RUBBER_CDB_SCHEMA = QgsRubberBand(self.CANVAS, QgsWkbTypes.PolygonGeometry)
        self.RUBBER_LAYERS = QgsRubberBand(self.CANVAS, QgsWkbTypes.PolygonGeometry)

        # Variable to store an additional canvas (to show the extents in the LAYERS TAB).
        self.CANVAS_L = QgsMapCanvas()
        self.CANVAS_L.enableAntiAliasing(True)
        self.CANVAS_L.setMinimumWidth(300)
        self.CANVAS_L.setMaximumHeight(350)

        # Variable to store a rubberband formed by the current extents.
        # QgsWkbTypes.PolygonGeometry works from 3.22 till (at least) 3.34
        # Qgis.GeometryType.Polygon won't work in 3.22 and 3.28. Introduced in 3.32.
        self.RUBBER_CDB_SCHEMA_L = QgsRubberBand(self.CANVAS_L, QgsWkbTypes.PolygonGeometry)
        self.RUBBER_LAYERS_L = QgsRubberBand(self.CANVAS_L, QgsWkbTypes.PolygonGeometry)
        self.RUBBER_QGIS_L = QgsRubberBand(self.CANVAS_L, QgsWkbTypes.PolygonGeometry)

        # Enhance various Qt Objects with their initial text. 
        # This is used in order to revert to the original state in reset operations when original text has already changed.

        ### TAB Connection
        self.btnConnectToDb.init_text = c.btnConnectToDB_t
        self.btnRefreshCDBExtents.init_text = c.btnRefreshCDBExtents_t
        self.btnCityExtents.init_text = c.btnCityExtents_t
        self.btnCreateLayers.init_text = c.btnCreateLayers_t
        self.btnRefreshLayers.init_text = c.btnRefreshLayers_t
        # self.btnDropLayers.init_text = c.btnDropLayers_t

        ### TAB Layers
        self.lblInfoText.init_text = c.lblInfoText_t
        self.btnLayerExtentsL.init_text = c.btnLayerExtentsL_t
        self.ccbxLayers.init_text = c.ccbxFeatures_t

        ### SIGNALS (start) ############################

        #### 'User Connection' tab
        # 'Connection' group box signals
        self.cbxExistingConn.currentIndexChanged.connect(self.evt_cbxExistingConn_changed)
        self.btnNewConn.clicked.connect(self.evt_btnNewConn_clicked)

        # 'Database' group box signals
        self.btnConnectToDb.clicked.connect(self.evt_btnConnectToDb_clicked)
        self.cbxSchema.currentIndexChanged.connect(lambda: self.evt_cbxSchema_changed(cdbMain))

        # Basemap (OSM) group box signals
        # Link the additional canvas to the extents qgroupbox and enable "MapCanvasExtent" Button (Byproduct).
        self.qgbxExtents.setMapCanvas(canvas=self.CANVAS, drawOnCanvasOption=False)
        # Draw on Canvas tool is disabled. Check notes in tc_wf.qgbxExtents_setup()
        # 'Extents' groupbox signals
        self.btnRefreshCDBExtents.clicked.connect(self.evt_btnRefreshCDBExtents_clicked)

        self.btnCityExtents.clicked.connect(self.evt_btnCityExtents_clicked)
        self.CANVAS.extentsChanged.connect(self.evt_canvasC_ext_changed)
        self.qgbxExtents.extentChanged.connect(self.evt_qgbxExtents_ext_changed)
        self.btnGeoCoder.clicked.connect(self.evt_btnGeoCoder_clicked)

        self.gbxFeatSel.toggled.connect(self.evt_gbxFeatSel_toggled)

        self.btnCreateLayers.clicked.connect(self.evt_btnCreateLayers_clicked)
        self.btnRefreshLayers.clicked.connect(self.evt_btnRefreshLayers_clicked)
        self.btnDropLayers.clicked.connect(self.evt_btnDropLayers_clicked)

        self.btnCloseConn.clicked.connect(self.evt_btnCloseConn_clicked)

        #### 'Layer' tab
        # Link the addition canvas to the extents qgroupbox and enable "MapCanvasExtent" options (Byproduct).
        self.qgbxExtentsL.setMapCanvas(canvas=self.CANVAS_L, drawOnCanvasOption=False)
        # Draw on Canvas tool is disabled. Check Note on main>widget_setup>ws_layers_tab.py>qgbxExtentsL_setup

        # 'Extents' groupbox signals
        self.qgbxExtentsL.extentChanged.connect(self.evt_qgbxExtentsL_ext_changed)
        self.btnLayerExtentsL.clicked.connect(self.evt_btnLayerExtentsL_clicked)          

        # 'Parameters' groupbox signals
        self.cbxFeatureType.currentIndexChanged.connect(self.evt_cbxFeatureType_changed)
        self.cbxLod.currentIndexChanged.connect(self.evt_cbxLod_changed)

        # 'Features to Import' groupbox signals
        self.ccbxLayers.checkedItemsChanged.connect(self.evt_cbxLayers_changed)
        self.btnImport.clicked.connect(self.evt_btnImport_clicked)

        #### 'Settings' tab
        self.gbxGeomSimp.toggled.connect(self.evt_cbxGeoSimp_toggled)

        self.btnResetToDefault.clicked.connect(self.evt_btnResetToDefault_clicked)
        self.btnSaveSettings.clicked.connect(self.evt_btnSaveSettings_clicked)
        self.btnLoadSettings.clicked.connect(self.evt_btnLoadSettings_clicked)

        ### SIGNALS (end) ##############################


    ### Required functions BEGIN ############################

    def dlg_reset_all(self) -> None:
        """ Function that resets the whole dialog.
        """
        ts_wf.tabSettings_reset(dlg=self)
        tl_wf.tabLayers_reset(dlg=self)
        tc_wf.tabConnection_reset(dlg=self)

        return None


    def create_progress_bar(self, layout: QVBoxLayout, position: int) -> None:
        """Function that creates a QProgressBar embedded into a QgsMessageBar, in a specific position in the GUI.

        *   :param layout: QLayout of the gui where the bar is to be
                assigned.
            :type layout: QBoxLayout

        *   :param position: The place (index) in the layout to place
                the progress bar
            :type position: int
        """
        # Create QgsMessageBar instance.
        self.msg_bar = QgsMessageBar()

        # Add the message bar into the input layer and position.
        layout.insertWidget(position, self.msg_bar)

        # Create QProgressBar instance into QgsMessageBar.
        self.bar = QProgressBar(parent=self.msg_bar)

        # Setup progress bar.
        self.bar.setAlignment(Qt.AlignLeft|Qt.AlignVCenter)
        self.bar.setStyleSheet("text-align: left;")

        # Show progress bar in message bar.
        self.msg_bar.pushWidget(widget=self.bar, level=Qgis.MessageLevel.Info)


    def evt_update_bar(self, step: int, text: str) -> None:
        """Function to setup the progress bar upon update. Important: Progress Bar needs to be already created
        in self.msg_bar: QgsMessageBar and self.bar: QProgressBar.
        This event is not linked to any widet_setup function as it isn't responsible for changes in different 
        widgets in the GUI.

        *   :param dialog: The dialog to hold the bar.
            e.g. "admin_dlg" or "loader_dlg"
            :type step: str

        *   :param step: Current value of the progress
            :type step: int

        *   :param text: Text to display on the bar
            :type text: str
        """
        # Show text instead of completed percentage.
        if text:
            self.bar.setFormat(text)

        # Update progress with current step
        self.bar.setValue(step)

    ### Required functions END ############################

    ### EVENTS (start) ############################

    ## Events for 'User connection' tab BEGIN

    #'Connection' group box events (in 'User Connection' tab)
    def evt_cbxExistingConn_changed(self) -> None:
        """Event that is called when the 'Existing Connection' comboBox (cbxExistingConn) current index changes.
        This function runs every time the current selection of 'Existing Connection' changes.
        """
        # Set the current database connection object variable
        self.DB: DBConnectionInfo = self.cbxExistingConn.currentData()
        if not self.DB:
            return None

        # Reset the tabs
        tl_wf.tabLayers_reset(dlg=self)
        ts_wf.tabSettings_reset(dlg=self)
        tc_wf.tabConnection_reset(dlg=self)

        # Reset and (re)enable the "3D City Database" connection box and buttons
        self.gbxDatabase.setDisabled(False)   # Activate the group box
        self.btnConnectToDb.setText(self.btnConnectToDb.init_text.format(db=self.DB.database_name))  # set the label
        self.btnConnectToDb.setDisabled(False)  # Activate the button 
        self.lblConnectToDB.setDisabled(False)   # Activate the label

        # Close the current open connection.
        if self.conn:
            self.conn.close()


    def evt_btnNewConn_clicked(self) -> None:
        """Event that is called when the 'New Connection' pushButton
        (btnNewConn) is pressed.

        Responsible to add a new VALID connection to the 'Existing connections'.
        """
        # Create/Show/Execute additional dialog for the new connection
        dlgConnector = DBConnectorDialog()
        dlgConnector.setWindowModality(Qt.ApplicationModal) # i.e. the window is Modal to the application and blocks input to all other windows.
        dlgConnector.show()
        dlgConnector.exec_()

        # Add new connection to the existing connections
        if dlgConnector.conn_params:
            self.cbxExistingConn.addItem(f"{dlgConnector.conn_params.connection_name}", dlgConnector.conn_params)


    # 'Database' group box events (in 'User Connection' tab)
    def evt_btnConnectToDb_clicked(self) -> None:
        """Event that is called when the current 'Connect to {db}' pushButton
        (btnConnectToDb) is pressed. It sets up the GUI after a click signal is emitted.
        """
        msg: str = None

        # In 'Connection Status' groupbox
        self.gbxConnStatus.setDisabled(False) # Activate the connection status box (red/green checks)
        self.btnCloseConn.setDisabled(False) # Activate the close connection button at the bottom

        # -------------------------------------------------------------------------------------------
        # Series of tests to be carried out when I connect as user.
        #
        # 1) Can I connect to the database? If yes, continue
        # 2) Can I connect to the qgis_pkg (and access its functions?) If yes, continue.
        # 3) Is the installed QGIS package version compatible with this version of the plugin? If yes, continue
        # 4) Is my qgis_user schema installed? If yes, continue.
        # 5) Are there cdb_schemas I am allowed to connect to? If yes, continue
        # 6) Can I connect to at least one non-empty cdb_schema? If yes, continue
        # -------------------------------------------------------------------------------------------

        # 1) Can I connect to the database? If yes, continue
        # Attempt to connect to the database
        self.conn = conn_f.open_db_connection(db_connection=self.DB, app_name = self.PLUGIN_NAME)

        if self.conn:
            # Set self.DB.pg_server_version
            self.DB.pg_server_version = conn_f.get_posgresql_server_version(dlg=self)
            # Show database name
            self.lblConnToDb_out.setText(c.success_html.format(text=" AS ".join([self.DB.db_toc_node_label, self.DB.username])))
            self.checks.is_conn_successful = True

            if self.DB.pg_server_version:
                # Show server version
                self.lblPostInst_out.setText(c.success_html.format(text=self.DB.pg_server_version))
                self.checks.is_postgis_installed = True
            else:
                self.lblPostInst_out.setText(c.failure_html.format(text=c.PG_SERVER_FAIL_MSG))
                self.checks.is_postgis_installed = False
                return None # Exit

        else: # Connection failed!
            tc_wf.gbxConnStatus_reset(dlg=self)
            self.gbxConnStatus.setDisabled(False)
            self.lblConnToDb_out.setText(c.failure_html.format(text=c.CONN_FAIL_MSG))
            self.checks.is_conn_successful = False
            self.checks.is_postgis_installed = False

            msg = "The selected connection to the PostgreSQL server cannot be established. Please check whether it is still valid: the connection parameters may have to be updated!"
            QMessageBox.warning(self, "Connection error", msg)

            tl_wf.tabLayers_reset(dlg=self)
            ts_wf.tabSettings_reset(dlg=self)
            tc_wf.tabConnection_reset(dlg=self)

            # Close the current connection.
            if self.conn:
                self.conn.close()

            return None # Exit

        # 2) Can I connect to the qgis_pkg (and access its functions?) If yes, continue.

        # Check if the qgis_pkg schema (main installation) is installed in database.
        # This checks that:
        # a) I have been granted usage of the database AND
        # b) the QGIS Package has been indeed installed.
        is_qgis_pkg_installed = sh_sql.is_qgis_pkg_installed(dlg=self)

        if is_qgis_pkg_installed:
            # I can now access the functions of the qgis_pkg (at least the public ones)
            # Set the current user schema name.
            # self.USR_SCHEMA = sh_sql.create_qgis_usr_schema_name(dlg=self)
            pass
        else:
            self.lblMainInst_out.setText(c.failure_html.format(text=c.NO_DB_ACCESS_MSG))
            self.checks.is_qgis_pkg_installed = False

            msg = "The QGIS Package is either not installed in this database or you are not granted permission to use it.\n\nEither way, please contact your database administrator."
            QMessageBox.warning(self, "Unavailable QGIS Package", msg)

            tl_wf.tabLayers_reset(dlg=self)
            ts_wf.tabSettings_reset(dlg=self)
            tc_wf.tabConnection_reset(dlg=self)

            # Close the current open connection.
            if self.conn:
                self.conn.close()

            return None # Exit

        # 3) Is the installed QGIS package version compatible with this version of the plugin? If yes, continue

        # Get the current qgis_pkg version and check that it is compatible.
        # Named tuple: version, full_version, major_version, minor_version, minor_revision, code_name, release_date
        qgis_pkg_curr_version = sh_sql.get_qgis_pkg_version(dlg=self)

        # print(qgis_pkg_curr_version)
        qgis_pkg_curr_version_txt      : str = qgis_pkg_curr_version.version
        qgis_pkg_curr_version_major    : int = qgis_pkg_curr_version.major_version
        qgis_pkg_curr_version_minor    : int = qgis_pkg_curr_version.minor_version
        qgis_pkg_curr_version_minor_rev: int = qgis_pkg_curr_version.minor_revision

        # Only for testing purposes
        #qgis_pkg_curr_version_txt      : str = "0.7.3"
        #qgis_pkg_curr_version_major    : int = 0
        #qgis_pkg_curr_version_minor    : int = 7
        #qgis_pkg_curr_version_minor_rev: int = 3

        # Check that the QGIS Package version is >= than the minimum required for this versin of the plugin (see cdb4_constants.py)
        if all((qgis_pkg_curr_version_major == c.QGIS_PKG_MIN_VERSION_MAJOR, 
                qgis_pkg_curr_version_minor == c.QGIS_PKG_MIN_VERSION_MINOR, 
                qgis_pkg_curr_version_minor_rev >= c.QGIS_PKG_MIN_VERSION_MINOR_REV)):

            # Show message in Connection Status the Qgis Package is installed (and version)
            self.lblMainInst_out.setText(c.success_html.format(text=" ".join([c.INST_MSG, f"(v.{qgis_pkg_curr_version_txt})"]).format(pkg=self.QGIS_PKG_SCHEMA)))
            self.checks.is_qgis_pkg_installed = True
        else:
            self.lblMainInst_out.setText(c.failure_html.format(text=c.INST_FAIL_VERSION_MSG))
            self.checks.is_qgis_pkg_installed = False

            msg_rich = f"The QGIS Package installed in this PostgreSQL database is version <b>{qgis_pkg_curr_version_txt}</b> and is not supported anymore. The minimum required is version <b>{c.QGIS_PKG_MIN_VERSION_TXT}</b>.<br><br>Please contact your database administrator to update to the latest version of the QGIS Package."
            QMessageBox.warning(self, "Unsupported version of QGIS Package", msg_rich)

            tl_wf.tabLayers_reset(dlg=self)
            ts_wf.tabSettings_reset(dlg=self)
            tc_wf.tabConnection_reset(dlg=self)

            # Close the current open connection.
            if self.conn:
                self.conn.close()

            return None # Exit

        # 4) Is my usr_schema installed? If yes, continue.

        # Set the current user schema name.
        self.USR_SCHEMA = sh_sql.create_qgis_usr_schema_name(dlg=self)
        # Check if qgis_{usr} schema (e.g. qgis_giorgio) is installed in the database.
        is_usr_schema_inst: bool = sh_sql.is_usr_schema_installed(dlg=self)

        if is_usr_schema_inst:
            # Show message in Connection Status the 3DCityDB version if installed
            self.DB.citydb_version = sh_sql.get_3dcitydb_version(dlg=self)
            self.lbl3DCityDBInst_out.setText(c.success_html.format(text=self.DB.citydb_version))
            self.checks.is_3dcitydb_installed = True

            # Show message in Connection Status that the qgis_{usr} schema is installed               
            self.lblUserInst_out.setText(c.success_html.format(text=c.INST_MSG.format(pkg=self.USR_SCHEMA)))
            self.checks.is_usr_pkg_installed = True
        else:
            self.lblUserInst_out.setText(c.failure_html.format(text=c.INST_FAIL_MSG.format(pkg=f"qgis_{self.DB.username}")))
            self.checks.is_usr_pkg_installed = False

            msg = f"The required user schema 'qgis_{self.DB.username}' is missing.\n\nPlease contact your database administrator to install it."
            QMessageBox.warning(self, "User schema not found", msg)

            tl_wf.tabLayers_reset(dlg=self)
            ts_wf.tabSettings_reset(dlg=self)
            tc_wf.tabConnection_reset(dlg=self)

            # Close the current open connection.
            if self.conn:
                self.conn.close()

            return None # Exit

        # 5) Are there cdb_schemas I am allowed to connect to? If yes, continue

        # Namedtuple with: cdb_schema, is_empty, priv_type
        cdb_schemas_all = sql.list_cdb_schemas_privs(dlg=self)
        # print('cdb_schema_extended', cdb_schemas_extended)

        # Select tuples of cdb_schemas that have number of cityobjects <> 0
        # AND the user has 'rw' privileges.
        cdb_schemas_rw_ro = [cdb_schema for cdb_schema in cdb_schemas_all if cdb_schema.priv_type in ["ro", "rw"]]
        cdb_schemas = [cdb_schema for cdb_schema in cdb_schemas_rw_ro if not cdb_schema.is_empty]
        # print(cdb_schemas_rw_ro)
        # print(cdb_schemas)

        if len(cdb_schemas_rw_ro) == 0: 
            # Inform the user that there are no cdb_schemas to be chosen from.
            msg = "No citydb schemas could be retrieved from the database. You may lack proper privileges to access them.\n\nPlease contact your database administrator."
            QMessageBox.warning(self, "No accessible citydb schemas found", msg)

            tl_wf.tabLayers_reset(dlg=self)
            ts_wf.tabSettings_reset(dlg=self)
            tc_wf.tabConnection_reset(dlg=self)

            # Close the current open connection.
            if self.conn:
                self.conn.close()

            return None # Exit

        else:
            if len(cdb_schemas) == 0:
                tc_f.fill_cdb_schemas_box(dlg=self, cdb_schemas=None)
                # tc_f.fill_cdb_schemas_box_feat_count(self, None)
                # Inform the use that all available cdb_schemas are empty.
                msg = "The available citydb schema(s) is/are all empty.\n\nPlease load data into the database first."
                QMessageBox.warning(self, "Empty citydb schema(s)", msg)

                tl_wf.tabLayers_reset(dlg=self)
                ts_wf.tabSettings_reset(dlg=self)
                tc_wf.tabConnection_reset(dlg=self)
                # Close the current open connection.
                if self.conn:
                    self.conn.close()

                return None

            else: # Finally, we have all conditions to fill the cdb_schema combobox
                tc_f.fill_cdb_schemas_box(dlg=self, cdb_schemas=cdb_schemas)
                # At this point, filling the schema box, activates the 'evt_cbxSchema_changed' event.
                # So if you're following the code line by line, go to citydb_loader.py>evt_cbxSchema_changed or at 'cbxSchema_setup' function below

        return None # Exit


    def evt_cbxSchema_changed(self, cdbMain: CDBToolsMain) -> None:
        """Event that is called when the 'schemas' comboBox (cbxSchema) current index changes.
        Function to setup the GUI after an 'indexChanged' signal is emitted from the cbxSchema combo box.
        This function runs every time the selected schema is changed (in 'User Connection' tab)
        Checks if the connection + schema meet the necessary requirements.
        """
        
        # By now, the schema variable must have beeen assigned.
        # Simple redundant check just to be sure...
        if not self.cbxSchema.currentData():
            return None

        # Named tuple with: cdb_schema, co_number, priv_type
        sel_cdb_schema: CDBSchemaPrivs = self.cbxSchema.currentData()

        # Set the current schema variable
        self.CDB_SCHEMA = sel_cdb_schema.cdb_schema

        # Check that we are not accessing the same cdb_schema from other GUI dialogs of the plugin
        is_connection_unique = conn_f.check_connection_uniqueness(dlg=self, cdbMain=cdbMain)
        if not is_connection_unique:
            return None # Stop and do not proceed

        # Set the current schema privileges
        self.CDBSchemaPrivileges = sel_cdb_schema.priv_type
        # print("CDBSchemaPrivileges", self.CDBSchemaPrivileges)

        # Reset the Layer and Settings tabs in case they were open/changed from before 
        tl_wf.tabLayers_reset(dlg=self) # Reset the Layers tab
        ts_wf.tabSettings_reset(dlg=self) # Reset the Settings tab to the Default settings

        self.tabSettings.setDisabled(False) # Reactivate the tab Settings

        self.CDB_SCHEMA_EXTENTS = QgsRectangle()
        self.LAYER_EXTENTS = QgsRectangle()
        self.QGIS_EXTENTS = QgsRectangle()

        # Initialize/create the FeatureTypeRegistry
        tc_f.initialize_feature_type_registry(dlg=self)

        # Clear status of previous schema.
        self.lblLayerExist_out.clear()
        self.lblLayerRefr_out.clear()

        # Enable cdb_schema comboBox
        self.cbxSchema.setDisabled(False)
        self.lblSchema.setDisabled(False)

        # Update labels with the name of the selected cdb_schema
        self.btnRefreshCDBExtents.setText(self.btnRefreshCDBExtents.init_text.format(sch=self.CDB_SCHEMA))
        self.btnCityExtents.setText(self.btnCityExtents.init_text.format(sch=self.CDB_SCHEMA))

        # Update labels with on the layer Create/Refresh/(Drop) buttons with the selected cdb_schema
        self.btnCreateLayers.setText(self.btnCreateLayers.init_text.format(sch=self.CDB_SCHEMA))
        self.btnRefreshLayers.setText(self.btnRefreshLayers.init_text.format(sch=self.CDB_SCHEMA))
        # self.btnDropLayers.setText(self.btnDropLayers.init_text.format(sch=self.CDB_SCHEMA))

        # Setup the 'Basemap (OSM)' groupbox.
        tc_wf.gbxBasemap_setup(dlg=self)
        # This fires an update of the medadata library

        # Check whether layers exist, have been refreshed, and set up the GUI elements accordinly
        tc_f.check_layers_status(dlg=self)

        return None

    # 'Basemap (OSM)' group box events (in 'User Connection' tab)
    def evt_canvasC_ext_changed(self) -> None:
        """Event that is called when the current canvas extents (pan over map) changes.
        Reads the new current extents from the map and sets it in the 'Extents'
        (qgbxExtents) widget.
        """
        if not self.CRS:
            # do nothing
            # print('no CRS yet')
            pass
        else:
        # Get canvas's current extents
            new_extent: QgsRectangle = self.CANVAS.extent()
            old_extent: QgsRectangle = self.qgbxExtents.currentExtent()
            new_poly = QgsGeometry.fromRect(rect=new_extent)
            old_poly = QgsGeometry.fromRect(rect=old_extent)

            if new_poly.equals(geometry=old_poly):
                # print("same extents, same CRS, do nothing")
                pass
            else:
                self.qgbxExtents.setCurrentExtent(currentExtent=new_extent, currentCrs=self.CRS) # Signal emitted for qgbxExtents


    def evt_qgbxExtents_ext_changed(self) -> None:
        """Event that is called when the 'Extents' groubBox (qgbxExtents) extent in widget changes.
        """
        # Update current extents variable with the ones that fired the signal.
        self.CURRENT_EXTENTS: QgsRectangle = self.qgbxExtents.outputExtent()

        if self.CURRENT_EXTENTS.isNull() or self.CDB_SCHEMA_EXTENTS.isNull():
            return None

        # Check validity of user extents relative to the City Model's cdb_extents.
        layer_extents_poly = QgsGeometry.fromRect(rect=self.CURRENT_EXTENTS)
        cdb_extents_poly = QgsGeometry.fromRect(rect=self.CDB_SCHEMA_EXTENTS)

        if layer_extents_poly.intersects(geometry=cdb_extents_poly):
            self.LAYER_EXTENTS: QgsRectangle = self.CURRENT_EXTENTS

            # Draw the red rubber band
            canvas.insert_rubber_band(band=self.RUBBER_LAYERS, extents=self.LAYER_EXTENTS, crs=self.CRS, width=2, color=c.LAYER_EXTENTS_COLOUR)

            # Update the existence status of the Feature Type metadata
            tc_f.update_feature_type_registry_exists(dlg=self)
            # If the Feature Type checkable combobox is activated, refresh its contents
            if self.gbxFeatSel.isChecked():
                # (Re)fill 'Feature type' checkable combobox
                tc_f.fill_feature_types_box(dlg=self)

        else:
            msg: str = f"Pick a region intersecting the extents of '{self.CDB_SCHEMA}' (black area)."
            QMessageBox.critical(self, "Warning", msg)


    def evt_btnRefreshCDBExtents_clicked(self) -> None:
        """Event that is called when the button (btnRefreshCDBExtents) is pressed.
        It will check whether the cdb_extents
        - are null, i.e. the database has been emptied (reset all, the cdb_schema will disappear from the list)
        - have not changed (do nothing)
        - have changed and the new cdb extents contain the old ones (only update the ribbons)
        - have changed and the new cdb extents do not strictly contain the old ones (drop existing layers, update ribbons)
        """

        is_geom_null, x_min, y_min, x_max, y_max, srid = sql.compute_cdb_schema_extents(dlg=self)
        srid = None # Discard unneeded variable.

        if not is_geom_null:

            cdb_extents_old: QgsRectangle = self.CDB_SCHEMA_EXTENTS
            
            cdb_extents_new = QgsRectangle()
            cdb_extents_new.set(xMin=x_min, yMin=y_min, xMax=x_max, yMax=y_max, normalize=False)

            #########################################
            # Only for testing purposes
            # cdb_extents_new.set(-200, 0, -150, 50, False)
            # cdb_extents_new = cdb_extents_new.buffered(50.0)
            #########################################

            if cdb_extents_new == cdb_extents_old:
                # Do nothing, the extents have not changed. No need to do anything
                msg: str = f"Extents of '{self.CDB_SCHEMA}' are unchanged. No need to update them."
                QgsMessageLog.logMessage(msg, self.PLUGIN_NAME, level=Qgis.MessageLevel.Info, notifyUser=True)
                return None
            else: # The extents have changed. Show them on the map as dashed line

                # Disable the Feature Type selection checkbox.
                tc_wf.gbxFeatSel_reset(dlg=self)

                # Backup the original Layer extents
                # (They will be changed by the event evt_qgbxExtents_ext_changed later on)
                layer_extents_wkt: str = self.LAYER_EXTENTS.asWktPolygon()
                temp_layer_extents: QgsRectangle = QgsRectangle().fromWkt(wkt=layer_extents_wkt)

                # Create the extents containing both old and new cdb_schema extents
                cdb_extents_union: QgsRectangle = QgsRectangle(cdb_extents_old)
                cdb_extents_union.combineExtentWith(rect=cdb_extents_new)

                # Create new rubber band, with dahed line style
                cdb_extents_new_rubber_band = QgsRubberBand(mapCanvas=self.CANVAS, geometryType=QgsWkbTypes.PolygonGeometry)
                # cdb_extents_new_rubber_band = QgsRubberBand(mapCanvas=self.CANVAS, geometryType=Qgis.GeometryType.Polygon)
                cdb_extents_new_rubber_band.setLineStyle(penStyle=Qt.DashLine)

                # Set up the canvas to the new extents of the cdb_schema.
                # Fires evt_qgbxExtents_ext_changed and evt_canvas_ext_changed
                canvas.canvas_setup(dlg=self, canvas=self.CANVAS, extents=cdb_extents_union, crs=self.CRS, clear=False)

                # Drop the rubber band of the layers extents, it will be redone later.
                self.RUBBER_LAYERS.reset()
                # Reset the red layer extents to the original size
                self.LAYER_EXTENTS = temp_layer_extents

                # Add the rubber bands
                canvas.insert_rubber_band(band=cdb_extents_new_rubber_band, extents=cdb_extents_new, crs=self.CRS, width=3, color=c.CDB_EXTENTS_COLOUR)
                canvas.insert_rubber_band(band=self.RUBBER_CDB_SCHEMA, extents=self.CDB_SCHEMA_EXTENTS, crs=self.CRS, width=3, color=c.CDB_EXTENTS_COLOUR)

                # Zoom to the rubber band of the new cdb_extents. Fires evt_canvas_ext_changed
                canvas.zoom_to_extents(canvas=self.CANVAS, extents=cdb_extents_union)

                msg: str = f"Extents of '{self.CDB_SCHEMA}' have changed (black dashed line). Now they will be automatically updated."
                QMessageBox.warning(self, "Extents changed!", msg)

                # Drop the existing rubber bands, they need to be redone
                cdb_extents_new_rubber_band.reset()
                self.RUBBER_CDB_SCHEMA.reset()

                if cdb_extents_new.contains(rect=self.LAYER_EXTENTS):

                    # Update the cdb_extents in the database
                    sql.upsert_extents(dlg=self, bbox_type=BBoxType.CDB_SCHEMA, extents_wkt_2d_poly=cdb_extents_new.asWktPolygon())

                    # Update the canvas and the rubber bands in both tabs.

                    # In CONNECTION TAB, update canvas:
                    self.CDB_SCHEMA_EXTENTS = cdb_extents_new

                    # Set up the canvas to the new extents of the cdb_schema. Fires evt_qgbxExtents_ext_changed and evt_canvas_ext_changed
                    canvas.canvas_setup(dlg=self, canvas=self.CANVAS, extents=self.CDB_SCHEMA_EXTENTS, crs=self.CRS, clear=False)
                    # Reset the layer extents after the previous function modifies them
                    self.LAYER_EXTENTS = temp_layer_extents

                    # Show only the cdb extents and the layer extents, no need anymore for the dashed line.
                    canvas.insert_rubber_band(band=self.RUBBER_CDB_SCHEMA, extents=self.CDB_SCHEMA_EXTENTS, crs=self.CRS, width=3, color=c.CDB_EXTENTS_COLOUR)
                    canvas.insert_rubber_band(band=self.RUBBER_LAYERS, extents=temp_layer_extents, crs=self.CRS, width=2, color=c.LAYER_EXTENTS_COLOUR)

                    canvas.zoom_to_extents(canvas=self.CANVAS, extents=self.CDB_SCHEMA_EXTENTS)

                    # In LAYER TAB, update canvas:

                    # Drop the existing rubber bands, they need to be redone
                    self.RUBBER_CDB_SCHEMA_L.reset()
                    self.RUBBER_LAYERS_L.reset()
                    self.RUBBER_QGIS_L.reset()

                    # Draw the canvas to the new extents of the cdb_schema
                    # First set up and update canvas with the OSM map on cdb_schema extents and crs (Fires evt_qgbxExtents_ext_changed and evt_canvas_ext_changed)
                    canvas.canvas_setup(dlg=self, canvas=self.CANVAS_L, extents=self.LAYER_EXTENTS, crs=self.CRS, clear=False)
                    # Reset the layer extents after the previous function modifies them
                    self.LAYER_EXTENTS = temp_layer_extents
                    self.QGIS_EXTENTS = self.LAYER_EXTENTS

                    # Second, draw the rubber bands of the extents
                    canvas.insert_rubber_band(band=self.RUBBER_CDB_SCHEMA_L, extents=self.CDB_SCHEMA_EXTENTS, crs=self.CRS, width=3, color=c.CDB_EXTENTS_COLOUR)
                    canvas.insert_rubber_band(band=self.RUBBER_LAYERS_L, extents=temp_layer_extents, crs=self.CRS, width=2, color=c.LAYER_EXTENTS_COLOUR)
                    canvas.insert_rubber_band(band=self.RUBBER_QGIS_L, extents=temp_layer_extents, crs=self.CRS, width=2, color=c.QGIS_EXTENTS_COLOUR)

                    canvas.zoom_to_extents(canvas=self.CANVAS_L, extents=self.LAYER_EXTENTS)
                    
                    return None # Exit
                
                else: # The new cdb_extents do not contain the old ones, it is completely somewhere else (e.g. dropped all old data, added new data somewhere else)

                    # Update the cdb_extents in the database to the new ones
                    sql.upsert_extents(dlg=self, bbox_type=BBoxType.CDB_SCHEMA, extents_wkt_2d_poly=cdb_extents_new.asWktPolygon())
                    # Update the layer_extents and set them to null
                    sql.upsert_extents(dlg=self, bbox_type=BBoxType.MAT_VIEW, extents_wkt_2d_poly=None)

                    self.CDB_SCHEMA_EXTENTS = cdb_extents_new
                    self.LAYER_EXTENTS = cdb_extents_new

                    # Draw the canvas to the new extents of the cdb_schema
                    # First set up and update canvas with the OSM map on cdb_schema extents and crs (Fires evt_qgbxExtents_ext_changed and evt_canvas_ext_changed)
                    canvas.canvas_setup(dlg=self, canvas=self.CANVAS, extents=self.CDB_SCHEMA_EXTENTS, crs=self.CRS, clear=False)
                    # Reset the layer extents after the previous function modifies them
                    self.LAYER_EXTENTS = cdb_extents_new

                    # Second, create polygon rubber band corresponding to the cdb_schema extents
                    canvas.insert_rubber_band(band=self.RUBBER_CDB_SCHEMA, extents=self.CDB_SCHEMA_EXTENTS, crs=self.CRS, width=3, color=c.CDB_EXTENTS_COLOUR)

                    # Third, create polygon rubber band corresponding to the layers extents
                    canvas.insert_rubber_band(band=self.RUBBER_LAYERS, extents=self.LAYER_EXTENTS, crs=self.CRS, width=2, color=c.LAYER_EXTENTS_COLOUR)

                    # Eventually, zoom to the cdb_schema extents
                    canvas.zoom_to_extents(canvas=self.CANVAS, extents=self.CDB_SCHEMA_EXTENTS)

                    has_layers_in_current_schema = sql.has_layers_for_cdb_schema(dlg=self)
                    if has_layers_in_current_schema:
                        msg: str = f"To align with the next extents of '{self.CDB_SCHEMA}', layers will be dropped (from the QGIS Package).\n\nYou may want to manually remove the layers also from QGIS Layers Panel and then, if desired, recreate, refresh and reload them in QGIS."
                        QMessageBox.warning(self, "Extents changed!", msg)

                        thr.run_drop_layers_thread(dlg=self) # this eventually checks the layer status
    
                    return None # Exit

        else: # This is the case when the database has been emptied.

            # Inform the user
            msg: str = f"The '{self.CDB_SCHEMA}' schema has been emptied. It will disappear from the drop down menu until you upload new data again."
            QMessageBox.information(self, "Extents changed!", msg)
            QgsMessageLog.logMessage(msg, self.PLUGIN_NAME, level=Qgis.MessageLevel.Info, notifyUser=True)

            # Reset to null the cdb_extents in the extents table in PostgreSQL
            sql.upsert_extents(dlg=self, bbox_type=BBoxType.CDB_SCHEMA, extents_wkt_2d_poly=None)
            # Reset to null the layers_extents in the extents table in PostgreSQL
            sql.upsert_extents(dlg=self, bbox_type=BBoxType.MAT_VIEW, extents_wkt_2d_poly=None)

            # Drop the layers (if necessary)
            has_layers_in_current_schema = sql.has_layers_for_cdb_schema(dlg=self)
            if has_layers_in_current_schema:
                thr.run_drop_layers_thread(dlg=self)

            # Reset the tabs
            tl_wf.tabLayers_reset(dlg=self)
            ts_wf.tabSettings_reset(dlg=self)
            tc_wf.tabConnection_reset(dlg=self)

            # Close the connection
            if self.conn:
                self.conn.close()

            return None # Exit

        
    def evt_btnCityExtents_clicked(self) -> None:
        """Event that is called when the current 'Calculate from City model' pushButton (btnCityExtents) is pressed.
        """
        # Get the extents stored in server (already computed at this point).
        cdb_extents_wkt = sql.get_precomputed_extents(dlg=self, bbox_type=BBoxType.CDB_SCHEMA)

        # Convert extents format to QgsRectangle object.
        cdb_extents = QgsRectangle.fromWkt(wkt=cdb_extents_wkt)
        # Update extents in plugin variable.
        self.CDB_SCHEMA_EXTENTS = cdb_extents
        self.CURRENT_EXTENTS = cdb_extents

        # Put extents coordinates into the widget.
        self.qgbxExtents.setOutputExtentFromUser(self.CURRENT_EXTENTS, self.CRS)
        # At this point an extentChanged signal is emitted.

        # Update the existence status of the Feature Type metadata
        tc_f.update_feature_type_registry_exists(dlg=self)
        
        # If the Feature Type checkable combobox is activated, refresh its contents
        if self.gbxFeatSel.isChecked():
            # (Re)fill 'Feature type' checkable combobox
            tc_f.fill_feature_types_box(dlg=self)

        # Zoom to layer extents
        canvas.zoom_to_extents(canvas=self.CANVAS, extents=self.CDB_SCHEMA_EXTENTS)


    def evt_btnGeoCoder_clicked(self) -> None:
        """Event that is called when the 'Geocoder' button (btnGeoCoder) is pressed.
        """
        dlg_crs = self.CRS
        dlg_cdb_extents = self.CDB_SCHEMA_EXTENTS
        dlg_canvas = self.CANVAS

        dlgGeocoder = GeoCoderDialog(dlg_crs=dlg_crs, dlg_cdb_extents=dlg_cdb_extents, dlg_canvas=dlg_canvas)
        dlgGeocoder.setWindowModality(Qt.ApplicationModal) # i.e. The window blocks input to all other windows.
        dlgGeocoder.show()
        dlgGeocoder.exec_()

        return None


    def evt_gbxFeatSel_toggled(self) -> None:
        """Event that is called when the groupbox 'Feature Selection' is toggled.
        """
        status: bool = self.gbxFeatSel.isChecked()

        if status:
            # Fill 'Feature type' checkable combobox
            tc_f.fill_feature_types_box(dlg=self)
            # Activate comboboxes
            self.cbxFeatType.setDisabled(False)
        else:
            # reset the FeatureTypeMetadata to is_selected True for all, and take care of the widget status
            tc_wf.gbxFeatSel_reset(dlg=self)

        return None


    def evt_btnCreateLayers_clicked(self) -> None:
        """Event that is called when the 'Create layers for schema {sch}' pushButton (btnCreateLayers) is pressed.
        """
        if self.gbxFeatSel.isChecked():
            # Update the FeatureTypeMetadata with the information about the selected ones
            tc_f.update_feature_type_registry_is_selected(self)
      
            selected_feat_types = gen_f.get_checkedItemsData(self.cbxFeatType)

            if len(selected_feat_types) == 0:
                msg = "You must select at least one Feature Type. Otherwise deactivate the Feature Type selection box."
                QMessageBox.warning(self, "User schema not found", msg)
                return None # Exit
        else:
            # All existing FeatureTypes are selected by default
            pass

        # Set the label to the "ongoing" message
        # Upon successful layer creation, the label will be set accordingly.
        self.lblLayerExist_out.setText(c.ongoing_html.format(text=c.SCHEMA_LAYER_ONGOING_MSG))

        # Run the thread
        thr.run_create_layers_thread(dlg=self)

        return None # Exit


    def evt_btnRefreshLayers_clicked(self) -> None:
        """Event that is called when the 'Refresh layers for schema {sch}' pushButton (btnRefreshLayers) is pressed.
        """
        msg: str = "Refreshing layers can take long time.\nDo you want to proceed?"
        res = QMessageBox.question(self, "Layer refresh", msg)
        if res == QMessageBox.Yes:
            # Set the label to the "ongoing" message
            # Upon successful layer refresh, the label will be set accordingly.
            self.lblLayerRefr_out.setText(c.ongoing_html.format(text=c.REFR_LAYERS_ONGOING_MSG))

            # Run the thread
            thr.run_refresh_layers_thread(dlg=self)

        return None


    def evt_btnDropLayers_clicked(self) -> None:
        """Event that is called when the 'Drop layers for schema {sch}' pushButton (btnRefreshLayers) is pressed.
        """
        # Check whether there are layers loaded in QGIS, and inform the user. If the user decides to drop the layers
        # all loaded layers will lose the link to the respective database view and should be removed before.
        drop_layers: bool = True

        curr_db_node = tl_f.get_citydb_node(dlg=self)
        # print("curr_db_node:", curr_db_node.name())
        curr_cdb_schema_node_label: str = "@".join([self.DB.username, self.CDB_SCHEMA])


        if curr_db_node:
            curr_cdb_schema_node = curr_db_node.findGroup(name=curr_cdb_schema_node_label)
            # print("curr_cdb_schema_node:", curr_cdb_schema_node.name())
            if curr_cdb_schema_node:
                
                msg_rich: str = f"This will force the <b>automatic removal of all layers, detail views and look-up tables</b> currently loaded in QGIS and visible in group <b>'{curr_cdb_schema_node_label}'</b> of <b>'{self.DB.db_toc_node_label}'</b>.<br><br>Do you want to proceed anyway?"
                res = QMessageBox.question(self, "Drop layers", msg_rich)

                if res == QMessageBox.Yes:
                    curr_db_node.removeChildNode(curr_cdb_schema_node)
                else:
                    drop_layers = False

        if drop_layers:
            # Run the thread
            thr.run_drop_layers_thread(dlg=self)

        return None


    def evt_btnCloseConn_clicked(self) -> None:
        """Event that is called when the 'Close current connection' pushButton (btnCloseConn) is pressed.
        """

        tc_wf.tabConnection_reset(dlg=self)
        tl_wf.tabLayers_reset(dlg=self)
        ts_wf.tabSettings_reset(dlg=self)

        return None

    ## Events for User connection tab END

##################################################################################

    ## Events for Layer tab BEGIN

    # 'Parameters' group box events (in 'Layers' tab)
    def evt_qgbxExtentsL_ext_changed(self) -> None:
        """Event that is called when the 'Extents' groubBox (qgbxExtentsL) extent changes.
        """
        # NOTE: 'Draw on Canvas'* has an undesired effect.
        # There is a hardcoded True value that causes the parent dialog to
        # toggle its visibility to let the user draw. But in our case
        # the parent dialog contains the canvas that we need to draw on.
        # Re-opening the plugin allows us to draw in the canvas but with the
        # caveat that the drawing tool never closes (also causes some QGIS crashes).
        # https://github.com/qgis/QGIS/blob/master/src/gui/qgsextentgroupbox.cpp
        # https://github.com/qgis/QGIS/blob/master/src/gui/qgsextentwidget.h
        # line 251 extentDrawn function
        # https://qgis.org/pyqgis/3.16/gui/QgsExtentGroupBox.html
        # https://qgis.org/pyqgis/3.16/gui/QgsExtentWidget.html

        # Update extents variable with the ones that fired the signal.
        self.CURRENT_EXTENTS: QgsRectangle = self.qgbxExtentsL.outputExtent()

        # Draw the extents in the addtional canvas (basemap)
        canvas.insert_rubber_band(band=self.RUBBER_QGIS_L, extents=self.CURRENT_EXTENTS, crs=self.CRS, width=2, color=c.QGIS_EXTENTS_COLOUR)

        # Compare original extents with user defined ones.
        qgis_exts = QgsGeometry.fromRect(rect=self.CURRENT_EXTENTS)
        layer_exts = QgsGeometry.fromRect(rect=self.LAYER_EXTENTS)

        # Check validity of user extents relative to the City Model's extents.
        if layer_exts.equals(geometry=qgis_exts) or layer_exts.intersects(geometry=qgis_exts):
            self.QGIS_EXTENTS = self.CURRENT_EXTENTS
        elif qgis_exts.equals(geometry=QgsGeometry.fromRect(QgsRectangle(0,0,0,0))):
            # When the basemap is initialized (the first time),
            # the current extents are 0,0,0,0 and are compared against the extents 
            # of the layers which are coming from the DB.
            # This causes the "else" to pass which we don't want.  
            pass 
        else:
            msg: str = "Pick a region inside the layers extents (blue area)."
            QMessageBox.critical(self, "Warning", msg)          
            return None

        tl_wf.gbxLayerSelection_reset(dlg=self)
        self.gbxLayerSelection.setDisabled(False)   # Enable the layer selection, no selected layers
        self.btnImport.setDisabled(True)            # Disable however the "Import selected layers" button
        
        # Based on the selected extents fill out the Feature Types combo box.
        tl_f.fill_feature_type_box(dlg=self)

        return None


    def evt_btnLayerExtentsL_clicked(self) -> None:
        """Event that is called when the current 'Set to layers extents' pushButton (btnCityExtents) is pressed.
        """
        # Get the layer extents stored in server (already computed at this point).
        extents_str = sql.get_precomputed_extents(dlg=self, bbox_type=BBoxType.MAT_VIEW)

        # Convert extents format to QgsRectangle object.
        extents: QgsRectangle = QgsRectangle.fromWkt(wkt=extents_str)
        # Update extents in plugin variable.
        self.CURRENT_EXTENTS = extents
        self.QGIS_EXTENTS = extents

        # Put extents coordinates into the widget.
        self.qgbxExtentsL.setOutputExtentFromUser(self.CURRENT_EXTENTS, self.CRS)
        # At this point an extentChanged signal is emitted.

        # Zoom to layer extents.
        canvas.zoom_to_extents(canvas=self.CANVAS, extents=self.QGIS_EXTENTS)

        return None


    def evt_cbxFeatureType_changed(self) -> None:
        """Event that is called when the 'Feature Type'comboBox (cbxFeatureType) current index changes.
        """
        # Clear 'Level of Detail' combo box from previous runs
        self.cbxLod.clear()

        # Fill out the LoDs, based on the selected extents and Feature Type
        tl_f.fill_lod_box(dlg=self)

        # Enable 'Level of Detail' combo box
        self.cbxLod.setDisabled(False)

        return None


    def evt_cbxLod_changed(self) -> None:
        """Event that is called when the 'Geometry Level'comboBox (cbxLod) current index changes.
        """
        # Clear 'Select Layers to Import' checkable combo box from previous runs.
        self.ccbxLayers.clear()

        # Revert 'Select Layers to Import' checkable combo box the to initial text.
        self.ccbxLayers.setDefaultText(self.ccbxLayers.init_text)

        # Disable 'Import' pushbutton
        self.btnImport.setDisabled(True)

        # Fill out the features.
        tl_f.fill_layers_box(dlg=self)

        # Enable 'Available Layers' group box.
        self.gbxAvailableL.setDisabled(False)

        return None


    def evt_cbxLayers_changed(self) -> None:
        """Event that is called when the 'Available Layers' checkable ComboBox (ccbxLayers) current index changes.
        """
        # Get all the selected layers (views).
        checked_views = self.ccbxLayers.checkedItems()

        if checked_views:
            # Enable 'Import' pushbutton.
            self.btnImport.setDisabled(False)
        else:
            # Disable 'Import' pushbutton
            self.btnImport.setDisabled(True)

        return None


    def evt_btnImport_clicked(self) -> None:
        """Event that is called when the 'Import Features' pushButton (btnImport) is pressed.
        """
        # Get the data that is checked from 'ccbxLayers'
        # Remember widget hold items in the form of (view_name, View_object)
        selected_layers: list[CDBLayer] = []
        selected_layers = gen_f.get_checkedItemsData(self.ccbxLayers)

        #checked_views = dlg.ccbxLayers.checkedItemsData()
        # NOTE: this built-in method works only for string types. 
        # Check https://qgis.org/api/qgscheckablecombobox_8cpp_source.html line 173

        # Get the total number of features to be imported. Since the button has been pressed
        # there is AT LEAST 1 layer to be imported.

        counter = 0
        for layer in selected_layers:
            counter += layer.n_selected

        # Warn user when too many features are to be imported.
        if counter > self.settings.max_features_to_import_default:
            msg: str = f"Many features ({counter}) within the selected area!\nThis could reduce QGIS performance and may lead to crashes.\n\nDo you want to continue anyway?"
            res = QMessageBox.question(self, "Warning", msg)
            if res != QMessageBox.Yes:
                return None # Import Cancelled

        # Codelist settings and loading of the configs
        sel_CityGML_codelist_set: str = self.cbxCodeListSelCityGML.currentData()
        if sel_CityGML_codelist_set == "None":
            # do nothing
            pass
        else:
            if any([not self.selectedCityGMLCodeListSet,                           # Nothing selected
                    self.selectedCityGMLCodeListSet != sel_CityGML_codelist_set,   # Different selection from before
                    ]):
                # Set the new value
                self.selectedCityGMLCodeListSet = sel_CityGML_codelist_set
                # print("Working with Codelist set:", self.selectedCodeListSet)
                # Initialize the enum_lookup_config_registry
                tl_f.populate_codelist_config_registry(dlg=self, codelist_set_name=sel_CityGML_codelist_set)

        # TODO: Add similar process for selected ADE codelist set

        num_imported_layers = tl_f.add_selected_layers_to_ToC(dlg=self, layers=selected_layers)

        # Fix the layout of the layer tree only if something has been really imported
        if num_imported_layers > 0:
            # Structure 'Table of Contents' tree.
            db_node = tl_f.get_citydb_node(dlg=self)
            tl_f.sort_ToC(group=db_node)
            tl_f.send_to_ToC_top(group=db_node)

            # Finally bring the Relief Feature type at the bottom of the ToC.
            tl_f.send_to_ToC_bottom(node=QgsProject.instance().layerTreeRoot())

            # Set CRS of the project to match the one of the 3DCityDB.
            QgsProject.instance().setCrs(crs=self.CRS)
            
            # A final success message.
            msg = "Layer(s) successfully imported"
            QgsMessageLog.logMessage(message=msg, tag=self.PLUGIN_NAME, level=Qgis.MessageLevel.Success, notifyUser=True)

            # When adding a new layer, the dv and lu tables are always expanded.
            # To reduce the space "consumed" in the layer tree tab, 
            # close all detail view groups and the look-up tables groups
            # Additionally, unselect them, thus making the spatial layers in the dv invisible

            dv_lu_nodes = tl_f.get_all_dv_and_lu_nodes(dlg=self)
            if len(dv_lu_nodes) != 0:
                for node in dv_lu_nodes:
                    # print(node.name())
                    if node.isExpanded():
                        node.setExpanded(False)
                        node.setItemVisibilityCheckedRecursive(False)

        return None

        # This is the final step, meaning that user did everything correct and can now close the window to continue working outside the plugin.
        # NOTE: extent groupbox doesn't work for manual user input.
        # For every value change in any of the 4 inputs the extent signal is emitted

    ## Events for Layer tab END

##################################################################################

    ## Events for Settings tab BEGIN

    def evt_cbxGeoSimp_toggled(self) -> None:
        """Event that is called when the combobox 'Geometry Simplification' is toggled
        """
        status: bool = self.gbxGeomSimp.isChecked()
        if status:
            self.lblDecimalPrec.setDisabled(False)
            self.lblMinArea.setDisabled(False)
            self.qspbDecimalPrec.setDisabled(False)
            self.qspbMinArea.setDisabled(False)
        else:
            self.lblDecimalPrec.setDisabled(True)
            self.lblMinArea.setDisabled(True)
            self.qspbDecimalPrec.setDisabled(True)
            self.qspbMinArea.setDisabled(True)

        return None


    def evt_btnResetToDefault_clicked(self) -> None:
        """Event that is called when the button 'Reset to default values' is clicked
        """
        ts_wf.tabSettings_reset(self) # This also disables it and the buttons.
        # Reactivate it, as well as the buttons
        self.tabSettings.setDisabled(False)
        
        return None


    def evt_btnSaveSettings_clicked(self) -> None:
        """Event that is called when the button 'Save settings' is clicked
        """
        geomSimpEn = self.gbxGeomSimp.isChecked()
        decPrec = self.qspbDecimalPrec.value()
        minArea = self.qspbMinArea.value()
        maxFeatImp = self.qspbMaxFeatImport.value()
        frcLayerGen = self.cbxForceLayerGen.checkState()
        enable3D = self.cbxEnable3D.checkState()

        if decPrec is None:
            decPrec = self.settings.simp_geom_dec_prec_default
        if minArea is None:
            minArea = self.settings.simp_geom_min_area_default
        if maxFeatImp is None:
            maxFeatImp = self.settings.max_features_to_import_default

        if all((geomSimpEn == self.settings.simp_geom_enabled_default,
                decPrec == self.settings.simp_geom_dec_prec_default,
                minArea == self.settings.simp_geom_min_area_default,
                maxFeatImp == self.settings.max_features_to_import_default,
                frcLayerGen == self.settings.force_all_layers_creation_default,
                enable3D == self.settings.enable_3d_renderer_default
                )):
            # No need to store the settings, they are unchanged. Inform the user
            msg: str = "No need to store the settings, they coincide with the default values."
            QgsMessageLog.logMessage(message=msg, tag=self.PLUGIN_NAME, level=Qgis.MessageLevel.Info, notifyUser=True)
            return None # Exit

        settings_list = [
            {'name': 'geomSimpEn' , 'data_type': 4, 'data_value': int(geomSimpEn) , 'label': self.settings.simp_geom_enabled_label},
            {'name': 'decPrec'    , 'data_type': 2, 'data_value': decPrec    , 'label': self.settings.simp_geom_dec_prec_label},
            {'name': 'minArea'    , 'data_type': 3, 'data_value': minArea    , 'label': self.settings.simp_geom_min_area_label},
            {'name': 'maxFeatImp' , 'data_type': 2, 'data_value': maxFeatImp , 'label': self.settings.max_features_to_import_label},
            {'name': 'frcLayerGen', 'data_type': 4, 'data_value': int(frcLayerGen), 'label': self.settings.force_all_layers_creation_label},
            {'name': 'enable3D'   , 'data_type': 4, 'data_value': int(enable3D)   , 'label': self.settings.enable_3d_renderer_label},
        ]
        # print(settings_list)

        res = sh_sql.upsert_plugin_settings(dlg=self, usr_schema=self.USR_SCHEMA, dialog_name=self.DLG_NAME_LABEL, settings_list=settings_list)

        if not res:
            # Inform the user
            msg: str = f"Settings for '{self.DLG_NAME_LABEL}' could not be saved!"
            QgsMessageLog.logMessage(message=msg, tag=self.PLUGIN_NAME, level=Qgis.MessageLevel.Warning, notifyUser=True)
            return None # Exit

        # Inform the user
        msg: str = f"Settings for '{self.DLG_NAME_LABEL}' have been saved!"
        QgsMessageLog.logMessage(message=msg, tag=self.PLUGIN_NAME, level=Qgis.MessageLevel.Info, notifyUser=True)

        return None


    def evt_btnLoadSettings_clicked(self) -> None:
        """Event that is called when the button 'Save settings' is clicked
        """
        settings_list = sh_sql.get_plugin_settings(dlg=self, usr_schema=self.USR_SCHEMA, dialog_name=self.DLG_NAME_LABEL)
        # print(settings_list)

        if not settings_list:
            # Inform the user
            msg: str = f"Settings for '{self.DLG_NAME_LABEL}' could not be loaded!"
            QgsMessageLog.logMessage(message=msg, tag=self.PLUGIN_NAME, level=Qgis.MessageLevel.Warning, notifyUser=True)
            return None # Exit without updating the settings

        s: dict
        for s in settings_list:
            n = s['name']
            if n == "geomSimpEn":
                self.gbxGeomSimp.setChecked(s["data_value"])
            elif n == "decPrec":
                self.qspbDecimalPrec.setValue(s["data_value"])
            elif n == "minArea":
                self.qspbMinArea.setValue(s["data_value"])
            elif n == "maxFeatImp":
                self.qspbMaxFeatImport.setValue(s["data_value"])
            elif n == "frcLayerGen":
                self.cbxForceLayerGen.setChecked(s["data_value"])
            elif n == "enable3D":
                self.cbxEnable3D.setChecked(s["data_value"])
            else:
                pass

        # Inform the user
        msg: str = f"Settings for '{self.DLG_NAME_LABEL}' have been loaded!"
        QgsMessageLog.logMessage(message=msg, tag=self.PLUGIN_NAME, level=Qgis.MessageLevel.Info, notifyUser=True)

        return None

    ### EVENTS (end) ############################