package org.example;

import org.graphstream.algorithm.Toolkit;
import org.graphstream.graph.BreadthFirstIterator;
import org.graphstream.graph.Edge;
import org.graphstream.graph.Graph;
import org.graphstream.graph.Node;
import org.graphstream.graph.implementations.SingleGraph;
import org.graphstream.stream.file.FileSourceEdge;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;

public class GraphManipulation {
    public static Graph getTheFileEdge(String path) {
        Graph graph = new SingleGraph("g");

        try {
            FileSourceEdge fs = new FileSourceEdge();
            fs.addSink(graph);
            fs.readAll(path);
        } catch (Exception e) {
            System.err.println("Erreur lors du chargement du graphe : " + e.getMessage());
            return null;
        }

        return graph;
    }

    public static int getNodeCount(Graph g) {
        return g.getNodeCount();
    }

    public static int getEdgeCount(Graph g) {
        return g.getEdgeCount();
    }


    public static double getAverageDegree(Graph g) {
        return Toolkit.averageDegree(g);
    }

    public static double getAverageClusteringCoefficient(Graph g) {
        return Toolkit.averageClusteringCoefficient(g);
    }

    public static double getRandomGraphClustering(double averageDegree, int NodeCount) {
        return averageDegree / (NodeCount - 1);
    }

    public static Boolean isOrientedGraph(Graph graph) {
        for (Edge edge : graph.edges().toList()) {
            if (edge.isDirected()) {
                return true;
            }
        }
        return false;
    }


    /**
     * Vérifie si un graphe non orienté est connexe
     *
     * @param g le graphe GraphStream
     * @return true si tous les nœuds sont connectés entre eux, false sinon
     */
    public static boolean isConnexe(Graph g) {
        if (g.getNodeCount() == 0) return true;

        Set<Node> visited = new HashSet<>();
        Queue<Node> queue = new LinkedList<>();

        Node start = g.getNode(0);
        queue.add(start);
        visited.add(start);

        while (!queue.isEmpty()) {
            Node current = queue.poll();
            current.edges().forEach(edge -> {
                Node neighbor = edge.getSourceNode().equals(current) ? edge.getTargetNode() : edge.getSourceNode();
                if (!visited.contains(neighbor)) {
                    visited.add(neighbor);
                    queue.add(neighbor);
                }
            });
        }

        return visited.size() == g.getNodeCount();
    }

    public static void getDegreeDistribution(Graph graph) {
        int[] dd = Toolkit.degreeDistribution(graph);
        String filePath = "src/main/gnuplot/dd_dblp.dat";
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
            for (int k = 0; k < dd.length; k++) {
                if (dd[k] != 0) {
                    double fraction = (double) dd[k] / graph.getNodeCount();
                    writer.write(String.format(Locale.US, "%d\t%.8f%n", k, fraction));
                    //System.out.printf(Locale.US, "%6d%20.8f%n", k, fraction);
                }
            }
            System.out.println("Distribution des degrés sauvegardée dans : " + filePath);
        } catch (IOException e) {
            System.err.println("Erreur lors de l'écriture du fichier : " + e.getMessage());
        }
    }

    /**
     * Sélectionne un nœud aléatoire dans un graphe donné.
     *
     * @param graph Le graphe dans lequel choisir un nœud.
     * @return Un nœud choisi aléatoirement parmi tous les nœuds du graphe.
     */
    public static Node getRandomNode(Graph graph) {
        return graph.getNode(ThreadLocalRandom.current().nextInt(0, graph.getNodeCount()));
    }


    public static Map<Integer, Integer> getProbaDistanceOfNNode(int numberOfNode, Graph graph) {
        Map<Integer, Integer> distanceCount = new HashMap<>();
        for (int i = 0; i < numberOfNode; i++) {
            Node start = getRandomNode(graph);
            BreadthFirstIterator bdi = new BreadthFirstIterator(start, false);

            Map<Node, Integer> distanceMap = new HashMap<>();
            distanceMap.put(start, 0);
            while (bdi.hasNext()) {
                Node current = bdi.next();
                int currentDist = distanceMap.get(current);

                current.edges().forEach(edge -> {
                    Node neighbor = edge.getSourceNode().equals(current) ? edge.getTargetNode() : edge.getSourceNode();
                    if (!distanceMap.containsKey(neighbor)) {
                        distanceMap.put(neighbor, currentDist + 1);
                    }
                });
            }
            for (Integer dist : distanceMap.values()){
                if (dist!=0){
                    distanceCount.put(dist, distanceCount.getOrDefault(dist, 0) + 1);
                }
            }
        }

        return distanceCount;
    }

    public static Map<Integer, Double> getDistanceProbabilities(Map<Integer, Integer> distanceCount) {
        Map<Integer, Double> probabilities = new HashMap<>();

        // Calcul du nombre total de distances comptées
        int total = distanceCount.values().stream().mapToInt(Integer::intValue).sum();

        // Calcul des probabilités
        for (Map.Entry<Integer, Integer> entry : distanceCount.entrySet()) {
            int distance = entry.getKey();
            int count = entry.getValue();
            probabilities.put(distance, count / (double) total);
        }

        return probabilities;
    }

    public static Double getAverageDistanceOfNNode(int numberOfNode, Graph graph) {
        List<Double> allResults = new ArrayList<>();
        for (int i = 0; i < numberOfNode; i++) {
            Node start = getRandomNode(graph);
            BreadthFirstIterator bdi = new BreadthFirstIterator(start, false);
            Map<Node, Integer> distanceMap = new HashMap<>();
            distanceMap.put(start, 0);
            while (bdi.hasNext()) {
                Node current = bdi.next();
                int currentDist = distanceMap.get(current);
                current.edges().forEach(edge -> {
                    Node neighbor = edge.getSourceNode().equals(current) ? edge.getTargetNode() : edge.getSourceNode();
                    if (!distanceMap.containsKey(neighbor)) {
                        distanceMap.put(neighbor, currentDist + 1);
                    }
                });
            }
            double sumDistances = distanceMap.values().stream().mapToInt(Integer::intValue).sum();
            double averageDistanceFromStart = sumDistances / (distanceMap.size() - 1);
            allResults.add(averageDistanceFromStart);
        }
        return allResults.stream()
                .mapToDouble(Double::doubleValue) // ou .mapToDouble(d -> d)
                .average()
                .orElse(0.0); // valeur par défaut si la liste est vide

    }



}
