package org.etsi.mts.tdl.execution.java.rt.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.etsi.mts.tdl.execution.java.tri.Argument;
import org.etsi.mts.tdl.execution.java.tri.Connection;
import org.etsi.mts.tdl.execution.java.tri.Data;
import org.etsi.mts.tdl.execution.java.tri.GateReference;
import org.etsi.mts.tdl.execution.java.tri.PredefinedFunctions;
import org.etsi.mts.tdl.execution.java.tri.Reporter;
import org.etsi.mts.tdl.execution.java.tri.SystemAdapter;
import org.etsi.mts.tdl.execution.java.tri.Validator;

import com.google.inject.Guice;
import com.google.inject.Injector;

public class TestControl {
	private Injector injector;

	public SystemAdapter systemAdapter;
	public Validator validator;
	public Reporter reporter;
	public PredefinedFunctions functions;

	private Connection[] connections;
	private Map<Connection, ReceiverHub> receiverHubs = Collections
			.synchronizedMap(new Hashtable<Connection, ReceiverHub>());

	protected ExecutorCompletionService<ExecutionResult> completionService;
	protected ExecutorService executor;

	private final long timeConstraintFrequency = 100;

	private List<ExceptionalBehaviour> exceptionalBehaviours = Collections
			.synchronizedList(new ArrayList<ExceptionalBehaviour>());

	protected Map<String, TypeImpl> types = new Hashtable<String, TypeImpl>();

	public TestControl(com.google.inject.Module guiceModule) {
		super();

		injector = Guice.createInjector(guiceModule);

		this.systemAdapter = injector.getInstance(SystemAdapter.class);
		this.validator = injector.getInstance(Validator.class);
		this.reporter = injector.getInstance(Reporter.class);
		this.functions = injector.getInstance(PredefinedFunctions.class);

		this.executor = Executors.newCachedThreadPool();
		this.completionService = new ExecutorCompletionService<ExecutionResult>(executor);
	}

	protected <A> A getInstance(Class<A> clazz) {
		return this.injector.getInstance(clazz);
	}

	private ReceiverHub getReceiver(Connection connection) {
		ReceiverHub hub = this.receiverHubs.get(connection);
		if (hub == null)
			throw new RuntimeException("Connection not configured: " + connection);
		return hub;
	}

	public void configure(Connection[] connections) {
		this.systemAdapter.configure(connections);
		this.connections = connections;

		this.receiverHubs.values().forEach(r -> r.stop());
		this.receiverHubs.clear();
		for (Connection c : connections) {
			ReceiverHub hub = new ReceiverHub(this.systemAdapter, c);
			receiverHubs.put(c, hub);

			ExceptionalBehaviour anyReceiver = new ExceptionalBehaviour(false);
			anyReceiver.behaviour = () -> {
				throw new StopExceptionImpl("Unexpected message received");
			};
			anyReceiver.callable = new ExecutionCallable() {
				@Override
				public ExecutionResult call() throws Exception {
					Data data = hub.receive(null, false);
					return data != null ? new InteractionResult(data) : null;
				}

			};
			hub.setAnyReceiver(anyReceiver);
		}

		declareTypes();
	}

	public Connection getConnection(String testerComponentName, String testerGateName, String remoteComponentName,
			String remoteGateName) {
		for (Connection c : connections) {
			int testerEP = -1;
			for (int i = 0; i <= 1; i++)
				if (c.getEndPoints()[i].getComponent().getName().equals(testerComponentName)
						&& c.getEndPoints()[i].getGate().getName().equals(testerGateName)) {
					testerEP = i;
					break;
				}
			if (testerEP == -1)
				continue;
			int targetEP = testerEP == 0 ? 1 : 0;
			if (c.getEndPoints()[targetEP].getComponent().getName().equals(remoteComponentName)
					&& c.getEndPoints()[targetEP].getGate().getName().equals(remoteGateName))
				return c;
		}
		throw new RuntimeException(String.format("Unknown connection: from %s.%s to %s.%s", testerComponentName,
				testerGateName, remoteComponentName, remoteGateName));
	}

	public GateReference getGateReference(String testerComponentName, String testerGateName) {
		for (Connection c : connections) {
			for (int i = 0; i <= 1; i++)
				if (c.getEndPoints()[i].getComponent().getName().equals(testerComponentName)
						&& c.getEndPoints()[i].getGate().getName().equals(testerGateName)) {
					return c.getEndPoints()[i];
				}
		}
		throw new RuntimeException(String.format("Unknown gate: %s.%s", testerComponentName, testerGateName));
	}

	public interface ExecutionResult {
	}

	public class TimeoutResult implements ExecutionResult {
	}

	public class InteractionResult implements ExecutionResult {
		public Data data;
		public String body;

		public InteractionResult(Data data) {
			this.data = data;
		}

		public InteractionResult(String body) {
			this.body = body;
		}

	}

	public abstract class ExecutionCallable implements Callable<ExecutionResult> {
		public Future<ExecutionResult> execute() {
			return completionService.submit(this);
		}
	}

	public abstract class ExecutionCallableWithDefaults extends ExecutionCallable {
		@Override
		public Future<ExecutionResult> execute() {
			Future<ExecutionResult> future = super.execute();
			return future;
		}
	}

	public List<Future<ExecutionResult>> executeExceptionals() {
		List<Future<ExecutionResult>> futures = new ArrayList<>();
		exceptionalBehaviours.forEach(exc -> futures.add(exc.execute()));
		receiverHubs.values().forEach(hub -> futures.add(hub.getAnyReceiver().execute()));
		return futures;
	}

	public ExecutionCallable timeConstraint(Constraint constraint) {
		ExecutionCallable c = new ExecutionCallable() {
			@Override
			public ExecutionResult call() throws Exception {
				while (!constraint.evaluate())
					try {
						Thread.sleep(timeConstraintFrequency);
					} catch (InterruptedException e) {
						return constraint.evaluate() ? new TimeoutResult() : null;
					}
				return new TimeoutResult();
			}
		};
		return c;
	}

	public ExecutionCallable timeout(Timer timer) {
		ExecutionCallable c = new ExecutionCallable() {
			@Override
			public ExecutionResult call() throws Exception {
				TimeLabel startLabel = timer.getStartLabel();
				try {
					long period = startLabel.currentTime() - startLabel.previous() + timer.getPeriod();
					Thread.sleep(timer.getUnit().toMillis(period));
				} catch (InterruptedException e) {
					return null;
				}
				return new TimeoutResult();
			}
		};
		return c;
	}

	public ExecutionCallable sleep(long period) {
		ExecutionCallable c = new ExecutionCallable() {
			@Override
			public ExecutionResult call() throws Exception {
				;
				try {
					Thread.sleep(period);
				} catch (InterruptedException e) {
					return null;
				}
				return new TimeoutResult();
			}
		};
		return c;
	}

	public ExecutionCallable noInput(long period, GateReference gate) {
		// XXX noInput
		return sleep(period);
	}

	public ExecutionCallable receive(Data expected, Connection connection) {
		return receive(expected, connection, false);
	}

	public ExecutionCallable trigger(Data expected, Connection connection) {
		return receive(expected, connection, true);
	}

	public ExecutionCallable receive(Data expected, Connection connection, boolean isTrigger) {
		ExecutionCallableWithDefaults c = new ExecutionCallableWithDefaults() {
			@Override
			public ExecutionResult call() throws Exception {
				Data data = getReceiver(connection).receive(expected, isTrigger);
				return data != null ? new InteractionResult(data) : null;
			}
		};
		return c;
	}

	public ExecutionCallable call(Object operation, Argument[] arguments, Data expectedReturn, Data expectedException,
			Connection connection) {
		ExecutionCallableWithDefaults c = new ExecutionCallableWithDefaults() {
			@Override
			public ExecutionResult call() throws Exception {
				Data result = getReceiver(connection).call(operation, arguments, expectedReturn, expectedException);
				if (Thread.interrupted())
					return null;
				return new InteractionResult(result);
			}
		};
		return c;
	}

	public ExecutionCallable call(MethodCall call) {
		ExecutionCallableWithDefaults c = new ExecutionCallableWithDefaults() {
			@Override
			public ExecutionResult call() throws Exception {
				Object result;
				try {
					result = call.call();
				} catch (ExceptionResult er) {
					return new InteractionResult(new PojoData(er.getData()));
				}
				if (Thread.interrupted())
					return null;
				return new InteractionResult(new PojoData(result));
			}
		};
		return c;
	}

	public void addExceptionalBehaviour(ExceptionalBehaviour b) {
		// TODO Inner exceptional behaviours have precedence
		synchronized (exceptionalBehaviours) {
			if (!this.exceptionalBehaviours.contains(b))
				this.exceptionalBehaviours.add(b);
		}
	}

	public void removeExceptionalBehaviour(ExceptionalBehaviour b) {
		this.exceptionalBehaviours.remove(b);
		b.purgeFutures().forEach(f -> stop(f));
	}

	public ExceptionalBehaviour getExceptionalBehaviour(Future<ExecutionResult> future) {
		synchronized (this.exceptionalBehaviours) {
			for (ExceptionalBehaviour b : this.exceptionalBehaviours) {
				if (b.isFuture(future))
					return b;
			}
		}
		return null;
	}

	public Future<ExecutionResult> next() {
		try {
			Future<ExecutionResult> future;
			do {
				future = this.completionService.take();
			} while (future.isCancelled());
			return future;
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}

	public void stop(Future<ExecutionResult> future) {
		// XXX stop and remove?
		future.cancel(true);
	}

	// XXX
	void test() {
		Callable<ExecutionResult> c1 = new Callable<ExecutionResult>() {
			@Override
			public ExecutionResult call() throws Exception {
				// TODO Auto-generated method stub
				return null;
			}
		};
		Callable<ExecutionResult> c2 = new Callable<ExecutionResult>() {
			@Override
			public ExecutionResult call() throws Exception {
				// TODO Auto-generated method stub
				return null;
			}
		};

		Future<ExecutionResult> f1 = this.completionService.submit(c1);
		Future<ExecutionResult> f2 = this.completionService.submit(c2);

		try {
			Future<ExecutionResult> f;
			try {
				f = this.completionService.take();
			} catch (InterruptedException e) {
				// TODO wait was interrupted
				e.printStackTrace();
				return;
			}

			try {
				ExecutionResult r = f.get();
				// XXX
			} catch (CancellationException e) {
				// TODO callable was canceled
				e.printStackTrace();

			} catch (InterruptedException e) {
				// Won't happen
			} catch (ExecutionException e) {
				// TODO exception from callable
				e.printStackTrace();
			}

		} finally {
			f1.cancel(true);
			f2.cancel(true);
		}
	}

	/**
	 * This is called once before each test case. Implemented by generated code to
	 * declare types and mappings if using unmapped data in TRI.
	 */
	protected void declareTypes() {
	}

	protected void addType(String name, TypeImpl type) {
		this.types.put(name, type);
	}

	protected TypeImpl getType(String name) {
		return this.types.get(name);
	}
}
