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.ParameterizedIdentifierIsFutureWork;
import models.algebra.Term;
import models.algebra.UnificationFailed;
import models.algebra.ValueUndefined;
import models.algebra.Variable;
import models.dataConstraintModel.Channel;
import models.dataConstraintModel.ChannelMember;
import models.dataConstraintModel.ResourceHierarchy;
import models.dataConstraintModel.ResourcePath;
import models.dataConstraintModel.Selector;
import models.dataFlowModel.DataTransferModel;
import models.dataFlowModel.ResolvingMultipleDefinitionIsFutureWork;
import models.dataFlowModel.DataTransferChannel;
import models.dataFlowModel.DataTransferChannel.IResourceStateAccessor;
import simulator.Event.IResourceStateValueProvider;
import simulator.interfaces.INativeReceiver;

public class Simulator {
	private DataTransferModel model;
	private SystemState curState;
	private List<INativeReceiver> systemReceivers = new ArrayList<>();
	private Map<DataTransferChannel, Map<ResourceIdentifier, INativeReceiver>> nativeReceivers = new HashMap<>(); 
	private Map<DataTransferChannel, INativeReceiver> nativeChannelReceivers = new HashMap<>(); 
	
	public Simulator(DataTransferModel model) {
		this.model = model;
		init();
	}
	
	public DataTransferModel getModel() {
		return model;
	}
	
	public SystemState init() {
		curState = new SystemState();
		for (ResourceHierarchy res: model.getResourceHierarchies()) {
			if (res.getParent() == null) {
				// root resource
				Resource resource = new Resource(res);
				curState.addResource(resource);
				Expression initialValue = res.getInitialValue();
				if (initialValue != null) {
					if (initialValue instanceof Term) {
						initialValue = ((Term) initialValue).reduce();
					}
					curState.updateResourceState(resource.getResourceIdentifier(), null, null, initialValue);
				}
			}
		}
		for (Channel channel: model.getChannels()) {
			curState.addChannel((DataTransferChannel) channel);
		}
		return curState;
	}
	
	public SystemState getCurState() {
		return curState;
	}
	
	/**
	 * Change the state of the system for a given input event.
	 * 
	 * @param inputEvent an input event
	 * @return the next system state
	 * @throws ParameterizedIdentifierIsFutureWork
	 * @throws ResolvingMultipleDefinitionIsFutureWork
	 * @throws InvalidMessage
	 * @throws UnificationFailed
	 * @throws ValueUndefined
	 */
	public SystemState transition(Event inputEvent) 
			throws ParameterizedIdentifierIsFutureWork, ResolvingMultipleDefinitionIsFutureWork, InvalidMessage, UnificationFailed, ValueUndefined {
		SystemState nextSystemState = new SystemState(curState);
		inputEvent.updateDependingParameters(new ResourceStateValueProvider(curState, nextSystemState));
		nextSystemState.addEvent(inputEvent);
		
		fireEvent(inputEvent, curState, nextSystemState);
		
		curState = nextSystemState;
		for (INativeReceiver receiver: systemReceivers) {
			receiver.onReceiveFromModel(inputEvent, nextSystemState);
		}
		return nextSystemState;
	}
	
	public void addSystemReceiver(INativeReceiver receiver) {
		systemReceivers.add(receiver);
	}
	
	public void addNativeReceiver(INativeReceiver receiver, DataTransferChannel channel) {
		nativeChannelReceivers.put(channel, receiver);
	}
	
	public void addNativeReceiver(INativeReceiver receiver, DataTransferChannel channel, Resource resource) {
		Map<ResourceIdentifier, INativeReceiver> receivers = nativeReceivers.get(channel);
		if (receivers == null) {
			receivers = new HashMap<>();
			nativeReceivers.put(channel, receivers);
		}
		receivers.put(resource.getResourceIdentifier(), receiver);
	}
	
	public void removeSystemReceiver(INativeReceiver receiver) {
		systemReceivers.remove(receiver);
	}
	
	public void removeNativeReceiver(DataTransferChannel channel) {
		nativeChannelReceivers.remove(channel);
	}
	
	public void removeNativeReceiver(DataTransferChannel channel, Resource resource) {
		Map<ResourceIdentifier, INativeReceiver> receivers = nativeReceivers.get(channel);
		if (receivers != null) {
			receivers.remove(resource.getResourceIdentifier());
			if (receivers.size() == 0) {
				nativeReceivers.remove(channel);
			}
		}
	}

	/**
	 * Fire an given event and construct the next system state from the current system state.
	 * 
	 * @param event an event
	 * @param curSystemState the current state of the system
	 * @param nextSystemState the next state of the system to be constructed
	 * @throws ParameterizedIdentifierIsFutureWork
	 * @throws ResolvingMultipleDefinitionIsFutureWork
	 * @throws InvalidMessage
	 * @throws UnificationFailed
	 * @throws ValueUndefined
	 */
	private void fireEvent(final Event event, final SystemState curSystemState, final SystemState nextSystemState)
			throws ParameterizedIdentifierIsFutureWork, ResolvingMultipleDefinitionIsFutureWork, InvalidMessage, UnificationFailed, ValueUndefined {
		final ChannelMember[] outTarget = new ChannelMember[1];
		final Variable[] outResVar = new Variable[1];
		IResourceStateAccessor resouceStateAccessor = new IResourceStateAccessor() {
			@Override
			public Expression getCurrentStateAccessorFor(ChannelMember target, ChannelMember from) {
				if (target == outTarget[0]) return outResVar[0];		// the current state of each output resource is not to be replaced with its value.
				ResourceIdentifier resId = event.getResourceIdentifier(target.getResource());
				Resource res = curSystemState.getResource(resId);
				if (res == null) return null;
				return res.getState().getValue();
			}

			@Override
			public Expression getNextStateAccessorFor(ChannelMember target, ChannelMember from) {
				ResourceIdentifier resId = event.getResourceIdentifier(target.getResource());
				Resource res = nextSystemState.getResource(resId);
				if (res == null) return null;
				return res.getState().getValue();
			}

			@Override
			public Expression getDirectStateAccessorFor(ResourcePath target, ResourcePath from) {
				ResourceIdentifier resId = event.getResourceIdentifier(target);
				Resource res = curSystemState.getResource(resId);
				if (res == null) return null;
				return res.getState().getValue();
			}
		};
		
		DataTransferChannel channel = event.getChannel();
		if (channel.getOutputResources().size() > 0) {
			// For each output resource, calculate the next state.
			for (ChannelMember out: channel.getOutputChannelMembers()) {
				// Calculate the next state expression.
				Expression nextResState = null;
				outTarget[0] = out;
				outResVar[0] = new Variable(channel.getChannelName() + "$" + out.getResource().toString() + "$this");		// A special variable to represent the current state of each output resource.
				if (!event.isInput()) {
					nextResState = channel.deriveUpdateExpressionOf(out, resouceStateAccessor).getKey();
				} else {
					nextResState = channel.deriveUpdateExpressionOf(out, (Term) event.getMessage(), resouceStateAccessor);
				}
				// Substitute each channel selector in the expression to a value.
				for (Map.Entry<Selector, Constant> selestorAndVal: event.getChannelSelectorAndValues()) {
					Expression selExp = selestorAndVal.getKey().getExpression();
					if (nextResState instanceof Term && selExp instanceof Variable) {
						nextResState = ((Term) nextResState).substitute((Variable) selExp, selestorAndVal.getValue());
					}
				}
				// Update the resource state.
				ResourceIdentifier outResId = event.getOutputResourceIdentifier(out.getResource());
				Expression curResState = null;
				if (curSystemState.getResource(outResId) != null) {
					curResState = curSystemState.getResource(outResId).getState().getValue();
				}
				List<ResourceIdentifier> updatedOutResIds = nextSystemState.updateResourceState(outResId, outResVar[0], curResState, nextResState);
				if (updatedOutResIds != null) {
					Set<ResourceIdentifier> ancestors = new HashSet<>();
					for (ResourceIdentifier updatedOutResId: updatedOutResIds) {
						while (updatedOutResId != null && !ancestors.contains(updatedOutResId)) {		// In addition to the target state, its ancestors' states are also changed.
							ancestors.add(updatedOutResId);
							for (Event nextEvent: getNextEvents(updatedOutResId, curSystemState, nextSystemState)) {
								fireEvent(nextEvent, curSystemState, nextSystemState);
							}
							if (channel.isNative()) break;		// To avoid multiple updates of the same resource.
							updatedOutResId = (ResourceIdentifier) updatedOutResId.getParent();
						}
					}
				}
			}
		} else if (channel.isNative()) {
			// A native output event channel
			INativeReceiver receiver = nativeChannelReceivers.get(channel);		// receiver for the channel
			if (receiver != null) receiver.onReceiveFromModel(event, nextSystemState);
			if (nativeReceivers.get(channel) != null) {
				receiver = nativeReceivers.get(channel).get(event.getInputResource().getResourceIdentifier());		// receiver for the channel and resource
				if (receiver != null) receiver.onReceiveFromModel(event, nextSystemState);
			}
		}
	}

	private Set<Event> getNextEvents(ResourceIdentifier inResId, SystemState curSystemState, SystemState nextSystemState) 
			throws ParameterizedIdentifierIsFutureWork, ResolvingMultipleDefinitionIsFutureWork, InvalidMessage, UnificationFailed, ValueUndefined {
		Set<Event> nextEvents = new HashSet<>();
		IResourceStateValueProvider resourceStateValueProvider = new ResourceStateValueProvider(curSystemState, nextSystemState);
		for (Map.Entry<DataTransferChannel, ChannelState> chEntry: nextSystemState.getChannelStates().entrySet()) {
			DataTransferChannel channel = chEntry.getKey();
			ChannelState nextChState = chEntry.getValue();
			Map<ChannelMember, Set<ChannelMember>> dependency = channel.getMemberDependency();
			Map<ChannelMember, Set<ChannelMember>> invDependency = new HashMap<>();
			for (ChannelMember dependingMem: dependency.keySet()) {
				for (ChannelMember dependedMem: dependency.get(dependingMem)) {
					Set<ChannelMember> dependings = invDependency.get(dependedMem);
					if (dependings == null) {
						dependings = new HashSet<>();
						invDependency.put(dependedMem, dependings);
					}
					dependings.add(dependingMem);
				}
			}
			for (ResourcePath inResPath: channel.getInputResources()) {
				if (inResId.isInstanceOf(inResPath)) {
					// Update the channel state and resource identifiers by the update of the input resource.
					boolean isInputResourceDepended = false;
					for (ChannelMember dependedMem: invDependency.keySet()) {
						if (inResPath == dependedMem.getResource()) {
							// If some depending resources are to be updated by the update of an depended input resource.
							if (doesSatifsyPathConstraints(inResPath, inResId)) {
								Event nextEvent = new Event(channel, inResPath, nextSystemState.getResource(inResId));
								Expression message = nextEvent.updateDependingParameters(resourceStateValueProvider);
								if (message != null) {
									if (nextChState == null) {
										nextChState = new ChannelState(channel);
										nextSystemState.updateChannelState(channel, nextChState);
									}
									List<Constant> channelSelValues = nextEvent.getChannelSelectorValues();
									for (Map.Entry<Expression, Expression> paramEnt: nextEvent.getDependingParameters().entrySet()) {
										nextChState.addDependingParamAndValue(channelSelValues, paramEnt.getKey(), paramEnt.getValue());								
									}
									nextEvents.add(nextEvent);
								}
							}
							isInputResourceDepended = true;
						}
					}
					boolean isInputResourceDepending = false;
					for (ChannelMember dependingMem: dependency.keySet()) {
						if (inResPath == dependingMem.getResource()) {
							isInputResourceDepending = true;
						}
					}
					if (!isInputResourceDepended && !isInputResourceDepending) {
						if (doesSatifsyPathConstraints(inResPath, inResId)) {
							Event nextEvent = new Event(channel, inResPath, nextSystemState.getResource(inResId));
							Expression message = nextEvent.updateDependingParameters(resourceStateValueProvider);
							if (message != null) {
								nextEvent.setMessage(message);
								nextEvents.add(nextEvent);
							}
						}
					}
					if (nextChState != null) {
						for (ChannelMember dependingMem: dependency.keySet()) {
							if (inResPath == dependingMem.getResource()) {
								// If a depending resource is directly updated.
								ResourcePath filledResPath = inResId;
								ResourcePath unfilledResPath = inResPath;
								// Extract the values of path parameters from the updated resource identifier.
								Map<Expression, Expression> selectorVarToVal = new HashMap<>();
								Map<Expression, Expression> dependingVarToVal = new HashMap<>();
								boolean doesSatisfyConstraint = true;
								for (int i = 0; i < unfilledResPath.getPathParamsAndConstraints().size(); i++) {
									Expression var = unfilledResPath.getPathParamsAndConstraints().get(i).getKey();
									Expression val = filledResPath.getPathParamsAndConstraints().get(i).getKey();
									Expression constraint = unfilledResPath.getPathParamsAndConstraints().get(i).getValue();
									if (constraint != null && !constraint.equals(val)) {
										// The value of the path parameter does not satisfy the constraint defined for the parameter.
										doesSatisfyConstraint = false;
										break;
									}
									boolean isSelector = false;
									for (Selector sel: channel.getAllSelectors()) {
										if (sel.getExpression().equals(var)) {
											isSelector = true;
											break;
										}
									}
									if (isSelector) {
										selectorVarToVal.put(var, val);
									} else {
										dependingVarToVal.put(var, val);
									}
								}
								if (doesSatisfyConstraint) {
									List<List<Constant>> dependedChannelSelectorValues = nextChState.getDependedChannelSelectorValues(dependingVarToVal);
									if (dependedChannelSelectorValues != null) {
										for (List<Constant> channelSelectorValues: dependedChannelSelectorValues) {
											// Guess every tuple of channel selector values that may affects the updated resource.
											boolean doesMatch = true;
											for (Expression var: selectorVarToVal.keySet()) {
												for (int i = 0; i < channel.getAllSelectors().size(); i++) {
													if (channel.getAllSelectors().get(i).getExpression().equals(var)) {
														if (!channelSelectorValues.get(i).equals(selectorVarToVal.get(var))) {
															// If the value of a selector in the updated resource path does not matches a guessed channel selector value.
															doesMatch = false;
														}
													}
												}
											}
											if (doesMatch) {
												Event nextEvent = new Event(channel, inResPath, nextSystemState.getResource(inResId), channelSelectorValues, dependingVarToVal);
												nextEvents.add(nextEvent);
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}
		return nextEvents;
	}
	
	private boolean doesSatifsyPathConstraints(ResourcePath resPath, ResourceIdentifier resId) {
		for (int i = 0; i < resPath.getPathParamsAndConstraints().size(); i++) {
			Expression val = resId.getPathParamsAndConstraints().get(i).getKey();
			Expression constraint = resPath.getPathParamsAndConstraints().get(i).getValue();
			if (constraint != null) {
				String valStr = val.toString();
				if (val instanceof Constant) {
					valStr = (String) ((Constant) val).getValue();
				}
				String constStr = constraint.toString();
				if (constraint instanceof Constant) {
					constStr = (String) ((Constant) constraint).getValue();
				}
				if (!constStr.equals(valStr)) {
					// The value of the path parameter does not satisfy the constraint defined for the parameter.
					return false;		// Not to fire this event.
				}
			}
		}
		return true;
	}

	private static class ResourceStateValueProvider implements IResourceStateValueProvider {
		SystemState curSystemState;
		SystemState nextSystemState;
		
		public ResourceStateValueProvider(SystemState curSystemState, SystemState nextSystemState) {
			this.curSystemState = curSystemState;
			this.nextSystemState = nextSystemState;
		}
		
		@Override
		public Expression getCurrentStateValueOf(ResourceIdentifier resId) {
			if (curSystemState.getResource(resId) == null) return null;
			return curSystemState.getResource(resId).getState().getValue();
		}

		@Override
		public Expression getNextStateValueOf(ResourceIdentifier resId) {
			if (nextSystemState.getResource(resId) == null) return null;
			return nextSystemState.getResource(resId).getState().getValue();
		}
	};
}
