package org.example;

import org.graphstream.algorithm.Toolkit;
import org.graphstream.graph.*;
import org.graphstream.graph.implementations.*;
import org.graphstream.stream.file.FileSourceEdge;
import org.graphstream.algorithm.generator.BarabasiAlbertGenerator;
import org.graphstream.algorithm.generator.RandomGenerator;

import java.io.FileWriter;
import java.util.*;

public class DBLPNetworkAnalysis {
    public static void main(String[] args) throws Exception {
        String filePath = "/home/c2i/IdeaProjects/TP_RI/com-dblp.ungraph.txt"; // Chemin du fichier d'entrée
        Graph graph = new SingleGraph("DBLP Collaboration Network");

        // Chargement des données
        FileSourceEdge fileSource = new FileSourceEdge();
        fileSource.addSink(graph);

        try {
            fileSource.readAll(filePath);
        } finally {
            fileSource.removeSink(graph);
        }

        // Mesures de base
        int nodeCount = graph.getNodeCount();
        int edgeCount = graph.getEdgeCount();
        double averageDegree = Toolkit.averageDegree(graph);
        double clusteringCoefficient = Toolkit.averageClusteringCoefficient(graph);

        // Coefficient de clustering pour un graphe aléatoire
        double randomClusteringCoefficient = averageDegree / nodeCount;

        // Vérification de connexité
        boolean isConnected = Toolkit.isConnected(graph);

        // Calcul du degré moyen minimal pour qu'un graphe aléatoire soit connexe
        double minAverageDegreeForConnectivity = Math.log(nodeCount);

        // Calcul de la distribution des degrés
        calculateDegreeDistribution(graph, "degree_distribution.txt");

        // Estimation de la distance moyenne par échantillonnage
        double averageDistance = estimateAverageDistance(graph, 100);

        // Exporter la distribution des distances
        exportDistanceDistribution(graph, "distance_distribution.txt", 100);

        // Affichage des résultats
        System.out.println("Nombre de nœuds : " + nodeCount);
        System.out.println("Nombre de liens : " + edgeCount);
        System.out.println("Degré moyen : " + averageDegree);
        System.out.println("Coefficient de clustering (réel) : " + clusteringCoefficient);
        System.out.println("Coefficient de clustering (aléatoire) : " + randomClusteringCoefficient);
        System.out.println("Le réseau est-il connexe ? : " + (isConnected ? "Oui" : "Non"));
        System.out.println("Degré moyen minimal pour qu'un graphe aléatoire soit connexe : " + minAverageDegreeForConnectivity);
        System.out.println("Distance moyenne estimée : " + averageDistance);

        // Génération et comparaison des réseaux aléatoires et Barabasi-Albert
        compareWithGeneratedNetworks(nodeCount, edgeCount);

        // Génération d'un réseau Barabasi-Albert natif
        Graph baGraphNative = generateBarabasiAlbertGraphNative(nodeCount, edgeCount / nodeCount);
        System.out.println("\nRéseau Barabasi-Albert (GraphStream natif) :");
        printGraphMeasures(baGraphNative);

        // Test du générateur de méthode de copie
        Graph copyModelGraph = generateCopyModelGraph(nodeCount, averageDegree, 0.1);
        System.out.println("\nRéseau généré avec la méthode de copie :");
        printGraphMeasures(copyModelGraph);
    }

    public static void calculateDegreeDistribution(Graph graph, String outputFile) throws Exception {
        Map<Integer, Integer> degreeCounts = new HashMap<>();

        // Compter les occurrences de chaque degré
        for (Node node : graph) {
            int degree = node.getDegree();
            degreeCounts.put(degree, degreeCounts.getOrDefault(degree, 0) + 1);
        }

        // Nombre total de nœuds
        int totalNodes = graph.getNodeCount();

        try (FileWriter writer = new FileWriter(outputFile)) {
            writer.write("Degree\tProbability\n");

            // Calcul des probabilités pour chaque degré
            for (Map.Entry<Integer, Integer> entry : degreeCounts.entrySet()) {
                double probability = (double) entry.getValue() / totalNodes;
                writer.write(entry.getKey() + "\t" + probability + "\n");
            }
        }

        System.out.println("Distribution des degrés (probabilités) enregistrée dans : " + outputFile);
    }

    public static double estimateAverageDistance(Graph graph, int sampleSize) {
        List<Node> nodes = new ArrayList<>();
        graph.nodes().forEach(nodes::add);

        Random random = new Random();
        double totalDistance = 0;
        int count = 0;

        for (int i = 0; i < sampleSize; i++) {
            Node start = nodes.get(random.nextInt(nodes.size()));
            Map<Node, Integer> distances = bfsDistances(graph, start, 100); // Limite de profondeur
            for (int distance : distances.values()) {
                totalDistance += distance;
                count++;
            }
        }

        return totalDistance / count;
    }

    public static void exportDistanceDistribution(Graph graph, String outputFile, int sampleSize) throws Exception {
        Map<Integer, Integer> distanceCounts = new HashMap<>();
        List<Node> nodes = new ArrayList<>();
        graph.nodes().forEach(nodes::add);

        Random random = new Random();

        for (int i = 0; i < sampleSize; i++) {
            Node start = nodes.get(random.nextInt(nodes.size()));
            Map<Node, Integer> distances = bfsDistances(graph, start, 100);

            for (int distance : distances.values()) {
                distanceCounts.put(distance, distanceCounts.getOrDefault(distance, 0) + 1);
            }
        }

        try (FileWriter writer = new FileWriter(outputFile)) {
            writer.write("Distance\tFrequency\n");

            for (Map.Entry<Integer, Integer> entry : distanceCounts.entrySet()) {
                writer.write(entry.getKey() + "\t" + entry.getValue() + "\n");
            }
        }

        System.out.println("Distribution des distances exportée dans : " + outputFile);
    }

    public static Map<Node, Integer> bfsDistances(Graph graph, Node start, int maxDepth) {
        Map<Node, Integer> distances = new HashMap<>();
        Queue<Node> queue = new LinkedList<>();
        distances.put(start, 0);
        queue.add(start);

        while (!queue.isEmpty()) {
            Node current = queue.poll();
            int currentDistance = distances.get(current);

            if (currentDistance >= maxDepth) continue;

            current.edges().forEach(edge -> {
                Node neighbor = edge.getOpposite(current);
                if (!distances.containsKey(neighbor)) {
                    distances.put(neighbor, currentDistance + 1);
                    queue.add(neighbor);
                }
            });
        }

        return distances;
    }

    public static void compareWithGeneratedNetworks(int nodeCount, int edgeCount) {
        // Génération d'un réseau aléatoire
        Graph randomGraph = createRandomGraph(nodeCount, edgeCount);

        // Génération d'un réseau Barabasi-Albert optimisé
        Graph baGraph = createBarabasiAlbertGraphOptimized(nodeCount, (int) (edgeCount / nodeCount));

        // Calcul des mesures pour les réseaux générés
        System.out.println("\nRéseau aléatoire :");
        printGraphMeasures(randomGraph);

        System.out.println("\nRéseau Barabasi-Albert :");
        printGraphMeasures(baGraph);
    }

    public static Graph createRandomGraph(int nodeCount, int edgeCount) {
        Graph randomGraph = new SingleGraph("Random Network");
        Random random = new Random();

        for (int i = 0; i < nodeCount; i++) {
            randomGraph.addNode(String.valueOf(i));
        }

        Set<String> edges = new HashSet<>();

        while (randomGraph.getEdgeCount() < edgeCount) {
            int source = random.nextInt(nodeCount);
            int target = random.nextInt(nodeCount);

            if (source != target) {
                String edge = source + "-" + target;
                String reverseEdge = target + "-" + source;

                if (!edges.contains(edge) && !edges.contains(reverseEdge)) {
                    randomGraph.addEdge(edge, String.valueOf(source), String.valueOf(target));
                    edges.add(edge);
                }
            }
        }

        return randomGraph;
    }

    public static Graph createBarabasiAlbertGraphOptimized(int nodeCount, int initialDegree) {
        Graph baGraph = new SingleGraph("Barabasi-Albert Network");
        Random random = new Random();
        List<String> cumulativeNodeList = new ArrayList<>();

        // Ajouter les nœuds initiaux et les connecter
        for (int i = 0; i < initialDegree; i++) {
            baGraph.addNode(String.valueOf(i));
            cumulativeNodeList.add(String.valueOf(i)); // Ajouter plusieurs fois pour le degré initial
        }

        for (int i = initialDegree; i < nodeCount; i++) {
            baGraph.addNode(String.valueOf(i));
            Set<String> targets = new HashSet<>();

            while (targets.size() < initialDegree) {
                // Sélection aléatoire basée sur la liste cumulative
                String target = cumulativeNodeList.get(random.nextInt(cumulativeNodeList.size()));
                targets.add(target);
            }

            for (String target : targets) {
                baGraph.addEdge(i + "-" + target, String.valueOf(i), target);
                cumulativeNodeList.add(String.valueOf(i)); // Mettre à jour la liste cumulative
                cumulativeNodeList.add(target);           // Mettre à jour pour le nœud cible
            }
        }

        return baGraph;
    }

    public static Graph generateBarabasiAlbertGraphNative(int nodeCount, int edgesPerNode) {
        Graph graph = new SingleGraph("Barabasi-Albert Native");
        BarabasiAlbertGenerator generator = new BarabasiAlbertGenerator(edgesPerNode);

        generator.addSink(graph);
        generator.begin();
        for (int i = 0; i < nodeCount; i++) {
            generator.nextEvents();
        }
        generator.end();

        return graph;
    }

    public static Graph generateCopyModelGraph(int nodeCount, double averageDegree, double probability) {
        Graph copyGraph = new SingleGraph("Copy Model Network");
        Random random = new Random();

        // Ajouter le premier nœud
        copyGraph.addNode("0");

        for (int i = 1; i < nodeCount; i++) {
            copyGraph.addNode(String.valueOf(i));
            Node targetNode = copyGraph.getNode(String.valueOf(random.nextInt(i)));

            // Connexion avec probabilité p aux voisins de la cible
            int finalI = i;
            targetNode.neighborNodes().forEach(neighbor -> {
                if (random.nextDouble() < probability) {
                    copyGraph.addEdge(finalI + "-" + neighbor.getId(), String.valueOf(finalI), neighbor.getId());
                }
            });

            // Connexion au nœud cible
            copyGraph.addEdge(i + "-" + targetNode.getId(), String.valueOf(i), targetNode.getId());
        }

        return copyGraph;
    }

    public static void printGraphMeasures(Graph graph) {
        System.out.println("Nombre de nœuds : " + graph.getNodeCount());
        System.out.println("Nombre de liens : " + graph.getEdgeCount());
        System.out.println("Degré moyen : " + Toolkit.averageDegree(graph));
        System.out.println("Coefficient de clustering : " + Toolkit.averageClusteringCoefficient(graph));
        System.out.println("Distance moyenne estimée : " + estimateAverageDistance(graph, 100));
    }
}
