package com.hammurapi.reasoning.impl;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

import com.hammurapi.flow.runtime.ProcessingPathElement;
import com.hammurapi.flow.runtime.PropertySet;
import com.hammurapi.flow.runtime.impl.PropertySetMap;
import com.hammurapi.flow.runtime.impl.Joiner.InputConsumer;
import com.hammurapi.reasoning.ExceptionHandler;
import com.hammurapi.reasoning.Handle;
import com.hammurapi.reasoning.ReasoningException;
import com.hammurapi.reasoning.spi.InferenceContext;
import com.hammurapi.reasoning.impl.KnowledgeBase.PutDerivedResult;
import com.hammurapi.util.Context;

public class InferenceMethodNode<F> extends ReasoningNodeBase<F> {
	private static final Logger logger = Logger.getLogger(InferenceMethodNode.class.getName());

	private static AtomicInteger invocationCounter = new AtomicInteger();
	

	@SuppressWarnings("unchecked")
	@Override
	protected Collection<Future<?>> process(
			KnowledgeBase<F> flowState,
			List<InferenceToken<F>>[] inputs, 
			final PropertySet properties,
			Context context, List<ProcessingPathElement> processingPath,
			final InputConsumer consumer,
			final int activator) throws Exception {
			
		final Object[] args = new Object[parameterTypes.length];
		final HandleImpl<F>[] handles = new HandleImpl[args.length];
		final InferenceToken<F>[] inferenceTokens = new InferenceToken[args.length];
		final int[] argToInputMap = new int[args.length];
		int actualArgs = 0;
		for (int i=0; i<inputs.length; ++i) {
			PinEntry<F> pe = getInputPins().get(i);
			for (int j=0; j<inputs[i].size(); ++j, ++actualArgs) {
				TokenInfo ti = (TokenInfo) pe.config.get(j);
				InferenceToken<F> it = inputs[i].get(j);
				if (it.isConsumed()) {
					return Collections.emptyList();
				}
				HandleImpl<F> h = (HandleImpl<F>) it.getHandle();
				Object arg = h.get();
				if (arg==null) {
					return Collections.emptyList();					
				}
				args[ti.getParameterIndex()] = arg;
				handles[ti.getParameterIndex()] = h;
				inferenceTokens[ti.getParameterIndex()] = inputs[i].get(j);
				argToInputMap[ti.getParameterIndex()] = i;
			}
		}
		if (actualArgs!=args.length) {
			throw new IllegalArgumentException("Number of inputs ("+actualArgs+") doesn't match number of method arguments "+method);
		}
						
		final List<F> conclusions = new ArrayList<F>();
		
		InferenceContext inferenceContext = new InferenceContext() {
			
			private Map<String, ?> icProperties = new PropertySetMap(properties);

			@Override
			public void addUndo(Runnable undoCommand) {
				FutureTask<F> ft = new FutureTask<F>(undoCommand, null);
				for (HandleImpl<F> handle: handles) {
					handle.addUndo(ft);
				}				
			}

			@Override
			public boolean consumeInput(int position, boolean local) {				
				if (local) {
					if (consumer==null) {
						return false;
					}
					consumer.consume(argToInputMap[position]);
					return true;
				} 
								
				if (argToInputMap[position]==activator) {
					inferenceTokens[position].consume();
					return true;
				} 
				
				return false;					
			}

			@Override
			public boolean contains(Object fact) throws ReasoningException {
				return knowledgeBase.contains(fact);
			}

			@Override
			public Collection<?> getObjects() throws ReasoningException {
				return knowledgeBase.getObjects();
			}

			@Override
			public Map<String, ?> getProperties() throws ReasoningException {
				return icProperties;
			}

			@Override
			public int[] parameterIndices() {
				return null;
			}

			@Override
			public void post(Object... aConclusions) {
				for (Object conclusion: aConclusions) {
					if (conclusion==null) {
						throw new NullPointerException("Conclusion can not be null");
					}
					conclusions.add((F) conclusion);
					onConclusion(args, (F) conclusion);
				}
			}

			@Override
			public void remove(Object fact) throws ReasoningException {
				knowledgeBase.remove((F) fact, true);
			}

			@Override
			public Method targetMethod() {
				return null;
			}

			@Override
			public void put(Object fact) throws ReasoningException {
				knowledgeBase.put((F) fact);				
			}

			@Override
			public void update(Object fact) throws ReasoningException {
				update(fact, fact);
			}

			@Override
			public void update(Object originalFact, Object newFact)	throws ReasoningException {
				Handle<F> handle = knowledgeBase.getHandle(originalFact);
				if (handle==null) {
					throw new ReasoningException("Fact not found in the knowledge base, can't update");
				}
				
				knowledgeBase.updateObject(handle, (F) newFact);				
			}
			
		};
		
 		InferenceContextDispatcher.setThreadInferenceContext(inferenceContext);		
		try {
			F conclusion = (F) method.invoke(rule, args);
			if (conclusion!=null) {
				conclusions.add(conclusion);
				onConclusion(args, conclusion);
			}
			
		} catch (Exception e) {
			if (!handleException(args, e)) {
				throw e;
			}
			conclusions.clear();
		} finally {
			InferenceContextDispatcher.unsetThreadInferenceContext();
		}
		
		Collection<Future<?>> ret = new ArrayList<Future<?>>();

		StringBuilder sb = isFine ? new StringBuilder() : null;
		if (isFine) {
			sb.append("Invocation #"+invocationCounter.getAndIncrement() + ": "+method);
			sb.append("\n\tArguments");
			for (Object arg: args) {
				sb.append("\n\t\t"+arg);
			}
			sb.append("\n\tConclusions");
		}
		
		for (F conclusion: conclusions) {	
			List<Handle<F>> handleList = new ArrayList<Handle<F>>();
			for (List<InferenceToken<F>> itl: inputs) {
				for (InferenceToken<F> it: itl) {
					handleList.add(it.getHandle());						
				}
			}			
			
			PutDerivedResult<F> putDerivedResult = knowledgeBase.putConclusion(conclusion, rule, ruleName, ruleDescription, method, handleList);
			if (putDerivedResult.isNew()) {
				if (isFine) {
					logger.fine(conclusion+" - dispatching");
				}
				InferenceToken<F> inferenceToken = new InferenceToken<F>(putDerivedResult.getHandle());
				
				boolean dispatched = false; 
				for (PinEntry<F> pin: pins) {
					if (pin.getName().startsWith(Constants.OUTPUT)) {
						boolean matches = false;
						for (Object alt: pin.config) {
							List<TokenInfo> lti=(List<TokenInfo>) alt;
							if (lti.size()==1) {
								TokenInfo ti = lti.get(0);
								if (ti.getType().isInstance(conclusion)) {
									matches = true;
									break;
								}
							}
						}
						if (matches) {
							dispatched = true;
							for (Output<F> output: pin.outputs) {
								ret.addAll(output.invocable.invoke(flowState, new InferenceToken[] {inferenceToken}, properties, context, processingPath));
							}
						}
					}
				}
				if (!dispatched) {
					throw new IllegalArgumentException("Conclusion of type "+conclusion.getClass()+" was not dispatched to any output pins");
				}
			} else {
				if (isFine) {
					logger.fine(conclusion + " - rejected by the knowledge base");
				}
			}
		}
		
		if (isFine) {
			logger.fine(sb.toString());
		}
		
		return ret;
	}

	/**
	 * This method is invoked before conclusion is put to the knowledge base. 
	 * Rule classes can override this method to augment conclusions, e.g. inject some properties.
	 * @param conclusion
	 */
	protected void onConclusion(Object[] args, F conclusion) {		
		
	}	
	
	/**
	 * Handles method invocation exception. This implementation invokes ExceptionListener.onException()
	 * if exception listener is not null.  
	 * @param e Exception.
	 * @return True if exception was handled.
	 * @throws Exception 
	 */
	protected boolean handleException(Object[] args, Exception e) throws Exception {
		ExceptionHandler exceptionListener = knowledgeBase.getExceptionHandler();
		if (exceptionListener==null) {
			return false;
		}
		exceptionListener.handleException(e);
		return true;
	}
	
}
