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


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
        """
        self.G_scale = deepcopy(G)
        self.mise_a_echelle_graph()


    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, nom_ville):
        """
        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 voullons extraire les informations
        :return: dictionnaire représentatn 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")]
        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("d2_correlation_dimension")
        dict_retour.update(self.d2_correlation_dimension(nom_ville))
        #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 = {}
        basic_statistique = ox.basic_stats(G)
        dict_retour['gvvi_nb_intersection'] = basic_statistique['intersection_count']
        return dict_retour

    @staticmethod
    def lire_graphe(gvvi_nom):
        """
        Cette méthode permet de lire dans le dossier Data un grapml grâce au nom entré en paramètre.
        Ce paramètre doit être le nom de la ville sans le chamin ni l'extension graphml

        :param gvvi_nom: Le nom de la ville associé à un graphe dont nous voulons récupérer le graph.
        """
        try:
            path = OSMnx_recuperation_graph_information.chemin_vers_fichier_ville(gvvi_nom)
        except FileNotFoundError as e:
            raise e
        return ox.load_graphml(path)

    @staticmethod
    def chemin_vers_fichier_ville(gvvi_nom):
        """
        Cette méthode permet de créer un chemin vers un fichier de ville d'après le nom de la ville

        :param nom_ville: nom de la ville
        :return: le chemin vers le fichier Data et le nom de fichier normalement associé au nom du fichier
        """
        return os.path.join(settings.REGISTER_DIR + "/" + gvvi_nom + ".graphml")

    @staticmethod
    def get_diametre(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 = {}
        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 diametre 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:
                OSMnx_recuperation_graph_information.running.value -= 1
                print(f"Still running: {OSMnx_recuperation_graph_information.running.value}\n", end='', flush=True)
            e[n] = max(length.values())
        return max(e.values())


    @staticmethod
    def get_degre_moyen(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 = {}
        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 = {}
        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 à 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 un ratio organic permettant de discriminer les graphes
        de ville plannifié aux graphes de villes autoorganisé.

        :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 = {}
        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):
        """
        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
        """
        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):
            min, max = arg
            min = int(min)
            max = int(max)
            linregress = sp.stats.linregress(x[min:max], y[min:max])
            r_carre = linregress.rvalue ** 2
            slope = linregress.slope

            #fusion :::!!!!!
            #if 1 - r_carre > 0.01:
            #    return 10 + (1 - r_carre)
            #return -(1 - r_carre) + abs(slope)  -(1 + (max - min)/(extSizeBox[1] - extSizeBox[0]))
            #if 1 - r_carre > 0.01:
            #    return 10 + (1 - r_carre)
            #return -(1 - r_carre) + abs(slope)
            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
        dict_retour = {}
        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 à 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 = {}
        dict_retour['gvvi_d1_information_dimension'] = 0
        if not dir(self).__contains__("d0_R_carre"):
            self.get_d0_fractal_capacite_dimension(None)
        if len(self.G_scale.nodes) > 5:
            print("pppppppp : ", self.d0_R_carre)
        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 d2_correlation_dimension(self, nom_ville):
        """
        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
        """
        chemin_vers_fichier = OSMnx_recuperation_graph_information.chemin_vers_fichier_ville(nom_ville)
        dict_retour = {}
        dict_retour['gvvi_d2_correlation_dimension'] = 0
        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
        fichier_erreur=open("./fichier_erreur", "a")
        derniere_ligne = ""
        try:
            for path in self.execute_commande_en_continue(["/usr/bin/java", "-cp",
                                 settings.OSM_OUTILS_DIR + "/CorrelationDimension_Michele_Tirico_Java/target/CorrelationDimension_Michele_Tirico_Java-1.0-SNAPSHOT-jar-with-dependencies.jar",
                                 "CorrelationDimension", 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 = {}
        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 à 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 = {}
        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 = {}
        nb_test = 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:
                return len(G_copie) / len(G)
            per = per + percentAtEachStep
        return sizeGiant

    @staticmethod
    def get_cout(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. La donnée récupéré 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: retourne les statistiques basic que peut nous fournir la OSMnx
        sur le graphe entré en paramètre
        """
        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
