"""
/***************************************************************************
 Class CDB4DeleterDialog

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

/***************************************************************************
 *                                                                         *
   Copyright 2023 Giorgio Agugiaro, Tendai Mbwanda

   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 ..gui_db_connector.other_classes import DBConnectionInfo

import os
from psycopg2.extensions import connection as pyconn

from qgis.core import Qgis, QgsMessageLog, 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 ..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 sql as sh_sql
from ..shared.functions import general_functions as gen_f
from .functions import tab_conn_widget_functions as tc_wf
from .functions import tab_conn_functions as tc_f
from .functions import tab_settings_widget_functions as ts_wf
from .functions import canvas, sql
from .functions import threads as thr
from .other_classes import DialogChecks, DefaultSettings, FeatureType, TopLevelFeature
from . import deleter_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_deleter_dialog.ui"))

class CDB4DeleterDialog(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(CDB4DeleterDialog, self).__init__(parent)
        # 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 - see
        # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect

        self.setupUi(self)

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

        # Variable to store the plugin name
        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_DELETER
        # Variable to store the variable name (in cdbMain) of this dialog
        self.DLG_NAME: str = cdbMain.DLG_NAME_DELETER

        # 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) 
        self.FeatureTypesRegistry: dict[str, FeatureType] = {}
        self.TopLevelFeaturesRegistry: dict[str, TopLevelFeature] = {}

        # 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 delete extents
        self.DELETE_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.

        # 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 = QgsRubberBand(mapCanvas=self.CANVAS, geometryType=QgsWkbTypes.PolygonGeometry)
        # self.RUBBER_CDB_SCHEMA = QgsRubberBand(mapCanvas=self.CANVAS, geometryType=Qgis.GeometryType.Polygon)
        self.RUBBER_DELETE = QgsRubberBand(mapCanvas=self.CANVAS, geometryType=QgsWkbTypes.PolygonGeometry)
        # self.RUBBER_DELETE = QgsRubberBand(mapCanvas=self.CANVAS, geometryType=Qgis.GeometryType.Polygon)

        # 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.btnCleanUpSchema.init_text = c.btnCleanUpSchema_t

        self.btnRefreshCDBExtents.init_text = c.btnRefreshCDBExtents_t
        self.btnCityExtents.init_text = c.btnCityExtents_t

        self.btnDelSelFeatures.init_text = c.btnDelSelFeatures_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))

        # 'Cleanup schema' group box signals
        self.gbxCleanUpSchema.toggled.connect(self.evt_gbxCleanUpSchema_toggled)
        self.btnCleanUpSchema.clicked.connect(self.evt_btnCleanUpSchema_clicked)


        # Basemap (OSM) group box signals
        # 'Base map' group box signals    
        self.gbxBasemap.toggled.connect(self.evt_gbxBasemap_toggled)

        # Link the additional canvas to the extents qgroupbox and enable "MapCanvasExtent" options (Byproduct).
        self.qgbxExtents.setMapCanvas(canvas=self.CANVAS, drawOnCanvasOption=False)

        # Draw on Canvas tool is disabled.
        # Check Note on main>widget_setup>ws_layers_tab.py>qgbxExtents_setup
        #################################################################
        # self.qgbxExtents.setOutputCrs(outputCrs=self.CRS)
        #################################################################
        # 'Extents' groupbox signals
        self.qgbxExtents.extentChanged.connect(self.evt_qgbxExtents_ext_changed)
        self.CANVAS.extentsChanged.connect(self.evt_canvasC_ext_changed)

        self.btnRefreshCDBExtents.clicked.connect(self.evt_btnRefreshCDBExtents_clicked)
        self.btnCityExtents.clicked.connect(self.evt_btnCityExtents_clicked)

        self.btnGeoCoder.clicked.connect(self.evt_btnGeoCoder_clicked)


        # 'Feature selection' group box signals    
        self.gbxFeatSel.toggled.connect(self.evt_gbxFeatSel_toggled)

        self.gbxFeatType.toggled.connect(self.evt_gbxFeatType_toggled)
        self.ckbFeatTypeAll.toggled.connect(self.evt_ckbFeatTypeAll_toggled)

        self.gbxTopLevelClass.toggled.connect(self.evt_gbxTopLevelClass_toggled)
        self.ckbTopLevelClassAll.toggled.connect(self.evt_ckbTopLevelClassAll_toggled)

        self.btnDelSelFeatures.clicked.connect(self.evt_btnDelSelFeatures_clicked)

        # 'Close connection' group box signals  
        self.btnCloseConn.clicked.connect(self.evt_btnCloseConn_clicked)

        #### 'Settings' tab
        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)
        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)

        return None

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

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

    ## Events for 'User connection' tab BEGIN

    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
        ts_wf.tabSettings_reset(dlg=self)
        tc_wf.tabConnection_reset(dlg=self)

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

        return None


    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. 2, the window is modal to the application and blocks input to all 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)

        return None


    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.
        """
        # Variable to store the plugin main dialog.
        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? It is required to store the settings
        # 5) Are there non-empty cdb_schemas I am allowed to connect to? If yes, continue
        # 6) Can I connect with RW privileges to at least one non-empty cdb_schema? If yes, continue
        # ########### Currently, the next check is not necessary (anymore), the limitation is overcome
        # 7) Am I a superuser? Take note, as only superusers can truncate tables (limit of 3DCityDB current implementation)
        # -------------------------------------------------------------------------------------------

        # 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))
            else:
                self.lblPostInst_out.setText(c.failure_html.format(text=c.PG_SERVER_FAIL_MSG))
                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

            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)

            ts_wf.tabSettings_reset(dlg=self)
            tc_wf.tabConnection_reset(dlg=self)
            # Close the current open connection.
            if self.conn is not None:
                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.

        # Check if the qgis_pkg schema (main installation) is installed in database.
        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 usr_schema name in self.USR_SCHEMA.
            # self.USR_SCHEMA = sh_sql.create_qgis_usr_schema_name(dlg=self)
            pass
        else:
            self.lblMainInst_out.setText(c.failure_html.format(text=c.INST_FAIL_MISSING_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.<br><br>Either way, please contact your database administrator."
            QMessageBox.warning(self, "Unavailable QGIS Package", msg)

            ts_wf.tabSettings_reset(dlg=self)
            tc_wf.tabConnection_reset(dlg=self)
            # Close the current open connection.
            if self.conn is not None:
                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 = 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)

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

            return None # Exit

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

        # Set the current usr_schema name in self.USR_SCHEMA.
        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 = 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. Please contact your database administrator to install it."
            QMessageBox.warning(self, "User schema not found", msg)

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

            return None # Exit

        # 5) Are there cdb_schemas I am allowed to connect to? If yes, continue
        # 6) Can I connect with RW privileges to at least one non-empty cdb_schema? If yes, continue

        # Namedtuple with: cdb_schema, is_empty, priv_type
        cdb_schemas_extended = sql.list_cdb_schemas_with_priv(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 = [cdb_schema for cdb_schema in cdb_schemas_extended if cdb_schema.priv_type == 'rw']
        cdb_schemas = [cdb_schema for cdb_schema in cdb_schemas_rw if not cdb_schema.is_empty]
        # print(cdb_schemas_rw)
        # print(cdb_schemas)

        if len(cdb_schemas_rw) == 0: 
            # Inform the user that there are no cdb_schemas to be chosen from.
            msg = "No citydb schemas could be retrieved from the database for which you have read & write privileges.\nPlease contact your database administrator."
            QMessageBox.warning(self, "No accessible citydb schemas found", msg)
            return None # Exit
        else:
            if len(cdb_schemas) == 0:
                tc_f.fill_cdb_schemas_box(self, cdb_schemas=None)
                
                # Inform the use that all available cdb_schemas are empty.
                msg = "The available citydb schema(s) is/are all empty. Please load data into the database first."
                QMessageBox.warning(self, "Empty citydb schema(s)", msg)

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

                return None
            
            else: # Finally, we have all conditions to proceed: 

                # 7) Am I a superuser? Take note, as only superusers can truncate tables (limit of 3DCityDB current implementation)
                # At the moment, this check is not needed anymore, as we allow users with RW to truncate the database, too.
                # Should be this decision be changed, then actually only superusers are allowed to truncate tables

                # We can start:

                # 1) Initialize the registries
                tc_f.initialise_top_level_features_registry(dlg=self)
                tc_f.initialize_feature_types_registry(dlg=self)

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

        return None # Exit


    def evt_cbxSchema_changed(self, cdbMain) -> 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.
        """
        # Set the current schema variable
        self.CDB_SCHEMA: str = self.cbxSchema.currentText()
        # By now, the schema variable must have beeen assigned. Check:
        if not self.cbxSchema.currentData():
            return None

        is_connection_unique = conn_f.check_connection_uniqueness(dlg=self, cdbMain=cdbMain)
        if not is_connection_unique:
            return None # Stop and do not proceed

        # Reset the Settings tabs in case they were open/changed from before 
        ts_wf.tabSettings_reset(dlg=self) # Reset the Settings tab to the Default settings
        tc_wf.gbxCleanUpSchema_reset(dlg=self)
        tc_wf.gbxBasemap_reset(dlg=self)        
        tc_wf.gbxFeatSel_reset(dlg=self)

        self.CDB_SCHEMA_EXTENTS = QgsRectangle()
        self.DELETE_EXTENTS = QgsRectangle()

        # Update labels with the name of the selected cdb_schema
        self.btnCleanUpSchema.setText(self.btnCleanUpSchema.init_text.format(sch=self.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))
        
        self.btnDelSelFeatures.setText(self.btnDelSelFeatures.init_text.format(sch=self.CDB_SCHEMA)) 

        # Enable the settings tab
        self.tabSettings.setDisabled(False)

        # Enable the Cleanup Schema groupbox
        self.gbxCleanUpSchema.setDisabled(False)
        # Enable the Base Map groupbox
        self.gbxBasemap.setDisabled(False)

        # Enable the Feature Selection groupbox
        self.gbxFeatSel.setDisabled(False)

        # Setup the 'Basemap (OSM)' groupbox.
        tc_wf.gbxBasemap_setup(dlg=self)
        # This will eventually fire a evt_qgbxExtents_ext_changed event

        tc_wf.gbxFeatType_reset(dlg=self)
        tc_wf.gbxTopLevelClass_reset(dlg=self)

        return None


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

        tc_wf.gbxFeatType_reset(dlg=self)
        tc_wf.gbxTopLevelClass_reset(dlg=self)

        if status: # it is checked to be enabled
            # Enable the button
            self.btnCleanUpSchema.setDisabled(False)

            # Disable the groupbox gbxBasemap
            self.gbxBasemap.setDisabled(True)

            # Disable the FeatureSelection group box 
            self.gbxFeatSel.setDisabled(True)

        else: # if unchecked, it disables itself automatically
            # Disable the button
            self.btnCleanUpSchema.setDisabled(True)

            # Enable the groupbox gbxBasemap
            self.gbxBasemap.setDisabled(False)
            # Enable the Feature Selection group box
            self.gbxFeatSel.setDisabled(False)

        return None


    def evt_btnCleanUpSchema_clicked(self) -> None:
        """Event that is called when the 'Truncate tables' button (btnCleanUpSchema) is pressed.
        """
        msg1: str = f"All tables in citydb schema {self.CDB_SCHEMA} will be truncated and all data will be deleted.\n\nDo you really want to proceed?"
        msg2: str = f"ALL tables in citydb schema {self.CDB_SCHEMA} will be truncated and ALL data will be deleted.\n\nDo you REALLY want to proceed?"
        msg3: str = f"ALL tables in citydb schema {self.CDB_SCHEMA} will be truncated and ALL data will be deleted FOREVER.\n\nDo you REALLY REALLY want to proceed?\n\nIf you'll loose data, don't tell we didn't warn you..."
        res = QMessageBox.question(self, "Clean up citydb schema", msg1)
        if res == QMessageBox.Yes:
            res = QMessageBox.question(self, "Clean up citydb schema", msg2)
            if res == QMessageBox.Yes:
                res = QMessageBox.question(self, "Clean up citydb schema", msg3)
                if res == QMessageBox.Yes:
                    thr.run_cleanup_schema_thread(self)
        return None


    def evt_gbxBasemap_toggled(self) -> None:
        """Event that is called when the groupbox 'gbxBasemap' is toggled.
        """
        status: bool = self.gbxBasemap.isChecked()

        if status: # it is checked to be enabled
            # (Re) Setup the 'Basemap (OSM)' groupbox.
            tc_wf.gbxBasemap_setup(dlg=self) # fires a evt_qgbxExtents_ext_changed() event
            self.gvCanvas.setDisabled(False)
            self.qgbxExtents.setDisabled(False)
            # Enable the "Set to schema" button
            self.btnCityExtents.setDisabled(False)
            # Enable the Refresh extents button
            self.btnRefreshCDBExtents.setDisabled(False)
            # Enable the "Geocoder" button
            self.btnGeoCoder.setDisabled(False)

            # tc_wf.workaround_gbxFeatType(self)
            # tc_wf.workaround_gbxTopLevelClass(self)

            if self.gbxFeatSel.isChecked():
                # Both are unchecked: leave them disabled
                if not self.gbxFeatType.isChecked() and not self.gbxTopLevelClass.isChecked():
                    self.gbxFeatType.setDisabled(False)
                    self.gbxTopLevelClass.setDisabled(False)
                # FeatType is checked: leave it enabled, disable the other
                if self.gbxFeatType.isChecked() and not self.gbxTopLevelClass.isChecked():
                    self.gbxFeatType.setDisabled(False)
                    self.gbxTopLevelClass.setDisabled(True)
                # TopClass is checked: leave it enabled, disable the other
                if not self.gbxFeatType.isChecked() and self.gbxTopLevelClass.isChecked():
                    self.gbxFeatType.setDisabled(True)
                    self.gbxTopLevelClass.setDisabled(False)

        else: # when unchecked, it disables itself automatically
            # We do not want to completely reset the groupbox (it would delete all, and clean the canvas, too)
            # So, we selectively disable some stuff.
            self.gvCanvas.setDisabled(True)
            self.qgbxExtents.setDisabled(True)
            # Disable the "Set to schema" button
            self.btnCityExtents.setDisabled(True)
            # Disable the Refresh extents button
            self.btnRefreshCDBExtents.setDisabled(True)
            # Disable the "Geocoder" button
            self.btnGeoCoder.setDisabled(True)

            # Reset the current extents to those of the cdb_schema
            self.CURRENT_EXTENTS = self.CDB_SCHEMA_EXTENTS

            # Then update canvas with cdb_schema extents and crs, this fires the gbcExtent event
            canvas.canvas_setup(dlg=self, canvas=self.CANVAS, extents=self.CURRENT_EXTENTS, crs=self.CRS, clear=True)

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

        return None


    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(new_extent)
            old_poly = QgsGeometry.fromRect(old_extent)

            if new_poly.equals(old_poly):
                # do nothing
                # print("same extents, same CRS, do nothing")
                pass
            else:
                # Set the current extent to show in the 'extent' widget.
                # self.qgbxExtents.blockSignals(True)
                # self.qgbxExtents.setOutputCrs(outputCrs=self.CRS) # Signal emitted for qgbxExtents. Avoid double signal by blocking signals
                # self.qgbxExtents.blockSignals(False)
                self.qgbxExtents.setCurrentExtent(currentExtent=new_extent, currentCrs=self.CRS) # Signal emitted for qgbxExtents

        return None


    def evt_qgbxExtents_ext_changed(self) -> None:
        """Event that is called when the 'Extents' groubBox extent (qgbxExtents) 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.
        delete_extents_poly = QgsGeometry.fromRect(self.CURRENT_EXTENTS)
        cdb_extents_poly = QgsGeometry.fromRect(self.CDB_SCHEMA_EXTENTS)

        if delete_extents_poly.intersects(cdb_extents_poly):
            self.DELETE_EXTENTS: QgsRectangle = self.CURRENT_EXTENTS

            # Draw the delete extents rubber band
            canvas.insert_rubber_band(band=self.RUBBER_DELETE, extents=self.DELETE_EXTENTS, crs=self.CRS, width=2, color=c.DELETE_EXTENTS_COLOUR)

            # print("from: evt_qgbxExtents_ext_changed")
            tc_f.refresh_registries(dlg=self)

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

        return None


    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)
        """
        tc_f.refresh_extents(dlg=self)

        return None
   
 
    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_cdb_schema_extents(dlg=self)

        # Convert extents format to QgsRectangle object.
        cdb_extents = QgsRectangle.fromWkt(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 extents_changed signal is emitted.

        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_cdb_extents, 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: # it is checked to be enabled
            # Disable the gbxCleanUpSchema (if enabled)
            if self.gbxCleanUpSchema.isEnabled():
                self.gbxCleanUpSchema.setDisabled(True)

            if not self.gbxBasemap.isEnabled():
            # Enable the groupbox gbxBasemap
                self.gbxBasemap.setDisabled(False)

            tc_wf.workaround_gbxFeatType(dlg=self)
            tc_wf.workaround_gbxTopLevelClass(dlg=self)

            # Set up/update the "Select Features" group box
            tc_f.refresh_registries(dlg=self)

            # Enable the groupbox FeatType
            self.gbxFeatType.setDisabled(False)
            # Enable the groupbox TopClass
            self.gbxTopLevelClass.setDisabled(False)

            # Enable the Delete Selected features button
            self.btnDelSelFeatures.setDisabled(False)

        else: # when unchecked, it disables itself automatically
            tc_wf.gbxFeatSel_reset(dlg=self) # it disables itself, I need to reenable it
            # (Re)enable the gbxFeatSel groupbox
            self.gbxFeatSel.setDisabled(False)

            # Uncheck the groupbox gbxBasemap
            # self.gbxBasemap.setChecked(False)
            # Disable the groupbox gbxBasemap
            self.gbxBasemap.setDisabled(False)

            # Enable the gbxCleanUpSchema group
            self.gbxCleanUpSchema.setDisabled(False)

        return None


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

        if status: # it is checked to be enabled
            # Disable check box ckbTopLevelClassAll
            self.ckbTopLevelClassAll.setDisabled(True)
            # Diable checkable combo box ccbxTopLevelClass
            self.ccbxTopLevelClass.setDisabled(True)
            # Disable the gbxTopLevelClass
            self.gbxTopLevelClass.setDisabled(True)

            # Enable combo box cbxFeatType
            self.ccbxFeatType.setDisabled(False)
            # Enable check box ckbFeatTypeAll
            self.ckbFeatTypeAll.setDisabled(False)
            # Enable the gbxFeatType
            self.gbxFeatType.setDisabled(False)

        else: # when unchecked, it disables itself automatically
            # Disable check box ckbFeatTypeAll
            self.ckbFeatTypeAll.setChecked(False)            
            self.ckbFeatTypeAll.setDisabled(True)
            # Clear the previous selected items
            for i in range(self.ccbxFeatType.count()):
                self.ccbxFeatType.setItemCheckState(i, Qt.Unchecked)
            # Disable combo box cbxFeatType
            self.ccbxFeatType.setDisabled(True)

            # Enable the gbxTopLevelClass
            self.gbxTopLevelClass.setDisabled(False)
            # Keep disabled check box ckbTopLevelClassAll
            self.ckbTopLevelClassAll.setDisabled(True)
            # Keep disabled combobox cbxTopClass
            self.ccbxTopLevelClass.setDisabled(True)

        return None


    def evt_ckbFeatTypeAll_toggled(self) -> None:
        """Event that is called when the check box 'Feature Type All' is toggled.
        If checked, it will turn all entries to checked.
        If unchecked, it will set all entries to unchecked.
        """
        status: bool = self.ckbFeatTypeAll.isChecked()

        if status: # Selected/Checked
            # Select all items in combobox with cdb_schemas, set status to 2 (Checked)
            for i in range(self.ccbxFeatType.count()):
                 self.ccbxFeatType.setItemCheckState(i, Qt.Checked)
            # Disable the drop down menu
            self.ccbxFeatType.setDisabled(True)
        else:
            # Unselect all items in combobox with cdb_schemas, set status to 0 (Unchecked)
            for i in range(self.ccbxFeatType.count()):
                 self.ccbxFeatType.setItemCheckState(i, Qt.Unchecked)
            # Enable the drop down menu
            self.ccbxFeatType.setDisabled(False)
            
        return None


    def evt_gbxTopLevelClass_toggled(self) -> None:
        """Event that is called when the check box 'Top-level Feature' is toggled.
        """
        status: bool = self.gbxTopLevelClass.isChecked()

        if status: # it is checked to be enabled
            # Disable check box ckbFeatTypeAll
            self.ckbFeatTypeAll.setDisabled(True)
            # Disable checkable combo box cbxFeatType
            self.ccbxFeatType.setDisabled(True)
            # Disable the group box gbxFeatType
            self.gbxFeatType.setDisabled(True)

            # Enable check box ckbTopLevelClassAll
            self.ckbTopLevelClassAll.setDisabled(False)
            # Enable combo box ccbxTopLevelClass
            self.ccbxTopLevelClass.setDisabled(False)
            # Enable the groupbox gbxTopLevelClass
            self.gbxTopLevelClass.setDisabled(False)

        else: # when unchecked, it disables itself automatically
            # Disable check box ckbFeatTypeAll
            self.ckbTopLevelClassAll.setChecked(False)
            self.ckbTopLevelClassAll.setDisabled(True)
            # Clear the previous selected items
            for i in range(self.ccbxTopLevelClass.count()):
                self.ccbxTopLevelClass.setItemCheckState(i, Qt.Unchecked)
            # Disable combo box cbxFeatType
            self.ccbxTopLevelClass.setDisabled(True)

            # Enable the group box gbxFeatType
            self.gbxFeatType.setDisabled(False)
            # Keep disabled check box ckbFeatTypeAll
            self.ckbFeatTypeAll.setDisabled(True)
            # Kepp disabled checkable combo box cbxFeatType
            self.ccbxFeatType.setDisabled(True)

        return None


    def evt_ckbTopLevelClassAll_toggled(self) -> None:
        """Event that is called when the check box 'Top-level Feature All' is toggled.
        If checked, it will turn all entries to checked.
        If unchecked, it will set all entries to unchecked.
        """
        status: bool = self.ckbTopLevelClassAll.isChecked()

        if status: # Selected/Checked
            # Select all items in combobox with cdb_schemas, set status to 2 (Checked)
            for i in range(self.ccbxTopLevelClass.count()):
                 self.ccbxTopLevelClass.setItemCheckState(i, Qt.Checked)
            # Disable the drop down menu
            self.ccbxTopLevelClass.setDisabled(True)
        else:
            # Unselect all items in combobox with cdb_schemas, set status to 0 (Unchecked)
            for i in range(self.ccbxTopLevelClass.count()):
                 self.ccbxTopLevelClass.setItemCheckState(i, Qt.Unchecked)
            # Enable the drop down menu
            self.ccbxTopLevelClass.setDisabled(False)

        return None


    def evt_btnDelSelFeatures_clicked(self) -> None:
        """Event that is called when the 'Delete selected features from schema {sch}' button (btnDelSelFeatures) is pressed.
        """
        delete_mode: str = None # Eiter "del_FeatureTypes" or "del_TopClassFeatures"

        # Check that there are indeed some features selected

        if all((not self.gbxFeatType.isChecked(),
                not self.gbxTopLevelClass.isChecked())):
            msg: str = "You must enable either the 'Feature types' or the 'Top-level features' boxes."
            res = QMessageBox.warning(self, "Missing selection", msg)
            return None # Exit
        
        elif self.gbxFeatType.isChecked():
            # Get the list (tuple) of selected Feature Types in the current cdb_schema
            sel_feat_types: list[FeatureType] = gen_f.get_checkedItemsData(self.ccbxFeatType)
            # print("Selected Feature Types:", sel_feat_types)

            if len(sel_feat_types) == 0:
                msg: str = "You must select at least a Feature type from the combo box."
                res = QMessageBox.warning(self, "Missing selection", msg)
                return None # Exit
            else:
                delete_mode = "del_FeatureTypes"
                tc_f.update_feature_type_registry_is_selected(dlg=self, sel_feat_types=sel_feat_types)

        elif self.gbxTopLevelClass.isChecked():
            # Get the list of selected Top-level Features in the current cdb_schema
            sel_top_level_features: list[TopLevelFeature] = gen_f.get_checkedItemsData(self.ccbxTopLevelClass)
            # print("Selected Top-level features:", sel_top_class_features)

            if len(sel_top_level_features) == 0:
                msg: str = "You must select at least a Top-level feature from the combo box."
                res = QMessageBox.warning(self, "Missing selection", msg)
                return None # Exit
            else:
                delete_mode = "del_TopLevelFeatures"
                tc_f.update_top_level_features_registry_is_selected(dlg=self, sel_top_level_features=sel_top_level_features)

        else:
            # This case should not happen.
            return None # Exit

        msg1: str = f"Data will be deleted from citydb schema '{self.CDB_SCHEMA}'.\n\nDo you really want to proceed?"
        msg2: str = f"Data will be deleted from citydb schema '{self.CDB_SCHEMA}'.\n\nDo you REALLY want to proceed?"
        msg3: str = f"Data will be deleted from citydb schema '{self.CDB_SCHEMA}'.\n\nDo you REALLY REALLY want to proceed?\n\nIf you'll lose data, don't tell we didn't warn you..."
        res = QMessageBox.question(self, "Clean up citydb schema", msg1)
        if res == QMessageBox.Yes:
            res = QMessageBox.question(self, "Clean up citydb schema", msg2)
            if res == QMessageBox.Yes:
                res = QMessageBox.question(self, "Clean up citydb schema", msg3)
                if res == QMessageBox.Yes:
                    # This thread will also take care of checking what happens after deletion,
                    # e.g. in case that the database is completely emptied.
                    # The user will be eventually informed
                    thr.run_bulk_delete_thread(dlg=self, delete_mode=delete_mode)

        return None


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

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

    ## Events for User connection tab END

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

    ## Events for Settings tab BEGIN

    def evt_btnResetToDefault_clicked(self) -> None:
        """Event that is called when the button 'Reset to default values' is clicked
        """
        ts_wf.tabSettings_reset(dlg=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
        """
        delArraySize = self.sbxArraySize.value()

        if all((delArraySize == self.settings.max_del_array_length_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(msg, self.PLUGIN_NAME, level=Qgis.MessageLevel.Info, notifyUser=True)
            return None # Exit

        # Quick reminder:
        # 1 string
        # 2 integer
        # 3 real
        # 4 boolean (cast to int())

        settings_list: list[dict] = []
        settings_list = [
            {'name': 'delArraySize' , 'data_type': 2, 'data_value': delArraySize , 'label': self.settings.max_del_array_length_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(msg, 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(msg, 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: list[dict] = []
        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(msg, 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 == "delArraySize":
                self.sbxArraySize.setValue(s["data_value"])
            else:
                pass

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

        return None

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