package fr.univ.dblp.analysis;

import fr.univ.dblp.export.ResultsPrinter;
import org.graphstream.algorithm.ConnectedComponents;
import org.graphstream.graph.Graph;
import org.graphstream.graph.implementations.SingleGraph;
import org.graphstream.algorithm.generator.RandomGenerator;

/**
 * Analyse la connexité d'un réseau (Question 3).
 *
 * Cette classe permet de vérifier si un réseau est connexe, de compter
 * le nombre de composantes connexes et de déterminer le degré critique
 * pour la connexité d'un réseau aléatoire.
 *
 * @author Hamadou BA
 * @see <a href="https://www-apps.univ-lehavre.fr/forge/bh243413/tp2-ri-mesures-de-reseaux-interaction.git">Dépôt Git</a>
 */
public class ConnectivityAnalyzer {

    /**
     * Vérifie si le graphe est connexe.
     *
     * Un graphe est connexe s'il existe un chemin entre toute paire de nœuds,
     * c'est-à-dire s'il ne contient qu'une seule composante connexe.
     *
     * @param g Le graphe à analyser
     * @return true si le graphe est connexe, false sinon
     */
    public static boolean isConnected(Graph g) {
        ConnectedComponents cc = new ConnectedComponents();
        cc.init(g);
        cc.compute();
        return cc.getConnectedComponentsCount() == 1;
    }

    /**
     * Compte le nombre de composantes connexes du graphe.
     *
     * Une composante connexe est un sous-ensemble maximal de nœuds
     * tel qu'il existe un chemin entre toute paire de nœuds de ce sous-ensemble.
     *
     * @param g Le graphe à analyser
     * @return Le nombre de composantes connexes
     */
    public static int countConnectedComponents(Graph g) {
        ConnectedComponents cc = new ConnectedComponents();
        cc.init(g);
        cc.compute();
        return cc.getConnectedComponentsCount();
    }

    /**
     * Génère un réseau aléatoire avec une taille et un degré moyen donnés.
     *
     * Utilise le modèle d'Erdős-Rényi pour créer un graphe aléatoire
     * où les arêtes sont créées aléatoirement.
     *
     * @param n Nombre de nœuds du réseau
     * @param avgDegree Degré moyen souhaité
     * @return Le graphe aléatoire généré
     */
    public static Graph generateRandomGraph(int n, double avgDegree) {
        Graph g = new SingleGraph("Random");
        g.setStrict(false);
        g.setAutoCreate(true);

        // Nombre d'arêtes nécessaires: E = (N × degré_moyen) / 2
        int targetEdges = (int) Math.round((n * avgDegree) / 2.0);

        // Utilisation du générateur aléatoire de GraphStream
        RandomGenerator gen = new RandomGenerator(avgDegree);
        gen.addSink(g);
        gen.begin();

        // Génération des nœuds et arêtes
        for (int i = 0; i < n && g.getEdgeCount() < targetEdges; i++) {
            gen.nextEvents();
        }

        gen.end();
        return g;
    }

    /**
     * Trouve le degré critique pour la connexité d'un réseau aléatoire.
     *
     * Utilise une recherche binaire pour déterminer le degré moyen minimum
     * au-dessus duquel un réseau aléatoire devient connexe. La théorie prédit
     * que ce degré critique est approximativement ln(N).
     *
     * @param nodeCount Nombre de nœuds du réseau
     * @return Le degré critique expérimental
     */
    public static double findCriticalAverageDegree(int nodeCount) {
        ResultsPrinter.printInfo("Recherche du degré critique pour la connexité...");
        ResultsPrinter.printInfo("(Cela peut prendre quelques minutes)");

        double theoreticalCritical = Math.log(nodeCount);
        ResultsPrinter.printInfo(
            String.format("Degré critique théorique: %.2f (ln(%,d))", theoreticalCritical, nodeCount));

        // Utilisation d'une taille réduite pour accélérer le calcul
        int sampleSize = Math.min(nodeCount, 10000);

        double low = 1.0;
        double high = theoreticalCritical * 2;
        double epsilon = 0.1;

        while (high - low > epsilon) {
            double mid = (low + high) / 2.0;

            ResultsPrinter.printInfo(String.format("  Test degré moyen = %.2f", mid));

            // Tests multiples pour plus de fiabilité statistique
            int connectedCount = 0;
            int trials = 5;

            for (int trial = 0; trial < trials; trial++) {
                Graph testGraph = generateRandomGraph(sampleSize, mid);
                if (isConnected(testGraph)) {
                    connectedCount++;
                }
            }

            // Si la majorité sont connexes, on teste avec un degré plus faible
            if (connectedCount >= trials / 2) {
                high = mid;
            } else {
                low = mid;
            }
        }

        double critical = (low + high) / 2.0;
        ResultsPrinter.printSuccess(String.format("Degré critique trouvé: %.2f", critical));

        return critical;
    }

    /**
     * Analyse la connexité d'un graphe et affiche les résultats.
     *
     * @param g Le graphe à analyser
     * @param networkName Le nom du réseau (pour l'affichage)
     */
    public static void analyzeConnectivity(Graph g, String networkName) {
        ResultsPrinter.printSubHeader("Analyse de connexité - " + networkName);

        boolean connected = isConnected(g);
        int componentCount = countConnectedComponents(g);

        ResultsPrinter.printMetric("Graphe connexe", connected ? "Oui" : "Non");
        ResultsPrinter.printMetric("Nombre de composantes connexes", componentCount);

        if (!connected) {
            ResultsPrinter.printWarning("Le graphe n'est pas connexe!");
        }
    }

    /**
     * Effectue l'analyse complète de la Question 3.
     *
     * Cette méthode analyse la connexité du réseau DBLP, génère et teste
     * un réseau aléatoire équivalent, et détermine le degré critique
     * pour la connexité.
     *
     * @param dblpGraph Le graphe DBLP à analyser
     */
    public static void analyze(Graph dblpGraph) {
        long startTime = System.currentTimeMillis();
        ResultsPrinter.printHeader("QUESTION 3: Analyse de connexité");

        // 1. Vérification de la connexité de DBLP
        analyzeConnectivity(dblpGraph, "DBLP");

        // 2. Génération et test d'un réseau aléatoire avec les mêmes paramètres
        ResultsPrinter.printSubHeader("Réseau aléatoire (même taille et degré moyen)");

        int n = dblpGraph.getNodeCount();
        double avgDegree = BasicMetrics.getAverageDegree(dblpGraph);

        ResultsPrinter.printInfo(
            String.format("Génération d'un réseau aléatoire: N=%,d, <k>=%.2f", n, avgDegree));

        // Utilisation d'une taille réduite pour accélérer le calcul
        int randomSize = Math.min(n, 50000);
        ResultsPrinter.printInfo(
            String.format("(Utilisation d'une taille réduite pour le calcul: N=%,d)", randomSize));

        Graph randomGraph = generateRandomGraph(randomSize, avgDegree);
        analyzeConnectivity(randomGraph, "Aléatoire");

        // 3. Recherche du degré critique
        ResultsPrinter.printSubHeader("Degré critique pour connexité");
        double critical = findCriticalAverageDegree(randomSize);

        double theoreticalCritical = Math.log(randomSize);
        ResultsPrinter.printSeparator();
        ResultsPrinter.printMetric("Degré critique (expérimental)", critical);
        ResultsPrinter.printMetric("Degré critique (théorique ln(N))", theoreticalCritical);
        ResultsPrinter.printMetric("Différence", Math.abs(critical - theoreticalCritical));

        ResultsPrinter.printElapsedTime(startTime);
    }
}
