package eu.hansolo.fx.charts.forcedirectedgraph;

import java.util.ArrayList;
import java.util.PriorityQueue;


/**
 * authors: Michael L\u00E4uchli, MLaeuchli (github)
 *          Stefan Mettler, orizion (github)
 */
public class GraphCalculator {

    double[] betweennessResults;
    public final String degreeKey = "DegreeCentrality";
    public final String closenessKey = "ClosenessCentrality";
    public final String betweennessKey = "BetweennessCentrality";
    public final String degreeNormalizedKey = "DegreeCentralityNormalized";
    public final String closenessNormalizedKey = "ClosenessCentrualityNormalized";


    private void recursiveBetweennesCalculatioin(ArrayList<GraphNode>[][] paths, ArrayList<GraphNode> graphNodes , double split, GraphNode from, GraphNode to){
        double nextSplit = split/paths[graphNodes.indexOf(from)][graphNodes.indexOf(to)].size();
        for(GraphNode node: paths[graphNodes.indexOf(from)][graphNodes.indexOf(to)] ){
            if(!node.equals(from)){
                betweennessResults[graphNodes.indexOf(node)] += split;
                recursiveBetweennesCalculatioin(paths, graphNodes, nextSplit, from, node);
            }
        }
    }

    private ArrayList<ArrayList<GraphNode>> createRealPathsRecursive(ArrayList<GraphNode>[][] paths, ArrayList<GraphNode> graphNodes, GraphNode from, GraphNode to){
        if(paths[graphNodes.indexOf(from)][graphNodes.indexOf(to)].size() == 1 && paths[graphNodes.indexOf(from)][graphNodes.indexOf(to)].get(0).equals(from)){
            return new ArrayList<>();
        } else{
            ArrayList<ArrayList<GraphNode>> ListOfLists = new ArrayList<>();
            for(GraphNode node: paths[graphNodes.indexOf(from)][graphNodes.indexOf(to)] ){
                //ListOfLists.add(createRealPathsRecursive(paths, graphNodes, node, from));
                ArrayList<ArrayList<GraphNode>> subpaths = createRealPathsRecursive(paths, graphNodes, node, from);
                if(subpaths.isEmpty()){
                    ArrayList<GraphNode> temp = new ArrayList<>();
                    temp.add(node);
                    ListOfLists.add(temp);
                } else {
                    for(ArrayList<GraphNode> ap: subpaths){
                        ap.add(node);
                        ListOfLists.add(ap);
                    }
                }
            }
            return ListOfLists;
        }
    }


    public NodeEdgeModel calculateDegreeCentrality(NodeEdgeModel nodeEdgeModel){
        for(GraphNode node: nodeEdgeModel.getNodes()) {
            node.setNumericAttribute(degreeKey, (double) node.getConnectedNodes().size());
        }
        return nodeEdgeModel;
    }

    public NodeEdgeModel calculateDegreeCentralityNormalized(NodeEdgeModel nodeEdgeModel){
        if(!nodeEdgeModel.getNodes().get(0).containsNumericAttribute(degreeKey)){
            calculateDegreeCentrality(nodeEdgeModel);
        }

        for(GraphNode node: nodeEdgeModel.getNodes()) {
            node.setNumericAttribute(degreeNormalizedKey, node.getNumericAttribute(degreeKey)/(nodeEdgeModel.getNodes().size()-1));
        }
        return nodeEdgeModel;
    }

    /*
    Implemented with Breadth first algorithm
     */
    public NodeEdgeModel calculateClosenessCentrality(NodeEdgeModel nodeEdgeModel){
        PriorityQueue<GraphNode> queue = new PriorityQueue<>();

        ArrayList<GraphNode> visited = new ArrayList<>();
        int level;
        int counterUntilNextLevel;
        int counterOfNextLevel;
        double result;
        GraphNode currentNode;
        ArrayList<GraphNode> GraphNodes = nodeEdgeModel.getNodes();

        for(GraphNode node: GraphNodes){
            visited.clear();
            queue.clear();
            queue.add(node);
            result = 0;
            level = 0;
            counterUntilNextLevel = 1;
            counterOfNextLevel = 0;

            while(!queue.isEmpty()){
                currentNode = queue.poll();
                counterOfNextLevel += currentNode.getConnectedNodes().size();
                visited.add(currentNode);
                ArrayList<GraphNode> currentNodeList = new ArrayList<>(currentNode.getConnectedNodes());
                for(GraphNode gNode: currentNodeList){
                    if(!visited.contains(gNode) && !queue.contains(gNode)){
                        queue.add(gNode);
                    }
                }
                if(level>0) {
                    result += 1.0 / (double) level;
                }
                counterUntilNextLevel--;
                if(counterUntilNextLevel == 0){
                    level++;
                    counterUntilNextLevel = counterOfNextLevel;
                    counterOfNextLevel = 0;
                }
            }
            node.setNumericAttribute(closenessKey, new Double(result));
        }
        return nodeEdgeModel;
    }


    public NodeEdgeModel calculateClosenessCentralityNormalized(NodeEdgeModel nodeEdgeModel){
        if(!nodeEdgeModel.getNodes().get(0).containsNumericAttribute(closenessKey)){
            calculateClosenessCentrality(nodeEdgeModel);
        }

        for(GraphNode node: nodeEdgeModel.getNodes()) {
            node.setNumericAttribute(closenessNormalizedKey, node.getNumericAttribute(closenessKey)/(nodeEdgeModel.getNodes().size()-1));
        }
        return nodeEdgeModel;
    }


    public void calculateBetweennessCentrality(NodeEdgeModel nodeEdgeModel){

        PriorityQueue<GraphNode> currentLevelQueue = new PriorityQueue<>();
        PriorityQueue<GraphNode> nextLevelQueue = new PriorityQueue<>();
        ArrayList<GraphNode> visited = new ArrayList<>();

        GraphNode currentNode;
        ArrayList<GraphNode> graphNodes = nodeEdgeModel.getNodes();
        betweennessResults = new double[graphNodes.size()];
        ArrayList<GraphNode>[][] paths = new ArrayList[graphNodes.size()][graphNodes.size()];
        ArrayList<ArrayList<GraphNode>>[][] realPaths = new ArrayList[graphNodes.size()][graphNodes.size()];

        for(int i=0; i<graphNodes.size(); i++){
            for(int j=0; j<graphNodes.size(); j++){
                paths[i][j] = new ArrayList<>();
            }
        }

        for(GraphNode node: graphNodes){
            visited.clear();
            currentLevelQueue.clear();
            currentLevelQueue.add(node);

            //save paths from node to all other nodes
            while(!currentLevelQueue.isEmpty()){
                currentNode = currentLevelQueue.poll();
                visited.add(currentNode);
                for(GraphNode gNode: currentNode.getConnectedNodes()){
                    if(!visited.contains(gNode) && !currentLevelQueue.contains(gNode)){
                        paths[graphNodes.indexOf(node)][graphNodes.indexOf(gNode)].add(currentNode);
                        if(!nextLevelQueue.contains(gNode)){
                            nextLevelQueue.add(gNode);
                        }
                    }
                }
                if(currentLevelQueue.isEmpty()){
                    while(!nextLevelQueue.isEmpty()){
                        currentLevelQueue.add(nextLevelQueue.poll());
                    }
                }
            }
        }

        for(int i=0; i<betweennessResults.length; i++){
            betweennessResults[i]=0;
        }

        for(int i=0; i<paths.length; i++){
            for(int j=0; j<paths[i].length; j++){
                if(i != j){
                    //recursiveBetweennesCalculatioin(paths, graphNodes,0.5, graphNodes.get(i), graphNodes.get(j));
                    realPaths[i][j] = createRealPathsRecursive(paths, graphNodes, graphNodes.get(i), graphNodes.get(j));
                }
            }
        }

        for(int i=0; i<paths.length; i++){
            for(int j=0; j<paths[i].length; j++){
                if(i != j){
                    for(ArrayList<GraphNode> al: realPaths[i][j]){
                        if(!al.isEmpty()){
                            for(GraphNode graphNode: al){
                                betweennessResults[graphNodes.indexOf(graphNode)] += 0.5/realPaths[i][j].size();
                            }
                        }
                    }
                }
            }
        }

        for(GraphNode node: graphNodes){
            node.setNumericAttribute(betweennessKey, betweennessResults[graphNodes.indexOf(node)]);
        }
    }




}