package com.hammurapi.common.concurrent;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import com.hammurapi.common.Context;
import com.hammurapi.convert.ConversionException;
import com.hammurapi.convert.Converter;

/**
 * Synapse which operates within JVM.
 * @author Pavel Vlasov
 *
 * @param <R>
 */
public class LocalSynapse<C, R, E extends Exception> implements Synapse<C, R, String, E> {
	
	// TODO - Option whether to chain property set or not, e.g. no chaining for service calls
	// TODO - Option to provide sub-set path so the invocable sees only the subset of invoker's property set.
	// TODO - One way, not to waste memory for collecting returns. Do it automatically if R is Void (if possible)
	
	private long delay;
	private long period;
	private TimeUnit timeUnit;
	private Invocable<C, R, String, E> invocable;
	private ClassLoader classLoader;
	private Class<?>[] parameterTypes;
	private boolean async;
	private boolean chainPropertySet;
	private String psPrefix;
	private Converter converter;
	private Context context;

//	/**
//	 * Creates synapse without delay and without repetitive execution.
//	 */
//	public LocalSynapse(boolean async, ClassLoader classLoader) {
//		this(async,classLoader,0,0,null,true,null);
//	}
	
	public LocalSynapse(
			boolean async, 
			ClassLoader classLoader, 
			Converter converter,
			Context context,
			long delay, 
			long period, 
			TimeUnit timeUnit,
			boolean chainPropertySet,
			String psPrefix) {
		this.async = async;
		this.classLoader = classLoader;
		if (this.classLoader==null) {
			this.classLoader = this.getClass().getClassLoader();
		}
		this.delay = delay;
		this.period = period;
		this.timeUnit = timeUnit;	
		this.chainPropertySet = chainPropertySet;
		this.psPrefix = psPrefix;
		this.converter = converter;
		this.context = context;
	}
	
	@Override
	public void setInvocable(Invocable<C, R, String, E> invocable) {
		this.invocable = invocable;
		this.parameterTypes = invocable.getParameterTypes();
	}
	
	@Override
	public Future<Iterable<R>> invoke(final C context, final Invocation<String> invocation, ExecutorService executorService) {
		// TODO - control invocation chain, substitute invocation and add invocation chain element.
		// Synchronous invocation
		if (async || executorService==null) {
			if (timeUnit!=null && (delay>0 || period>0)) {
				throw new InvocationException("Cannot schedule invocations");
			}
			
			final Collection<R> ret = new ArrayList<R>();
			try {
				boolean hasArgumentIterators = false;
				boolean needsConversion = false;
				int idx=0;
				for (Object arg: invocation.getArguments()) {
					if (arg instanceof ArgumentIterator) {
						hasArgumentIterators = true;
						break;
					}					
					if (arg!=null && !parameterTypes[idx].isInstance(arg)) {
						needsConversion = true;
					}
					++idx;
				}
				
				if (hasArgumentIterators) {
					Object[] args = Arrays.copyOf(invocation.getArguments(), invocation.getArguments().length);
					_invoke(context, invocation, executorService, args, 0, ret); 					
				} else {
					if (needsConversion) {
						final Object[] args = Arrays.copyOf(invocation.getArguments(), invocation.getArguments().length);
						for (int i=0; i<args.length; ++i) {
							args[i] = convert(args[i], i);
						}
						InvocationImpl<String> iv = new InvocationImpl<String>(invocation);
						iv.setPropertySet(preparePropertySet(executorService, invocation.getPropertySet()));
						ret.add(invocable.invoke(context, iv, executorService));
					} else {
						ret.add(invocable.invoke(context, invocation, executorService));
					}
				}
				
				return new Future<Iterable<R>>() {
	
					@Override
					public boolean cancel(boolean mayInterruptIfRunning) {
						return false;
					}
	
					@Override
					public boolean isCancelled() {
						return false;
					}
	
					@Override
					public boolean isDone() {
						return true;
					}
	
					@Override
					public Iterable<R> get() {
						return ret; 
					}
	
					@Override
					public Iterable<R> get(long timeout, TimeUnit unit) {
						return ret;  
					}				
				};
			} catch (final Exception e) {
				return new Future<Iterable<R>>() {
					
					@Override
					public boolean cancel(boolean mayInterruptIfRunning) {
						return false;
					}
	
					@Override
					public boolean isCancelled() {
						return false;
					}
	
					@Override
					public boolean isDone() {
						return true;
					}
	
					@Override
					public Iterable<R> get() throws ExecutionException {
						throw new ExecutionException(e); 
					}
	
					@Override
					public Iterable<R> get(long timeout, TimeUnit unit) throws ExecutionException {
						throw new ExecutionException(e); 
					}				
				};
				
			}
		}
		
		if (timeUnit==null || (delay<=0 && period<=0)) {
			// TODO - Asynchronous invocations.
			
			
		}
		
		if (!(executorService instanceof ScheduledExecutorService)) {
			throw new InvocationException("Cannot schedule invocations - executor service is not a ScheduledExecutorService");
		}
		
		// TODO Scheduled invocations.
		return null;
	}

	private PropertySet<String> preparePropertySet(ExecutorService executorService, PropertySet<String> propertySet) {
		if (propertySet==null) {
			if (chainPropertySet) {
				return new LocalStringPropertySet(executorService, converter, context, classLoader);
			}
			return null;
		}
		
		if (psPrefix!=null) {
			if (chainPropertySet) {
				return new LocalStringPropertySet(executorService, converter, context, classLoader, propertySet.subset(psPrefix));
			}
			return propertySet.subset(psPrefix);			
		}
		if (chainPropertySet) {
			return new LocalStringPropertySet(executorService, converter, context, classLoader, propertySet);
		}
		return propertySet;			
	}

	/**
	 * Iterates over argument iterators
	 * @param invocation
	 * @param executorService
	 * @param args
	 * @param i Current argument index
	 */
	private void _invoke(final C context, final Invocation<String> invocation, ExecutorService executorService, final Object[] args, int idx, Collection<R> collector) throws E {
		if (idx==args.length) {
			InvocationImpl<String> iv = new InvocationImpl<String>(invocation);
			iv.setArguments(args);
			iv.setPropertySet(preparePropertySet(executorService, invocation.getPropertySet()));
			
			collector.add(invocable.invoke(context, iv, executorService));
		} else {
			if (args[idx] instanceof ArgumentIterator) {
				ArgumentIterator<Object> ai = (ArgumentIterator<Object>) args[idx];
				while (ai.hasNext()) {
					args[idx] = convert(ai.next(), idx);
					_invoke(context, invocation, executorService, args, idx+1, collector);
				}
			} else {
				if (args[idx]!=null && !parameterTypes[idx].isInstance(args[idx])) {
					args[idx] = convert(args[idx], idx);
				}
				_invoke(context, invocation, executorService, args, idx+1, collector);
			}
		}
		
	}
	
	private Object convert(Object arg, int idx) {
		if (arg==null || parameterTypes[idx].isInstance(arg)) {
			return null;
		}
		Object ret = converter.convert(arg, parameterTypes[idx], context);
		if (ret==null) {
			throw new ConversionException("Cannot convert "+arg+" to "+parameterTypes[idx]);
		}
		return ret;
	}

	@Override
	public Class<Future<Iterable<R>>> getReturnType() {
		// Strange, but doesn't compile otherwise.
		return (Class<Future<Iterable<R>>>) (Class) Future.class;
	}

	@Override
	public Class<?>[] getParameterTypes() {
		return invocable.getParameterTypes();
	}		

}
