package com.hammurapi.reasoning.impl;

import java.io.File;
import java.io.StringReader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.util.Diagnostician;
import org.eclipse.emf.ecore.util.EcoreUtil;

import antlr.RecognitionException;
import antlr.TokenStreamException;
import antlr.collections.AST;

import com.hammurapi.config.Collection;
import com.hammurapi.config.ConfigFactory;
import com.hammurapi.config.Factory;
import com.hammurapi.config.Named;
import com.hammurapi.config.NamedCollection;
import com.hammurapi.config.NamedObjectDefinition;
import com.hammurapi.config.ObjectDefinition;
import com.hammurapi.config.PropertySource;
import com.hammurapi.config.bootstrap.ConfigurationException;
import com.hammurapi.config.impl.ObjectDefinitionImpl;
import com.hammurapi.config.runtime.FactoryConfig;
import com.hammurapi.config.runtime.ResourceLoader;
import com.hammurapi.config.util.ConfigUtil;
import com.hammurapi.flow.Flow;
import com.hammurapi.flow.FlowFactory;
import com.hammurapi.flow.Node;
import com.hammurapi.flow.Pin;
import com.hammurapi.flow.Transition;
import com.hammurapi.flow.runtime.impl.PassThroughTransition;
import com.hammurapi.grammar.java.JavaLexer;
import com.hammurapi.grammar.java.JavaRecognizer;
import com.hammurapi.grammar.java.JavaTokenTypes;
import com.hammurapi.reasoning.spi.Accept;
import com.hammurapi.reasoning.spi.Condition;
import com.hammurapi.reasoning.spi.Infer;
import com.hammurapi.reasoning.spi.model.Rule;
import com.hammurapi.reasoning.spi.model.RuleSet;

/**
 * Compiles (transforms) rule set to flow definition.
 * @author Pavel Vlasov
 *
 */
public class RuleSetToFlowCompiler implements Constants {
		
	private static final String PARAMETER_INDEX = "parameterIndex";
	private static final String TYPE = "type";
	
	private static int pinCounter;
	
	private static final Logger logger = Logger.getLogger(RuleSetToFlowCompiler.class.getName());
	
	private class PinInfo {
		int priority;
		Pin pin;
		int getPriority() {
			return priority;
		}
		Pin getPin() {
			return pin;
		}
		PinInfo(int priority, Pin pin) {
			super();
			this.priority = priority;
			this.pin = pin;
		}				
	}
	
	private class RuleInfo {
		Rule rule;
		Class<?> ruleClass;
		
		public RuleInfo(Rule rule, FactoryConfig ruleFactoryConfig) throws ConfigurationException, ClassNotFoundException {
			this.rule = rule;
			ClassLoader ruleClassLoader = ObjectDefinitionImpl.propertySourceClassLoader(rule, ruleFactoryConfig);				
			ruleClass = ruleClassLoader.loadClass(rule.getType());			
		}
	}

	/**
	 * Compiles rule set to flow.
	 * @param ruleSet Rule set. The rule set gets modified during compilation and shall be
	 * discarded after compilation.
	 * @param classLoader Classloader to load rule classes for introspection. If null, then
	 * this class' classloader is used.
	 * @return Flow definition.
	 * @throws ConfigurationException
	 */
	public Flow compile(RuleSet source, FactoryConfig factoryConfig) throws ConfigurationException {
		Flow ret = FlowFactory.eINSTANCE.createFlow();
		RuleSet ruleSet = (RuleSet) EcoreUtil.copy(source);
//		ret.setFacadeInterface(ForwardReasoningSession.class.getName());
		ret.setDescription("Compiled rule set "+ruleSet.getName());
		
		if (ruleSet.getName()!=null) {
			ConfigUtil.addScalarProperty(ret, "name", ruleSet.getName(), true);
		}
		
		if (ruleSet.getDescription()!=null) {
			ConfigUtil.addScalarProperty(ret, "description", ruleSet.getDescription(), true);
		}
		
		Pin flowInputPin = FlowFactory.eINSTANCE.createPin();
		flowInputPin.setName(PUT);
		ret.getPin().add(flowInputPin);
				
		Map<Class<?>, List<PinInfo>> classSources = new HashMap<Class<?>, List<PinInfo>>();
		Map<Class<?>, List<PinInfo>> classConsumers = new HashMap<Class<?>, List<PinInfo>>(); 		
		
		ArrayList<RuleInfo> allRules = new ArrayList<RuleInfo>();
		collectRules(ruleSet, factoryConfig, allRules);
		
		ConfigUtil.pump(ruleSet, ret);
		if (ret.getType()==null || ret.getType().trim().length()==0 || String.class.getName().equals(ret.getType().trim())) { 
			ret.setType(RuleSetFlow.class.getName());
		}
		
		Iterator<RuleInfo> rit = allRules.iterator();
		NamedCollection rulesCollection = ConfigFactory.eINSTANCE.createNamedCollection();
		rulesCollection.setName("rules");
		rulesCollection.setDescription("Collection of rule definitions");
		rulesCollection.setType(ArrayList.class.getName());
		ret.getProperty().add(rulesCollection);
		for (int i=0; rit.hasNext(); ++i) {
			RuleInfo ruleInfo = rit.next();
			rulesCollection.getElement().add(ruleInfo.rule);
							
			int rulePriority = ruleInfo.rule.getPriority(); 
			Method[] methods = ruleInfo.ruleClass.getMethods();
			for (Method method: methods) {
				Infer inferAnnotation = getInferAnnotation(method);
				if (inferAnnotation!=null && !Modifier.isVolatile(method.getModifiers())) {
					Class<?>[] pt = method.getParameterTypes();
					int methodPriority = inferAnnotation.priority();
					if (methodPriority==0) {
						methodPriority = rulePriority;
					}
					
					Node inferenceNode = createInferenceMethodNode(ruleSet, ruleInfo.rule, method, ret);	
					inferenceNode.setName(method.getName());						
					inferenceNode.setDescription("Inference node for method '"+method+"' in rule #"+i+" '"+ruleInfo.rule.getName()+"'");
					if (pt.length==0) {
						throw new ConfigurationException("Inference method shall have at least one parameter: "+method);
					} else {
						setParameterTypes(inferenceNode, pt);
					}
					
					ConfigUtil.addScalarProperty(inferenceNode, "methodName", method.getName(), true);
					if (ruleInfo.rule.getName()!=null) {
						ConfigUtil.addScalarProperty(inferenceNode, "ruleName", ruleInfo.rule.getName(), true);
					}
					if(ruleInfo.rule.getDescription()!=null) {
						ConfigUtil.addScalarProperty(inferenceNode, "ruleDescription", ruleInfo.rule.getDescription(), true);
					}
					ConfigUtil.addScalarProperty(inferenceNode, RULE_INDEX, i, true);
					
					// Infer attributes - priority, value - conclusion classes.
					ConfigUtil.addScalarProperty(inferenceNode, "priority", methodPriority, true);
					Class<?>[] conclusionTypes = inferAnnotation.value();
					if (conclusionTypes.length==0) {
						if (!void.class.equals(method.getReturnType())) {
							conclusionTypes = new Class<?>[] {method.getReturnType()};
						}
					}
					
					// Outputs
					for (Class<?> conclusionType: conclusionTypes) {
						Pin conclusionPin = createOutputPin(inferenceNode, -1, conclusionType);
						List<PinInfo> conclusionBucket = classSources.get(conclusionType);
						if (conclusionBucket==null) {
							conclusionBucket = new ArrayList<PinInfo>();
							classSources.put(conclusionType, conclusionBucket);
						}
						conclusionBucket.add(new PinInfo(methodPriority, conclusionPin));
					}
					
					// Create parameter pins
					Pin[] parameterPins = new Pin[pt.length];
					for (int pIdx=0; pIdx<pt.length; ++pIdx) {							
						parameterPins[pIdx] = createInputPin(inferenceNode, pIdx, pt[pIdx]);
					}
					
					List<ConditionEntry> conditionList = new ArrayList<ConditionEntry>();
					// Method level conditions
					Condition condition = method.getAnnotation(Condition.class);
					if (condition!=null) {
						for (String c: condition.value()) {
							try {
								ExpressionConditionEntry ce = analyzeCondition(c, pt.length, true);
								ce.position = conditionList.size();
//									ce.methodLevel = true;
								conditionList.add(ce);
							} catch (RecognitionException e) {
								throw new ConfigurationException("Error in condition: '"+c+"': "+e,e);
							} catch (TokenStreamException e) {
								throw new ConfigurationException("Error in condition: '"+c+"': "+e,e);
							}
						}
					}
					
					Annotation[][] parameterAnnotations = method.getParameterAnnotations();
					
					// Parameters
					for (int pIdx=0; pIdx<pt.length; ++pIdx) {
						
						// Find accept methods
						for (Method acceptMethod: methods) {
							Accept acceptAnnotation = acceptMethod.getAnnotation(Accept.class);
							if (acceptAnnotation!=null) {
								if (!boolean.class.equals(acceptMethod.getReturnType())) {
									throw new ConfigurationException("Accept method shall return boolean: "+acceptMethod);
								}
								Class<?>[] apt = acceptMethod.getParameterTypes();
								if (apt.length==0) {
									throw new ConfigurationException("Accept method shall have parameters.");
								} 
								
								if (acceptAnnotation.method().trim().length()==0 || acceptAnnotation.method().equals(method.getName())) { // Method name match
									if (apt.length==1) {
										if (apt[0].isAssignableFrom(pt[pIdx])) { // Parameter compatibility.
											for (int apIdx: acceptAnnotation.parameterPositions()) {
												if (apIdx==-1 || apIdx==pIdx) { // Position match
													if (acceptAnnotation.parameterTypes().length==0 || Arrays.equals(acceptAnnotation.parameterTypes(), pt)) { // Parameter types match											
														AcceptConditionEntry ce = new AcceptConditionEntry(acceptMethod, pIdx);
														ce.position = conditionList.size();
														conditionList.add(ce);
													}														
												}
											}
										}
									} else {
										throw new UnsupportedOperationException("Multi-parameter accept methods are not supported yet");
									}
								}									
							}
						}
							
						// Parameter level conditions
						for (Annotation pAnn: parameterAnnotations[pIdx]) {
							if (pAnn instanceof Condition) {
								for (String c: ((Condition) pAnn).value()) {
									ConditionEntry ce = new ExpressionConditionEntry(c, Collections.singletonMap(pIdx, "arg"), null);
									ce.position = conditionList.size();
									conditionList.add(ce);
								}
							}									
						}														
					}
					
					// Sort conditions in the reverse order of number of parameters to join.
					Collections.sort(conditionList, new Comparator<ConditionEntry>() {

						@Override
						public int compare(ConditionEntry c1, ConditionEntry c2) {
							int s1 = c1.getParamIndexes().size();
							int s2 = c2.getParamIndexes().size();									
							return s1==s2 ? c2.position - c1.position : s2 - s1;
						}
					});														

					// Insert condition nodes starting from parameter pins, replace pins
					int conditionNo = 0;
					for (ConditionEntry ce: conditionList) {
						Node conditionNode; 
						if (ce instanceof ExpressionConditionEntry) {
							conditionNode = createNode(ret, ExpressionConditionNode.class);
							ExpressionConditionEntry ece = (ExpressionConditionEntry) ce;
							ConfigUtil.addScalarProperty(conditionNode, "condition", ece.text, true);
							for (Map.Entry<Integer,String> e: ece.paramNames.entrySet()) {
								addConditionArgument(conditionNode, e.getValue(), pt[e.getKey()], e.getKey());
							}
							conditionNode.setName(ece.text);
						} else if (ce instanceof AcceptConditionEntry) {
							conditionNode = createNode(ret, AcceptMethodConditionNode.class);								
							Method acceptMethod = ((AcceptConditionEntry) ce).acceptMethod;
							ConfigUtil.addScalarProperty(conditionNode, "methodName", acceptMethod.getName(), true);
							setParameterTypes(conditionNode, acceptMethod.getParameterTypes());
							conditionNode.setName(acceptMethod.getName());
							setParameterMap(conditionNode, ((AcceptConditionEntry) ce).pMap);
						} else {
							throw new ConfigurationException("Unexpected condition type: "+ce.getClass().getName());
						}
						
						if (ruleInfo.rule.getName()!=null) {
							ConfigUtil.addScalarProperty(inferenceNode, "ruleName", ruleInfo.rule.getName(), true);
						}
						if(ruleInfo.rule.getDescription()!=null) {
							ConfigUtil.addScalarProperty(inferenceNode, "ruleDescription", ruleInfo.rule.getDescription(), true);
						}
						
						ConfigUtil.addScalarProperty(conditionNode, RULE_INDEX, i, true);
						ConfigUtil.addScalarProperty(conditionNode, CONDITION_NO, conditionNo++, false);
						
						if (ce.getParamIndexes().isEmpty()) {
							throw new ConfigurationException("Condition doesn't use any arguments");
						} else if (ce.getParamIndexes().size()==1) {
							Integer parameterIndex = ce.getParamIndexes().iterator().next();
							Pin inputPin = createInputPin(conditionNode, parameterIndex, pt[parameterIndex]);
							Pin outputPin = createOutputPin(conditionNode, parameterIndex, pt[parameterIndex]);																														
							
							connect(outputPin, parameterPins[parameterIndex], PassThroughTransition.class, methodPriority, ret);															
							// TODO - use toKey addScalarProperty(acceptTransition, END_CONNECTION_KEY, "parameter:"+pIdx);
							parameterPins[parameterIndex] = inputPin;
						} else {
							Pin[] toMerge = new Pin[ce.getParamIndexes().size()];
							java.util.Collection<TokenInfo> tic = new ArrayList<TokenInfo>();
							int cpi = 0;
							for (Integer param: ce.getParamIndexes()) {
								tic.add(new TokenInfo(param, pt[param]));
								toMerge[cpi++]=parameterPins[param];
							}
							Pin merged = mergeInputPins(toMerge);
							Pin conditionOutput = createOutputPin(conditionNode, tic);
							connect(conditionOutput, merged, PassThroughTransition.class, methodPriority, ret);
							for (Integer param: ce.getParamIndexes()) {
								parameterPins[param] = createInputPin(conditionNode, param, pt[param]);
							}									
						}
					}
					
					// Add parameter pins to consumer buckets
					for (int pIdx=0; pIdx<pt.length; ++pIdx) {
						List<PinInfo> consumerBucket = classConsumers.get(pt[pIdx]);
						if (consumerBucket==null) {
							consumerBucket = new ArrayList<PinInfo>();
							classConsumers.put(pt[pIdx], consumerBucket);
						}
						consumerBucket.add(new PinInfo(methodPriority, parameterPins[pIdx]));										
					}
				}										
			}
		}
		
		for (Map.Entry<Class<?>, List<PinInfo>> consumerEntry: classConsumers.entrySet()) {
			Node passThroughNode = FlowFactory.eINSTANCE.createNode();
			passThroughNode.setDescription("Pass-through (NOP) node for consumers of type "+consumerEntry.getKey());
			ret.getFlowElement().add(passThroughNode);
			Pin passThroughPin = FlowFactory.eINSTANCE.createPin();
			passThroughPin.setName(DEFAULT);
			passThroughNode.getPin().add(passThroughPin);
			passThroughNode.setType(ReasoningPassThroughNode.class.getName());
			
			Transition inputTransition = connect(flowInputPin, passThroughPin, TypeFilterTransition.class, 0, ret);
			inputTransition.setDescription("Input transition for type "+consumerEntry.getKey());
			ConfigUtil.addScalarProperty(inputTransition, TYPE, consumerEntry.getKey(), true);
			inputTransition.setName(consumerEntry.getKey().getName());
			
			if (consumerEntry.getValue().isEmpty()) {
				throw new IllegalStateException("Consumer bucket shall not be empty for "+consumerEntry.getKey());
			}
			for (PinInfo consumerPinInfo: consumerEntry.getValue()) {
				connect(passThroughPin, consumerPinInfo.getPin(), PassThroughTransition.class, consumerPinInfo.getPriority(), ret);
			}
			
			for (Map.Entry<Class<?>, List<PinInfo>> sourceEntry: classSources.entrySet()) {
				if (consumerEntry.getKey().isAssignableFrom(sourceEntry.getKey())) { // Compatible types, connect.
					if (sourceEntry.getValue().isEmpty()) {
						throw new IllegalStateException("Source bucket shall not be empty for "+sourceEntry.getKey());
					}
					for (PinInfo sourcePinInfo: sourceEntry.getValue()) {
						connect(sourcePinInfo.getPin(), passThroughPin, PassThroughTransition.class,  sourcePinInfo.getPriority(), ret);
					}						
				}
			}
			if (passThroughPin.getOutput().isEmpty()) {
				throw new IllegalStateException("No consumers for "+consumerEntry.getKey());
			}
		}				
		
		// Optimizes flow, chains optimizers.
		Iterator<RuleSetFlowOptimizer> rsFo = ServiceLoader.load(RuleSetFlowOptimizer.class, this.getClass().getClassLoader()).iterator();
		List<RuleSetFlowOptimizer> rsFol = new ArrayList<RuleSetFlowOptimizer>();
		while (rsFo.hasNext()) {
			rsFol.add(rsFo.next());
		}
		Collections.sort(rsFol, new Comparator<RuleSetFlowOptimizer>() {

			@Override
			public int compare(RuleSetFlowOptimizer o1, RuleSetFlowOptimizer o2) {
				int diff = o1.getOrder() - o2.getOrder();
				return diff==0 ? o1.hashCode() - o2.hashCode() : diff;
			}
			
		});
		
		for (RuleSetFlowOptimizer rsfo: rsFol) {
			rsfo.optimize(ret);
		}
		
		TreeIterator<EObject> rcit = ret.eAllContents();
		while (rcit.hasNext()) {
			EObject next = rcit.next();
			if (next instanceof Transition) {
				Transition t = (Transition) next;
				if (t.getToPin()==null) {
					throw new IllegalStateException("To pin is null for "+t);
				}
				if (t.getFromPin()==null) {
					throw new IllegalStateException("From pin is null for "+t);
				}
			}			
		}
		
		return ret;
	}

	private void collectRules(RuleSet ruleSet, FactoryConfig factoryConfig,	ArrayList<RuleInfo> allRules) throws ConfigurationException {
		for (RuleSet base: ruleSet.getBase()) {
			collectRules(base, factoryConfig, allRules);
		}
		
		ClassLoader ruleSetClassLoader = ObjectDefinitionImpl.propertySourceClassLoader(ruleSet, factoryConfig);
		FactoryConfig ruleFactoryConfig;
		try {
			ruleFactoryConfig = (FactoryConfig) factoryConfig.clone();
		} catch (CloneNotSupportedException ex) {
			throw new ConfigurationException(ex);
		}
		ruleFactoryConfig.setClassLoader(ruleSetClassLoader);
		for (Rule rule: ruleSet.getRule()) {
			try {
				allRules.add(new RuleInfo(rule, ruleFactoryConfig));				
			} catch (ClassNotFoundException e) {
				throw new ConfigurationException("Rule class not found: "+e, e);
			}							
		}
	}

	/**
	 * Creates inference method node.
	 * Subclasses can override this method to provide custom
	 * node implementations and additional configuration. 
	 * @param method 
	 * @param rule 
	 * @param ruleSet 
	 * @param ret
	 * @return
	 */
	protected Node createInferenceMethodNode(RuleSet ruleSet, Rule rule, Method method, Flow owner) {
		return createNode(owner, InferenceMethodNode.class);
	}

	/**
	 * Extracts inference information from the method.
	 * This implementation takes it from Infer annotation.
	 * Subclasses can do it in a different way. 
	 * @param method
	 * @return
	 */
	protected Infer getInferAnnotation(Method method) {
		return method.getAnnotation(Infer.class);
	}
	
	/**
	 * Merges input pins.
	 * @param pin1
	 * @param pin2
	 */
	private Pin mergeInputPins(Pin... pins) {
		if (pins.length==0) {
			throw new IllegalArgumentException("No input pins to merge");
		}
		// Sort pins so the first pin always belongs to the outmost condition.
		Arrays.sort(pins, new Comparator<Pin>() {

			@Override
			public int compare(Pin o1, Pin o2) {
				if (belongsToCondition(o1)) {
					if (!belongsToCondition(o2)) {
						return -1;
					}
					
					int cn1 = getConditionNo(o1);
					if (cn1==-1) {
						throw new IllegalStateException("Condition number shall not be -1");
					}
					int cn2 = getConditionNo(o2);
					if (cn2==-1) {
						throw new IllegalStateException("Condition number shall not be -1");
					}
					if (cn1!=cn2) {
						return cn2-cn1;
					}
				} 
				
				// Both belong to method node
				return o1.getName().compareTo(o2.getName());
			}

			private int getConditionNo(Pin pin) {
				for (Named property: pin.getNode().getProperty()) {
					if (CONDITION_NO.equals(property.getName()) && property instanceof NamedObjectDefinition) {
						return Integer.parseInt(((NamedObjectDefinition) property).getValue());
					}
				}
				return -1;
			}

			private boolean belongsToCondition(Pin o1) {
				String type = o1.getNode().getType();
				try {
					return ConditionNode.class.isAssignableFrom(Class.forName(type));
				} catch (ClassNotFoundException e) {
					logger.log(Level.WARNING, "Condition class not found: "+type, e);
					return false;
				}
			}
			
		});
		
		for (int i=1; i<pins.length; ++i) {
			java.util.Collection<Factory> toMove = new ArrayList<Factory>();
			toMove.addAll(((Collection) pins[i].getPinConfig()).getElement());
			addTokens(toMove, (Collection) pins[0].getPinConfig());
			if (pins[0].getNode()==pins[i].getNode()) {
				// Same node pins - remove second pin, no need to change output pins.
				pins[i].getNode().getPin().remove(pins[i]);
			} else {
				// Different nodes. Change pin[0] name, change output name, merge output with the second pin.
				merge(pins[0].getNode(), pins[i], toMove);
			}
		}
		return pins[0];
	}
	
	private void addTokens(java.util.Collection<Factory> tokens, Collection target) {
		for (Factory f: tokens) {
			if (f instanceof ObjectDefinition) {
				ObjectDefinition token = (ObjectDefinition) f;
				if (!TokenInfo.class.getName().equals(token.getType())) {
					throw new IllegalArgumentException("Invalid token info type: "+token.getType());					
				}
				String className = null;
				int pIdx = -1;
				for (Named property: token.getProperty()) {
					if (TYPE.equals(property.getName())) {
						className = ((ObjectDefinition) property).getValue();
					} else if (PARAMETER_INDEX.equals(property.getName())) {
						pIdx = Integer.parseInt(((ObjectDefinition) property).getValue());						
					}
				}
				target.getElement().add(createTokenInfo(pIdx, className));
			} else {
				throw new IllegalArgumentException("Invalid token type: "+f.getClass());
			}
		}
	}
	
	/**
	 * Merges input pins.
	 * @param pin1
	 * @param pin2
	 */
	private void merge(Node cNode, Pin input2, java.util.Collection<Factory> tokens) {		
		if (!ConditionNode.class.getName().equals(cNode.getType())) {
			throw new IllegalArgumentException("Not a condition node: "+cNode);
		}
		for (Pin output: cNode.getPin()) {
			if (output.getName().startsWith(OUTPUT)) {
				for (Factory f: ((Collection) output.getPinConfig()).getElement()) {
					addTokens(tokens, (Collection) f);					
				}
				if (output.getOutput().size()!=1) {
					throw new IllegalStateException("Condition node output shall have one transition");
				}
				Transition transition = output.getOutput().get(0);
				Pin outputTarget = transition.getToPin();
				if (input2.getNode()==outputTarget.getNode()) {
					// Merge two input nodes, connect to 
					addTokens(tokens, (Collection) outputTarget.getPinConfig());
					input2.getNode().getPin().remove(input2);
				} else {
					addTokens(tokens, (Collection) input2.getPinConfig());
					transition.setToPin(input2);
					if (input2.getInput().size()!=1) {
						throw new IllegalStateException("Condition node input shall have one transition");
					}
					merge(input2.getNode(), outputTarget, tokens);
				}
				return;
			}
		}
		throw new IllegalStateException("Could not find output pin for condition node "+cNode);
	}
	

	protected Node createNode(Flow flow, Class<?> type) {
		Node ret = FlowFactory.eINSTANCE.createNode();
		flow.getFlowElement().add(ret);
		ret.setType(type.getName());
		return ret;
	}

	private Pin createInputPin(Node node, int idx, Class<?> parameterType) {
		Pin ret = FlowFactory.eINSTANCE.createPin();
		ret.setName(INPUT + pinCounter++);
		node.getPin().add(ret);
		
		Collection top = ConfigFactory.eINSTANCE.createCollection();
		ret.setPinConfig(top);
		
		top.getElement().add(createTokenInfo(idx, parameterType));
		
		return ret;
	}

	private ObjectDefinition createTokenInfo(int idx, Class<?> type) {
		return createTokenInfo(idx, type.getName());
	}
	
	private ObjectDefinition createTokenInfo(int idx, String typeName) {
		ObjectDefinition tokenInfo = ConfigFactory.eINSTANCE.createObjectDefinition();
		tokenInfo.setType(TokenInfo.class.getName());
		
		NamedObjectDefinition ct = ConfigFactory.eINSTANCE.createNamedObjectDefinition();
		ct.setType(Class.class.getName());
		ct.setValue(typeName);
		ct.setName(TYPE);
		tokenInfo.getProperty().add(ct);
		
		if (idx!=-1) {
			NamedObjectDefinition pIdx = ConfigFactory.eINSTANCE.createNamedObjectDefinition();
			pIdx.setType(Integer.class.getName());
			pIdx.setValue(String.valueOf(idx));
			pIdx.setName(PARAMETER_INDEX);
			tokenInfo.getProperty().add(pIdx);
		}
		
		return tokenInfo;
	}
	
	private Pin createOutputPin(Node node, int idx, Class<?> conclusionType) {
		Pin ret = FlowFactory.eINSTANCE.createPin();
		ret.setName(OUTPUT + pinCounter++);
		node.getPin().add(ret);
		
		Collection top = ConfigFactory.eINSTANCE.createCollection();
		ret.setPinConfig(top);
		
		Collection second = ConfigFactory.eINSTANCE.createCollection();
		top.getElement().add(second);
		
		second.getElement().add(createTokenInfo(idx, conclusionType));
		
		return ret;
	}
	
	private Pin createOutputPin(Node node, java.util.Collection<TokenInfo> tic) {
		Pin ret = FlowFactory.eINSTANCE.createPin();
		ret.setName(OUTPUT + pinCounter++);
		node.getPin().add(ret);
		
		Collection top = ConfigFactory.eINSTANCE.createCollection();
		ret.setPinConfig(top);
		
		Collection second = ConfigFactory.eINSTANCE.createCollection();
		top.getElement().add(second);
		
		for (TokenInfo ti: tic) {
			second.getElement().add(createTokenInfo(ti.getParameterIndex(), ti.getType()));
		}
		
		return ret;
	}
	
	private Transition connect(Pin from, Pin to, Class<?> transitionType, int priority, Flow flow) {
		if (from==null) {
			throw new NullPointerException("From pin is null");
		}
		if (to==null) {
			throw new NullPointerException("To pin is null");
		}
		Transition ret = FlowFactory.eINSTANCE.createTransition();
		flow.getFlowElement().add(ret);
		ret.setFromPin(from);
		ret.setToPin(to);
		ret.setType(transitionType.getName());
		
		ObjectDefinition priorityKeyFrom = ConfigFactory.eINSTANCE.createObjectDefinition();
		priorityKeyFrom.setType(Integer.class.getName());
		priorityKeyFrom.setValue(String.valueOf(priority));
		ret.setFromKey(priorityKeyFrom);

		// Not needed, for consistency
		ObjectDefinition priorityKeyTo = ConfigFactory.eINSTANCE.createObjectDefinition();
		priorityKeyTo.setType(Integer.class.getName());
		priorityKeyTo.setValue(String.valueOf(priority));
		ret.setToKey(priorityKeyTo);
		
		return ret;
	}

	private void setParameterTypes(PropertySource target, Class<?>[] pt) {
		NamedCollection parameterTypes = ConfigFactory.eINSTANCE.createNamedCollection();
		parameterTypes.setName("parameterTypes");
		target.getProperty().add(parameterTypes);
		for (Class<?> pType: pt) {
			ObjectDefinition ptDef = ConfigFactory.eINSTANCE.createObjectDefinition();
			ptDef.setValue(pType.getName());
			ptDef.setType(Class.class.getName());
			parameterTypes.getElement().add(ptDef);
		}
	}
	
	private void setParameterMap(PropertySource target, int[] pMap) {
		NamedCollection parameterMap = ConfigFactory.eINSTANCE.createNamedCollection();
		parameterMap.setName("parameterMap");
		target.getProperty().add(parameterMap);
		for (int pIdx: pMap) {
			ObjectDefinition ptDef = ConfigFactory.eINSTANCE.createObjectDefinition();
			ptDef.setValue(String.valueOf(pIdx));
			ptDef.setType(Integer.class.getName());
			parameterMap.getElement().add(ptDef);
		}
	}
		
	private static void addConditionArgument(PropertySource target, String argName, Class<?> argType, int parameterPosition) {
		NamedObjectDefinition prop = ConfigFactory.eINSTANCE.createNamedObjectDefinition();
		prop.setName("conditionArgument");
		prop.setType(ConditionArgument.class.getName());
		prop.setRuntime(true);
		target.getProperty().add(prop);
		
		ConfigUtil.addScalarProperty(prop, "type", argType, true);
		ConfigUtil.addScalarProperty(prop, "name", argName, true);
		ConfigUtil.addScalarProperty(prop, PARAMETER_INDEX, parameterPosition, true);
	}
	
	private static abstract class ConditionEntry {
		int position;		
		
		abstract java.util.Collection<Integer> getParamIndexes();
	}
	
	private static class AcceptConditionEntry extends ConditionEntry {
		Method acceptMethod;
		int[] pMap; // Array of the size of number of accept method parameters, each element contains index of target method parameter bound to accept method parameter.	
		
		AcceptConditionEntry(Method acceptMethod, int... pMap) {
			this.acceptMethod = acceptMethod;
			this.pMap = pMap;
		}
		
		@Override
		java.util.Collection<Integer> getParamIndexes() {
			ArrayList<Integer> ret = new ArrayList<Integer>();
			for (int i: pMap) {
				ret.add(i);
			}
			return ret; 
		}
	}
	
	private static class ExpressionConditionEntry extends ConditionEntry {
		Map<Integer, String> paramNames;
		String text;
		AST node;
//		private int[] paramMap;
//		boolean methodLevel;
				
		ExpressionConditionEntry(String text, Map<Integer, String> paramNames, AST node) {
			super();
			this.text = text;
			this.node = node;
			this.paramNames = paramNames;
		}	
		
		@Override
		public String toString() {
			return "{'"+text+"', "+position+"}";
		}
		
		@Override
		java.util.Collection<Integer> getParamIndexes() {
			return paramNames.keySet();
		}
		
	}
	
	// TODO - Break conditions along && and || ???
	static ExpressionConditionEntry analyzeCondition(String condition, int params, boolean methodLevel) throws RecognitionException, TokenStreamException {
		Map<Integer, String> paramNames = new HashMap<Integer, String>();
		int colonIdx = condition.indexOf(":");
		int questionIdx = condition.indexOf("?");
		if (colonIdx==-1 || (questionIdx!=-1 && colonIdx>questionIdx)) {
			// No parameter spec, use argX
			for (int i=0; i<params; ++i) {
				paramNames.put(i, methodLevel ? "arg"+i : "arg");
			}
		} else {
			String[] pNames = condition.substring(0, colonIdx).split(",");
			for (int i=0; i<pNames.length; ++i) {
				String pName = pNames[i].trim();
				if (pName.length()>0) {
					paramNames.put(i, pName);
				}
			}
			condition = condition.substring(colonIdx+1);
		}
		
		JavaLexer lexer = new JavaLexer(new StringReader("{ result = "+condition+"; }"));
		JavaRecognizer parser = new JavaRecognizer(lexer);
		parser.compoundStatement();
		AST ast = parser.getAST();
		Set<Integer> ids = new HashSet<Integer>();
		AST expressionNode = ast.getFirstChild().getFirstChild().getFirstChild().getNextSibling();
		extractIds(expressionNode, ids, paramNames);
		paramNames.keySet().retainAll(ids); // Leave only used id's
		return new ExpressionConditionEntry(condition, paramNames, expressionNode);
	}
		
	private static void extractIds(AST ast, Set<Integer> ids, Map<Integer, String> paramNames) {
		switch (ast.getType()) {
		case JavaTokenTypes.IDENT:
			// Top-level ident - can be argument name.
			for (Map.Entry<Integer, String> e: paramNames.entrySet()) {
				if (e.getValue().equals(ast.getText())) {
					ids.add(e.getKey());
				}
			}
			break;
		case JavaTokenTypes.METHOD_CALL:
			// Analyze name - if first element is argument name.
			extractIds(ast.getFirstChild(), ids, paramNames);			
			// Analyze method arguments
			extractIds(ast.getFirstChild().getNextSibling(), ids, paramNames);						
			break;
		case JavaTokenTypes.DOT:
			// DOT - analyze if the first child is argument.
			List<AST> astl = deDot(ast);
			extractIds(astl.get(0), ids, paramNames);
			break;
		default:
			// Analyze children
			for (AST child = ast.getFirstChild(); child!=null; child=child.getNextSibling()) {
				extractIds(child, ids, paramNames);
			}			
		}
	}
	
	private static List<AST> deDot(AST ast) {
		List<AST> ret = new ArrayList<AST>();
		if (ast.getType()==JavaTokenTypes.DOT) {
			ret.addAll(deDot(ast.getFirstChild()));
			ret.addAll(deDot(ast.getFirstChild().getNextSibling()));
		} else {
			ret.add(ast);
		}
		return ret;
	}
	
	private static void dump(AST ast, String indent) {
		System.out.println(indent+JavaRecognizer._tokenNames[ast.getType()]+" "+ast.getText());
		for (AST child = ast.getFirstChild(); child!=null; child=child.getNextSibling()) {
			dump(child, indent+"  ");
		}
	}
	
//	public static void main(String[] args) throws Exception {
//		ExpressionConditionEntry ce = analyzeCondition("child, , parent: !child.getSubject().equals(parent.getSubject(arg2)) && child.getObject().equals(parent.getObject())", 3, true);
//		dump(ce.node, "");
//	}
	
	/**
	 * @param args
	 */
	public static void main(String[] args) throws Exception {
		System.out.println("Usage: java [options] "+RuleSetToFlowCompiler.class.getName()+" <source file> <output file>");
		Resource resource = ResourceLoader.load(args[0]);
		System.out.println("Loaded " + args[0]);

		// Validate the contents of the loaded resource.
		//
		for (EObject eObject : resource.getContents()) {
			Diagnostic diagnostic = Diagnostician.INSTANCE.validate(eObject);
			if (diagnostic.getSeverity() != Diagnostic.OK) {
				printDiagnostic(diagnostic, "");
			}
		}
				
		RuleSetToFlowCompiler compiler = new RuleSetToFlowCompiler();
		File sourceFile = new File(args[0]);
		URL contextUrl = sourceFile.isFile() ? sourceFile.toURI().toURL() : new URL(args[0]);
		FactoryConfig factoryConfig = new FactoryConfig();
		factoryConfig.setContextUrl(contextUrl);
		Flow flow = compiler.compile((RuleSet) resource.getContents().get(0), factoryConfig);
		Resource flowResource = resource.getResourceSet().createResource(URI.createFileURI(new File(args[1]).getAbsolutePath()));
		flowResource.getContents().add(flow);
//		flowResource.save(System.out, null);
		flowResource.save(null);
	}

	protected static void printDiagnostic(Diagnostic diagnostic, String indent) {
		System.out.print(indent);
		System.out.println(diagnostic.getMessage());
		for (Diagnostic child : diagnostic.getChildren()) {
			printDiagnostic(child, indent + "  ");
		}
	}

}
