import concurrent.futures
import math
import multiprocessing
import os.path
import random
import subprocess
import sys
import time
from collections import Counter
from copy import deepcopy
from itertools import repeat
from multiprocessing import Pool

import networkx as nx
import osmnx as ox
import scipy as sp
from django.conf import settings
from scipy.optimize import dual_annealing
from OSM_outils.OMSnx_enregistrement import OMSnx_enregistrement


class OSMnx_recuperation_graph_information:
    """
         Cette classe a pour but de récupérer toutes les informations d'un graph que nous voullons garder
     dans notre base de donnée.
    """

    running = multiprocessing.Value('i', 0)
    lock = multiprocessing.Lock()

    def __init__(self, G):
        """
        Cette méthode va permettre d'initialiser les variables partagées par plusieurs fonctions get. Les opérations
        sont :
            - mettre à l'échelle le graph en modifiant les positions des nœuds afin de pouvoir l'utiliser dans les
            calculs de fractal.

        :param G: le graph que nous allons étudier dans notre classe
        """
        if G is None:
            self.G_scale = None
            return
        self.G_scale = deepcopy(G)
        self.mise_a_echelle_graph()

    @staticmethod
    def sgbd_liste_fonction():
        """
        Cette méthode a pour but de retourner une liste de nom de fonction. Ces fonctions sont celles qui permettent de
        remplir la table Graph_viaire_ville_information

        :return: une liste d'objet fonction
        """
        liste_recupere_info = [func for func in dir(OSMnx_recuperation_graph_information) if callable(
            getattr(OSMnx_recuperation_graph_information, func)) and func.startswith("get_")]
        return liste_recupere_info

    @staticmethod
    def creer_tableau_associatif_fonction_variable_table_graph_viaire_ville_information():
        """
        Cette méthode a pour but de créer un tableau associatif entre les fonctions de récupérations de cette classe
        et les colonnes de la table graph_viaire_ville_information_instance.

        :return: tableau associatif entre les fonctions de récupérations de cette classe
        et les colonnes de la table graph_viaire_ville_information_instance
        """
        liste_fonction_recup_graph_information = OSMnx_recuperation_graph_information.sgbd_liste_fonction()
        tableau_associatif_nom_variable_nom_fonction = {}
        var_OSMnx_recuperation_graph_information = OSMnx_recuperation_graph_information(None)
        # création du lien entre les méthodes provenant de OSMnx_recuperation_graph_information et les colonnes de la
        # table graph_viaire_ville_information
        for func in liste_fonction_recup_graph_information:
            retour = getattr(var_OSMnx_recuperation_graph_information, func)(None)
            nom_variable_sgbd = next(iter(retour))
            tableau_associatif_nom_variable_nom_fonction[nom_variable_sgbd] = {"nom_fonction": func}
        return tableau_associatif_nom_variable_nom_fonction

    def mise_a_echelle_graph(self):
        """
        Cette méthode permet de mettre à l'échelle le graph en modifiant les positions des nœuds afin de pouvoir
        l'utiliser dans les calculs de fractal.
        """
        position_x_exteme = {"min": sys.maxsize, "max": -sys.maxsize}
        position_y_exteme = {"min": sys.maxsize, "max": -sys.maxsize}
        for noeud in self.G_scale.nodes.data():
            x_noeud = float(noeud[1]["x"])
            y_noeud = float(noeud[1]["y"])
            if position_x_exteme["min"] > x_noeud:
                position_x_exteme["min"] = x_noeud

            if position_x_exteme["max"] < x_noeud:
                position_x_exteme["max"] = x_noeud

            if position_y_exteme["min"] > y_noeud:
                position_y_exteme["min"] = y_noeud

            if position_y_exteme["max"] < y_noeud:
                position_y_exteme["max"] = y_noeud
        # déplacement vers l'origine
        position_x_exteme["max"] = position_x_exteme["max"] - position_x_exteme["min"]
        position_y_exteme["max"] = position_y_exteme["max"] - position_y_exteme["min"]

        self.size_space = [1000, 1000]
        self.maxSize = min(self.size_space)
        delta = 0.001
        scale = self.maxSize / max(position_x_exteme["max"], position_y_exteme["max"]) * (1 - delta)
        nouvelles_position = {}
        for noeud in self.G_scale.nodes.data():
            x_noeud = (float(noeud[1]["x"]) - position_x_exteme["min"]) * scale
            y_noeud = (float(noeud[1]["y"]) - position_y_exteme["min"]) * scale
            nouvelles_position[noeud[0]] = {"x": x_noeud, "y": y_noeud}
        nx.set_node_attributes(self.G_scale, nouvelles_position)

    def recuperation_information_graphe(self, G):
        """
        Cette méthode a pour but de récupérer les informations importantes du graphe. Les informations sont
        ensuite stockées dans un dictionnaire de la manière suivante :
        'clef sgbd' → résultat
        puis retournés.
        :param G: graphe OSMnx dont nous voulons extraire les informations
        :return: dictionnaire representation les informations importantes du graphe à stocker
        """
        dict_retour = {}
        liste_recupere_info = [func for func in dir(self) if callable(getattr(self, func)) and func.startswith("get_")]
        print(liste_recupere_info)
        id_fct = 0
        for fct in liste_recupere_info:
            print(fct, " : ", id_fct, " sur ", len(liste_recupere_info))
            dict_retour.update(getattr(self, fct)(G))
            id_fct = id_fct + 1
        #print("dict retour : ", dict_retour)
        return dict_retour

    @staticmethod
    def get_recuperation_osmnx_basic_information(G):
        """
        Cette méthode à pour but de récupérer les informations de statistiques basic que peut nous fournir la OSMnx
        sur le graphe entré en paramètre.

        :param G: Le graph du quelle nous voulons extraire les informations
        :return: retourne les statistiques basic que peut nous fournir la OSMnx
        sur le graphe entré en paramètre
        """
        dict_retour = {'gvvi_nb_intersection' : 0}
        if G is None:
            return dict_retour
        basic_statistique = ox.basic_stats(G)
        dict_retour['gvvi_nb_intersection'] = basic_statistique['intersection_count']
        return dict_retour

    @staticmethod
    def get_diametre(G):
        """
        Cette méthode permet de récupérer le diamètre du graph, c'est-à-dire la longueur du plus long des plus
        cours chemins. En cas de graph directionnel non connexe, le graph est transformé en graph non directionnel.

        :param G: Le graph du quelle nous voulons extraire les informations
        :return: retourne les statistiques basic que peut nous fournir la OSMnx
        sur le graphe entré en paramètre
        """
        dict_retour = {'gvvi_diametre' : 0}
        if G is None:
            return dict_retour
        try:
            dict_retour['gvvi_diametre'] = OSMnx_recuperation_graph_information.diameter(G)
        except nx.NetworkXError:
            dict_retour['gvvi_diametre'] = OSMnx_recuperation_graph_information.diameter(G.to_undirected())
        return dict_retour

    @staticmethod
    def diameter(G):
        """
        Cette méthode permet de paralléliser le calcul du diamètre du graph en divisant les nœuds du graphe en sous
        tableau, le maximum de sous tableaux est actuellement 8.

        :param G: le graph dont nous voulons connaitre le diamètre
        :return:
        """
        nodes = list(G.nodes())
        OSMnx_recuperation_graph_information.running.value = len(G.nodes())

        nb_coeurs = min(8, len(G.nodes))
        k, m = divmod(len(nodes), nb_coeurs)
        nodes_split = list((nodes[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(nb_coeurs)))
        with concurrent.futures.ProcessPoolExecutor() as executor:
            N = [result for result in executor.map(OSMnx_recuperation_graph_information.
                                                           diameter_groupe_iteration, nodes_split, repeat(G))]
        return max(N)

    @staticmethod
    def diameter_groupe_iteration(nodes, G):
        """
        Cette méthode a été copiée depuis networkx eccentricity distance_measures.py modifié sur les arguments entrants
        et sur la valeur de retour. Elle prend en paramètre un tableau de nœuds et un graph et retourne la longueur
         du chemin long des chemins les plus cours des nœuds entré en paramètre.
         La méthode d'origine prend le graph et peux prendre un nœud et un tableau de plus cours chemin et retourn le
         tableau des plus cours chemins.

        :param nodes: le tableau de noeuds dont on va chercher les chemin les plus cours
        :param G: le graph a traiter
        :return: la longueur du chemin le plus long des plus cours chemins
        """
        e = {}
        for n in nodes:
            length = nx.single_source_shortest_path_length(G, n)
            if len(length) != len(G.nodes()):
                if G.is_directed():
                    msg = (
                        "Found infinite path length because the digraph is not"
                        " strongly connected"
                    )
                else:
                    msg = "Found infinite path length because the graph is not" " connected"
                raise nx.NetworkXError(msg)
            with OSMnx_recuperation_graph_information.lock:
                if OSMnx_recuperation_graph_information.running.value % math.ceil(len(G.nodes) / 10) == 0:
                    print(f"calcul diametre reste : {OSMnx_recuperation_graph_information.running.value}\n", end='', flush=True)
                OSMnx_recuperation_graph_information.running.value -= 1
            e[n] = max(length.values())
        return max(e.values())


    @staticmethod
    def get_degre_moyen(G):
        """
        Cette méthode à pour but de récupérer le degré moyen des nœuds du graph.

        :param G: Le graph du quelle nous voulons extraire les informations
        :return: retourne les statistiques basic que peut nous fournir la OSMnx
        sur le graphe entré en paramètre
        """
        dict_retour = {'gvvi_degre_moyen' : 0}
        if G is None:
            return dict_retour
        dict_retour['gvvi_degre_moyen'] = ox.basic_stats(G)['k_avg']
        return dict_retour

    @staticmethod
    def get_connectivity(G):
        """
        Cette méthode à pour but de récupérer les informations de statistiques basic que peut nous fournir la OSMnx
        sur le graphe entré en paramètre. Cette statistique est la connectivité du graph

        :param G: Le graph du quelle nous voulons extraire les informations
        :return: retourne les statistiques basic que peut nous fournir la OSMnx
        sur le graphe entré en paramètre
        """
        dict_retour = {'gvvi_connectivity' : 0}
        if G is None:
            return dict_retour
        if nx.number_of_nodes(G) == 2:
            if len(G.edges) == 1:
                dict_retour['gvvi_connectivity'] = 1
        else:
            dict_retour['gvvi_connectivity'] = nx.number_of_edges(G) / (3 * (nx.number_of_nodes(G) - 2))
        return dict_retour

    @staticmethod
    def get_planning_vs_self_organised_cities(G):
        """
        Cette méthode a pour but de calculer le ratio organic permettant de discriminer les graphes
        de ville plannifié aux graphes de villes autoorganisé, est considéré comme planifié un graph viaire dont la
        proportion de nœuds de degré 4 est le plus important (un graph en grillage).
        Un graph non planifié sera lui plus composé de nœuds de degré 1 et 3.

        Le coefficient doit être compris entre 0 et 1, plus un graph est planifié plus son coefficient est proche de 1.
        :param G: Le graph du quelle nous voulons extraire les informations
        :return: le coefficient d'organisation compris entre 0 et 1.
        """
        dict_retour = {'gvvi_org_plan_vs_organized' : 0}
        if G is None:
            return dict_retour
        orgvs = Counter(d for n, d in G.degree())
        dict_retour['gvvi_org_plan_vs_organized'] = (orgvs[1] + orgvs[3]) / nx.number_of_edges(G)
        return dict_retour

    def get_d0_fractal_capacite_dimension(self, G):
        """
        Le calcul de la capacité dimensionnel de la figure fractale formé par la position des nœuds du graph.
        Elle calcule également la taille minimum et maximal des cases du quadrillage pou les méthodes :
        - get_d1_information_dimension
        - get_d2_correlation_dimension

        Lors de son calcul si le coefficient r-carré des résultats est inférieur à 0.99, nous ne calculons pas :
        - get_d1_information_dimension
        - get_d2_correlation_dimension

        :param G: Le graph du quelle nous voulons extraire les informations
        :return: retourne les statistiques basic que peut nous fournir la OSMnx
        sur le graphe entré en paramètre
        """
        dict_retour = {'gvvi_d0_fractal_capacite_dimension' : 0}
        if self.G_scale is None:
            return dict_retour
        extSizeBox = [1, 500]
        incSizeBox = 1
        size_box = extSizeBox[0]
        x = []
        y = []
        while size_box < extSizeBox[1]:
            v0 = size_box
            v1, _ = self.calculs_informations_quadrillage_supperposition_graph(size_box, self.size_space)
            x.append(math.log(v0))
            y.append(math.log(v1))
            size_box = size_box + incSizeBox

        def objectif(arg):
            """
            La méthode objective a pour but de trouver pour le calcul de descente de gradient une valeur r-carre
            supérieur à 0.99 et de prendre en compte le maximum de points dans la courbe.
            :param arg: un tuple comprenant le minimum de taille des boites et le maximum de la taille des boites pour
            le calcul de la capacité dimensionnel fractal.
            :return:
            """
            min, max = arg
            min = int(min)
            max = int(max)
            linregress = sp.stats.linregress(x[min:max], y[min:max])
            r_carre = linregress.rvalue ** 2
            if 1 - r_carre > 0.01:
                return 1 - r_carre
            return -(1 - r_carre) -(1 + (max - min)/(extSizeBox[1] - extSizeBox[0]))


        limites = [[extSizeBox[0], 30], [200, extSizeBox[1] + 1]]
        result = dual_annealing(objectif, bounds=limites)

        self.min_trouve = int(result.x[0])
        self.max_trouve = int(result.x[1])
        regression_lineaire = sp.stats.linregress(x[self.min_trouve: self.max_trouve], y[self.min_trouve: self.max_trouve])
        self.d0_R_carre = regression_lineaire.rvalue ** 2
        print("p : ", self.d0_R_carre)
        dict_retour['gvvi_d0_fractal_capacite_dimension'] = abs(regression_lineaire.slope)
        return dict_retour

    def calculs_informations_quadrillage_supperposition_graph(self, sizeBox, size_space):
        """
        Cette méthode permet lors de la superposition d'un quadrillage avec le graph, de connaitre le nombre de cases
        possédant des nœuds et le nombre de nœuds par cases.

        :param sizeBox: la taille des boites
        :param size_space: le nombre de boites sur la largeur et la hauteur du graph
        :param G: le graph à utiliser
        :return: le nombre de boites contenant des nœuds et le nombre de nœuds dans chaques boites.
        """
        numNoEmpty = 0
        numBox = [int(size_space[0] / sizeBox), int(size_space[1] / sizeBox)]
        boxes = [[0] * numBox[0] for _ in range(numBox[1])]
        sizeBox = [size_space[0] / numBox[0], size_space[1] / numBox[1]]
        for noeud in self.G_scale.nodes.data():
            minX = math.floor(float(noeud[1]["x"]) / sizeBox[0])
            minY = math.floor(float(noeud[1]["y"]) / sizeBox[1])
            if boxes[minX][minY] == 0:
                numNoEmpty = numNoEmpty + 1
            boxes[minX][minY] = boxes[minX][minY] + 1
        return numNoEmpty, boxes

    def get_d1_information_dimension(self, G):
        """
        Cette méthode permet de calculer les informations de dimensions fractales du graph. Elle dépend de la méthode
        get_d0_fractal_capacite_dimension pour connaitre la taille des cases du quadrillage ainsi que du coefficient
        r-carré.

        :param G: Le graph du quelle nous voulons extraire les informations
        :return: l'information dimensionnelle d1 du graph
        """
        dict_retour = {'gvvi_d1_information_dimension' : 0}
        if self.G_scale is None:
            return dict_retour
        if not dir(self).__contains__("d0_R_carre"):
            self.get_d0_fractal_capacite_dimension(None)
        if self.d0_R_carre < 0.99:
            return dict_retour
        extSizeBox = [self.min_trouve, self.max_trouve]
        incSizeBox = 1
        size_box = extSizeBox[0]
        x = []
        y = []
        while size_box < extSizeBox[1]:
            nc = len(self.G_scale.nodes)
            vals = self.nb_noeud_par_case(size_box, self.size_space)
            e = 0
            for v in vals:
                e = e + v / nc * math.log(v / nc)
            x.append(math.log(size_box))
            y.append(abs(e))
            size_box = size_box + incSizeBox
        regression_lineaire = sp.stats.linregress(x, y)
        dict_retour['gvvi_d1_information_dimension'] = abs(regression_lineaire.slope)
        return dict_retour

    def nb_noeud_par_case(self, size_box, size_space):
        """
        Cette méthode permet de remplir une liste contenant le nombre de nœuds par cases excepté lorsqu'il n'y a aucuns
        nœuds dans la case.

        :param size_box: la taille des boites
        :param size_space: le nombre de boites sur la largeur et la hauteur du graph
        :param G: le graph à utiliser
        :return: coll une liste contenant le nombre de nœuds par cases excepté lorsqu'il n'y a aucuns nœuds dans la case
        """
        coll = []
        _, boxes = self.calculs_informations_quadrillage_supperposition_graph(size_box, size_space)
        for ligne_boites in boxes:
            for boites in ligne_boites:
                if boites != 0:
                    coll.append(boites)
        return coll

    def get_d2_correlation_dimension(self, G):
        """
        Cette méthode a pour but de calculer les informations de correlations de la dimension du graph. Elle dépend de
        la méthode get_d0_fractal_capacite_dimension pour connaitre la taille des cases du quadrillage ainsi que du
        coefficient r-carré

        :param G: Le graph du quelle nous voulons extraire les informations
        :return: retourne les statistiques basic que peut nous fournir la OSMnx
        sur le graphe entré en paramètre
        """
        dict_retour = {'gvvi_d2_correlation_dimension' : 0}
        if G is None :
            return dict_retour
        nom_ville = G.name
        if nom_ville is None or nom_ville == "":
            return dict_retour
        if not dir(self).__contains__("d0_R_carre"):
            self.get_d0_fractal_capacite_dimension(self.G_scale)
        if self.d0_R_carre < 0.99:
            return dict_retour
        chemin_vers_fichier = OMSnx_enregistrement.chemin_vers_fichier_ville(nom_ville)
        fichier_erreur=open("./fichier_erreur", "a")
        derniere_ligne = ""
        try:
            for path in self.execute_commande_en_continue(["/usr/bin/java", "-jar",
                                 settings.OSM_OUTILS_DIR + "/CorrelationDimension_Michele_Tirico_Java/target/CorrelationDimension_Michele_Tirico_Java-1.0-SNAPSHOT-jar-with-dependencies.jar",
                                 chemin_vers_fichier, str(self.min_trouve), str(self.max_trouve)]):
                derniere_ligne = path
                print("p : ", path, end="")
            try:
                res = float(derniere_ligne)
                dict_retour['gvvi_d2_correlation_dimension'] = res
            except ValueError as val_err:
                fichier_erreur.write(chemin_vers_fichier + "\n")
                fichier_erreur.write("La dernière ligne affiché par le programme java n'est pas le résultat\n" + val_err.__str__())
        except subprocess.CalledProcessError as sub_proc:
            fichier_erreur.write(chemin_vers_fichier + "\n")
            fichier_erreur.write("le fichier graphml n'a pas put s'ouvrir corectement\n" + sub_proc.__str__())
        return dict_retour

    @staticmethod
    def execute_commande_en_continue(cmd):
        """
        Le but de cete fonction est d'exécuter une commande dans le shell et de permettre d'afficher ce que la commande
        affiche dans sa sortie standard.

        :param cmd: La commande à executer dans le shell
        :return: la dernière ligne affichée dans la sortie standard par le programme lancé dans le sous processus.
        """
        popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
        # affiche aussi les System.err.println
        stdout_line = ""
        for stdout_line in iter(popen.stdout.readline, ""):
            yield stdout_line
        popen.stdout.close()
        return_code = popen.wait()
        if return_code:
            raise subprocess.CalledProcessError(return_code, cmd)
    @staticmethod
    def get_tree_vs_complete_graph(G):
        """
        Cette méthode à pour but de récupérer les informations de statistiques basic que peut nous fournir la OSMnx
        sur le graphe entré en paramètre. Ici, nous calculons le coefficient meshedness. Le coefficient est égal à 0 si
        c'est un arbre, 1 si c'est un graph complet

        :param G: Le graph du quelle nous voulons extraire les informations
        :return: retourne les statistiques basic que peut nous fournir la OSMnx
        sur le graphe entré en paramètre
        """
        dict_retour = {'gvvi_tree_vs_complete_graph' : 0}
        if G is None:
            return dict_retour
        dict_retour['gvvi_tree_vs_complete_graph'] = (nx.number_of_edges(G) - nx.number_of_nodes(G) + 1)/\
                                                     (2 * nx.number_of_edges(G) - 5)
        return dict_retour

    @staticmethod
    def get_longueur_moyennes_rues(G):
        """
        Cette méthode a pour but de récupérer la longueur moyenne des rues du graph

        :param G: Le graph du quelle nous voulons extraire les informations
        :return: La longueur moyenne des rues
        """
        dict_retour = {'gvvi_longueur_moyenne_rues' : 0}
        if G is None:
            return dict_retour
        dict_retour['gvvi_longueur_moyenne_rues'] = ox.stats.edge_length_total(G) / len(G.edges)
        return dict_retour
    @staticmethod
    def get_robustesse_moyenne(G):
        """
        Cette méthode à pour but de calculer la robustesse moyenne du graph entré en paramètre

        :param G: Le graph du quelle nous voulons extraire les informations
        :return: retourne la robustesse moyenne du graph
        """
        dict_retour = {'gvvi_robustesse' : 0}
        if G is None:
            return dict_retour
        nb_test = 100
        OSMnx_recuperation_graph_information.running.value = 100
        res = Pool().map(OSMnx_recuperation_graph_information.calcul_robustesse_iteration, repeat(G, nb_test))
        dict_retour['gvvi_robustesse'] = 1 - (sum(res) / len(res))
        return dict_retour

    @staticmethod
    def calcul_robustesse_iteration(G):
        """
        Calcul la robustesse sur une itération du graph entré en paramètre sans le modifier.

        :param G: le graph dont on veut calculer la robustesse
        :return: la robustesse du graph sur un essai
        """
        percentAtEachStep = 0.01

        nb_noeuds_supprimmer = len(G) * percentAtEachStep
        per = 0
        perMax = 0.8
        taille_recherche_graph_geant = 0.5 * len(G)
        sizeGiant = []
        G_copie = G.copy().to_undirected()
        liste_noeuds = list(G_copie)
        step = 0
        # récupére élément le connecté le plus gros et compte le nombre de noeuds
        sizeGiant.append([len(c) for c in sorted(nx.connected_components(G_copie), key=len, reverse=True)][0])
        while (per < perMax):
            numNode = 0
            while (numNode < nb_noeuds_supprimmer):
                try:
                    G_copie.remove_node(random.choice(liste_noeuds))
                    numNode = numNode + 1
                except nx.NetworkXError:
                    numNode = numNode + 1
            step = step + nb_noeuds_supprimmer
            sizeGiant.append([len(c) for c in sorted(nx.connected_components(G_copie), key=len, reverse=True)][0])
            if sizeGiant[-1] <= taille_recherche_graph_geant:
                with OSMnx_recuperation_graph_information.lock:
                    if OSMnx_recuperation_graph_information.running.value % 10 == 0:
                        print(f"calcul robustesse reste : {OSMnx_recuperation_graph_information.running.value}\n",
                              end='', flush=True)
                    OSMnx_recuperation_graph_information.running.value -= 1
                return len(G_copie) / len(G)
            per = per + percentAtEachStep
        with OSMnx_recuperation_graph_information.lock:
            if OSMnx_recuperation_graph_information.running.value % 10 == 0:
                print(f"calcul robustesse reste : {OSMnx_recuperation_graph_information.running.value}\n", end='', flush=True)
            OSMnx_recuperation_graph_information.running.value -= 1
        return sizeGiant

    @staticmethod
    def get_cout(G):
        """
        La donnée récupérée par la fonction est ici le cout, une mesure de
        l'accessibilité des intersections.

        :param G: Le graph du quelle nous voulons extraire les informations
        :return: le cout d'un graph (une mesure d'accessibilité des intersections)
        """
        dict_retour = {'gvvi_cout' : 0}
        if G is None:
            return dict_retour
        G_spanning_tree = nx.minimum_spanning_tree(G.to_undirected(), "length", algorithm="kruskal")
        dict_retour['gvvi_cout'] = (ox.stats.edge_length_total(G_spanning_tree) / ox.stats.edge_length_total(G))
        return dict_retour
