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 initilValue = res.getInitialValue();
				if (initilValue != null) {
					if (initilValue instanceof Term) {
						initilValue = ((Term) initilValue).reduce();
					}
					curState.updateResourceState(resource.getResourceIdentifier(), initilValue);
				}
			}
		}
		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);
		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 {
		IResourceStateAccessor resouceStateAccessor = new IResourceStateAccessor() {
			@Override
			public Expression getCurrentStateAccessorFor(ChannelMember target, ChannelMember from) {
				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()) {
				Expression nextResState = null; 
				if (!event.isInput()) {
					nextResState = channel.deriveUpdateExpressionOf(out, resouceStateAccessor).getKey();
				} else {
					nextResState = channel.deriveUpdateExpressionOf(out, (Term) event.getMessage(), resouceStateAccessor);
				}
				ResourceIdentifier outResId = event.getOutputResourceIdentifier(out.getResource());
				if (nextResState instanceof Term) {
					nextResState = ((Term) nextResState).reduce();
				}
				ResourceIdentifier updatedOutResId = nextSystemState.updateResourceState(outResId, nextResState);
				while (updatedOutResId != null) {		// In addition to the target state, its descendants' states are also changed.
					for (Event nextEvent: getNextEvents(updatedOutResId, curSystemState, nextSystemState)) {
						fireEvent(nextEvent, curSystemState, nextSystemState);
					}
					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, final SystemState curSystemState, final SystemState nextSystemState) 
			throws ParameterizedIdentifierIsFutureWork, ResolvingMultipleDefinitionIsFutureWork, InvalidMessage, UnificationFailed, ValueUndefined {
		Set<Event> nextEvents = new HashSet<>();
		IResourceStateValueProvider resourceStateValueProvider = new IResourceStateValueProvider() {
			@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();
			}
		};
		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.
							Event nextEvent = new Event(channel, inResPath, nextSystemState.getResource(inResId));
							nextEvent.updateDependingParameters(resourceStateValueProvider);
							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) {
						Event nextEvent = new Event(channel, inResPath, nextSystemState.getResource(inResId));
						nextEvent.setMessage(nextEvent.updateDependingParameters(resourceStateValueProvider));
						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;
								Map<Expression, Expression> selectorVarToVal = new HashMap<>();
								Map<Expression, Expression> dependingVarToVal = new HashMap<>();
								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) {
										selectorVarToVal.put(var, val);
									} else {
										dependingVarToVal.put(var, val);
									}
								}
								for (List<Constant> channelSelectorValues: nextChState.getDependedChannelSelectorValues(dependingVarToVal)) {
									// 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;
	}
}
