package org.openlca.app.results.analysis.sankey;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;

import org.openlca.core.matrix.ProcessLinkSearchMap;
import org.openlca.core.model.ProcessLink;

/**
 * Calculates the processes that are visible in the Sankey diagram.
 */
class SankeyProcessList {

	private SankeyResult sankeyResult;
	private long refProcess;
	private double cutoff;
	private ProcessLinkSearchMap linkSearchMap;

	private SankeyProcessList(SankeyResult result, long refProcess,
			double cutoff, ProcessLinkSearchMap linkSearchMap) {
		this.sankeyResult = result;
		this.refProcess = refProcess;
		this.cutoff = cutoff;
		this.linkSearchMap = linkSearchMap;
	}

	public static Set<Long> calculate(SankeyResult result, long refProcess,
			double cutoff, ProcessLinkSearchMap linkSearchMap) {
		return new SankeyProcessList(result, refProcess, cutoff, linkSearchMap)
				.calculate();
	}

	private Set<Long> calculate() {
		List<Long> initial = sankeyResult.getProcesseIdsAboveCutoff(cutoff);
		Set<Long> processesToDraw = new HashSet<Long>(initial);
		processesToDraw.add(refProcess);
		fillUp(processesToDraw);
		return processesToDraw;
	}

	/**
	 * Checks if each process has a path to the reference process, if not it
	 * searches a way to the reference or another connected node and adds the
	 * missing nodes.
	 */
	private void fillUp(Set<Long> processIds) {

		Set<Long> unconnected = new HashSet<>(processIds);
		Set<Long> connected = new HashSet<>();
		Queue<Long> queue = new ArrayDeque<>();
		queue.add(refProcess);
		while (!queue.isEmpty()) {
			Long recipient = queue.poll();
			unconnected.remove(recipient);
			connected.add(recipient);
			for (ProcessLink link : linkSearchMap.getIncomingLinks(recipient)) {
				Long provider = link.providerId;
				if (!processIds.contains(provider))
					continue;
				if (!queue.contains(provider) && !connected.contains(provider))
					queue.add(provider);
			}
		}
		for (Long processId : unconnected) {
			Stack<Long> path = searchPathFor(processId, connected);
			for (Long id : path) {
				processIds.add(id);
				connected.add(id);
			}
		}
	}

	/**
	 * Find a way from the given process to the connected graph following the
	 * path with biggest weight and return the list of new processes that
	 */
	private Stack<Long> searchPathFor(long processToConnect,
			Set<Long> connectedGraph) {

		Stack<Long> path = new Stack<>();
		path.push(processToConnect);
		HashSet<Long> visited = new HashSet<>();
		visited.add(processToConnect);

		Stack<List<Long>> candidateStack = new Stack<>();
		candidateStack.push(getWeightedRecipients(processToConnect));

		while (!candidateStack.isEmpty()) {

			List<Long> candidates = candidateStack.peek();
			if (candidates.isEmpty()) {
				candidateStack.pop();
				Long v = path.pop();
				visited.add(v);
				continue;
			}

			for (Long candidate : candidates) {
				if (connectedGraph.contains(candidate))
					return path; // found a way to the connected graph
			}

			Long next = null;
			for (int i = 0; i < candidates.size(); i++) {
				Long candidate = candidates.remove(0); // take the first = 0
				if (!path.contains(candidate)) {
					next = candidate;
					break;
				}
			}

			if (next != null) {
				path.push(next);
				List<Long> nextCandidates = new ArrayList<>();
				for (Long nextCandidate : getWeightedRecipients(next)) {
					if (!visited.contains(nextCandidate)
							&& !path.contains(nextCandidate)) {
						nextCandidates.add(nextCandidate);
					}
				}
				candidateStack.push(nextCandidates);
			}
		}
		return path;
	}

	private List<Long> getWeightedRecipients(long processId) {
		List<WeightedProcess> recipients = new ArrayList<>();
		for (ProcessLink link : linkSearchMap.getOutgoingLinks(processId)) {
			WeightedProcess wp = new WeightedProcess();
			wp.id = link.processId;
			wp.weight = Math.abs(sankeyResult.getLinkContribution(link));
			recipients.add(wp);
		}
		Collections.sort(recipients);
		List<Long> ids = new ArrayList<>();
		for (WeightedProcess recipient : recipients)
			ids.add(recipient.id);
		return ids;
	}

	private class WeightedProcess implements Comparable<WeightedProcess> {

		private long id;
		private double weight;

		@Override
		public int compareTo(final WeightedProcess o) {
			return -Double.compare(weight, o.weight);
		}
	}

}