package simulator;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import models.algebra.Constant;
import models.algebra.Expression;
import models.algebra.InvalidMessage;
import models.algebra.Position;
import models.algebra.Term;
import models.algebra.UnificationFailed;
import models.dataConstraintModel.ChannelMember;
import models.dataConstraintModel.ResourcePath;
import models.dataConstraintModel.Selector;
import models.dataFlowModel.DataTransferChannel;
import models.dataFlowModel.DataTransferChannel.IResourceStateAccessor;
import models.dataFlowModel.ResolvingMultipleDefinitionIsFutureWork;

public class Event {
	private DataTransferChannel channel;
	private Expression message;
	private boolean isInput = false;
	private ResourcePath inputResourcePath = null;
	private Resource inputResource = null;
	
	private List<Map.Entry<Selector, Constant>> channelSelectorAndValues = new ArrayList<>();
	private Map<Expression, Expression> dependingParameters = new HashMap<>();
	private Set<Resource> outputResources = new HashSet<>();
	private Set<Event> succEvents = new HashSet<>();
	private Map<Selector, Map<ResourcePath, Integer>> channelSelectorToInputResourcePathParam = new HashMap<>();
	private Map<Selector, Map<ResourcePath, Integer>> channelSelectorToOutputResourcePathParam = new HashMap<>();
	
	public Event(DataTransferChannel channel, Expression message, ResourcePath outputResPath, Resource outputResource) {
		this.channel = channel;
		this.message = message;
		this.isInput = true;
		this.outputResources.add(outputResource);
		connectChannelSelectorAndPathParameters();
		
		// Extract channel parameters from the output resource.
		List<Selector> channelSelectors = channel.getAllSelectors();
		for (Selector sel: channelSelectors) {
			int paramIdx = channelSelectorToOutputResourcePathParam.get(sel).get(outputResPath);
			Resource ancestor = outputResource;
			int idx = outputResPath.getPathParams().size();
			while (ancestor != null) {
				if (ancestor.getResourceHierarchy().getNumParameters() > 0) {
					idx--;
					if (idx == paramIdx) break;
				}
				ancestor = ancestor.getParent();
			}
			if (ancestor!= null) channelSelectorAndValues.add(new AbstractMap.SimpleEntry<>(sel, ancestor.getParameter()));
		}
	}
	
	public Event(DataTransferChannel channel, ResourcePath inputResPath, Resource inputResource) {
		this.channel = channel;
		this.isInput = false;
		this.inputResourcePath = inputResPath;
		this.inputResource = inputResource;
		connectChannelSelectorAndPathParameters();

		// Extract channel parameters from the input resource.
		List<Selector> channelSelectors = channel.getAllSelectors();
		for (Selector sel: channelSelectors) {
			if (inputResPath != null) {
				Integer paramIdx = channelSelectorToInputResourcePathParam.get(sel).get(inputResPath);
				if (paramIdx != null) {
					Resource ancestor = inputResource;
					int idx = inputResPath.getPathParams().size();
					while (ancestor != null) {
						if (ancestor.getResourceHierarchy().getNumParameters() > 0) {
							idx--;
							if (idx == paramIdx) break;
						}
						ancestor = ancestor.getParent();
					}
					channelSelectorAndValues.add(new AbstractMap.SimpleEntry<>(sel, ancestor.getParameter()));
				}
			}
		}
	}
	
	public Event(DataTransferChannel channel, ResourcePath inputResPath, Resource inputResource, List<Constant> channelSelectorValues, Map<Expression, Expression> dependingVarToVal) {
		this.channel = channel;
		this.isInput = false;
		this.inputResourcePath = inputResPath;
		this.inputResource = inputResource;
		connectChannelSelectorAndPathParameters();

		// Extract channel parameters from the input resource.
		List<Selector> channelSelectors = channel.getAllSelectors();
		for (int i = 0; i < channelSelectors.size(); i++) {
			Selector sel = channelSelectors.get(i);
			channelSelectorAndValues.add(new AbstractMap.SimpleEntry<>(sel, channelSelectorValues.get(i)));
		}
		this.dependingParameters = dependingVarToVal;
	}

	private void connectChannelSelectorAndPathParameters() {
		List<Selector> channelSelectors = channel.getAllSelectors();
		for (Selector sel: channelSelectors) {
			for (ResourcePath resPath: channel.getInputResources()) {
				int paramIdx = resPath.getPathParams().indexOf(sel.getExpression());
				if (paramIdx >= 0) {
					Map<ResourcePath, Integer> pathToIdx = channelSelectorToInputResourcePathParam.get(sel);
					if (pathToIdx == null) {
						pathToIdx = new HashMap<>();
						channelSelectorToInputResourcePathParam.put(sel, pathToIdx);
					}
					pathToIdx.put(resPath, paramIdx);
				}
			}
			for (ResourcePath resPath: channel.getOutputResources()) {
				int paramIdx = resPath.getPathParams().indexOf(sel.getExpression());
				if (paramIdx >= 0) {
					Map<ResourcePath, Integer> pathToIdx = channelSelectorToOutputResourcePathParam.get(sel);
					if (pathToIdx == null) {
						pathToIdx = new HashMap<>();
						channelSelectorToOutputResourcePathParam.put(sel, pathToIdx);
					}
					pathToIdx.put(resPath, paramIdx);
				}
			}
		}
	}
	
	public DataTransferChannel getChannel() {
		return channel;
	}

	public Expression getMessage() {
		return message;
	}
	
	public void setMessage(Expression message) {
		this.message = message;
	}

	public boolean isInput() {
		return isInput;
	}

	public List<Map.Entry<Selector, Constant>> getChannelSelectorAndValues() {
		return channelSelectorAndValues;
	}

	public List<Constant> getChannelSelectorValues() {
		List<Constant> channelValues = new ArrayList<>();
		for (Map.Entry<Selector, Constant> chEnt: channelSelectorAndValues) {
			channelValues.add(chEnt.getValue());
		}
		return channelValues;
	}
	
	public void updateChannelSelectorValues(List<Constant> channelSelectorValues) {
		for (int i = 0; i < channelSelectorValues.size(); i++) {
			Map.Entry<Selector, Constant> chEnt = channelSelectorAndValues.get(i);
			chEnt.setValue(channelSelectorValues.get(i));
		}
	}
	
	public Map<Expression, Expression> getDependingParameters() {
		return dependingParameters;
	}
	
	
	public Term updateDependingParameters(IResourceStateValueProvider resourceStateValueProvider) {
		IResourceStateAccessor resouceStateAccessor = new IResourceStateAccessor() {
			@Override
			public Expression getCurrentStateAccessorFor(ChannelMember target, ChannelMember from) {
				ResourceIdentifier resId = getInputResourceIdentifier(target.getResource());
				return resourceStateValueProvider.getCurrentStateValueOf(resId);
			}

			@Override
			public Expression getNextStateAccessorFor(ChannelMember target, ChannelMember from) {
				ResourceIdentifier resId = getInputResourceIdentifier(target.getResource());
				return resourceStateValueProvider.getNextStateValueOf(resId);
			}

			@Override
			public Expression getDirectStateAccessorFor(ResourcePath target, ResourcePath from) {
				ResourceIdentifier resId = getInputResourceIdentifier(target);
				return resourceStateValueProvider.getCurrentStateValueOf(resId);
			}
		};

		try {
			Map<ChannelMember, Set<Position>> substitutedPositionsInMessageFromChannels = new HashMap<>();
			Term unifiedMessage = null;
			
			// Calculate message constraints from leaf channel members on the channel member dependency graph.
			Map<ChannelMember, Set<ChannelMember>> dependency = channel.getMemberDependency();
			if (dependency.size() == 0) {
				// No channel member dependency.
				Expression messageConstraint = null;
				for (ChannelMember channelMember: channel.getInputChannelMembers()) {
					// Calculate message constraint from an input state transition
					messageConstraint = channel.calcMessageConstraintForInputMember(channelMember, null, resouceStateAccessor, null, substitutedPositionsInMessageFromChannels);
					if (unifiedMessage == null) {
						unifiedMessage = (Term) messageConstraint;
					} else {
						unifiedMessage = (Term) unifiedMessage.unify(messageConstraint);
						if (unifiedMessage == null) {
							throw new UnificationFailed();
						}
					}
				}
				for (ChannelMember channelMember: channel.getReferenceChannelMembers()) {
					// Calculate message constraint from a reference state transition
					messageConstraint = channel.calcMessageConstraintForReferenceMember(channelMember, null, resouceStateAccessor, null, substitutedPositionsInMessageFromChannels);
					if (unifiedMessage == null) {
						unifiedMessage = (Term) messageConstraint;
					} else {
						unifiedMessage = (Term) unifiedMessage.unify(messageConstraint);
						if (unifiedMessage == null) {
							throw new UnificationFailed();
						}
					}
				}
				return unifiedMessage;
			}
			Set<ChannelMember> toResolve = new HashSet<>();
			Set<ChannelMember> resolved = new HashSet<>();
			for (Set<ChannelMember> depended: dependency.values()) {
				toResolve.addAll(depended);
			}
			for (ChannelMember depending: dependency.keySet()) {
				toResolve.remove(depending);
			}
			for (ChannelMember leafMember: toResolve) {
				Expression messageConstraint = null;
				if (channel.getInputChannelMembers().contains(leafMember)) {
					// Calculate message constraint from an input state transition
					messageConstraint = channel.calcMessageConstraintForInputMember(leafMember, null, resouceStateAccessor, null, substitutedPositionsInMessageFromChannels);
				} else if (channel.getReferenceChannelMembers().contains(leafMember)) {
					// Calculate message constraint from a reference state transition
					messageConstraint = channel.calcMessageConstraintForReferenceMember(leafMember, null, resouceStateAccessor, null, substitutedPositionsInMessageFromChannels);
				}
				if (unifiedMessage == null) {
					unifiedMessage = (Term) messageConstraint;
				} else {
					unifiedMessage = (Term) unifiedMessage.unify(messageConstraint);
					if (unifiedMessage == null) {
						throw new UnificationFailed();
					}
				}
			}
			resolved.addAll(toResolve);
			toResolve.clear();
			
			for (;;) {
				for (Map.Entry<ChannelMember, Set<ChannelMember>> dependEnt: dependency.entrySet()) {
					ChannelMember dependingMem = dependEnt.getKey();
					Set<ChannelMember> dependedMems = dependEnt.getValue();
					if (!resolved.contains(dependingMem) && resolved.containsAll(dependedMems)) {
						toResolve.add(dependingMem);
					}
				}
				if (toResolve.size() == 0) break;
				for (ChannelMember dependingMem: toResolve) {
					// Fill the path parameters of the resource path of a depending channel member.
					Set<Position> dependingVarPosInMessage = new HashSet<>();
					ResourcePath filledResPath = channel.fillOutsideResourcePath(dependingMem.getResource(), unifiedMessage, dependingMem.getStateTransition().getMessageExpression(), dependingVarPosInMessage);
					ResourcePath unfilledResPath = dependingMem.getResource();
					for (int i = 0; i < unfilledResPath.getPathParams().size(); i++) {
						Expression var = unfilledResPath.getPathParams().get(i);
						Expression val = filledResPath.getPathParams().get(i);
						boolean isSelector = false;
						for (Selector sel: channel.getAllSelectors()) {
							if (sel.getExpression().equals(var)) {
								isSelector = true;
								break;
							}
						}
						if (!isSelector) {
							// A depending channel parameter
							updateDependingParameter(var, val);
						}
					}
					
					// Calculate message constraint
					Expression messageConstraint = null;
					if (channel.getInputChannelMembers().contains(dependingMem)) {
						// Calculate message constraint from an input state transition
						messageConstraint = channel.calcMessageConstraintForInputMember(dependingMem, null, resouceStateAccessor, null, substitutedPositionsInMessageFromChannels);
					} else if (channel.getReferenceChannelMembers().contains(dependingMem)) {
						// Calculate message constraint from a reference state transition
						messageConstraint = channel.calcMessageConstraintForReferenceMember(dependingMem, null, resouceStateAccessor, null, substitutedPositionsInMessageFromChannels);
					}
					if (unifiedMessage == null) {
						unifiedMessage = (Term) messageConstraint;
					} else {
						unifiedMessage = (Term) unifiedMessage.unify(messageConstraint);
						if (unifiedMessage == null) {
							throw new UnificationFailed();
						}
					}
				}
				resolved.addAll(toResolve);
				toResolve.clear();
			}
			return unifiedMessage;
		} catch (ResolvingMultipleDefinitionIsFutureWork | InvalidMessage | UnificationFailed e) {
			e.printStackTrace();
		}
		return null;
	}
	
	public void updateDependingParameter(Expression variable, Expression value) {
		dependingParameters.put(variable, value);
	}

	public ResourcePath getInputResourcePath() {
		return inputResourcePath;
	}

	public Resource getInputResource() {
		return inputResource;
	}

	public Set<Resource> getOutputResources() {
		return outputResources;
	}
	
	public Set<Event> getSuccessors() {
		return succEvents;
	}
	
	public void addSuccessor(Event succEvt) {
		succEvents.add(succEvt);
	}
	
	public ResourceIdentifier getResourceIdentifier(ResourcePath resPath) {
		ResourceIdentifier resId = ResourceIdentifier.createFrom(resPath);
		for (Map.Entry<Selector, Constant> chParamEnt: channelSelectorAndValues) {
			Selector sel = chParamEnt.getKey();
			Map<ResourcePath, Integer> inputPathParamEnt = channelSelectorToInputResourcePathParam.get(sel);
			if (inputPathParamEnt != null) {
				Integer paramIdx = inputPathParamEnt.get(resPath);
				if (paramIdx != null) {
					resId.setPathParam(paramIdx, chParamEnt.getValue());
				}
			}
		}
		for (Map.Entry<Selector, Constant> chParamEnt: channelSelectorAndValues) {
			Selector sel = chParamEnt.getKey();
			Map<ResourcePath, Integer> outputPathParamEnt = channelSelectorToOutputResourcePathParam.get(sel);
			if (outputPathParamEnt != null) {
				Integer paramIdx = outputPathParamEnt.get(resPath);
				if (paramIdx != null) {
					resId.setPathParam(paramIdx, chParamEnt.getValue());
				}
			}
		}
		for (Expression var: dependingParameters.keySet()) {
			int paramIdx = resId.getPathParams().indexOf(var);
			if (paramIdx >= 0) {
				resId.setPathParam(paramIdx, (Constant) dependingParameters.get(var));
			}
		}
		return resId;
	}
	
	public ResourceIdentifier getInputResourceIdentifier(ResourcePath inputResPath) {
		ResourceIdentifier resId = ResourceIdentifier.createFrom(inputResPath);
		for (Map.Entry<Selector, Constant> chParamEnt: channelSelectorAndValues) {
			Selector sel = chParamEnt.getKey();
			if (channelSelectorToInputResourcePathParam.get(sel) != null) {
				Integer paramIdx = channelSelectorToInputResourcePathParam.get(sel).get(inputResPath);
				if (paramIdx != null) {
					resId.setPathParam(paramIdx, chParamEnt.getValue());
				}
			}
		}
		for (Expression var: dependingParameters.keySet()) {
			int paramIdx = resId.getPathParams().indexOf(var);
			if (paramIdx >= 0) {
				resId.setPathParam(paramIdx, (Constant) dependingParameters.get(var));
			}
		}
		return resId;
	}

	public ResourceIdentifier getOutputResourceIdentifier(ResourcePath outputResPath) {
		ResourceIdentifier resId = ResourceIdentifier.createFrom(outputResPath);
		for (Map.Entry<Selector, Constant> chParamEnt: channelSelectorAndValues) {
			Selector sel = chParamEnt.getKey();
			if (channelSelectorToOutputResourcePathParam.get(sel) != null) {
				Integer paramIdx = channelSelectorToOutputResourcePathParam.get(sel).get(outputResPath);
				if (paramIdx != null) {
					resId.setPathParam(paramIdx, chParamEnt.getValue());
				}
			}
		}
		for (Expression var: dependingParameters.keySet()) {
			int paramIdx = resId.getPathParams().indexOf(var);
			if (paramIdx >= 0) {
				resId.setPathParam(paramIdx, (Constant) dependingParameters.get(var));
			}
		}
		return resId;
	}
	
	
	public interface IResourceStateValueProvider {
		Expression getCurrentStateValueOf(ResourceIdentifier resId);
		Expression getNextStateValueOf(ResourceIdentifier resId);
	}
}
