package algorithms;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

import models.*;
import models.algebra.*;
import models.dataConstraintModel.*;
import models.dataFlowModel.*;

/**
 * Algorithms to analyze data transfer model.
 * 
 * @author Nitta
 *
 */
public class DataTransferModelAnalyzer {
	/**
	 * Create data flow graph annotated with node attributes that indicate whether each resource state needs to be stored.
	 * @param model a data transfer model
	 * @return annotated data flow graph
	 */
	static public DataFlowGraph createDataFlowGraphWithStateStoringAttribute(DataTransferModel model) {
		DataFlowGraph graph = model.getDataFlowGraph();
		Collection<Channel> channels = new HashSet<>(model.getInputChannels());
		channels.addAll(model.getChannels());
		for (Channel channel: channels) {
			for (ChannelMember member: ((DataTransferChannel) channel).getOutputChannelMembers()) {
				boolean toBeStored = !member.getStateTransition().isRightUnary();		// The state does not need to be stored if the state transition function is right unary.
				for (Node node : graph.getResourceNodes()) {
					if (((ResourceNode) node).getInSideResources().contains(member.getResource())) {
						setStoreAttribute((ResourceNode) node, toBeStored);
					}
				}
			}
		}
		for (Node node: graph.getResourceNodes()) {
			HashSet<Channel> inChannels = new HashSet<>();
			for(Edge inEdge: ((ResourceNode) node).getInEdges()) {
				if (inEdge instanceof DataFlowEdge) {
					DataFlowEdge dfEdge = (DataFlowEdge) inEdge;
					if (dfEdge.isChannelToResource()) {
						inChannels.add(((ChannelNode) dfEdge.getSource()).getChannel());
					}
				}
			}
			if ((inChannels.size() > 1)) {
				// If the resource has multiple input channels, then the state of the resource needs to be stored.
				setStoreAttribute((ResourceNode) node, true);
			} else if (((ResourceNode) node).getAttribute() == null) {
				setStoreAttribute((ResourceNode) node, false);
			}
		}
		return graph;
	}

	static private void setStoreAttribute(ResourceNode node, boolean toBeStored) {
		NodeAttribute attr = node.getAttribute();
		StoreAttribute store;
		if (attr != null && attr instanceof NodeAttribute) {
			store = (StoreAttribute) attr;
			store.setNeeded(store.isNeeded() || toBeStored);
		} else {
			store = new StoreAttribute();
			store.setNeeded(toBeStored);
			node.setAttribute(store);
		}
	}

	/**
	 * Annotate data flow graph with edge attributes that indicate selectable data transfer methods.
	 * @param graph a data flow graph
	 * @return annotated data flow graph
	 */
	static public DataFlowGraph annotateWithSelectableDataTransferAttiribute(DataFlowGraph graph) {
		List<Node> resNodes = new ArrayList<>(graph.getResourceNodes());
		// set push only attributes
		for (Node resNode: graph.getResourceNodes()) {
			if (resNodes.contains(resNode) && ((StoreAttribute) ((ResourceNode) resNode).getAttribute()).isNeeded()) {
				resNodes.remove(resNode);
				trackEdges(resNode, resNodes);
			}
		}
		// set push/pull attributes to the remaining edges
		for (Edge e : graph.getEdges()) {
			if (!((DataFlowEdge) e).isChannelToResource() && ((DataFlowEdge) e).getAttribute() == null) {
				PushPullAttribute ppat = new PushPullAttribute();
				ppat.addOption(PushPullValue.PUSHorPULL);
				ppat.addOption(PushPullValue.PUSH);
				ppat.addOption(PushPullValue.PULL);
				((DataFlowEdge) e).setAttribute(ppat);
			}
		}
		return graph;
	}

	static private void trackEdges(Node resNode, List<Node> resNodes) {
		// recursively set push only attributes to input side edges
		for (Edge chToRes : ((ResourceNode) resNode).getInEdges()) {
			Node chNode = chToRes.getSource();
			for (Edge resToCh : ((ChannelNode) chNode).getInEdges()) {
				PushPullAttribute ppat = new PushPullAttribute();
				ppat.addOption(PushPullValue.PUSH);
				((DataFlowEdge) resToCh).setAttribute(ppat);
				Node resNode2 = resToCh.getSource();
				if (resNodes.contains(resNode2)) {
					resNodes.remove(resNode2);
					trackEdges(resNode2, resNodes);
				}
			}
		}
	}
}
