import sys
import random
import numpy as np

import triangulation
import tas
import stack


class Graphe:
    """Implémente un graphe non orienté."""

    class Cout:
        CARBURANT = 0
        TEMPS = 1

    def __init__(self, nom):
        """Initialise un graphe vide.

        :param nom: Nom du graphe
        """
        self.nom = nom
        self.sommets = []
        self.aretes = []
        self.cout = None
        self.arrivee = None

    def renomme(self, nom):
        """Renome le graphe

        :param nom: Nouveau nom du graphe.
        """
        self.nom = nom

    def ajouteSommet(self, x, y):
        """Ajoute le sommet s au graphe.

        :param x: abcisse du sommet.
        :param y: ordonnée du sommet.

        :return: Le sommet créé.
        """
        s = Sommet((x, y), len(self.sommets), graphe=self)
        self.sommets.append(s)
        return s

    def connecte(self, s1, s2, longueur, v_moyenne):
        """Connecte les sommets s1 et s2.

        :param s1: premier sommet.
        :param s2: deuxième sommet.
        :param longueur: longueur de l'arrête.
        :param v_moyenne: vitesse moyenne sur l'arrête.

        :return: L'arête créée.
        """
        a = Arete(s1, s2, longueur, v_moyenne, self)
        self.aretes.append(a)
        s1.aretes.add(a)
        s2.aretes.add(a)
        return a

    def n(self):
        """Retourne le nombre de sommets du graphe."""
        return len(self.sommets)

    def m(self):
        """Retourne le nombre d'arrêtes du graphe."""
        return len(self.aretes)

    def __str__(self):
        return "V({nom})=[\n{noeuds}\n]\nE({nom})=[\n{aretes}\n]\n".format(
            nom=self.nom,
            aretes='\n'.join([str(v) for v in self.aretes]),
            noeuds='\n'.join([str(n) for n in self.sommets])
        )

    def trace(self, dest):
        """Affiche le graphe sur la destination.

        :param dest: instance de Afficheur.
        """
        dest.renomme(self.nom)
        for s in self.sommets:
            dest.changeCouleur(s.couleur)
            dest.tracePoint((s.x(), s.y()))
            if hasattr(s, 'nom'):
                dest.traceTexte((s.x(), s.y()), s.nom)
            else:
                dest.traceTexte((s.x(), s.y()), 'v' + str(s.num))

        for a in self.aretes:
            dest.changeCouleur(a.couleur)
            dest.traceLigne((a.s1.x(), a.s1.y()), (a.s2.x(), a.s2.y()))

    def ajouteRoute(self, v1, v2, vmax):
        """Ajoute une route de distance euclidienne entre v1 et v2.

        :param v1: Premier sommet.
        :param v2: Deuxième sommet.
        :param vmax: vitesse maximale sur la route.

        :return: Route créée.
        """
        return self.connecte(v1, v2, v1.distance(v2), vmax)

    def ajouteNationale(self, v1, v2):
        """Ajoute une nationale au graphe. (rouge et vmax = 90km/h).

        :param v1: Premier sommet.
        :param v2: Deuxième sommet.

        :return: route créée.
        """
        a = self.ajouteRoute(v1, v2, 90)
        a.couleur = (1., 0., 0.)
        return a

    def ajouteDepartementale(self, v1, v2):
        """Ajoute une départementale au graphe. (jaune et vmax = 60km/h).

        :param v1: Premier sommet.
        :param v2: Deuxième sommet.

        :return: route créée.
        """
        a = self.ajouteRoute(v1, v2, 60)
        a.couleur = (1., 1., 0.)
        return a

    def dijkstra(self, depart=None):
        """Calcule les plus courts chemins depuis le sommet `depart` (attention,
        effets de bord).

        :param depart: Sommet de départ.
        """
        for s in self.sommets:
            s.cumul = None
            s.precedent = None
        sommets = [depart or self.sommets[0]]
        sommets[0].cumul = 0
        while sommets:
            i, s = min(enumerate(sommets), key=lambda x: x[1].cumul)
            sommets.pop(i)
            for arete in s.aretes:
                voisin = arete.voisin(s)
                if voisin.cumul is None:  # cumul infini
                    sommets.append(voisin)
                if voisin.cumul is None or s.cumul + arete.cout < voisin.cumul:
                    voisin.cumul = s.cumul + arete.cout
                    voisin.precedent = arete

    def dijkstraAvecTas(self, depart=None):
        """Calcule les plus courts chemins depuis le sommet `depart` (attention,
        effets de bord).

        :param depart: Sommet de départ.
        """
        for s in self.sommets:
            s.cumul = None
            s.precedent = None
        sommets = tas.Tas(lambda x: -x.cumul)
        depart = depart or self.sommets[0]
        depart.cumul = 0
        depart.cle = sommets.ajoute(depart)
        while not sommets.empty():
            s = sommets.pop()
            for arete in s.aretes:
                voisin = arete.voisin(s)
                inf = voisin.cumul is None
                if inf or s.cumul + arete.cout < voisin.cumul:
                    voisin.cumul = s.cumul + arete.cout
                    if inf:  # cumul infini
                        voisin.cle = sommets.ajoute(voisin)
                    else:
                        sommets.actualise(voisin.cle)
                    voisin.precedent = arete

    def dijkstraPartiel(self, depart=None, arrivee=None):
        """Calcule les plus courts chemins depuis le sommet `depart` vers
        `arrivee` (attention, effets de bord).

        :param depart: Sommet de départ.
        """
        for s in self.sommets:
            s.cumul = None
            s.precedent = None
        sommets = tas.Tas(lambda x: -x.cumul)
        depart = depart or self.sommets[0]
        arrivee = arrivee or self.sommets[-1]
        depart.cumul = 0
        depart.cle = sommets.ajoute(depart)
        n_visite = 0
        quit = False
        while not (sommets.empty() or quit):
            s = sommets.pop()
            for arete in s.aretes:
                voisin = arete.voisin(s)
                inf = voisin.cumul is None
                if inf or s.cumul + arete.cout < voisin.cumul:
                    voisin.cumul = s.cumul + arete.cout
                    voisin.precedent = arete
                    if inf:  # cumul infini
                        n_visite += 1
                        voisin.couleur = (0.5, 0.9, 0.1)
                        if voisin == arrivee:
                            quit = True
                        voisin.cle = sommets.ajoute(voisin)
                    else:
                        sommets.actualise(voisin.cle)
        return self.cheminOptimal(arrivee), n_visite

    def astar(self, depart=None, arrivee=None):
        """Calcule les plus courts chemins depuis le sommet `depart` vers
        `arrivee` (attention, effets de bord).

        :param depart: Sommet de départ.
        """
        for s in self.sommets:
            s.cumul = None
            s.precedent = None
        self.arrivee = arrivee or self.sommets[-1]
        arrivee = self.arrivee
        sommets = tas.Tas(lambda x: -(x.cumul + x.minCumulRestant))
        depart = depart or self.sommets[0]
        depart.cumul = 0
        depart.cle = sommets.ajoute(depart)
        n_visite = 0
        quit = False
        while not (sommets.empty() or quit):
            s = sommets.pop()
            for arete in s.aretes:
                voisin = arete.voisin(s)
                inf = voisin.cumul is None
                if inf or s.cumul + arete.cout < voisin.cumul:
                    voisin.cumul = s.cumul + arete.cout
                    voisin.precedent = arete
                    if inf:  # cumul infini
                        n_visite += 1
                        voisin.couleur = (0.5, 0.9, 0.1)
                        if voisin == arrivee:
                            quit = True
                        voisin.cle = sommets.ajoute(voisin)
                    else:
                        sommets.actualise(voisin.cle)
        return self.cheminOptimal(arrivee), n_visite

    def traceArbreDesChemins(self):
        """Change la couleur des chemins optimaux en violet-rose
        (252, 17, 189) et le départ en bleu (10, 98, 252).
        """
        for s in self.sommets:
            if s.precedent:
                s.precedent.couleur = (252 / 255, 17 / 255, 189 / 255)
            if not s.cumul:  # sommet de départ
                s.couleur = (10 / 255, 98 / 255, 252 / 255)

    def fixeCarburantCommeCout(self):
        """Fixe le carburant pour cout lors du calcul de plus court chemin."""
        self.cout = Graphe.Cout.CARBURANT

    def fixeTempsCommeCout(self):
        """Fixe le temps pour cout lors du calcul de plus court chemin."""
        self.cout = Graphe.Cout.TEMPS

    def cheminOptimal(self, arrivee):
        """Donne le chemin optimal pour aller à `arrivee` depuis le point
        de départ donné à `dijkstra`.

        :param arrivee: Sommet d'arrivee

        :return: le chemin (liste des arêtes)
        """
        chemin = []
        suivant = arrivee
        while suivant.cumul:
            chemin.append(suivant.precedent)
            suivant = suivant.precedent.voisin(suivant)
        chemin.reverse()
        return chemin

    def colorieChemin(self, chemin, c):
        """Colorie le chemin.

        :param chemin: une liste d'arrêtes
        :param c: une couleur
        """
        for a in chemin:
            a.couleur = c

    def matriceCout(self, tournee):
        """Calcule la matrice de cout d'une tournée.

        :param tournee:La liste des étapes avec en première position le départ.
        :return: la matrice des couts.
        """
        n = len(tournee)
        matrice = np.zeros((n, n))
        for x, sx in enumerate(tournee):
            self.dijkstra(sx)
            for y, sy in enumerate(tournee):
                matrice[x, y] = matrice[y, x] = sy.cumul
        return matrice

    def voyageurDeCommerceNaif(self, tournee):
        """Résout le problème du voyageur de commerce par backtracking.

        :param tournee: La liste des sommets à visiter.
        :return: le cout minimal et l'itinéraire associé.
        """
        meilleurItineraire = []
        minCout = sys.float_info.max
        matrice = self.matriceCout(tournee)
        cout = 0
        visite = [False] * len(tournee)
        visite[0] = True
        itineraire = [0]

        def backtrack():
            nonlocal meilleurItineraire, minCout, matrice, cout, visite, itineraire
            if cout < minCout:
                if len(itineraire) == len(tournee):
                    if cout + matrice[0, itineraire[-1]] < minCout:
                        minCout = cout + matrice[0, itineraire[-1]]
                        meilleurItineraire = [tournee[x] for x in itineraire]
                else:
                    for i, sommet_visite in enumerate(visite):
                        if not sommet_visite:
                            visite[i] = True
                            indicePrec = itineraire[-1]
                            itineraire.append(i)
                            if cout + matrice[i, indicePrec] < minCout:
                                cout += matrice[i, indicePrec]
                                backtrack()
                                cout -= matrice[i, indicePrec]
                            visite[i] = False
                            itineraire.pop()
        backtrack()
        return minCout, meilleurItineraire

    def traceItineraire(self, itineraire):
        """Colorie les arêtes d'un itinéraire.

        :param itineraire: les points de la tournée ordonnés.
        """
        for i, x in enumerate(itineraire):
            x.nom = "s" + str(i)
            suivant = itineraire[(i + 1) % len(itineraire)]
            c, _ = self.astar(x, suivant)
            self.colorieChemin(c, (0.8, 0.1, 0.8))

    def Prim(self):
        """Applique l'algorithme de Prim sur le graphe.
        Les sommets et arêtes sont ajoutés à l'arbre en passant leur flag
        selection à True.

        :return: le poids de l'arbre minimal
        """
        for s in self.sommets:
            s.selection = False
        for a in self.aretes:
            a.selection = False
        aretes = tas.Tas(lambda x: -x.cout)
        for a in self.sommets[0].aretes:
            aretes.ajoute(a)
        self.sommets[0].selection = True
        ptot = 0
        while not aretes.empty():
            a = aretes.pop()
            s1, s2 = a.s1, a.s2
            if not s1.selection or not s2.selection:
                a.selection = True
                ptot += a.cout
                if not s1.selection:
                    s1.selection = True
                    for a_1 in s1.aretes:
                        aretes.ajoute(a_1)
                if not s2.selection:
                    s2.selection = True
                    for a_2 in s2.aretes:
                        aretes.ajoute(a_2)
        return ptot

    def colorieSelection(self, couleur=(0.1, 0.6, 0.2)):
        """Colorie les sommets et arêtes avec le drapeau `selection` à True."""
        for s in self.sommets:
            if s.selection:
                s.couleur = couleur
        for s in self.aretes:
            if s.selection:
                s.couleur = couleur

    def grapheDeCout(self, tournee):
        """Génère le graphe de coût pour la tournée.

        :param tournee: la tournée.
        :return: le graphe de coûts.
        """
        g = Graphe("Graphe de cout de " + self.nom)
        for s in tournee:
            s_a = g.ajouteSommet(s.x(), s.y())
            s_a.image = s
            s.image = s_a

        for i, s in enumerate(g.sommets):
            self.dijkstraAvecTas(s.image)
            for s_v in g.sommets[i:]:
                d = sum((a.cout for a in self.cheminOptimal(s_v.image)))
                g.connecte(s, s_v, d, 1)
        return g

    def tourneeApproximative(self, tournee):
        """Résout le problème du voyageur de commerce de manière approximative.

        :param tournee: les sommets à visiter
        :return: Le chemin optimal approché
        """
        g_cout = self.grapheDeCout(tournee)
        g_cout.Prim()
        pile = stack.Stack()
        pile.add(tournee[0].image)
        tournee[0].image.selection = False
        chemin = [tournee[0]]
        while not pile.empty():
            actuel = pile.pop()
            fils = list(filter(lambda x: x.selection, actuel.aretes))
            if fils:
                a = fils.pop()
                a.selection = False
                nouveau = a.voisin(actuel)
                nouveau.selection = False
                chemin.append(nouveau.image)
                pile.add(actuel)
                pile.add(nouveau)
        return chemin


class Sommet:
    """Implémente un sommet de graphe."""

    def __init__(self, pos, num, couleur=(0., 0., 0.), graphe=None):
        """Initialise un sommet.

        :param pos: couple donnant la position du point.
        :param num: numéro du sommet.
        :param couleur: couleur du sommet.
        """
        self.pos = pos
        self.num = num
        self.couleur = couleur
        self.aretes = set()
        self.cumul = None
        self.precedent = None
        self.graphe = graphe
        self.selection = False
        self.image = None

    @property
    def minCumulRestant(self):
        """Minore le cumul restant pour le sommet considéré.
        """
        x, y = self.graphe.arrivee.pos
        d = ((self.pos[0] - x)**2 + (self.pos[1] - y)**2)**(1 / 2)
        if self.graphe.cout is Graphe.Cout.TEMPS:
            return d / 90  # On minore le temps en divisant par la vitesse max
        elif self.graphe.cout is Graphe.Cout.CARBURANT:
            return d
        else:
            return 0

    def __str__(self):
        return "v{} (x = {} km y = {} km)".format(
            str(self.num),
            str(self.x()),
            str(self.y())
        )

    def distance(self, v):
        """Calcule la distance entre deux points.

        :param v: Point de calcul de la distance.
        """
        return ((self.x() - v.x())**2 + (self.y() - v.y())**2)**(1 / 2)

    def x(self):
        """Retourne l'abcisse de x."""
        return self.pos[0]

    def y(self):
        """Retourne l'ordonnée de y."""
        return self.pos[1]


class Arete:
    """Implémente une arête de graphe."""

    def __init__(self, **kwargs):
        """Initialise une arête.

        :param s1: sommet 1.
        :param s2: sommet 2.
        :param longueur: longueur de l'arête.
        :param v_moyenne: vitesse moyenne sur l'arête.
        :param couleur: couleur de l'arête.
        :param graph: le graphe de l'arête.
        """
        self.s1 = kwargs.get('s1')
        self.s2 = kwargs.get('s2')
        self.s1.aretes.add(self)
        self.s2.aretes.add(self)
        self.longueur = kwargs.get('longueur')
        self.v_moyenne = kwargs.get('v_moyenne')
        self.couleur = kwargs.get('couleur', (0., 0., 0.))
        self.graph = kwargs.get('graph')
        self.selection = False

    def voisin(self, s):
        """Retourne le sommet voisin de s dans l'arête.

        :param s: Un sommet de l'arête.
        """
        if s == self.s1:
            return self.s2
        else:
            return self.s1

    @property
    def cout(self):
        """Retourne le cout de l'arête."""
        if self.graph.cout is Graphe.Cout.TEMPS:
            return self.longueur / self.v_moyenne
        elif self.graph.cout is Graphe.Cout.CARBURANT:
            return self.longueur
        else:
            return 1

    def __str__(self):
        return " v{v1}, v{v2} (long. = {lon} km vlim. = {v} km/h)".format(
            v1=str(self.s1.num),
            v2=str(self.s2.num),
            lon=str(self.longueur),
            v=str(self.v_moyenne)
        )


def pointsAleatoires(n, L):
    """Crée un graphe de n points aléatoires non reliés dans le carré centré en
    0 de largeure L.

    :param n: nombre de points.
    :param L: Côté du carré.
    """
    g = Graphe("Graphe aléatoire")
    for _ in range(n):
        x, y = random.uniform(-L / 2, L / 2), random.uniform(-L / 2, L / 2)
        g.ajouteSommet(x, y)
    return g


def test_gabriel(g, v1, v2):
    """Teste si on peut connecter deux sommets selon le critère de Gabriel.

    :param g: Le graphe.
    :param v1: Premier sommet.
    :param v2: Deuxième sommet.
    """
    milieu = Sommet(((v1.x() + v2.x()) / 2, (v1.y() + v2.y()) / 2), -1)
    rayon = v1.distance(v2) / 2
    ajoute = True
    for v3 in g.sommets:
        if v3 in [v1, v2]:
            continue
        elif v3.distance(milieu) < rayon:
            ajoute = False
            break
    return ajoute


def gabriel(g, ignore_gvr=False):
    """Crée le graphe de Gabriel associé à g avec des routes démartementales.

    :param g: Un graphe sans arrêtes.
    :param ignore_gvr: booléen indiquant si on ne doit pas ajouter une arrête
    quand gvr en ajoute une.
    """
    for v1 in g.sommets:
        for v2 in g.sommets:
            if v2 == v1:
                continue
            if test_gabriel(g, v1, v2):
                if (ignore_gvr and not test_gvr(g, v1, v2)) or not ignore_gvr:
                    g.ajouteDepartementale(v1, v2)


def test_gvr(g, v1, v2):
    """Teste si on peut connecter deux sommets selon le critère GVR.

    :param g: Le graphe.
    :param v1: Premier sommet.
    :param v2: Deuxième sommet.
    """
    rayon = v1.distance(v2)
    ajoute = True
    for v3 in g.sommets:
        if v3 in [v1, v2]:
            continue
        elif v3.distance(v1) < rayon and v3.distance(v2) < rayon:
            ajoute = False
            break
    return ajoute


def gvr(g):
    """Crée le graphe GVR associé à g avec des routes nationales.

    :param g: Un graphe sans arrêtes.
    """
    for v1 in g.sommets:
        for v2 in g.sommets:
            if v2 == v1:
                continue
            if test_gvr(g, v1, v2):
                g.ajouteNationale(v1, v2)


def reseau(g):
    """Construit une carte routière avec comme nationales les routes obtenues
    par la méthode GVR et départementales celles de Gabriel.

    :param g: Un raphe sans arêtes.
    """
    gabriel(g, ignore_gvr=True)
    gvr(g)


def delaunay(g):
    """Crée une triangulation de Delaunay à partir d ' un nuage de point"""
    t = triangulation.Triangulation(g)
    # Définit une fonction destinée à être appelée
    # pour toute paire (s1,s2) de sommets
    # qui forme une arête dans la triangulation de Delaunay.
    # v3 et v4 sont les troisièmes sommets des deux triangles
    # ayant (s1,s2) comme côté.

    def selectionneAretes(graphe, v1, v2, v3, v4):
        # Fait de chaque arête de la triangulation
        # une nationale
        graphe.ajouteNationale(v1, v2)
    # Construit le graphe de retour, égal à la triangulation de Delaunay
    g = t.construitGrapheDeSortie(selectionneAretes)
    g.renomme("Delaunay(" + str(g.n()) + ")")
    return g


def reseauRapide(g):
    """Crée une triangulation de Delaunay à partir d ' un nuage de point"""
    t = triangulation.Triangulation(g)
    # Définit une fonction destinée à être appelée
    # pour toute paire (s1,s2) de sommets
    # qui forme une arête dans la triangulation de Delaunay.
    # v3 et v4 sont les troisièmes sommets des deux triangles
    # ayant (s1,s2) comme côté.

    def selectionneAretes(graphe, v1, v2, v3, v4):
        if test_gabriel(graphe, v1, v2):
            if test_gvr(graphe, v1, v2):
                graphe.ajouteNationale(v1, v2)
            else:
                graphe.ajouteDepartementale(v1, v2)
    # Construit le graphe de retour, égal à la triangulation de Delaunay
    g = t.construitGrapheDeSortie(selectionneAretes)
    g.renomme("Delaunay(" + str(g.n()) + ")")
    return g
