package pert;

import java.awt.*;
import java.util.*;
import java.util.List;

import org.jgrapht.Graphs;
import org.jgrapht.graph.SimpleDirectedGraph;

public class PertGenerator {

	private int maxNodesCount;
	private int maxRegularTasksCounts;
	private int maxDummyCount;
	private int maxDuration;
	private int currentNodesCount;
	private int currentDummyCount;
	private int duplicatedSuccessorPool;
	private Map<Integer, PertNode> idMap;
	private SimpleDirectedGraph<PertNode, PertEdge> graph;

	/**
	 * Instancie un générateur d'exercices de diagrammes de PERT.
	 * @param nbNodes Nombre total de sommets
	 * @param nbTasks Nombre de tâches régulières
	 * @param nbDummyTasks Nombre de tâches fictives
	 * @param maxDuration Durée maximale d'une tâche régulière
	 */
	public PertGenerator(int nbNodes, int nbTasks, int nbDummyTasks, int maxDuration) {
		if(!checkConsistency(nbNodes, nbTasks, nbDummyTasks))
			throw new IllegalArgumentException("Inconsistent graph");
		this.maxNodesCount = nbNodes;
		this.maxRegularTasksCounts = nbTasks;
		this.maxDummyCount = nbDummyTasks;
		this.maxDuration = maxDuration;
		this.currentNodesCount = 0;
		this.currentDummyCount = 0;
		this.duplicatedSuccessorPool = (maxRegularTasksCounts + maxDummyCount) - (maxNodesCount-1);
		this.idMap = new HashMap<>(maxNodesCount, 1f);
		this.graph = new SimpleDirectedGraph<>(PertEdge.class);
	}

	/**
	 * @return graphe JGraphT du diagramme de PERT généré
	 */
	public SimpleDirectedGraph<PertNode, PertEdge> getGraph() {
		return this.graph;
	}

	/**
	 * @return dictionnaire associant à chaque tâche l'ensemble des tâches prédécesseurs
	 */
	public Map<String, Set<String>> getPrecedences() {
		Map<String, Set<String>> prec = new HashMap<>();
		Map<PertNode, Set<String>> memo = new HashMap<>();

		for (PertEdge edge : this.graph.edgeSet()) {
			if (edge.getDuration() == 0) continue;
			prec.put(edge.getTask(), getPredecessorsOf(graph.getEdgeSource(edge), memo));
		}
		return prec;
	}

	private Set<String> getPredecessorsOf(PertNode node, Map<PertNode, Set<String>> memo) {
		if (memo.containsKey(node)) return memo.get(node);

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

		for (PertEdge prec : graph.incomingEdgesOf(node)) {
			if (prec.getDuration() == 0) {
				res.addAll(getPredecessorsOf(graph.getEdgeSource(prec), memo));
			} else {
				res.add(prec.getTask());
			}
		}

		memo.put(node, res);
		return res;
	}

	/**
	 * Génère un diagramme de PERT aléatoire satisfaisant les contraintes données
	 */
	public void generatePertExercise() {
		generateNodes();
		generateTasks();
		computeDeadlines();
	}

	private void generateNodes(){
		linkToPreviousNode(generateNode(0, 0));
		int x = 75;
		int y = 75;
		while(currentNodesCount < maxNodesCount-2) {
			int nbNodes = randomValue(1, maxNodesCount - currentNodesCount - 2);
			if(nbNodes == 0 || currentNodesCount+nbNodes >= maxNodesCount-1) break;
			x += 75+(currentNodesCount);
			y = 100+(currentNodesCount);
			for (int j = 0; j < nbNodes; j++) {
				y += 100;
				PertNode node = generateNode(x, y);
				linkToPreviousNode(node);
			}
		}

		while(currentNodesCount < maxNodesCount){
			y += 100+(currentNodesCount);
			PertNode node = generateNode(x, y);
			linkToPreviousNode(node);
		}
	}

	private PertNode generateNode(int x, int y){
		PertNode node = new PertNode(currentNodesCount+1, x, y, 30, String.valueOf(currentNodesCount+1), Color.WHITE, 0, 0);
		this.idMap.put(node.getId(), node);
		graph.addVertex(node);
		currentNodesCount += 1;
		return node;
	}

	private void linkToPreviousNode(PertNode node){
		if(node.getId() == 1) return;
		int duration;

		if(duplicatedSuccessorPool == 0){
			PertNode previousNode = this.idMap.get(node.getId()-1);
			duration = generateDuration(previousNode.getId() == 1 || node.getId() == maxNodesCount);
			linkNodes(previousNode, node,  new PertEdge(generateLabel(), duration, false));
			return;
		}

		int previousId = randomValue(1, node.getId()-1);
		PertNode previousNode = this.idMap.get(previousId);
		if(Graphs.vertexHasSuccessors(graph, previousNode)) duplicatedSuccessorPool -= 1;
		duration = generateDuration(previousNode.getId() == 1 || node.getId() == maxNodesCount);
		linkNodes(previousNode, node,  new PertEdge(generateLabel(), duration, false));
	}

	private void generateTasks() {
		List<PertNode> nodes = new ArrayList<>(this.graph.vertexSet());
		int tasksCount = getTasksCount();

		//Give the dangling nodes a successor
		for(PertNode node : nodes){
			if(node.getId() == maxNodesCount || Graphs.vertexHasSuccessors(graph, node)) continue;

			int secondNodeIndex = randomValue(node.getId()+1, currentNodesCount-1);
			PertNode secondNode = nodes.get(secondNodeIndex);

			boolean isOnBorder = node.getId() == 1 || secondNode.getId() == 1 || secondNode.getId() == maxNodesCount;
			int duration = this.generateDuration(isOnBorder);
			this.linkNodes(node, secondNode, new PertEdge(generateLabel(), duration, false));
			tasksCount = getTasksCount();
		}

		//Add the remaining tasks
		while(tasksCount < maxRegularTasksCounts + maxDummyCount){

			int firstNodeIndex;
			int secondNodeIndex;
			if(tasksCount < maxDummyCount){
				firstNodeIndex = randomValue(1, currentNodesCount-2);
			} else {
				//-1 to handle index notation then -1 to avoid selecting the last node
				firstNodeIndex = randomValue(0, currentNodesCount-2);
			}
			secondNodeIndex = randomValue(firstNodeIndex+1, currentNodesCount-1);

			PertNode node1 = nodes.get(firstNodeIndex);
			PertNode node2 = nodes.get(secondNodeIndex);
			if (graph.containsEdge(node1, node2)) continue;
			boolean isOnBorder = node1.getId() == 1 || node2.getId() == 1 || node1.getId() == maxNodesCount || node2.getId() == maxNodesCount;
			int duration = generateDuration(isOnBorder);
			this.linkNodes(node1, node2, new PertEdge(generateLabel(), duration, false));

			tasksCount = getTasksCount();
		}
	}

	private void linkNodes(PertNode node1, PertNode node2, PertEdge edge){
		if(node1.getId() > node2.getId()){
			graph.addEdge(node2, node1, edge);
		} else {
			graph.addEdge(node1, node2, edge);
		}
	}

	private void computeDeadlines(){
		List<PertNode> nodes = new ArrayList<>(this.graph.vertexSet());
		computeStarts(nodes);
		computeEnds(nodes);
	}

	private void computeStarts(List<PertNode> nodes){

		for (PertNode node : nodes) {
			if(node.getId() == 1) continue;
			Set<PertEdge> edges = graph.incomingEdgesOf(node);
			PertEdge longestPath = Collections.max(edges, (a, b) -> {
				int startOfA = graph.getEdgeSource(a).getStart();
				int startOfB = graph.getEdgeSource(b).getStart();
				return (startOfA + a.getDuration()) - (startOfB + b.getDuration());
			});

			int previousStart = graph.getEdgeSource(longestPath).getStart();
			int currentStart = previousStart + longestPath.getDuration();
			node.setStart(currentStart);

			if(node.getId() == maxNodesCount) node.setEnd(node.getStart());
		}
	}

	private void computeEnds(List<PertNode> nodes){
		for (int i = nodes.size()-1; i >= 0; i--){
			PertNode node = nodes.get(i);
			if(node.getId() == maxNodesCount) continue;

            Set<PertEdge> edges = graph.outgoingEdgesOf(node);
            PertEdge shortestPath = Collections.max(edges, (a, b) -> {
				int endOfA = graph.getEdgeTarget(a).getEnd();
				int endOfB = graph.getEdgeTarget(b).getEnd();
				return (endOfB - b.getDuration()) - (endOfA - a.getDuration());
			});

			int nextEnd = graph.getEdgeTarget(shortestPath).getEnd();
			int currentEnd = nextEnd - shortestPath.getDuration();
			node.setEnd(currentEnd);

			PertNode target = graph.getEdgeTarget(shortestPath);
			if (node.getStart() == target.getEnd() - shortestPath.getDuration()) {
				shortestPath.setCritical(true);
			}

			if(node.getId() == 1) node.setEnd(0);
		}
	}

	private int generateDuration(boolean onBorderLayer){

		if(currentDummyCount < maxDummyCount && !onBorderLayer){
			currentDummyCount++;
			return 0;
		}

		return randomValue(1, maxDuration);
	}

	private int getTasksCount(){
		return this.graph.edgeSet().size();
	}

	private String generateLabel(){
		StringBuilder bob = new StringBuilder();
		int rest = graph.edgeSet().size()+1;
		while (rest > 0) {
			rest--;
			char digit = (char) ('A' + (rest % 26));
			bob.append(digit);
			rest /= 26;
		}
		return bob.reverse().toString();
	}

	private int randomValue(int start, int end){
		if(start >= end) return end;
		return (int)((Math.random() * ((end+1) - start)) + start);
	}

	private boolean checkConsistency(long nodes, long regularTasks, long dummyTasks){

		long maxTasksCount = (nodes * (nodes - 1)) / 2;
		long unusableDummies = (2*nodes)-3;

		return nodes-1 <= regularTasks
				&& regularTasks + dummyTasks <= maxTasksCount;
	}
}
