package iwocs.graphs;

import java.util.HashMap;
import java.util.List;

import org.graphstream.algorithm.Toolkit;
import org.graphstream.algorithm.generator.BarabasiAlbertGenerator;
import org.graphstream.algorithm.generator.Generator;
import org.graphstream.algorithm.generator.RandomGenerator;
import org.graphstream.graph.BreadthFirstIterator;
import org.graphstream.graph.Graph;
import org.graphstream.graph.Node;
import org.graphstream.graph.implementations.SingleGraph;

public class GraphUtils {
  /**
   * Handy method that generates a random graph with a given average degree.
   * 
   * @param id identifier of the resulting graph.
   * @param n the number of nodes in the resulting graph.
   * @param avgDegree the average degree of the resulting graph.
   * @return the resulting random graph.
   */
  public static Graph getRandomGraph(String id, int n, double avgDegree) {
    Generator gen = new RandomGenerator(avgDegree);

    Graph result = new SingleGraph(id);

    gen.addSink(result);

    gen.begin();

    for (int i = 0; i < n; i++)
      gen.nextEvents();

    gen.end();

    return result;
  }

  /**
   * Handy method that generates a preferential
   * graph with a given average degree.
   * 
   * @param id identifier of the resulting graph.
   * @param n the number of nodes in the resulting graph.
   * @param avgDegree the average degree of the resulting graph.
   * @return the resulting preferential graph.
   */
  public static Graph getPreferentialGraph(String id, int n, double avgDegree) {
    int m = (int)Math.round(avgDegree);

    Generator gen = new BarabasiAlbertGenerator(m);

    Graph result = new SingleGraph(id);

    gen.addSink(result);

    gen.begin();

    System.out.printf("Barabasi gen(n=%d, m=%d)\n", n, m);

    for (int i = 0; i < n; i++) {
      System.out.printf("\r(%d/%d)", i, n);
      gen.nextEvents();
    }

    gen.end();

    return result;
  }

  /**
   * Calculate a mean distance from a sample of nodes from a given graph.
   * 
   * @param g          the graph to analyse.
   * @param sampleSize number of nodes to include in the analysis.
   * @return mean distance calculated from the sample.
   */
  public static double calculateMeanDistanceFromSample(Graph g, int sampleSize) {
    List<Node> sampleNodes = Toolkit.randomNodeSet(g, sampleSize);

    double meanSum = 0;

    for (Node n : sampleNodes) {
      System.out.format("\r(%d/%d)", sampleNodes.indexOf(n), sampleSize);
      double localSum = 0;
      int nbVisited;
      BreadthFirstIterator<Node> iter = (BreadthFirstIterator<Node>) n.getBreadthFirstIterator();

      for (nbVisited = 0; iter.hasNext(); nbVisited++)
        localSum += iter.getDepthOf(iter.next());

      meanSum += localSum / nbVisited;
    }

    System.out.println();
    return meanSum / sampleSize;
  }

  /**
   * Creates a distribution of distances from a random set of nodes from a given
   * graph of a given size.
   * 
   * @param g          the graph to analyse.
   * @param sampleSize number of nodes to analyse.
   * @return the distribution calculated.
   */
  public static HashMap<Integer, Double> calculateDistancesDistributionFromSample(Graph g, int sampleSize, boolean normalized) {
    List<Node> sampleNodes = Toolkit.randomNodeSet(g, sampleSize);

    HashMap<Integer, Double> distribution = new HashMap<>();

    int i = 0;

    for (Node n : sampleNodes) {
      System.out.format("\r(%d/%d)", ++i, sampleSize);
      BreadthFirstIterator<Node> iter = (BreadthFirstIterator<Node>) n.getBreadthFirstIterator();

      while (iter.hasNext()) {
        int depth = iter.getDepthOf(iter.next());
        Double depthCount = distribution.get(depth);

        if (depthCount == null)
          distribution.put(depth, 1.0);
        else
          distribution.put(depth, depthCount + 1);
      }
    }

    System.out.println();

    if(normalized) {
      Double totalMeasures = distribution.values().stream().reduce(0.0, Double::sum);

      distribution.forEach((k, v) -> distribution.put(k, v / totalMeasures));
    }

    return distribution;
  }
}