"""
Functions that can be classified as utilities, usable by the module's various tools
"""
import os
import requests
import concurrent.futures
import json
import urllib.parse
import time
import itertools
from osgeo import ogr

from qgis import *
from qgis.PyQt import *
from qgis.core import *
from qgis.utils import *
from qgis.PyQt.QtGui import *
from qgis.PyQt.QtCore import *
from qgis.PyQt.QtWidgets import *
from qgis.PyQt.QtCore import *

try:
    from PyQt6.QtNetwork import QNetworkRequest
except ImportError:
    from qgis.PyQt.QtNetwork import QNetworkRequest

from .Fonctions_QGIS import *


# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer
path=os.path.dirname(os.path.abspath(__file__))
path=path.replace("Fonctions",'Siligites_dialog_base.ui')
FORM_CLASS, _ = uic.loadUiType(path)

plugin_dir = os.path.dirname(__file__)
metadata = configparser.ConfigParser()
metadata.read(plugin_dir.replace("Fonctions", "metadata.txt"))
wfs_SILEX = metadata["general"]["wfs"]

class Utilitaires(QtWidgets.QDialog, FORM_CLASS) :
    def __init__(self, parent=None):
        """Constructor."""
        super(Utilitaires, 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)

    @staticmethod
    def tr(message):
        return QCoreApplication.translate("Utilitaires", message)

    
    def progression(bar, label, message, percent, color):
        """Updating a loading bar according to stages

        :param bar: Bar to update
        :type bar: QgsProgessBar

        :param label: Text to modify in order to give informations
        :type label: str

        :param message: Message to display
        :type message: str

        :param percent: Advancement's percent
        :type percent: int

        :param color: Color to apply for the text
        :type percent: str
        """
        # Reset the bar to 0 to make it easier to go back and forth through the stages
        bar.setValue(0)

        # Changing the progress bar message
        label.setText(message)
        label.setStyleSheet("color: {}".format(color))

        # Function to move the loading bar forward
        completed = 0
        while completed < percent:
            completed += 1
            bar.setValue(completed)


    def recuperation(self, path, name):
        """
        Recuperation of different utility layers from WFS

        :param path: Path to the extension's folder
        :type path: str

        :param name: Layer's name
        :type name: str
        """
        os.makedirs(path, exist_ok=True)
        dst = os.path.join(path, f"{name}.gpkg")

        # If already in the project, no modifications
        if os.path.isfile(dst):
            return

        # URL ArcGIS FeatureServer
        url = f"https://services3.arcgis.com/zy1UGXGNCAGm516O/arcgis/rest/services/{name}/FeatureServer/0"
        vlayer = QgsVectorLayer(f"url={url}", name, "arcgisfeatureserver") 
        
        if not vlayer.isValid(): 
            raise RuntimeError(Utilitaires.tr( "Le calque '{}' est invalide ou inaccessible : {}").format(name, url))

        QgsVectorFileWriter.writeAsVectorFormat(
            vlayer,
            dst,
            "UTF-8",
            vlayer.crs(),
            "GPKG"
        )

    
    def add_layers():
        """
        Automatic addition of vector layers when using the module
        """
        settings = QgsSettings()
        prev_zoom = settings.value("/qgis/zoomToAddedLayer", True, type=bool)
        settings.setValue("/qgis/zoomToAddedLayer", False)
        iface.mapCanvas().freeze(True)

        gpkgs = ["Regions_Naturelles","Departements","Communes",wfs_SILEX]
        names = [Utilitaires.tr( "Regions_Naturelles"),
                 Utilitaires.tr( "Departements"),
                 Utilitaires.tr( "Centroïde des communes françaises"),
                 "WFS_GDR_SILEX"]
        styles = ["Regions_Naturelles","Departements","Communes","Legende_CODE_HEXA_ETAGE_FINAL_vEV"]

        existing_layers = list(QgsProject.instance().mapLayers().values())
        existing_sources = [layer.source() for layer in existing_layers]
        existing_names = {layer.name() for layer in existing_layers}

        url = "https://services3.arcgis.com/zy1UGXGNCAGm516O/arcgis/rest/services/{}/FeatureServer/0"

        path = os.path.dirname(os.path.abspath(__file__))
        layer_path = path.replace('Fonctions', 'Couches//{}.gpkg')
        style_path = path.replace('Fonctions', 'Styles//{}.qml')
 
        for idx in range(1,5):
            layer_name = f"{idx} - {names[idx-1]}"

            path_gpkg = layer_path.format(gpkgs[idx-1])
            url_gpkg = url.format(gpkgs[idx-1])
            style_gpkg = style_path.format(styles[idx-1])

            layer = None

            if layer_name not in existing_names and path_gpkg not in existing_sources and url_gpkg not in existing_sources:
                if os.path.exists(path_gpkg):
                    layer = QgsVectorLayer(path_gpkg, layer_name, "ogr")
                else:
                    uri = QgsDataSourceUri()
                    uri.setParam('url', url_gpkg)
                    uri.setSrid('EPSG:3857')
                    layer = QgsVectorLayer(uri.uri(), layer_name, "arcgisfeatureserver")
        
            if layer:
                if os.path.exists(style_gpkg):
                    layer.loadNamedStyle(style_gpkg)
                    layer.triggerRepaint()
                QgsProject.instance().addMapLayer(layer)

                if names[idx-1] == "WFS_GDR_SILEX":
                    try: 
                        QgsProject.instance().layerTreeRoot().findLayer(layer.id()).setItemVisibilityChecked(False)
                    except Exception: 
                        pass

                # Acceleration of the loading of the entities
                provider = layer.dataProvider()
                try:
                    provider.setProviderProperty(
                        provider.SkipFeatureCount, True
                    )
                except Exception:
                    pass

                try:
                    provider.setProviderProperty(
                        provider.MaxFeaturesPerRequest, 2000
                    )
                except Exception:
                    pass
                
                request = QgsFeatureRequest()
                request.setSubsetOfAttributes(layer.fields().allAttributesList())
                request.setFlags(QgsFeatureRequest.NoFlags) 
                layer.dataProvider().createSpatialIndex()
                layer.triggerRepaint()

        # Zoom on the main part of the layers
        box = QgsRectangle(-85000, 6000000, 1300000, 7200000)

        crs_src = QgsCoordinateReferenceSystem("EPSG:2154")
        crs_dest = QgsProject.instance().crs()

        if crs_src != crs_dest:
            transform = QgsCoordinateTransform(
                crs_src,
                crs_dest,
                QgsProject.instance()
            )
            try:
                box = transform.transformBoundingBox(box)
            except TypeError:
                box = transform.transform(box)

        iface.mapCanvas().setExtent(box)
        iface.mapCanvas().refresh()

        settings.setValue("/qgis/zoomToAddedLayer", prev_zoom)
        iface.mapCanvas().freeze(False)
  

    def layer_finder(ComboBox):
        """Select a layer from the name selected in a drop-down list

        :param ComboBox: Drop-down list of the layer to be retrieved from
        :type ComboBox: QgsComboBox
        """
        name = ComboBox.currentText()

        # Select the layer by name
        layer = QgsProject.instance().mapLayersByName(name)[0]

        return layer
    

    def prepare_extraction(self, layer, name):
        """Loading WFS flows from the RT SILEX based on a department name

        :param layer: Layer of the extraction zone selected
        :type layer: QgsVectorLayer

        :param name: Name of the extraction zone
        :type name: str

        :returns: Extraction from the WFS of the RT SILEX
        :rtypes: QgsVectorLayer
        """
        # Creation of a buffered zone to prevent missing geological formations
        # t0=time.perf_counter() # debugtest
        URL="https://services3.arcgis.com/zy1UGXGNCAGm516O/arcgis/rest/services/{}/FeatureServer/0/query".format(wfs_SILEX)
 
        """Recuperation of the extraction zone"""
        # Creation of the bounding box of the extraction area
        bb = layer.selectedFeatures()[0].geometry().boundingBox()

        # Gestion of the projection if necessary
        if layer.crs().authid()!=f"EPSG:3857":
            bb = QgsCoordinateTransform(layer.crs(),
                                      QgsCoordinateReferenceSystem(f"EPSG:3857"),
                                      QgsProject.instance().transformContext()
                                      ).transformBoundingBox(bb)
        
        # Creation of a buffer to be sure to catch all geological formations
        ext = QgsRectangle(bb.xMinimum()-12000,bb.yMinimum()-12000,bb.xMaximum()+12000,bb.yMaximum()+12000)

        """Recuperation of entities from the WFS"""
        # Initialisation of the parameters
        s = requests.Session()
        s.headers.update({"Connection":"keep-alive","Accept-Encoding":"gzip"})
        feats = []
        off = 0

        # Recuperation of the entities in a json
        while True:
            r=s.get(URL,params = {"f":"json",
                                "geometry":f"{ext.xMinimum()},{ext.yMinimum()},{ext.xMaximum()},{ext.yMaximum()}",
                                "geometryType":"esriGeometryEnvelope",
                                "inSR":3857,
                                "outSR":3857,
                                "spatialRel":"esriSpatialRelIntersects",
                                "outFields":"*",
                                "where":"1=1",
                                "resultOffset":off,
                                "resultRecordCount":2000},
                                timeout=60)
            if r.status_code != 200:
                raise Exception(f"Erreur HTTP {r.status_code}: {r.text[:200]}")
            batch = r.json().get("features",[])
            if not batch:
                break
            feats += batch
            off += 2000
            if len(batch) < 2000:
                break
        if not feats: 
            raise Exception(Utilitaires.tr( "Aucune entité récupérée"))

        """Creation of the entities from the json"""
        # Creation of the layer for the extraction
        gtype = "Polygon"
        extraction=QgsVectorLayer(f"{gtype}?crs=EPSG:3857","Formations_{}".format(name),"memory")

        # Creation of the attributes fields
        dp=extraction.dataProvider()

        # Getting all the attributes
        attributes = feats[0]["attributes"].items()

        # Determining the type of attribute
        fields = []
        for k, v in attributes:
            if isinstance(v, int):
                field_type = QVariant.Int
            elif isinstance(v, float):
                field_type = QVariant.Double
            else:
                field_type = QVariant.String
            
            # Creating the attribute
            field = QgsField(k, field_type)
            fields.append(field)

        # Adding the attribute to the layer
        dp.addAttributes(fields)
        extraction.updateFields()

        # Transforming the json entities in gqis entities
        qgis_feats = []
        for f in feats:
            # Preparation of the new entity
            feat = QgsFeature(extraction.fields())
            # Recuperation of the geometry from the extracted data
            g = f["geometry"]
            rings = []
            for r in g["rings"]:
                ring_points = [QgsPointXY(x, y) for x, y in r]
                rings.append(ring_points)

            # Creation of the geometry for th new entity
            geom = QgsGeometry.fromPolygonXY(rings)
            feat.setGeometry(geom)

            # Adding the attributes to the new entity
            feat.setAttributes(list(f["attributes"].values()))
            qgis_feats.append(feat)

        # Adding the entities in the layer
        extraction.startEditing()
        dp.addFeatures(qgis_feats)
        extraction.commitChanges()
        extraction.dataProvider().createSpatialIndex()

        # Gestion of the layer style
        style_path = os.path.dirname(os.path.abspath(__file__)).replace('Fonctions','Styles//Legende_CODE_HEXA_ETAGE_FINAL_vEV.qml')
        if os.path.exists(style_path):
            try:
                extraction.loadNamedStyle(style_path)
            except Exception:
                pass
        # print(f"{len(feats)} entités {gtype} chargées en mémoire — {time.perf_counter()-t0:.2f}s") # debugtest

        return extraction

    
    def layer_creation(type, EPSG, name, reference):
        """Creating a layer from a reference model

        :param type: Vector type (Point, LineString, Polygon)
        :type type: str

        :param EPSG: Layer projection system
        :type EPSG: int

        :param name: Name of the output layer
        :type name: str

        :param reference: Layer used as a model for attribute table columns
        :type reference: str

        :returns: New layer created
        :rtypes: QgsVectorLayer
        """
        # Layer creation
        new_layer = QgsVectorLayer("{}?crs=epsg:{}".format(type,EPSG), str(name), "memory")
        layer_data = new_layer.dataProvider()

        # Retrieving columns from the attribute table of the reference layer
        attr = reference.dataProvider().fields().toList()

        # Updating the columns in the new layer
        layer_data.addAttributes(attr)
        new_layer.updateFields()

        return new_layer
    

    def closest_point(reference, nodes):
        """Find the closest node in a network to a reference point

        :param reference: Reference point
        :type reference: QgsFeature

        :param nodes: Set of points to compare
        :type nodes: list of QgsFeature

        :returns: Closest point to the reference point
        :rtypes: QgsFeature
        """
        try:
            xa = reference.geometry().asPoint()[0]
            ya = reference.geometry().asPoint()[1]
        except Exception:
            xa = reference.x()
            ya = reference.y()

        # Creating a value to initialise the search
        dist_min = 0

        # Search all the vertices of the transformed polygon
        if isinstance(nodes, list):
            for vertice in nodes:
                # Retrieving vertex coordinates
                xb = vertice.geometry().asPoint()[0]
                yb = vertice.geometry().asPoint()[1]

                # Calculation of the distance separating the vertex from the origin of the LCP
                dist = (((xa - xb)**2) + ((ya - yb)**2))**0.5

                # Checking whether it is furthest or closest to the origin of the LCP
                if dist < dist_min or dist_min == 0:
                    feat_min = vertice
                    dist_min = dist
        else:
            for vertice in nodes.getFeatures():
                # Retrieving vertex coordinates
                xb = vertice.geometry().asPoint()[0]
                yb = vertice.geometry().asPoint()[1]

                # Calculation of the distance separating the vertex from the origin of the LCP
                dist = (((xa - xb)**2) + ((ya - yb)**2))**0.5

                # Checking whether it is furthest or closest to the origin of the LCP
                if dist < dist_min or dist_min == 0:
                    feat_min = vertice
                    dist_min = dist
        return feat_min
    

    def furthest_point(reference, nodes):
        """Find the furthest node in a network to a reference point

        :param reference: Reference point
        :type reference: (QgsFeature)

        :param nodes: Set of points to compare
        :type nodes: list of QgsFeature

        :returns: Furthest point to the reference point
        :rtypes: QgsFeature
        """
        try:
            xa = reference.geometry().asPoint()[0]
            ya = reference.geometry().asPoint()[1]
        except Exception:
            xa = reference.x()
            ya = reference.y()

        # Creating a value to initialise the search
        dist_max = 0

        # Search all the vertices of the transformed polygon
        if isinstance(nodes, list):
            for vertice in nodes:
                # Retrieving vertex coordinates
                xb = vertice.geometry().asPoint()[0]
                yb = vertice.geometry().asPoint()[1]

                # Calculation of the distance separating the vertex from the origin of the LCP
                dist = (((xa - xb)**2) + ((ya - yb)**2))**0.5

                # Checking whether it is furthest or closest to the origin of the LCP
                if dist > dist_max or dist_max == 0 :
                    feat_max = vertice
                    dist_max = dist
        else:
            for vertice in nodes.getFeatures() :
                # Retrieving vertex coordinates
                xb = vertice.geometry().asPoint()[0]
                yb = vertice.geometry().asPoint()[1]

                # Calculation of the distance separating the vertex from the origin of the LCP
                dist=(((xa - xb)**2) + ((ya - yb)**2))**0.5
                
                # Checking whether it is furthest or closest to the origin of the LCP
                if dist > dist_max or dist_max == 0 :
                    feat_max = vertice
                    dist_max = dist
        return feat_max
    

    def order_steps(start, inter, end):
        """Find the furthest node in a network to a reference point

        :param start: Starting point
        :type start: QgsFeature

        :param inter: Set of points to order
        :type inter: list of QgsFeature

        :param end: Arrival point
        :type end: QgsFeature

        :returns: Sorted points
        :rtypes: list of QgsFeature
        """
        paths = itertools.permutations(inter)

        sorted_stops = None
        dist_min = float("inf")

        for path in paths:
            # Construire le chemin complet
            chemin_complet = [start] + list(path) + [end]
            dist = 0

            for n in range(len(chemin_complet) - 1):
                # Retrieving vertices coordinates
                xa = chemin_complet[n].geometry().asPoint()[0]
                ya = chemin_complet[n].geometry().asPoint()[1]

                xb = chemin_complet[n+1].geometry().asPoint()[0]
                yb = chemin_complet[n+1].geometry().asPoint()[1]

                # Calculation of the distance separating the vertex from the origin of the LCP
                dist += (((xa - xb)**2) + ((ya - yb)**2))**0.5

            if dist < dist_min:
                dist_min = dist
                sorted_stops = path
        
        return sorted_stops

