"""This module contains functions that relate to the 'Import Tab' (in the GUI look for the plugin logo).
These functions are usually called from widget_setup functions relating to child widgets of the 'Import Tab'.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Union, cast, Iterable
if TYPE_CHECKING:       
    from ...gui_loader.loader_dialog import CDB4LoaderDialog
    from ..other_classes import FeatureType, CDBDetailView, EnumConfig 

from collections import OrderedDict
from qgis.core import (QgsProject, QgsMessageLog, QgsEditorWidgetSetup, 
                        QgsVectorLayer, QgsDataSourceUri, QgsAttributeEditorElement,
                        QgsAttributeEditorRelation, Qgis, QgsLayerTreeGroup,
                        QgsRelation, QgsAttributeEditorContainer, QgsMapLayer, QgsLayerTreeLayer)

from ..other_classes import CDBLayer, CodeListConfig
from .. import loader_constants as c
from . import sql

def populate_codelist_config_registry(dlg: CDB4LoaderDialog, codelist_set_name: str = None) -> None:
    """Function to create the dictionary containing Codelist Lookup Config metadata.
    """
    ####################################
    # codelist_set_name = ["CityGML 2.0"]
    ####################################

    if not codelist_set_name:
        return None # Exit, nothing to do

    # This is a list of named tuples, extracted from the db sorting by gen_name
    config_metadata = sql.get_codelist_lookup_config(dlg, codelist_set_name)
    if not config_metadata:
        return None # Exit, nothing to do
    # print(config_metadata)

    config_metadata_keys = [(elem.source_class, elem.source_table, elem.source_column) for elem in config_metadata]
    # Sort
    config_metadata_keys.sort(key=lambda x: (x[0], x[1], x[2]), reverse=False)
    # print(config_metadata_keys)

    config_metadata_values = [CodeListConfig(*elem) for elem in config_metadata]
    # Sort
    config_metadata_values.sort(key=lambda x: (x.source_class, x.source_table, x.source_column))

    dlg.CodeListConfigRegistry: dict = {}
    dlg.CodeListConfigRegistry = dict(zip(config_metadata_keys, config_metadata_values))

    # print('Initializing:\n', dlg.CodeListConfigRegistry)
    # print('Initializing:\n', dlg.CodeListConfigRegistry[("Building", "building", "class")].__dict__)

    return None


def add_layers_to_feature_type_registry(dlg: CDB4LoaderDialog) -> None:
    """Function to instantiate python objects from the 'layer_metadata' table in the usr_schema.
    """
    # Clean up the layers in the registry from previous runs
    feat_type: FeatureType
    for feat_type in dlg.FeatureTypesRegistry.values():
        feat_type.layers = [] # Empty the list the will contain CDBLayer objects

    # Get field names and metadata values from server.
    col_names, layer_metadata = sql.get_layer_metadata(dlg)

    # Format metadata into a list of dictionaries where each element is a layer.
    layer_metadata_dict_items: list = [dict(zip(col_names, values)) for values in layer_metadata]

    for layer_metadata_dict_item in layer_metadata_dict_items:
        # keys: id, cdb_schema, layer_type, feature_type, lod, root_class, curr_class, layer_name, 
        #       gv_name, av_name, n_features, creation_data, refresh_date,
        #       qml_form, qml_symb, qml_3d, 
        #       qml_form_with_path, qml_symb_with_path, qml_3d_with_path
        #       n_selected

        if layer_metadata_dict_item["n_features"] == 0:        # ignore those layers that have no features
            continue
        if layer_metadata_dict_item["refresh_date"] is None:   # ignore those layers that have not been refreshed
            continue

        # Create a Layer object with all the values extracted from 'layer_metadata'.
        layer = CDBLayer(*layer_metadata_dict_item.values())

        # Count the number of features that the layer has in the current extents.
        sql.exec_gview_counter(dlg=dlg, layer=layer) # Stores number in layer.n_selected.

        # Get the FeatureType object of the current layer
        curr_FeatureType: FeatureType = dlg.FeatureTypesRegistry[layer_metadata_dict_item['feature_type']]

        # Add the view to the FeatureObject views list
        curr_FeatureType.layers.append(layer)

    return None


def fill_feature_type_box(dlg: CDB4LoaderDialog) -> None:
    """Function that fills out the 'Feature Types' combo box.
    Uses the 'layer_metadata' table in usr_schema to instantiate useful python objects
    """
    # Every time we need to check, because the (QGIS) bbox may have changed
    add_layers_to_feature_type_registry(dlg)

    # Clear from previous runs
    dlg.cbxFeatureType.clear()

    layer: CDBLayer
    # Add only those Feature Types that have at least one view containing > 0 features.
    for key, ft in dlg.FeatureTypesRegistry.items():
        for layer in ft.layers:
            if layer.n_selected > 0:
                dlg.cbxFeatureType.addItem(key, ft)
                # The first FeatureType object added in 'cbxFeatureType' emits a 'currentIndexChanged' signal.
                break # We need only one view to have > 0 features.


def fill_lod_box(dlg: CDB4LoaderDialog) -> None:
    """Function that fills out the 'Geometry Level' combo box (LoD).
    """
    # Get 'FeatureType' object from combo box data.
    selected_ft: FeatureType = dlg.cbxFeatureType.currentData()
    if not selected_ft:
        return None

    geom_set = set() # To store the unique lods.
    layer: CDBLayer
    for layer in selected_ft.layers:
        geom_set.add(layer.lod)

    # Clean from previous runs
    dlg.cbxLod.clear()

    # Add lod string into both text and data holder of combo box.
    for lod in sorted(list(geom_set)):
        # The first LoD string added in 'cbxLod' emits a 'currentIndexChanged' signal.
       dlg.cbxLod.addItem(lod, lod)


def fill_layers_box(dlg: CDB4LoaderDialog) -> None:
    """Function that fills the 'Layers' checkable combo box.
    """
    selected_ft: FeatureType
    # Get current 'Feature Type' from widget.
    selected_ft = dlg.cbxFeatureType.currentData()

    # Get current 'LoD' from widget.
    selected_lod = dlg.cbxLod.currentText()

    if not selected_ft or not selected_lod:
        return None

    # Clear from previous run
    dlg.ccbxLayers.clear()

    layer: CDBLayer
    #selected_FeatureType: FeatureTypeLayersGroup
    for layer in selected_ft.layers:
        if layer.lod == selected_lod:
            if layer.n_selected > 0:
                dlg.ccbxLayers.addItemWithCheckState(
                    text=f'{layer.layer_name} ({layer.n_selected})',
                    state=0,
                    userData=layer)
    # TODO: 05-02-2021 Add separator between different features
    # REMEMBER: don't use method 'setSeparator', it adds a custom separator to join string of selected items


def get_attForm_child(container: QgsAttributeEditorContainer, child_name: str) -> QgsAttributeEditorElement:
    """Function that retrieves a child object from an 'attribute form' container.
    
    *   :param container: An attribute form container object.
        :type container: QgsAttributeEditorContainer

    *   :param child_name: The name of the child to be found.
        :type child_name: str

    *   :returns: The 'attribute form' child element (when found)
        :type child_name: QgsAttributeEditorElement | None
    """
    # print('looking for ', child_name)
    for child in container.children():
        # print(child.name())
        if child.name() == child_name:
            return child
    return None


def send_to_ToC_top(group: QgsLayerTreeGroup) -> None: 
    """Function that send the input group to the top of the project's 'Table of Contents' tree.
    #NOTE: this function could be generalized to accept ToC index location as a parameter (int).
    """
    # According to qgis docs, this is the prefered way.
    root = QgsProject.instance().layerTreeRoot()
    move_group =  group.clone()
    root.insertChildNode(index=0, node=move_group)
    root.removeChildNode(node=group)


def send_to_ToC_bottom(node: QgsLayerTreeGroup) -> None:
    """Function that send the input group to the bottom of the project's 'Table of Contents' tree.
    # NOTE: this function could be generalized to accept ToC index location as a parameter (int).
    """
    group = None
    names = [child.name() for child in node.children()]
    if 'FeatureType: Relief' in names:
        for c,i in enumerate(node.children()):
            if i.name() == 'FeatureType: Relief':
                group = i
                break
        if group:
            idx=len(node.children())-2
            move_group = group.clone()
            node.insertChildNode(index=idx, node=move_group)
            node.removeChildNode(node=group)
        return None
    
    for child in node.children():
        send_to_ToC_bottom(child)


def get_citydb_node(dlg: CDB4LoaderDialog) -> QgsLayerTreeGroup:
    """Function that finds the citydb node in the project's 'Table of Contents' tree (by name).

    *   :returns: citydb node
        :rtype: QgsLayerTreeGroup
    """

    root = QgsProject.instance().layerTreeRoot()
    cdb_node = root.findGroup(dlg.DB.db_toc_node_label)
    # cdb_node = root.findGroup(dlg.DB.database_name)
    return cdb_node


def get_all_dv_and_lu_nodes(dlg: CDB4LoaderDialog) -> list[QgsLayerTreeGroup]:
    """Function that finds the citydb node in the project's 'Table of Contents' tree (by name).

    *   :returns: citydb node (qgis group)
        :rtype: QgsLayerTreeGroup
    """

    root = QgsProject.instance().layerTreeRoot()
    group_nodes = root.findGroups(recursive=True) # Get all Group nodes, also those containing the dv and lu tables
    dv_lu_nodes: list[QgsLayerTreeGroup] = []

    if len(group_nodes) != 0:
        for group_node in group_nodes:
            if group_node.name() == c.detail_views_group_alias or group_node.name() == c.lookup_tables_group_alias:
                dv_lu_nodes.append(group_node)
            else:
                # nothing to add
                pass

    return dv_lu_nodes


def sort_ToC(group: QgsLayerTreeGroup) -> None:
    """Recursive function to sort the entire 'Table of Contents' tree,
    including both groups and underlying layers.
    """
    # Germán Carrillo BEGIN: https://gis.stackexchange.com/questions/397789/sorting-layers-by-name-in-one-specific-group-of-qgis-layer-tree #
    LayerNamesEnumDict = lambda listCh:{listCh[q[0]].name() + str(q[0]):q[1] for q in enumerate(listCh)}

    # group instead of root
    mLNED = LayerNamesEnumDict(group.children())
    mLNEDkeys = OrderedDict(sorted(LayerNamesEnumDict(group.children()).items(), reverse=False)).keys()

    mLNEDsorted = [mLNED[k].clone() for k in mLNEDkeys]
    group.insertChildNodes(0, mLNEDsorted)  # group instead of root
    for n in mLNED.values():
        group.removeChildNode(n)  # group instead of root
    # Germán Carrillo END #

    group.setExpanded(True)
    for child in group.children():
        if isinstance(child, QgsLayerTreeGroup):
            sort_ToC(child)
        else: 
            return None

    return None


def add_group_node_to_ToC(parent_node: QgsLayerTreeGroup, child_name: str) -> QgsLayerTreeGroup:
    """Function that adds a node (group) into the qgis project 'Table of Contents' tree (by name).
    It also checks if the node already exists and returns it.

    *   :param parent_node: A node on which the new node is going to be added
            (or returned if it already exists).
        :type parent_node: QgsLayerTreeGroup

    *   :param child_name: A string name of the new or existing node (group).
        :type child_name: str

    *   :returns: The newly created node object (or the existing one).
        :rtype: QgsLayerTreeGroup
    """
    # node_name group (e.g. test_db)
    if not parent_node.findGroup(child_name):
        # Create group
        node = parent_node.addGroup(child_name)
    else:
        # Get existing group
        node = parent_node.findGroup(child_name)
    return node


def add_layer_node_to_ToC(dlg: CDB4LoaderDialog, layer: CDBLayer) -> QgsLayerTreeGroup:
    """Function that populates the project's 'Table of Contents' tree.

    *   :param view: The view used to build the ToC.
        :type view: View

    *   :returns: The node (group) where the view is going to be added.
        :rtype: QgsLayerTreeGroup
    """
    root = QgsProject.instance().layerTreeRoot()

    # Database group (e.g. delft @ localhost:5432)
    node_cdb = add_group_node_to_ToC(parent_node=root, child_name=dlg.DB.db_toc_node_label)
    # Database group (e.g. delft)
    # node_cdb = add_group_node_to_ToC(parent_node=root, child_name=dlg.DB.database_name)
    
    # Schema group (e.g. citydb)
    node_cdb_schema = add_group_node_to_ToC(parent_node=node_cdb, child_name="@".join([dlg.DB.username, layer.cdb_schema]))
    # FeatureType group (e.g. Building)
    node_featureType = add_group_node_to_ToC(parent_node=node_cdb_schema, child_name=f"FeatureType: {layer.feature_type}")
    # Feature group (e.g. Building Part)
    node_feature = add_group_node_to_ToC(parent_node=node_featureType, child_name=layer.root_class)
    # LoD group (e.g. lod2)
    node_lod = add_group_node_to_ToC(parent_node=node_feature, child_name=layer.lod)

    return node_lod # Node where the view has been added


def is_layer_already_in_ToC_group(group: QgsLayerTreeGroup, layer_name: str) -> bool:
    """Function that checks whether a specific group has a specific underlying layer (by name).

    *   :param group: Node object to check for layer existence.
        :type group: QgsLayerTreeGroup

    *   :param layer_name: Layer name to check if it exists.
        :type layer_name: str

    *   :returns: Search result.
        :rtype: bool
    """
    if layer_name in [child.name() for child in group.children()]:
        return True
    return False


def create_layer_relation_to_dv_address(dlg: CDB4LoaderDialog, layer: QgsVectorLayer, dv_gen_name: str) -> None:
    """Function to set up the relations for an input layer (e.g. a view).
    - New relation objects are created that reference the detail views of the address(es) tables.
    - Relations are also set for 'Value Relation' widget.

    *   :param layer: vector layer to set up the relationships for.
        :type layer: QgsVectorLayer
    """
    dv_gen_names: list = [k for k in dlg.DetailViewsRegistry.keys() if k.startswith("address_")]
    # print("dv_gen_names", dv_gen_names)

    if dv_gen_name not in dv_gen_names:
        # We're creating relations that may not be valid, so exit.
        return None

    # dv: CDBDetailView
    dv = [v for k,v in dlg.DetailViewsRegistry.items() if k == dv_gen_name][0]

    # Isolate the layers' ToC environment to avoid grabbing the first layer encountered in the WHOLE ToC.
    root = QgsProject.instance().layerTreeRoot()

    db_node = root.findGroup(dlg.DB.db_toc_node_label)
    # db_node = root.findGroup(dlg.DB.database_name)
    
    schema_node = db_node.findGroup("@".join([dlg.DB.username,dlg.CDB_SCHEMA]))
    detail_views_node = schema_node.findGroup(c.detail_views_group_alias)
    dv_layers: list = detail_views_node.findLayers()
    # print("dv_layers", dv_layers)
    # dv_layer: QgsLayerTreeLayer
    dv_layer = [elem for elem in cast(Iterable[QgsLayerTreeLayer], dv_layers) if elem.name().endswith(dv.gen_name)][0] # it should be only one!
    # print("dv_layer", dv_layer)

    # Create new Relation object
    rel = QgsRelation()
    rel.setReferencedLayer(id=layer.id())  # i.e. the (QGIS  internal) id of the CityObject layer
    rel.setReferencingLayer(id=dv_layer.layerId()) # i.e. the (QGIS  internal) id of the Address layer
    rel.addFieldPair(referencingField='cityobject_id', referencedField='id')
    rel.setName(name='re_' + layer.name() + "_" + dv_layer.name())    
    rel.setId(id="id_" + rel.name())

    # Till QGIS 3.26 the argument of setStrength is numeric, from QGIS 3.28 it is an enumeration
    if dlg.QGIS_VERSION_MAJOR == 3 and dlg.QGIS_VERSION_MINOR < 28:
        rel.setStrength(0) # integer, 0 is association, 1 composition
    else:
        # rel_strength = Qgis.RelationshipStrength(0) # integer, 0 is association, 1 composition
        rel_strength = Qgis.RelationshipStrength.Association # New way of defining it, as enumeration
        # print(rel_strength)
        rel.setStrength(rel_strength)

    # print("rel.is_valid", rel.isValid())
    if rel.isValid(): # Success
        QgsProject.instance().relationManager().addRelation(rel)
    else:
        msg: str = f"Invalid relation: {rel.name()}"
        QgsMessageLog.logMessage(message=msg, tag=dlg.PLUGIN_NAME, level=Qgis.MessageLevel.Critical, notifyUser=True)

    # ###############################################
    # Now start working on the form attached to the layer
    if dlg.settings.enable_ui_based_forms is False:

        # Get the layer configuration
        layer_configuration = layer.editFormConfig()
        # print('layer_configuration', layer_configuration)

        # Get the root container of all objects contained in the form (widgets, etc.)
        layer_root_container = layer_configuration.invisibleRootContainer()
        # print('layer_root_container', layer_root_container)

        # Find the element containing the "Generic Attributes" in the form.
        container_dv = get_attForm_child(container=layer_root_container, child_name=dv.form_tab_name)
        # Clean the element before inserting the relation
        container_dv.clear()

        # Create an 'attribute form' relation object from the 'relation' object
        relation_field = QgsAttributeEditorRelation(relation=rel, parent=container_dv)
        relation_field.setLabel(c.detail_views_group_alias)
        relation_field.setShowLabel(False) # No point setting a label then.
        # Add the relation to the 'Address(es)' container (tab).
        container_dv.addChildElement(relation_field)

        layer.setEditFormConfig(layer_configuration)
       
    return None


def create_layer_relation_to_dv_ext_ref(dlg: CDB4LoaderDialog, layer: QgsVectorLayer) -> None:
    """Function to set up the relations for an input layer (e.g. a view).
    - New relation objects are created that reference the detail views of the address(es) tables.
    - Relations are also set for 'Value Relation' widget.

    *   :param layer: vector layer to set up the relationships for.
        :type layer: QgsVectorLayer
    """
    detail_views: list = [v for k,v in dlg.DetailViewsRegistry.items() if k.startswith("ext_ref_")]

    # Isolate the layers' ToC environment to avoid grabbing the first layer encountered in the WHOLE ToC.
    root = QgsProject.instance().layerTreeRoot()

    db_node = root.findGroup(dlg.DB.db_toc_node_label)
    # db_node = root.findGroup(dlg.DB.database_name)

    schema_node = db_node.findGroup("@".join([dlg.DB.username,dlg.CDB_SCHEMA]))
    detail_views_node = schema_node.findGroup(c.detail_views_group_alias)
    dv_layers: list = detail_views_node.findLayers()

    # Get the layer configuration
    layer_configuration = layer.editFormConfig()
    # print('layer_configuration', layer_configuration)
    # Get the root container of all objects contained in the form (widgets, etc.)
    layer_root_container = layer_configuration.invisibleRootContainer()
    # print('layer_root_container', layer_root_container)

    dv: CDBDetailView
    for dv in detail_views:

        dv_layer: QgsLayerTreeLayer
        dv_layer = [elem for elem in dv_layers if elem.name().endswith(dv.gen_name)][0] # it should be only one!
        
        # Create new Relation object
        rel = QgsRelation()
        rel.setReferencedLayer(id=layer.id())  # i.e. the (QGIS  internal) id of the CityObject layer
        rel.setReferencingLayer(id=dv_layer.layerId()) # i.e. the (QGIS  internal) id of the Address layer
        rel.addFieldPair(referencingField='cityobject_id', referencedField='id')
        rel.setName(name='re_' + layer.name() + "_" + dv_layer.name())
        rel.setId(id="id_" + rel.name())

        # Till QGIS 3.26 the argument of setStrength is numeric, from QGIS 3.28 it is an enumeration
        if dlg.QGIS_VERSION_MAJOR == 3 and dlg.QGIS_VERSION_MINOR < 28:
            rel.setStrength(0) # integer, 0 is association, 1 composition
        else:
            rel_strength = Qgis.RelationshipStrength(0) # integer, 0 is association, 1 composition
            # print(rel_strength)
            rel.setStrength(rel_strength)

        # print("rel.is_valid", rel.isValid())
        if rel.isValid(): # Success
            QgsProject.instance().relationManager().addRelation(rel)
        else:
            msg: str = f"Invalid relation: {rel.name()}"
            QgsMessageLog.logMessage(message=msg, tag=dlg.PLUGIN_NAME, level=Qgis.MessageLevel.Critical, notifyUser=True)

        # Now set up the tab in the qml tab of the attribute form attached to the layer
        if dlg.settings.enable_ui_based_forms is False:

            # Find the element containing the "Gen Attrib (XXXX)" tab in the form.
            container_dv = get_attForm_child(container=layer_root_container, child_name=dv.form_tab_name)
            # Clean the element before inserting the relation
            container_dv.clear()

            # Create an 'attribute form' relation object from the 'relation' object
            relation_field = QgsAttributeEditorRelation(relation=rel, parent=container_dv)
            relation_field.setLabel(c.detail_views_group_alias)
            relation_field.setShowLabel(False) # No point setting a label then.
            # Add the relation to the 'Address(es)' container (tab).
            container_dv.addChildElement(relation_field)

    layer.setEditFormConfig(layer_configuration)


def create_layer_relation_to_dv_gen_attrib(dlg: CDB4LoaderDialog, layer: QgsVectorLayer) -> None:
    """Function to set up the relations for an input layer (e.g. a view).
    - New relation objects are created that reference the detail views of the address(es) tables.
    - Relations are also set for 'Value Relation' widget.

    *   :param layer: vector layer to set up the relationships for.
        :type layer: QgsVectorLayer
    """
    detail_views: list = [v for k,v in dlg.DetailViewsRegistry.items() if k.startswith("gen_attrib_")]

    # Isolate the layers' ToC environment to avoid grabbing the first layer encountered in the WHOLE ToC.
    root = QgsProject.instance().layerTreeRoot()

    db_node = root.findGroup(dlg.DB.db_toc_node_label)
    # db_node = root.findGroup(dlg.DB.database_name)
    
    schema_node = db_node.findGroup("@".join([dlg.DB.username,dlg.CDB_SCHEMA]))
    detail_views_node = schema_node.findGroup(c.detail_views_group_alias)
    dv_layers: list = detail_views_node.findLayers()

    # Get the layer configuration
    layer_configuration = layer.editFormConfig()
    # print('layer_configuration', layer_configuration)
    # Get the root container of all objects contained in the form (widgets, etc.)
    layer_root_container = layer_configuration.invisibleRootContainer()
    # print('layer_root_container', layer_root_container)

    dv: CDBDetailView
    # print("----------Registry")
    # for k, dv in dlg.DetailViewsRegistry.items():
    #     print(k, "--", dv.name, dv.curr_class, dv.gen_name, dv.form_tab_name)

    # print("----------Selection")
    # for dv in detail_views:
    #     print(dv.curr_class, dv.gen_name, dv.form_tab_name)

    for dv in detail_views:

        dv_layer: QgsLayerTreeLayer
        dv_layer = [elem for elem in dv_layers if elem.name().endswith(dv.gen_name)][0] # it should be only one!
        
        # Create new Relation object
        rel = QgsRelation()
        rel.setReferencedLayer(id=layer.id())  # i.e. the (QGIS  internal) id of the CityObject layer
        rel.setReferencingLayer(id=dv_layer.layerId()) # i.e. the (QGIS  internal) id of the Address layer
        rel.addFieldPair(referencingField='cityobject_id', referencedField='id')
        rel.setName(name='re_' + layer.name() + "_" + dv_layer.name())
        rel.setId(id="id_" + rel.name())

        # Till QGIS 3.26 the argument of setStrength is numeric, from QGIS 3.28 it is an enumeration
        if dlg.QGIS_VERSION_MAJOR == 3 and dlg.QGIS_VERSION_MINOR < 28:
            rel.setStrength(0) # integer, 0 is association, 1 composition
        else:
            rel_strength = Qgis.RelationshipStrength(0) # integer, 0 is association, 1 composition
            # print(rel_strength)
            rel.setStrength(rel_strength)

        # print("rel.is_valid", rel.isValid())
        if rel.isValid(): # Success
            QgsProject.instance().relationManager().addRelation(rel)
        else:
            msg: str = f"Invalid relation: {rel.name()}"
            QgsMessageLog.logMessage(message=msg, tag=dlg.PLUGIN_NAME, level=Qgis.MessageLevel.Critical, notifyUser=True)

        # Now set up the tab in the qml tab of the attribute form attached to the layer
        if dlg.settings.enable_ui_based_forms is False:

            # Find the element containing the "Gen Attrib (XXXX)" tab in the form.
            container_dv = get_attForm_child(container=layer_root_container, child_name=dv.form_tab_name)
            # Clean the element before inserting the relation
            container_dv.clear()

            # Create an 'attribute form' relation object from the 'relation' object
            relation_field = QgsAttributeEditorRelation(relation=rel, parent=container_dv)
            relation_field.setLabel(c.detail_views_group_alias)
            relation_field.setShowLabel(False) # No point setting a label then.
            # Add the relation to the 'Address(es)' container (tab).
            container_dv.addChildElement(relation_field)

    layer.setEditFormConfig(layer_configuration)
        
    return None


def qgsEditorWidgetSetup_factory(lu_config: Union[EnumConfig, CodeListConfig], layer_id: str) -> QgsEditorWidgetSetup:
    """Function to setup the configuration dictionary for the 'ValueRelation' widget.

        *   :returns: The object to set up the widget (ValueRelation)
        :rtype: QgsEditorWidgetSetup
    """
    config = {'AllowMulti': lu_config.allow_multi,
            'AllowNull': lu_config.allow_null,
            'FilterExpression': lu_config.filter_expression,
            'Layer': layer_id,
            'Key': lu_config.key_column,
            'Value': lu_config.value_column,
            'NofColumns': lu_config.num_columns,
            'OrderByValue': lu_config.order_by_value,
            'UseCompleter': lu_config.use_completer}
            
    return QgsEditorWidgetSetup(type='ValueRelation', config=config)


def create_layer_relation_to_enumerations(dlg: CDB4LoaderDialog, layer: QgsVectorLayer, layer_metadata: CDBLayer) -> None:
    """Function that sets up the ValueRelation widget for the look-up tables.

    *   :param layer: Layer to search for and set up its 'Value Relation' widget according to the look-up tables.
        :type layer: QgsVectorLayer
    """
    if not layer_metadata.enum_cols:
        return None # Exit, there are no enumerations to link to this layer

    # Isolate the layer's ToC environment to avoid grabbing the first layer encountered in the WHOLE ToC.
    root = QgsProject.instance().layerTreeRoot()

    db_node = root.findGroup(dlg.DB.db_toc_node_label)
    # db_node = root.findGroup(dlg.DB.database_name)

    schema_node = db_node.findGroup("@".join([dlg.DB.username, dlg.CDB_SCHEMA]))
    enums_node = schema_node.findGroup(c.lookup_tables_group_alias)
    enum_layers = enums_node.findLayers()
    enum_layer_id = [i.layerId() for i in enum_layers if c.enumerations_table in i.layerId()][0]

    # Create a dictionary with field names and field index
    fields_dict = {}
    for field in layer.fields():
        field_name = field.name()
        field_idx = layer.fields().indexOf(field_name)
        fields_dict[field_name] = field_idx
    # print(fields_dict)

    for enum_table, enum_col in layer_metadata.enum_cols:
        # This sets 'relative_to_terrain' and 'relative_to_water'
        if enum_table == "cityobject":
            lu_config: EnumConfig = dlg.EnumConfigRegistry[("CityObject", enum_table, enum_col)]
            field_idx = fields_dict[enum_col]            
            layer.setEditorWidgetSetup(field_idx, qgsEditorWidgetSetup_factory(lu_config, enum_layer_id))
        else:
            pass


def create_layer_relation_to_codelists(dlg: CDB4LoaderDialog, layer: QgsVectorLayer, layer_metadata: CDBLayer) -> None:
    """Function that sets up the ValueRelation widget for the look-up tables.

    *   :param layer: Layer to search for and set up its 'Value Relation' widget according to the look-up tables.
        :type layer: QgsVectorLayer
    """
    if not layer_metadata.codelist_cols:
        return None # Exit, there are no codelists to link to this layer

    # Isolate the layer's ToC environment to avoid grabbing the first layer encountered in the WHOLE ToC.
    root = QgsProject.instance().layerTreeRoot()

    db_node = root.findGroup(dlg.DB.db_toc_node_label)
    # db_node = root.findGroup(dlg.DB.database_name)
    
    schema_node = db_node.findGroup("@".join([dlg.DB.username, dlg.CDB_SCHEMA]))
    cls_node = schema_node.findGroup(c.lookup_tables_group_alias)
    cl_layers = cls_node.findLayers()
    cl_layer_id = [i.layerId() for i in cl_layers if c.codelists_table in i.layerId()][0]

    # Create a dictionary with field names and field index
    fields_dict = {}
    for field in layer.fields():
        field_name = field.name()
        field_idx = layer.fields().indexOf(field_name)
        fields_dict[field_name] = field_idx
    # print(fields_dict)

    for cl_table, cl_col in layer_metadata.codelist_cols:
        cl_class = layer_metadata.curr_class
        lu_config: CodeListConfig = dlg.CodeListConfigRegistry.get((cl_class, cl_table, cl_col), None)

        if not lu_config:
            # print("No codelist found for this key:", (cl_class, cl_table, cl_col))
            pass
        else:
            field_idx = fields_dict[cl_col]            
            layer.setEditorWidgetSetup(field_idx, qgsEditorWidgetSetup_factory(lu_config, cl_layer_id))

    return None


def add_lookup_tables_to_ToC(dlg: CDB4LoaderDialog) -> None:
    """Function to import the look-up tables into the qgis project.
    """
    # Just to shorten the variables names.
    db = dlg.DB
    cdb_schema: str = dlg.CDB_SCHEMA
    usr_schema: str = dlg.USR_SCHEMA

    # Add look-up tables into their own group in ToC.
    node_cdb_schema = QgsProject.instance().layerTreeRoot().findGroup("@".join([db.username, cdb_schema]))

    lookups_node = add_group_node_to_ToC(parent_node=node_cdb_schema, child_name=c.lookup_tables_group_alias)

    # Get look-up tables names from the server.
    lookup_tables = sql.list_lookup_tables(dlg=dlg)

    for lookup_table in lookup_tables:
        # Create ONLY new layers.
        if not is_layer_already_in_ToC_group(group=lookups_node, layer_name=f"{dlg.CDB_SCHEMA}_{lookup_table}"):
            uri = QgsDataSourceUri()
            uri.setConnection(aHost=db.host, aPort=db.port, aDatabase=db.database_name, aUsername=db.username, aPassword=db.password)
            uri.setDataSource(aSchema=usr_schema, aTable=lookup_table, aGeometryColumn=None, aKeyColumn="id")
            layer = QgsVectorLayer(path=uri.uri(False), baseName=f"{cdb_schema}_{lookup_table}", providerLib="postgres")
            if layer or layer.isValid(): # Success
                lookups_node.addLayer(layer)
                QgsProject.instance().addMapLayer(layer, False)
                # msg: str = f"Look-up table import: {cdb_schema}_{lookup_table}"
                # QgsMessageLog.logMessage(message=msg, tag=dlg.PLUGIN_NAME, level=Qgis.MessageLevel.Critical, notifyUser=True)
            else: # Fail
                msg: str = f"Look-up table failed to properly load: {cdb_schema}_{lookup_table}"
                QgsMessageLog.logMessage(message=msg, tag=dlg.PLUGIN_NAME, level=Qgis.MessageLevel.Critical, notifyUser=True)

    # After loading all look-ups, sort them by name.
    sort_ToC(lookups_node)

    return None


def add_detail_view_tables_to_ToC(dlg: CDB4LoaderDialog) -> None:
    """Function to import the 'detail view' tables into the qgis project.
    """
    # Just to shorten the variables names.
    db = dlg.DB
    cdb_schema = dlg.CDB_SCHEMA
    usr_schema = dlg.USR_SCHEMA
    extents = dlg.QGIS_EXTENTS.asWktPolygon()
    crs = dlg.CRS

    # Add generics tables into their own group in ToC.
    root = QgsProject.instance().layerTreeRoot().findGroup("@".join([db.username, cdb_schema]))
    detail_view_node = add_group_node_to_ToC(parent_node=root, child_name=c.detail_views_group_alias)

    dv: CDBDetailView
    for dv in dlg.DetailViewsRegistry.values():

        # Check that the detail view is not already loaded
        if not is_layer_already_in_ToC_group(detail_view_node, dv.name):

            uri = QgsDataSourceUri()
            uri.setConnection(aHost=db.host, aPort=db.port, aDatabase=db.database_name, aUsername=db.username, aPassword=db.password)

            if dv.has_geom:
                if dlg.QGIS_EXTENTS == dlg.LAYER_EXTENTS:
                    # No need to add the spatial filter
                    uri.setDataSource(aSchema=usr_schema, aTable=dv.name, aGeometryColumn="geom", aKeyColumn="id")
                else:
                    uri.setDataSource(aSchema=usr_schema, aTable=dv.name, aGeometryColumn="geom", aSql=f"ST_GeomFromText('{extents}') && geom", aKeyColumn="id")
                # Create a spatial detail view as QgsVectorLayer
                dv_layer = QgsVectorLayer(path=uri.uri(False), baseName=dv.name, providerLib="postgres")
                dv_layer.setCrs(crs)
            else:
                uri.setDataSource(aSchema=usr_schema, aTable=dv.name, aGeometryColumn=None, aKeyColumn="id")
                # Create a non-spatial detail view as QgsVectorLayer (but without geometry)
                dv_layer = QgsVectorLayer(path=uri.uri(False), baseName=dv.name, providerLib="postgres")

            if dv_layer or dv_layer.isValid(): # Success
                # Add the qml-based forms
                if dv.qml_form:
                    #print(dv.qml_form_with_path)
                    dv_layer.loadNamedStyle(theURI=dv.qml_form_with_path, loadFromLocalDb=False, categories=QgsMapLayer.StyleCategory.Fields|QgsMapLayer.StyleCategory.Forms)
                    #dv_layer.loadNamedStyle(theURI=dv.qml_form_with_path, categories=QgsMapLayer.Fields|QgsMapLayer.Forms)
                    #print('qui si blocca')
                    # otherwise: categories=QgsMapLayer.AllStyleCategories

                # Get the index of the field "cityobject"
                # This is needed to avoid a warning in the Log telling us that the cityobject field
                # is missing the relation in its configuration
                co_idx: int = dv_layer.fields().indexOf("cityobject_id")
                # print(f"co_id of {detail_view_name} id {co_idx}")
                dv_layer.setEditorWidgetSetup(index=co_idx, setup=QgsEditorWidgetSetup('TextEdit',{}))

                # Set the layer as read-only if the current cdb_schema is read only
                if dlg.CDBSchemaPrivileges == "ro":
                    dv_layer.setReadOnly()

                # Add to layer tree node
                detail_view_node.addLayer(dv_layer)
                QgsProject.instance().addMapLayer(dv_layer, False)

            else:
                msg: str = f"Detail view '{dv.name}' is not valid"
                QgsMessageLog.logMessage(message=msg, tag=dlg.PLUGIN_NAME, level=Qgis.MessageLevel.Critical, notifyUser=True)

    return None


def create_qgis_vector_layer(dlg: CDB4LoaderDialog, layer_name: str) -> QgsVectorLayer:
    """Function that creates a PostgreSQL layer based on the input layer name. This function is used to import
    updatable views from the usr_schema queried to the selected spatial extents.

    *   :param v_name: View name to connect to server.
        :type v_name: str

    *   :returns: the created layer object
        :rtype: QgsVectorLayer
    """
    # Shorten the variable names.
    db = dlg.DB
    usr_schema = dlg.USR_SCHEMA
    extents = dlg.QGIS_EXTENTS.asWktPolygon()
    crs = dlg.CRS

    uri = QgsDataSourceUri()
    uri.setConnection(aHost=db.host, aPort=db.port, aDatabase=db.database_name, aUsername=db.username, aPassword=db.password)

    if dlg.QGIS_EXTENTS == dlg.LAYER_EXTENTS:  
        # No need to apply a spatial filter in QGIS
        uri.setDataSource(aSchema=usr_schema, aTable=layer_name, aGeometryColumn="geom", aKeyColumn="id")
    else:
        uri.setDataSource(aSchema=usr_schema, aTable=layer_name, aGeometryColumn="geom", aSql=f"ST_GeomFromText('{extents}') && geom", aKeyColumn="id")

    new_layer = QgsVectorLayer(uri.uri(False), layer_name, "postgres")
    new_layer.setCrs(crs)

    return new_layer
    

def add_selected_layers_to_ToC(dlg: CDB4LoaderDialog, layers: list[CDBLayer]) -> int:
    """Function to imports the selected layer(s) in the user's qgis project.

    *   :param layers: A list containing View object that correspond to the server views.
        :type layers: list(CDBLayer)

    *   :returns: The number of actually imported layers (excluding those already loaded)
        :rtype: int

    """
    import_counter: int = 0
    if not layers:
        # nothing to do
        return import_counter # Exit with zero imported layers.

    # Just to shorten the variables names.
    db = dlg.DB
    cdb_schema: str = dlg.CDB_SCHEMA

    root = QgsProject.instance().layerTreeRoot()
    node_cdb: QgsLayerTreeGroup = root.findGroup(db.db_toc_node_label)
    node_cdb_schema: QgsLayerTreeGroup = None
    node_featureType: QgsLayerTreeGroup = None
    node_feature: QgsLayerTreeGroup = None
    node_lod: QgsLayerTreeGroup = None

    lookup_found: bool = False
    detail_views_found: bool = False
    layer_found: bool = False

    if node_cdb:
        node_cdb_schema = root.findGroup("@".join([db.username, cdb_schema]))
        if node_cdb_schema:
            # Check whether the generic attribute table is already loaded
            node_dv = node_cdb_schema.findGroup(c.detail_views_group_alias)
            if node_dv:
                dv_layers: list = node_dv.findLayers()
                if len(dv_layers) == len(dlg.DetailViewsRegistry):
                    detail_views_found = True

            # Check whether the look-up table for enumerations is already loaded
            node_lookup = node_cdb_schema.findGroup(c.lookup_tables_group_alias)
            if node_lookup:
                lu_layers: list = node_lookup.findLayers()
                for lu_layer in lu_layers:  
                    if lu_layer.name() == "_".join([cdb_schema, c.enumerations_table]):
                        lookup_found = True
        else:
            node_cdb_schema = add_group_node_to_ToC(node_cdb, "@".join([db.username, cdb_schema]))
    else:
        node_cdb = add_group_node_to_ToC(root, db.db_toc_node_label)
        node_cdb_schema = add_group_node_to_ToC(node_cdb, "@".join([db.username, cdb_schema]))

    # Load the generic attributes table if it is not already loaded 
    if not detail_views_found:
        node_dv = add_group_node_to_ToC(node_cdb_schema, c.detail_views_group_alias)
        add_detail_view_tables_to_ToC(dlg)
    else:
        # msg: str = "Generic attributes table already loaded: skipping"
        # QgsMessageLog.logMessage(message=msg, tag=dlg.PLUGIN_NAME, level=Qgis.MessageLevel.Info, notifyUser=True)
        pass

    # Load the look-up tables if they are not already loaded 
    if not lookup_found:
        node_lookup = add_group_node_to_ToC(node_cdb_schema, c.lookup_tables_group_alias) 
        add_lookup_tables_to_ToC(dlg)
    else:
        # msg: str = "Look-up tables already loaded: skipping"
        # QgsMessageLog.logMessage(message=msg, tag=dlg.PLUGIN_NAME, level=Qgis.MessageLevel.Info, notifyUser=True)
        pass

    # Start loading the selected layer(s)
    for layer in layers:
        # Check if the layer has already been loaded before
        layer_found = False
        node_featureType = node_cdb_schema.findGroup(f"FeatureType: {layer.feature_type}")
        if node_featureType:
            node_feature = node_featureType.findGroup(layer.root_class)
            if node_feature:
                node_lod = node_feature.findGroup(layer.lod)
                if node_lod:
                    existing_layers = node_lod.findLayers()
                    for existing_layer in existing_layers:
                        if existing_layer.name() == layer.layer_name:
                            layer_found = True

        if layer_found:
            msg: str = f"Layer {layer.layer_name} already in Layer Tree: skip reloading"
            QgsMessageLog.logMessage(message=msg, tag=dlg.PLUGIN_NAME, level=Qgis.MessageLevel.Info, notifyUser=True)
            # Memory refresher: The "continue" statement rejects all the remaining statements
            # in the current iteration of the loop and moves the control back to the top of the loop
            continue

        # Build the Table of Contents Tree or Restructure it.
        node_lod = add_layer_node_to_ToC(dlg, layer)

        new_layer: QgsVectorLayer = create_qgis_vector_layer(dlg, layer_name=layer.layer_name)

        if new_layer or new_layer.isValid(): # Success
            pass
        else: # Fail
            msg: str = f"Failed to properly load: {layer.layer_name}"
            QgsMessageLog.logMessage(message=msg, tag=dlg.PLUGIN_NAME, level=Qgis.MessageLevel.Critical, notifyUser=True)
            return False

        # Set the layer as read-only if the current cdb_schema is read only
        if dlg.CDBSchemaPrivileges == "ro":
            new_layer.setReadOnly()

        ###########################################################################################
        # To use "normal" (old) forms, simply set the value to FALSE in the dlg.settings.
        # See the default in class LoaderDefaultSettings (see other_classes.py)
        ###########################################################################################
        if dlg.settings.enable_ui_based_forms is False:
            # Attach 'attribute form' from QML file.
            if layer.qml_form:
                new_layer.loadNamedStyle(theURI=layer.qml_form_with_path, loadFromLocalDb=False, categories=QgsMapLayer.StyleCategory.Fields|QgsMapLayer.StyleCategory.Forms)
                # otherwise: categories=QgsMapLayer.AllStyleCategories

            # Attach 'symbology' from QML file.
            if layer.qml_symb:
                new_layer.loadNamedStyle(layer.qml_symb_with_path, loadFromLocalDb=False, categories=QgsMapLayer.StyleCategory.Symbology)

            if dlg.cbxEnable3D.isChecked():
                # Attach '3d symbology' from QML file.
                if layer.qml_3d:
                    new_layer.loadNamedStyle(layer.qml_3d_with_path, loadFromLocalDb=False, categories=QgsMapLayer.StyleCategory.Symbology3D)
            else:
                # Deactivate 3D renderer to avoid crashes and slow downs.
                new_layer.setRenderer3D(None)

            # Insert the layer to the assigned group
            node_lod.addLayer(new_layer)
            QgsProject.instance().addMapLayer(new_layer, False)

            # Filter out those layers that are not cityobjects and for which there is no need for the Generic Attributes link
            if layer.curr_class != "Address":  # might change to: not in ["Address", "...", "..."]

                if layer.curr_class in ["Building", "BuildingPart"]:
                    create_layer_relation_to_dv_address(dlg, layer=new_layer, dv_gen_name="address_bdg")
                elif layer.curr_class == "BuildingDoor":
                    create_layer_relation_to_dv_address(dlg, layer=new_layer, dv_gen_name="address_bdg_door")
                if layer.curr_class in ["Bridge", "BridgePart"]:
                    create_layer_relation_to_dv_address(dlg, layer=new_layer, dv_gen_name="address_bri")
                elif layer.curr_class == "BridgeDoor":
                    create_layer_relation_to_dv_address(dlg, layer=new_layer, dv_gen_name="address_bri_door")
        
                # Now, for all layers that are CityObjects
                create_layer_relation_to_dv_gen_attrib(dlg, layer=new_layer)
                create_layer_relation_to_dv_ext_ref(dlg, layer=new_layer)

                # Setup the relations for this layer to the look-up tables
                create_layer_relation_to_enumerations(dlg, layer=new_layer, layer_metadata=layer)
                create_layer_relation_to_codelists(dlg, layer=new_layer, layer_metadata=layer)
            
            # Finally, increment the counter after loading the layer and all the associated stuff
            import_counter += 1

        else:
            # This is where we may decide to deal with ui-based attribute forms
            pass

    # Return the number of layers that have been really loaded.
    # print(f"Imported {import_counter} layers into QGIS")
    return import_counter 
