import org.apache.commons.math3.fitting.PolynomialCurveFitter;
import org.apache.commons.math3.fitting.WeightedObservedPoints;
import org.graphstream.graph.Graph;
import org.graphstream.graph.implementations.AdjacencyListGraph;
import org.graphstream.stream.file.FileSourceGraphML;
import org.graphstream.ui.graphicGraph.GraphPosLengthUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.stream.IntStream;

/**
 * The goal of this class is to compute the correlation Dimension of the graph store in a dgs file
 *
 */
public class CorrelationDimension {

	/**
	 * The goal of the function is to add to the input argument graph g all the nodes present in the dgs file at the
	 * argument path.
	 *
	 * @param path the path to the dgs file where the graph data is store
	 */
	public Graph read_Graph_from_graphml_file(String path) throws IOException {
		Graph g = new AdjacencyListGraph(path);
		FileSourceGraphML graphml = new FileSourceGraphML();
		graphml.setStrictMode(false);
		g.setStrict(false);
		graphml.addSink(g);
		try {
			graphml.readAll(path);
		} catch(IOException e) {
			throw new IOException(e);
		}
		graphml.removeSink(g);
		return g;
	}

	/**
	 * The goal of this function is to find the max and the value of all the x coordinates and the same for the y
	 * coordinates of the graph g
	 * @param g the graph in which we are searching the min and max coordinates
	 * @return a Hashtable to associate the values to understandable key :
	 * x_min = the minimal x coordinate
	 * x_max = the maximal x coordinate
	 * y_min = the y min coordinate
	 * y_max = the y max coordinate
	 */
	public Hashtable<String, Float> get_min_max_coord(Graph g) {
		Hashtable<String, Float> extreme_coord = new Hashtable<>();
		extreme_coord.put("x_min", Float.MAX_VALUE);
		extreme_coord.put("x_max", Float.MIN_VALUE);
		extreme_coord.put("y_min", Float.MAX_VALUE);
		extreme_coord.put("y_max", Float.MIN_VALUE);
		g.nodes().forEachOrdered((n) -> {
			double[] coords = GraphPosLengthUtils.nodePosition(n);
			if (coords[0] > extreme_coord.get("x_max")) {
				extreme_coord.put("x_max", (float) coords[0]);
			}
			if (coords[0] < extreme_coord.get("x_min")) {
				extreme_coord.put("x_min", (float) coords[0]);
			}
			if (coords[1] > extreme_coord.get("y_max")) {
				extreme_coord.put("y_max", (float) coords[1]);
			}
			if (coords[1] < extreme_coord.get("y_min")) {
				extreme_coord.put("y_min", (float) coords[1]);
			}
		});
		return extreme_coord;
	}

	/**
	 * The goal of this function is to move the graph to the origine with the min values. We are also going to rescale
	 * the nodes of the graph wit the attribut scale
	 *
	 * @param extreme_coord the extrem_values of the graph
	 * @param scale the scale value to use to rescale the graph
	 * @param g the graph in which we are going to move the nodes
	 */
	public void move_graph(Hashtable <String, Float>extreme_coord, double scale, Graph g) {
		g.nodes().forEach((n) -> {
			double[] coords = GraphPosLengthUtils.nodePosition(n);
			coords[0] = (coords[0] - extreme_coord.get("x_min")) * scale;
			coords[1] = (coords[1] - extreme_coord.get("y_min")) * scale;
			n.setAttribute("xy", coords[0], coords[1]);
		});
	}

	/**
	 * The goal of this function is to compute the correlation of the graph by changing the size of the boxes. The size
	 * of the boxes will begin from min_size_box to max_size_box. It computes the correlation for each size of box possible
	 * and register the result in an ArrayList.
	 *
	 * @param g the scale Graph we are using to find the correlation dimension
	 * @param extSize the variable to know the min size of the boxes, the increment value of the boxes between each
	 *                loop and the maximum size of the boxes
	 */
	public double[][] compute_correlation_dimension(Graph g, int[] extSize) {
		int nc = g.getNodeCount();
		double[][] distributionLog = new double[extSize[1]][2];
		IntStream.range(extSize[0], extSize[1]).parallel().forEach( size_box -> {
			double sum = this.get_number_of_nodes_in_area(size_box, g);
			double e = ( 2.0 / ( nc * (nc -1) ) ) * sum ;
			e = (e <= 0) ? 0.00001 : e;
			distributionLog[size_box] = new double[]{Math.log(size_box), Math.log(e)};
			System.out.println("size box : " + size_box);
		});
		return distributionLog;
	}

	/**
	 * This function is computing for each node of the graph the number of nodes present in their area of size sizeBox
	 *
	 * @param sizeBox the size of the area section where we are computing the number of nodes
	 * @return the number of nodes present in the area of each node
	 */
	private double get_number_of_nodes_in_area(double sizeBox, Graph g) {
		int[] sum = {0};
		g.nodes().forEach((n0) -> {
			double[] n0_coord = GraphPosLengthUtils.nodePosition(n0);
			g.nodes().forEach((n1) -> {
				double[] n1_coord = GraphPosLengthUtils.nodePosition(n1);
				//System.out.println(n0 + " ! " + n0_coord[0] + " - " + n1_coord[0] + ", " + n0_coord[1] + " - " + n1_coord[1] + " => " + Math.hypot(n0_coord[0] - n1_coord[0], n0_coord[1] - n1_coord[1]));
				if (Math.hypot(n0_coord[0] - n1_coord[0], n0_coord[1] - n1_coord[1])  < sizeBox) {
					sum[0] = sum[0] + 1 ;
				}
			});
			sum[0] = sum[0] - 1;
		});
		return sum[0];
	}

	/**
	 * This function is calculating the coefficient of the data passing thought the argument as a Polynomial Curve
	 * Fitter.
	 *
	 * @param distributionLog The distribution of data use in the PolynomialCurveFitter. The data are store in an
	 *                        ArrayList as :
	 *                        - 0 => x
	 *                        - 1 => y
	 * @return the coefficient find by the PolynomialCurveFitter following the data of the param distributionLog
	 */
	public double[]  getFractalDimension_information(double[][] distributionLog) {
		WeightedObservedPoints obs = new WeightedObservedPoints();
		for ( double[] vals : distributionLog) {
			if (vals[0] != 0 && vals[1] != 0) {
				obs.add(vals[0], vals[1]);
			}
		}
		return PolynomialCurveFitter.create(1).fit(obs.toList()) ;
	}

	/**
	 * This main methode of the class initialise and scale the graph to compute the correlation dimension of the graph.
	 *
	 * @param args it takes in argument :
	 *             - The path to the dgs file
	 *             - The number of nodes present in the file to only use the necessary data, because we don't need to
	 *             use the edges of the graph
	 */
	public static void main(String... args) {
		String path = args[0];
		int min_box_size = Integer.parseInt(args[1]);
		int max_box_size = Integer.parseInt(args[2]);

		if (path.endsWith("graphml")) {
			Graph g;
			CorrelationDimension correlationDimension = new CorrelationDimension();
			try {
				g = correlationDimension.read_Graph_from_graphml_file(path);
			} catch (IOException e) {
				e.printStackTrace();
				return;
			}
			Hashtable<String, Float> extreme_coord = correlationDimension.get_min_max_coord(g);
			extreme_coord.put("x_max", extreme_coord.get("x_max") - extreme_coord.get("x_min"));
			extreme_coord.put("y_max", extreme_coord.get("y_max") - extreme_coord.get("y_min"));

			int[] size_space = {1000, 1000};
			double maxSize = Arrays.stream(size_space).min().getAsInt();
			double delta = 0.001;
			double scale = maxSize / Math.max(extreme_coord.get("x_max"), extreme_coord.get("y_max")) * (1 - delta);
			correlationDimension.move_graph(extreme_coord, scale, g);
			int[] extSizeBox = {min_box_size, max_box_size};
			double[][] distributionLog = correlationDimension.compute_correlation_dimension(g, extSizeBox);
			double[] informations = correlationDimension.getFractalDimension_information(distributionLog);
			System.out.println(informations[1]);
		}
	}
}
