import math
import os.path
import subprocess
import sys
from collections import Counter
from copy import deepcopy
import scipy as sp
import osmnx as ox
from django.conf import settings
import networkx as nx


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.
    """

    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
        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 = {}
        #permettre d'éviter de calculer plusieurs fois le plus cours chemin'
        try:
            dict_retour['gvvi_diametre'] = nx.diameter(G)
        except nx.NetworkXError:
            dict_retour['gvvi_diametre'] = nx.diameter(G.to_undirected())
        return dict_retour

    @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, self.maxSize]
        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
        regression_lineaire = sp.stats.linregress(x, y)
        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(G)
        if self.d0_R_carre < 0.99:
            return dict_retour
        extSizeBox = [1, self.maxSize]
        incSizeBox = 1
        size_box = extSizeBox[0]
        x = []
        y = []
        while size_box < extSizeBox[1]:
            nc = len(G.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",
                                 "CorrelationDimension_Michele_Tirico_Java/target/CorrelationDimension_Michele_Tirico_Java-1.0-SNAPSHOT-jar-with-dependencies.jar",
                                 "CorrelationDimension", chemin_vers_fichier]):
                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