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.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.hammurapi.config.bootstrap.ConfigurationException;
import com.hammurapi.flow.runtime.Invocable;
import com.hammurapi.flow.runtime.Invoker;
import com.hammurapi.flow.runtime.ProcessingPathElement;
import com.hammurapi.flow.runtime.PropertySet;
import com.hammurapi.flow.runtime.impl.Joiner;
import com.hammurapi.flow.runtime.impl.ThreadInvocation;
import com.hammurapi.flow.runtime.impl.Joiner.InputConsumer;
import com.hammurapi.util.Context;

/**
 * Base class for reasoning nodes
 * @author Pavel Vlasov
 *
 * @param <F> Fact type.
 */
public abstract class ReasoningNodeBase<F> implements com.hammurapi.flow.runtime.Node<List<?>, Integer, KnowledgeBase<F>, InferenceToken<F>> {
	protected final Logger logger = Logger.getLogger(getClass().getName());
	protected final boolean isFine = logger.isLoggable(Level.FINE);

	private int ruleIndex;
	protected Object rule;
	protected KnowledgeBase<F> knowledgeBase;
	
	protected Method method;
	
	protected Class<?>[] parameterTypes;

	public void setParameterTypes(List<Class<?>> parameterTypes) {
		this.parameterTypes = parameterTypes.toArray(new Class[parameterTypes.size()]);
	}
		
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("["+getClass().getName()+"]");
		if (rule!=null) {
			sb.append(" rule="+rule);
		}
		if (method!=null) {
			sb.append(" method="+method);
		}
		return sb.toString();
	}
	
	private class JoinContext {
		PropertySet properties; 
		Context context;
		List<ProcessingPathElement> processingPath;
		
		JoinContext(PropertySet properties, Context context, List<ProcessingPathElement> processingPath) {
			super();
			this.properties = properties;
			this.context = context;
			this.processingPath = processingPath;
		}
		
	}
	
	private Joiner<List<InferenceToken<F>>, JoinContext, Collection<Future<?>>> joiner;
	
	public void setRuleIndex(int ruleIndex) {
		this.ruleIndex = ruleIndex;
	}

	public void setRules(List<?> rules) throws ConfigurationException {
		this.rule = rules.get(ruleIndex);
		if (methodName!=null) {
			try {
				method = rule.getClass().getMethod(methodName, parameterTypes);
			} catch (Exception e) {
				throw new ConfigurationException(e);
			}
		}
	}
	

	public void setKnowledgeBase(KnowledgeBase<F> knowledgeBase) {
		this.knowledgeBase = knowledgeBase;	
		for (InputCollectorRequest r: inputCollectorsRequests) {
			r.execute();
		}
		inputCollectorsRequests.clear();
	}
	
	private List<InputCollectorRequest> inputCollectorsRequests = new ArrayList<InputCollectorRequest>();
	private ArrayList<PinEntry<F>> inputs;
	
	protected ArrayList<PinEntry<F>> getInputPins() {
		return inputs;
	}
	
	private class InputCollectorRequest {
		private int idx;
		private Set<List<InferenceToken<F>>>[] inputCollectors;
		String pinName;
		
		InputCollectorRequest(int idx, String pinName, Set<List<InferenceToken<F>>>[] inputCollectors) {
			this.idx = idx;
			this.inputCollectors = inputCollectors;
			this.pinName = pinName;
		}
		
		@SuppressWarnings("unchecked")
		void execute() {
			inputCollectors[idx] = (Set<List<InferenceToken<F>>>) knowledgeBase.getSet(pinName, List.class); 			
		}
		
		
	}
	
	@Override
	public void beforeConnect() throws ConfigurationException {
		// NOP		
	}
		
	@SuppressWarnings("unchecked")
	@Override
	public void afterConnect() throws ConfigurationException {
		// Count input pins, if more than one - create joiner
		inputs = new ArrayList<PinEntry<F>>();
		for (PinEntry<F> pin: pins) {
			if (pin.getName().startsWith(Constants.INPUT)) {
				inputs.add(pin);
			} else if (pin.getName().startsWith(Constants.OUTPUT)) {
				if (pin.outputs!=null) {
					Collections.sort(pin.outputs);
				}
			}
		}
		
		if (inputs.isEmpty()) {
			throw new ConfigurationException("Inference node with no inputs");
		}
		
		sortInputs();
		
		if (inputs.size()>1) {			
			final Set<List<InferenceToken<F>>>[] inputCollectors = new Set[inputs.size()];
			
			Iterator<PinEntry<F>> it = inputs.iterator();
			for (int i=0; it.hasNext(); ++i) {
				PinEntry<F> pin = it.next();
				pin.index = i;
				inputCollectorsRequests.add(new InputCollectorRequest(i, pin.getName(), inputCollectors));
			}
			
			Class<?> cls =  List.class; 
			joiner = new Joiner<List<InferenceToken<F>>, JoinContext, Collection<Future<?>>>(inputCollectors, (Class<List<InferenceToken<F>>>) cls, false) {

				@Override
				protected boolean isValidInput(int index, List<InferenceToken<F>> input) {
					for (InferenceToken<F> t: input) {
						if (t.isConsumed() || ((HandleImpl<F>) t.getHandle()).get()==null) {
							return false;
						}
					}
					return true;
				}

				@Override
				protected Collection<Future<?>> join(List<InferenceToken<F>>[] inputs, JoinContext context, InputConsumer consumer, int activator) throws Exception {
					return ReasoningNodeBase.this.process(
							knowledgeBase,
							inputs,
							context.properties, 
							context.context, 
							context.processingPath, 
							consumer, 
							activator);
				}

				@Override
				protected boolean partialJoin(List<InferenceToken<F>>[] inputs,	int index) throws Exception {
					return ReasoningNodeBase.this.partialJoin(inputs, index);
				}
			};
		}
		
	}
	
	/**
	 * This implementation does nothing.
	 * Condition nodes can override this method to sort input pins to optimize inference by
	 * moving pins participating in condition to first places and evaluating conditions
	 * as soon as possible in partialJoin() method. 
	 * @param inputPins Input pins to sort.
	 */
	protected void sortInputs() {
		// NOP
	}

	/**
	 * Validates already joined inputs.
	 * @param inputs Inputs
	 * @param index Index of last already joined input.
	 * @return
	 */
	protected boolean partialJoin(List<InferenceToken<F>>[] inputs, int index) throws Exception {
		return true;
	}

	protected List<PinEntry<F>> pins = new ArrayList<PinEntry<F>>();

	@Override
	public void addPin(String pinName, List<?> pinConfig) {
		pins.add(new PinEntry<F>(pinName, pinConfig));		
	}

	@Override
	public Invocable<KnowledgeBase<F>, InferenceToken<F>> getInvocable(String pinName, Integer connectKey) throws ConfigurationException {
		for (PinEntry<F> pin: pins) {
			if (pinName.equals(pin.getName())) {
				if (pin.invocable==null) {
					final PinEntry<F> pe = pin;
					pin.invocable = new Invocable<KnowledgeBase<F>,InferenceToken<F>>() {

						@Override
						public Collection<Future<?>> invoke(
								KnowledgeBase<F> flowState,
								InferenceToken<F>[] args,
								PropertySet properties, 
								Context context,
								List<ProcessingPathElement> processingPath) throws Exception {
							return ReasoningNodeBase.this.invoke(flowState, args, pe, properties, context, processingPath);
						}
						
					};
				}
				return pin.invocable;
			}
		}

		throw new ConfigurationException("Unknown pin name: "+pinName);
	}
	
	/**
	 * Ivocations come here. Depending on whether joiner is null or not, do direct call to join or through
	 * joiner.
	 * @param pe
	 * @param properties
	 * @param context
	 * @param processingPath
	 * @param args
	 * @throws Exception 
	 */
	@SuppressWarnings("unchecked")
	protected Collection<Future<?>> invoke(
			KnowledgeBase<F> flowState,
			InferenceToken<F>[] args,
			PinEntry<F> pe, 
			PropertySet properties, 
			Context context,
			List<ProcessingPathElement> processingPath) throws Exception {
		
		ThreadInvocation.setThreadInvocation(flowState, args, properties, context, processingPath);
		try {
			List<InferenceToken<F>> argsList = new ArrayList<InferenceToken<F>>();
			for (InferenceToken<F> arg: args) {
				argsList.add(arg);
			}
			if (joiner==null) { // Direct call
				List<InferenceToken<F>>[] argsListArray = new ArrayList[1];
				argsListArray[0] = argsList;
				if (!partialJoin(argsListArray, 0)) {
					return Collections.emptyList();
				}
				return process(flowState, argsListArray, properties, context, processingPath, null, 0);
			} else { // Join
				if (isFine) {
					StringBuilder sb = new StringBuilder("New input at index "+pe.index+" for rule "+rule+", method "+method);
					for (InferenceToken<F> it: args) {
						sb.append(Constants.LINE_SEPARATOR+"\t"+knowledgeBase.get(it.getHandle()));
					}
					logger.fine(sb.toString());
				}
				Collection<Future<?>> ret = joiner.addInput(pe.index, argsList, new JoinContext(properties, context, processingPath));
				if (ret==null) {
					return Collections.emptyList();
				}
				return ret;
			}
		} finally {
			ThreadInvocation.unsetThreadInvocation();
		}
		
	}
	
	/**
	 * Processes inputs.
	 * @param properties Properties.
	 * @param context Context.
	 * @param processingPath Processing path.
	 * @param inputs Inputs.
	 * @param consumer Input consumer.
	 * @return Collection of future results.
	 * @throws Exception If anything goes wrong.
	 */
	protected abstract Collection<Future<?>> process(
			KnowledgeBase<F> flowState, 
			List<InferenceToken<F>>[] inputs, 
			PropertySet properties, 
			Context context,
			List<ProcessingPathElement> processingPath,
			InputConsumer consumer,
			int activator) throws Exception;

	@Override
	public Invoker<KnowledgeBase<F>, InferenceToken<F>> getInvoker(String pinName, Integer connectKey) throws ConfigurationException {
		for (PinEntry<F> pin: pins) {
			if (pinName.equals(pin.getName())) {
				Output<F> output = new Output<F>(pin, connectKey);
				pin.outputs.add(output);
				return output;
			}
		}

		throw new ConfigurationException("Unknown pin name: "+pinName);
	}

	protected String methodName;
	
	public void setMethodName(String methodName) {
		this.methodName = methodName;
	}
	
	private int priority;

	public int getPriority() {
		return priority;
	}

	public void setPriority(int priority) {
		this.priority = priority;
	}
	
	protected String ruleName;
	protected String ruleDescription;
	
	public void setRuleName(String ruleName) {
		this.ruleName = ruleName;
	}
	
	public void setRuleDescription(String ruleDescription) {
		this.ruleDescription = ruleDescription;
	}
		
}
