package org.etsi.mts.tdl.execution.java.codegen;

import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;
import java.util.stream.Collectors;

import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.EObject;
import org.etsi.mts.tdl.Action;
import org.etsi.mts.tdl.ActionBehaviour;
import org.etsi.mts.tdl.ActionReference;
import org.etsi.mts.tdl.AlternativeBehaviour;
import org.etsi.mts.tdl.Assertion;
import org.etsi.mts.tdl.Assignment;
import org.etsi.mts.tdl.AtomicBehaviour;
import org.etsi.mts.tdl.Behaviour;
import org.etsi.mts.tdl.Block;
import org.etsi.mts.tdl.BoundedLoopBehaviour;
import org.etsi.mts.tdl.Break;
import org.etsi.mts.tdl.CollectionDataType;
import org.etsi.mts.tdl.CombinedBehaviour;
import org.etsi.mts.tdl.ComponentInstance;
import org.etsi.mts.tdl.ComponentInstanceRole;
import org.etsi.mts.tdl.ComponentType;
import org.etsi.mts.tdl.CompoundBehaviour;
import org.etsi.mts.tdl.ConditionalBehaviour;
import org.etsi.mts.tdl.DataElementMapping;
import org.etsi.mts.tdl.DataElementUse;
import org.etsi.mts.tdl.DataInstance;
import org.etsi.mts.tdl.DataInstanceUse;
import org.etsi.mts.tdl.DataResourceMapping;
import org.etsi.mts.tdl.DataType;
import org.etsi.mts.tdl.DataUse;
import org.etsi.mts.tdl.Element;
import org.etsi.mts.tdl.ElementImport;
import org.etsi.mts.tdl.ExceptionalBehaviour;
import org.etsi.mts.tdl.FormalParameter;
import org.etsi.mts.tdl.Function;
import org.etsi.mts.tdl.FunctionCall;
import org.etsi.mts.tdl.GateReference;
import org.etsi.mts.tdl.InlineAction;
import org.etsi.mts.tdl.Interaction;
import org.etsi.mts.tdl.InterruptBehaviour;
import org.etsi.mts.tdl.LiteralValueUse;
import org.etsi.mts.tdl.LocalExpression;
import org.etsi.mts.tdl.MappableDataElement;
import org.etsi.mts.tdl.Member;
import org.etsi.mts.tdl.MemberAssignment;
import org.etsi.mts.tdl.MemberReference;
import org.etsi.mts.tdl.Message;
import org.etsi.mts.tdl.NamedElement;
import org.etsi.mts.tdl.OptionalBehaviour;
import org.etsi.mts.tdl.Package;
import org.etsi.mts.tdl.PackageableElement;
import org.etsi.mts.tdl.ParallelBehaviour;
import org.etsi.mts.tdl.Parameter;
import org.etsi.mts.tdl.ParameterBinding;
import org.etsi.mts.tdl.ParameterKind;
import org.etsi.mts.tdl.ParameterMapping;
import org.etsi.mts.tdl.PeriodicBehaviour;
import org.etsi.mts.tdl.PredefinedFunction;
import org.etsi.mts.tdl.PredefinedFunctionCall;
import org.etsi.mts.tdl.ProcedureCall;
import org.etsi.mts.tdl.ProcedureParameter;
import org.etsi.mts.tdl.ProcedureSignature;
import org.etsi.mts.tdl.Quiescence;
import org.etsi.mts.tdl.SingleCombinedBehaviour;
import org.etsi.mts.tdl.SpecialValueUse;
import org.etsi.mts.tdl.Stop;
import org.etsi.mts.tdl.StructuredDataInstance;
import org.etsi.mts.tdl.Target;
import org.etsi.mts.tdl.TestConfiguration;
import org.etsi.mts.tdl.TestDescription;
import org.etsi.mts.tdl.TestDescriptionReference;
import org.etsi.mts.tdl.TestObjective;
import org.etsi.mts.tdl.Time;
import org.etsi.mts.tdl.TimeConstraint;
import org.etsi.mts.tdl.TimeLabel;
import org.etsi.mts.tdl.TimeLabelUse;
import org.etsi.mts.tdl.TimeOperation;
import org.etsi.mts.tdl.TimeOut;
import org.etsi.mts.tdl.Timer;
import org.etsi.mts.tdl.TimerOperation;
import org.etsi.mts.tdl.TimerStart;
import org.etsi.mts.tdl.TimerStop;
import org.etsi.mts.tdl.UnboundedLoopBehaviour;
import org.etsi.mts.tdl.ValueAssignment;
import org.etsi.mts.tdl.Variable;
import org.etsi.mts.tdl.VariableUse;
import org.etsi.mts.tdl.VerdictAssignment;
import org.etsi.mts.tdl.Wait;

public class JUnitTestGenerator extends Renderer {

	public static final String PACKAGE_PREFIX = "org.etsi.mts.tdl.execution.java",
			CORE_PACKAGE = PACKAGE_PREFIX + ".rt.core", TRI_PACKAGE = PACKAGE_PREFIX + ".tri",
			TESTER_CLASS = "TestControl", FUNCTIONS_FIELD = "functions", SYSTEM_ADAPTER_FIELD = "systemAdapter",
			VALIDATOR_FIELD = "validator", REPORTER_FIELD = "reporter", HELPER_FIELD = "runtimeHelper";

	public static final String COMPONENT_CLASS_SUFFIX = "_Component",
			ASSERTION_EXCEPTION = "junit.framework.AssertionFailedError",
			FUTURE_EXECUTION_EXCEPTION = "java.util.concurrent.ExecutionException",
			INTERRUPTED_EXCEPTION = "InterruptedException", BREAK_EXCEPTION = "BreakException",
			STOP_EXCEPTION = "StopException";

	public static final String LANGUAGE_KEY = "Language", JAVA_LANGUAGE_VALUE = "Java", MAPPING_KEY = "MappingName",
			JAVA_MAPPING_VALUE = "Java", MAPPING_ANNOTATION_PREFIX = "Java",
			MAPPING_ANNOTATION_PACKAGE = MAPPING_ANNOTATION_PREFIX + "Package",
			MAPPING_ANNOTATION_METHOD = MAPPING_ANNOTATION_PREFIX + "Method",
			MAPPING_ANNOTATION_STATIC_METHOD = MAPPING_ANNOTATION_PREFIX + "StaticMethod",
			MAPPING_ANNOTATION_CLASS = MAPPING_ANNOTATION_PREFIX + "Class",
			MAPPING_ANNOTATION_FIELD = MAPPING_ANNOTATION_PREFIX + "Field",
			MAPPING_ANNOTATION_STATIC_FIELD = MAPPING_ANNOTATION_PREFIX + "StaticField",
			MAPPING_ANNOTATION_GETTER = MAPPING_ANNOTATION_PREFIX + "Getter",
			MAPPING_ANNOTATION_SETTER = MAPPING_ANNOTATION_PREFIX + "Setter";

	private Package model;

	private Settings settings;
	private String guiceModule;

	private boolean logEvents = true;

	protected Map<MappableDataElement, Set<DataElementMapping>> elementMappings = new Hashtable<MappableDataElement, Set<DataElementMapping>>();

	protected Map<Element, String> classNames = new Hashtable<Element, String>();

	private ComponentInstance currentTester = null;

	public JUnitTestGenerator(Package model, Settings settings) {
		super(settings.outputFile, settings.outPackage);
		this.model = model;
		this.settings = settings;
		this.guiceModule = settings.injector;
		if (this.guiceModule == "")
			throw new RuntimeException("Guice module must be configured in settings.");
	}

	public void generate() throws IOException {
		render();
	}

	public void render() throws IOException {

		resolveMappings(this.model);

		List<TestDescription> tdList = new ArrayList<TestDescription>();
		gatherPackageableElements(model, false, e -> {
			if (e instanceof TestDescription)
				if (((TestDescription)e).isIsLocallyOrdered())
					tdList.add((TestDescription) e);
		});

		Set<TestConfiguration> configurations = tdList.stream().map(td -> td.getTestConfiguration())
				.collect(Collectors.toSet());
		for (TestConfiguration tc : configurations) {
			renderTestConfiguration(tc);
		}
		for (TestDescription td : tdList) {
			renderTestCase(td);
		}

	}

	protected void resolveMappings(Package model) {
		gatherPackageableElements(model, true, e -> {
			if (e instanceof DataElementMapping) {
				MappableDataElement el = ((DataElementMapping) e).getMappableDataElement();
				Set<DataElementMapping> mappings = this.elementMappings.get(el);
				if (mappings == null)
					this.elementMappings.put(el, mappings = new HashSet<DataElementMapping>());
				mappings.add((DataElementMapping) e);
			}
		});
	}

	protected DataElementMapping getMapping(MappableDataElement e) {
		Set<DataElementMapping> mappings = this.elementMappings.get(e);
		if (mappings != null && !mappings.isEmpty()) {
			for (DataElementMapping m : mappings) {
				if (m.getDataResourceMapping().getAnnotation().stream().anyMatch(
						a -> a.getKey().getName().equals(MAPPING_KEY) && a.getValue().equals(JAVA_MAPPING_VALUE)))
					return m;
			}
		}
		return null;
	}

	protected DataElementMapping getMappingChecked(MappableDataElement e) {
		DataElementMapping m = getMapping(e);
		if (m != null)
			return m;
		throw new RuntimeException("Mapping missing for " + e.getQualifiedName());
	}

	private String getParameterMappingUri(Parameter p) {
		ParameterMapping pm = getParameterMapping(p);
		if (pm != null)
			return pm.getParameterURI();
		return null;
	}

	private ParameterMapping getParameterMapping(Parameter p) {
		DataElementMapping typeMapping = getMapping((MappableDataElement) p.container());
		if (typeMapping != null) {
			for (ParameterMapping pm : typeMapping.getParameterMapping()) {
				if (pm.getParameter().equals(p)) {
					return pm;
				}
			}
		}
		return null;
	}

	private boolean hasAnnotation(Element e, String keyName) {
		return e.getAnnotation().stream().anyMatch(a -> a.getKey().getName().equals(keyName));
	}

	protected void addImport(DataElementMapping m, Set<String> imports) {
		if (m == null)
			return;
		MappableDataElement e = m.getMappableDataElement();
		if (e instanceof DataType) {
			DataResourceMapping resource = m.getDataResourceMapping();
			boolean resourceIsClass = hasAnnotation(resource, MAPPING_ANNOTATION_CLASS);
			if (resourceIsClass) {
				imports.add(resource.getResourceURI());
				return;
			}
			boolean isPackage = hasAnnotation(resource, MAPPING_ANNOTATION_PACKAGE);
			if (!isPackage)
				throw new RuntimeException("Unsupported resource mapping: " + resource.getQualifiedName());
			boolean isClass = hasAnnotation(m, MAPPING_ANNOTATION_CLASS);
			if (isClass) {
				String classQName = m.getElementURI();
				if (classQName.startsWith("java."))
					imports.add(classQName);
				else
					imports.add(resource.getResourceURI() + "." + classQName);
			}
		}
	}

	private String getPackagefullName(Package p) {
		String packageName = getElementName(p);
		while ((p = (Package) p.container()) != null)
			packageName = getElementName(p) + "." + packageName;
		packageName = packageName.toLowerCase();
		if (this.getRootPackageName() != null && this.getRootPackageName().length() > 0)
			packageName = this.getRootPackageName() + "." + packageName;
		return packageName;
	}

	private void renderTestConfiguration(TestConfiguration tc) throws IOException {
		Package p = (Package) tc.container();
		String packageName = getPackagefullName(p);

		Set<ComponentType> testerTypes = tc.getComponentInstance().stream()
				.filter(c -> c.getRole() == ComponentInstanceRole.TESTER).map(c -> c.getType())
				.collect(Collectors.toSet());
		final String fPackageName = packageName;
		for (ComponentType ct : testerTypes) {
			String className = getElementName(ct) + COMPONENT_CLASS_SUFFIX;
			writeClassFile(packageName, className, () -> renderComponentTypeClass(fPackageName, tc, ct));
		}
	}

	private void renderComponentTypeClass(String packageName, TestConfiguration tc, ComponentType ct) {

		String className = getElementName(ct) + COMPONENT_CLASS_SUFFIX;
		classNames.put(ct, packageName + "." + className);

		line("package " + packageName + ";");
		newLine();

		Set<String> imports = new HashSet<String>();
		gatherImports(imports);
		gatherImports(ct, imports);
		writeImports(imports);

		append("public class " + className + " extends " + TESTER_CLASS);
		blockOpen();
		newLine();

		writeComponentType(tc, ct, className);

		blockClose();
	}

	private void writeComponentType(TestConfiguration tc, ComponentType ct, String className) {

		ct.getVariable().forEach(v -> {
			line("public " + getElementName(v.getDataType()) + " " + getElementName(v) + ";");
		});
		newLine();

		ct.getTimer().forEach(t -> {
			String timerClass = CORE_PACKAGE + ".Timer ";
			line("public " + timerClass + getTimerName(t) + " = new " + timerClass + "(TimeUnit.Second"
					+ ", \"" + t.getName() + "\", \"" + t.getQualifiedName() + "\");");
		});
		newLine();

		// XXX Gates

		append("protected " + className + "()");
		blockOpen();
		append("super(");
		append("new ");
		append(settings.injector);
		append("()");
		line(");");

		blockClose();
	}

	private String getTimerName(Timer t) {
		String timerNameSuffix = "_t";
		return getElementName(t) + timerNameSuffix;
	}

	private String getTimeLabelName(TimeLabel label) {
		AtomicBehaviour b = (AtomicBehaviour) label.container();
		String labelNameSuffix = "_tl";
		return getElementName(b) + labelNameSuffix;
	}

	private void renderTestCase(TestDescription td) throws IOException {

		Package p = (Package) td.container();
		String packageName = getPackagefullName(p);

		final String fPackageName = packageName;
		final List<IOException> ex = new ArrayList<IOException>();
		List<ComponentInstance> testers = td.getTestConfiguration().getComponentInstance().stream()
				.filter(c -> c.getRole() == ComponentInstanceRole.TESTER).collect(Collectors.toList());
		testers.forEach(tester -> {
			String className = getElementName(td);
			if (testers.size() > 1)
				className += "_" + getElementName(tester);
			try {
				this.currentTester = tester;
				String fClassName = className;
				writeClassFile(packageName, className,
						() -> renderTestDescriptionClass(fPackageName, fClassName, td, tester));
			} catch (IOException e) {
				ex.add(e);
				return;
			}
		});
		if (!ex.isEmpty())
			throw ex.get(0);

	}

	private void renderTestDescriptionClass(String packageName, String className, TestDescription td,
			ComponentInstance tester) {

		line("package " + packageName + ";");
		newLine();

		Set<String> imports = new HashSet<String>();
		gatherImports(imports);
		gatherImports(td, imports);
		writeImports(imports);

		String componentClassName = classNames.get(tester.getType());

		line("@TestInstance(TestInstance.Lifecycle.PER_CLASS)");
		append("public class " + className + " extends " + componentClassName);
		blockOpen();
		newLine();

		// Declare TimeLabels
		td.eAllContents().forEachRemaining(e -> {
			if (e instanceof TimeLabel) {
				String name = getTimeLabelName((TimeLabel) e);
				line("private TimeLabel " + name + " = new TimeLabel();");
			}
		});
		newLine();

		writeTestConfiguration(td);
		newLine();
		writeTestDescription(td);

		blockClose();

	}

	@FunctionalInterface
	public interface PackageableElementAcceptor {
		void accept(PackageableElement e);
	}

	protected void gatherPackageableElements(Package p, boolean includeImports, PackageableElementAcceptor a) {
		gatherPackageableElements(p, includeImports, a, new HashSet<Package>());
	}

	private void gatherPackageableElements(Package p, boolean includeImports, PackageableElementAcceptor a,
			Set<Package> scanned) {
		if (scanned.contains(p))
			return;
		scanned.add(p);
		p.getPackagedElement().forEach(a::accept);
		if (includeImports)
			p.getImport().forEach(i -> {
				List<PackageableElement> ie = i.getImportedElement();
				if (ie.isEmpty())
					gatherPackageableElements(((ElementImport) i).getImportedPackage(), includeImports, a, scanned);
				else
					ie.forEach(a::accept);
			});
		p.getNestedPackage().forEach(np -> gatherPackageableElements(np, includeImports, a, scanned));
	}

	protected void gatherImports(Set<String> imports) {
//		imports.add("org.junit.*");
		imports.add("org.junit.jupiter.api.*");
//		imports.add("org.junit.Test");
//		imports.add("org.junit.Assert");

		imports.add("com.google.inject.*");
		imports.add("javax.inject.*");

		imports.add("java.util.concurrent.Future");
		imports.add("java.util.List");

		imports.add(TRI_PACKAGE + ".*");
		imports.add(CORE_PACKAGE + ".*");
	}

	protected void gatherImports(Element el, Set<String> imports) {
		this.elementMappings.keySet().forEach(e -> addImport(getMapping(e), imports));
	}

	private void writeTestConfiguration(TestDescription tc) {
		TestConfiguration config = tc.getTestConfiguration();

		line("@BeforeAll");
		append("public void configure_" + getElementName(tc) + "()");
		blockOpen();

		final List<String> connectionNames = new ArrayList<String>();
		config.getConnection().forEach(conn -> {
			String connectionName = getElementName(conn);
			connectionNames.add(connectionName);

			List<GateReference> endpoints = conn.getEndPoint();
			append("Connection " + connectionName + " = new Connection");
			callOpen();
			line("\"" + conn.getName() + "\", ");
			writeGateReference(endpoints.get(0));
			line(", ");
			writeGateReference(endpoints.get(1));
			callClose();

		});
		append("configure(new Connection[] {");
		boolean first = true;
		for (String cn : connectionNames) {
			if (!first)
				append(", ");
			first = false;
			append(cn);
		}
		line("});");

		blockClose();
		newLine();
	}

	private void writeGateReference(GateReference e) {
		append("new GateReference(");
		writeElement(e.getGate());
		append(", ");
		writeElement(e.getGate().getType());
		append(", ");
		writeElement(e.getComponent());
		append(", ");
		writeElement(e.getComponent().getType());
		append(", ");
		append("ComponentInstanceRole." + e.getComponent().getRole());
		append(")");
	}

	private void writeGetConnection(GateReference tester, GateReference remote) {
		append("getConnection(\"" + tester.getComponent().getName() + "\", \"" + tester.getGate().getName() + "\", \""
				+ remote.getComponent().getName() + "\", \"" + remote.getGate().getName() + "\")");
	}

	private void writeGetGateReference(GateReference tester) {
		append("getGateReference(\"" + tester.getComponent().getName() + "\", \"" + tester.getGate().getName() + "\")");
	}

	private void writeElement(Element e) {
		append("new Element(\"" + e.getName() + "\")");
	}

	private void writeTestDescription(TestDescription tc) {

		for (TestObjective to : tc.getTestObjective()) {
			line("/**");
			String toDesc = to.getDescription();
			if (toDesc != null && toDesc.length() > 0) {
				line(" * " + toDesc);
			}
			line("*/");
		}

		line("@Test");

		append("public void test_" + getElementName(tc) + "()");
		blockOpen();
		newLine();

		append("try");
		blockOpen();

		Set<String> thrownExceptions = new HashSet<String>();
		write(tc.getBehaviourDescription().getBehaviour(), null, null, thrownExceptions);

		blockClose();

		if (!thrownExceptions.isEmpty()) {
			for (String ex : thrownExceptions) {
				append("catch (" + ex + " e)");
				blockOpen();
				if (ex.equals(STOP_EXCEPTION)) {
					append("if (e.getVerdict() != null)");
					blockOpen();
					line(VALIDATOR_FIELD + ".setVerdict(e.getVerdict());");
					blockClose();
				}
				line("throw new " + ASSERTION_EXCEPTION + "(e.getMessage());");
				blockClose();
			}
		}
		append("catch (RuntimeException e)");
		blockOpen();
		line(REPORTER_FIELD + ".runtimeError(e);");
		line("throw e;");
		blockClose();

		blockClose();
		newLine();
	}

	class FutureInfo {
		public String varName;
		public String kind;
		public Behaviour b;
		public boolean hasExceptionals = false;

		public FutureInfo(String varName, String kind, Behaviour b) {
			this.varName = varName;
			this.kind = kind;
			this.b = b;
		}
	}

	private void write(Behaviour b, Map<DataUse, String> dataUseVariables, List<FutureInfo> futures,
			Set<String> thrownExceptions) {

		lineComment(b.eClass().getName());

		writeComment(b);
		writeNotification(b, true);

		if (b instanceof CompoundBehaviour) {
			// XX why was this here?
//			write(((CompoundBehaviour) b).getBlock(), null, null, thrownExceptions);
//			return;

		} else if (dataUseVariables == null)
			throw new RuntimeException();

		List<FutureInfo> myFutures = new ArrayList<FutureInfo>();

		boolean writeAfter = true;
		List<String> exceptionalBehaviours = new ArrayList<String>();
		if (b instanceof AtomicBehaviour) {

			// Atomic behaviours

			// Component check
			if (b instanceof ActionBehaviour) {
				ComponentInstance c = ((ActionBehaviour) b).getComponentInstance();
				if (c != null && !isCurrentComponentInstance(c))
					return;
			} else if (b instanceof TimerOperation) {
				ComponentInstance c = ((TimerOperation) b).getComponentInstance();
				if (c != null && !isCurrentComponentInstance(c))
					return;
			} else if (b instanceof Assignment) {
				VariableUse v = ((Assignment) b).getVariable();
				if (!isCurrentComponentInstance(v.getComponentInstance()))
					return;
			}

			// Timing
			TimeLabel timeLabel = ((AtomicBehaviour) b).getTimeLabel();
			if (timeLabel != null) {
				String labelName = getTimeLabelName(timeLabel);
				line(labelName + ".timestamp();");
			}
			for (TimeConstraint tc : ((AtomicBehaviour) b).getTimeConstraint()) {
				FutureInfo futureInfo = declareFuture(b, tc, dataUseVariables);
				if (!isTesterInput(b)) {
					line(futureInfo.varName + ".get();");

				} else
					myFutures.add(futureInfo);
			}

			// Actions
			if (b instanceof ActionReference) {
				Action action = ((ActionReference) b).getAction();
				DataElementMapping mapping = getMapping(action);
				if (mapping == null || !(hasAnnotation(mapping, MAPPING_ANNOTATION_STATIC_METHOD)
						|| hasAnnotation(mapping, MAPPING_ANNOTATION_METHOD)))
					throw new RuntimeException("No supported mapping for action: " + action.getQualifiedName());

				for (ParameterBinding arg : ((ActionReference) b).getArgument())
					initializeDataUse(arg.getDataUse(), dataUseVariables);

				String mappingPrefix = mapping.getDataResourceMapping().getResourceURI();
				if (mappingPrefix.length() > 0)
					append(mappingPrefix + ".");
				append(getElementName(action));

				if (!((ActionReference) b).getArgument().isEmpty()) {
					append("(");
					boolean first = true;
					for (ParameterBinding arg : ((ActionReference) b).getArgument()) {
						write(arg.getDataUse(), dataUseVariables);
						if (first)
							first = false;
						else
							append(", ");
					}
					line(");");
				}

			} else if (b instanceof InlineAction) {
				if (b.getAnnotation().stream().anyMatch(
						a -> a.getKey().getName().equals(LANGUAGE_KEY) && a.getValue().equals(JAVA_LANGUAGE_VALUE))) {
					append(((InlineAction) b).getBody());
					newLine();
				}

			}
			// Variable assignment
			else if (b instanceof Assignment) {
				VariableUse v = ((Assignment) b).getVariable();
				initializeDataUse(((Assignment) b).getExpression(), dataUseVariables);
				append("this.");
				write(v, dataUseVariables);
				append(" = ");
				write(((Assignment) b).getExpression(), dataUseVariables);
				line(";");

			}
			// Verdicts
			else if (b instanceof Assertion) {
				initializeDataUse(((Assertion) b).getCondition(), dataUseVariables);
				DataUse verdict = ((Assertion) b).getOtherwise();
				if (verdict != null)
					initializeDataUse(verdict, dataUseVariables);

				append("if (!(");
				write(((Assertion) b).getCondition(), dataUseVariables);
				append("))");
				blockOpen();

				append(VALIDATOR_FIELD + ".setVerdict(");
				if (verdict != null)
					write(((Assertion) b).getOtherwise(), dataUseVariables);
				else
					append("Verdict.fail");
				line(");");

				writeNotification(b, false);
				writeObjective(b);
				writeAfter = false;
				line("throw new " + ASSERTION_EXCEPTION + "(\"" + getMessage(b) + "\");");

				blockClose();

			} else if (b instanceof VerdictAssignment) {
				DataUse verdict = ((VerdictAssignment) b).getVerdict();
				initializeDataUse(verdict, dataUseVariables);
				append(VALIDATOR_FIELD + ".setVerdict(");
				write(verdict, dataUseVariables);
				line(");");

			}
			// Control flow
			else if (b instanceof Break) {
				if (!(b.container() instanceof Block))
					throw new RuntimeException("Break not contained in a Block " + getQName(b));
				Block bl = (Block) ((Break) b).container();

				writeNotification(b, false);
				writeObjective(b);
				writeAfter = false;

				line("throw new " + BREAK_EXCEPTION + "(\"" + getElementName(bl) + "\");");
				thrownExceptions.add(BREAK_EXCEPTION);

			} else if (b instanceof Stop) {

				writeNotification(b, false);
				writeObjective(b);
				writeAfter = false;

				line("throw new " + STOP_EXCEPTION + "(\"Stop " + getQName(b) + "\");");
				thrownExceptions.add(STOP_EXCEPTION);

			}
			// Timer operations
			else if (b instanceof TimerStart) {
				DataUse period = ((TimerStart) b).getPeriod();
				Time timeType = resolveTimeType(period);

				initializeDataUse(period, dataUseVariables);

				append(getTimerName(((TimerOperation) b).getTimer()) + ".start(");
				write(period, dataUseVariables);
				if (timeType != null)
					append(", TimeUnit." + timeType.getName());
				line(");");

			} else if (b instanceof TimerStop) {
				append(getTimerName(((TimerOperation) b).getTimer()) + ".stop();");

			} else if (b instanceof TimeOut) {
				FutureInfo futureInfo = writeTesterInput(b, dataUseVariables, true, true);
				myFutures.add(futureInfo);

			}
			// Gate operations
			else if (b instanceof Wait) {
				FutureInfo futureInfo = declareFuture(b, dataUseVariables, true);
				line(futureInfo.varName + ".get();");

			} else if (b instanceof Quiescence) {
				FutureInfo futureInfo = writeTesterInput(b, dataUseVariables, true, true);
				myFutures.add(futureInfo);

			} else if (b instanceof Message) {

				if (isTesterInput(b)) {
					FutureInfo futureInfo = writeTesterInput(b, dataUseVariables, true, true);
					myFutures.add(futureInfo);
					thrownExceptions.add(INTERRUPTED_EXCEPTION);
					thrownExceptions.add(FUTURE_EXECUTION_EXCEPTION);

				} else {

					Message m = (Message) b;
					DataUse arg = m.getArgument();
					initializeDataUse(arg, dataUseVariables);

					for (Target t : m.getTarget()) {
						append(SYSTEM_ADAPTER_FIELD + ".send(");
						convertToData(arg, dataUseVariables);
						append(", ");
						writeGetConnection(m.getSourceGate(), t.getTargetGate());
						line(");");
					}

				}

			} else if (b instanceof ProcedureCall) {
				ProcedureCall pc = (ProcedureCall) b;

				boolean isCall = pc.getReplyTo() == null;
				boolean isTesterInput = isTesterInput(b);
				if (isCall) {
					if (!isTesterInput) {
						FutureInfo futureInfo = writeTesterInput(b, dataUseVariables, true, true);
						myFutures.add(futureInfo);

					} else {
						// TODO receive call if reply isTesterInput
					}
				}

			}
			// Test description call
			else if (b instanceof TestDescriptionReference) {
				TestDescription td = ((TestDescriptionReference) b).getTestDescription();
				// XXX

			}

			newLine();
		}

		// Combined behaviours

		else if (b instanceof CombinedBehaviour) {

			for (PeriodicBehaviour pb : ((CombinedBehaviour) b).getPeriodic()) {
				// XXX run periodic in parallel with separate completion service
				lineComment(pb.eClass().getName() + " not supported currently");
			}

			for (ExceptionalBehaviour eb : ((CombinedBehaviour) b).getExceptional()) {

				newLine();
				lineComment(eb.eClass().getName());

				ComponentInstance gc = eb.getGuardedComponent();
				if (gc != null && !isCurrentComponentInstance(gc))
					continue;

				String exceptionalBehaviourName = getElementName(eb);
				append("ExceptionalBehaviour " + exceptionalBehaviourName + " = new ExceptionalBehaviour(");
				append(Boolean.toString(eb instanceof InterruptBehaviour));
				line(");");

				// TODO what to do with those?
				Set<String> innerExceptions = new HashSet<String>();
				write(eb.getBlock(), exceptionalBehaviourName, null, innerExceptions);

				line("addExceptionalBehaviour(" + exceptionalBehaviourName + ");");
				newLine();
				exceptionalBehaviours.add(exceptionalBehaviourName);
			}

			// Single block
			if (b instanceof CompoundBehaviour) {
				write(((CompoundBehaviour) b).getBlock(), null, null, thrownExceptions);

			} else if (b instanceof BoundedLoopBehaviour) {
				Optional<LocalExpression> exp = ((BoundedLoopBehaviour) b).getNumIteration().stream()
						.filter(l -> isCurrentComponentInstance(l.getComponentInstance())).findFirst();
				if (exp.isEmpty())
					throw new RuntimeException("Appropriate local expression not provided for " + getMessage(b) + " on "
							+ this.currentTester.getName());
				DataUse d = exp.get().getExpression();
				initializeDataUse(d, dataUseVariables);

				String counterName = getElementName(b) + "_counter";
				// XXX counter type
				String counterType = "int";
				append("for (" + counterType + " " + counterName + " = 0; " + counterName + " < ");
				write(d, dataUseVariables);
				append("; " + counterName + "++)");

				blockOpen();
				write(((SingleCombinedBehaviour) b).getBlock(), null, null, thrownExceptions);
				blockClose();

			} else if (b instanceof UnboundedLoopBehaviour) {
				append("while (true)");
				blockOpen();
				write(((SingleCombinedBehaviour) b).getBlock(), null, null, thrownExceptions);
				blockClose();

			} else if (b instanceof OptionalBehaviour) {
				// XXX OptionalBehaviour
				lineComment(b.eClass().getName() + " not supported currently");
				// write(((SingleCombinedBehaviour) b).getBlock(), null, thrownExceptions);

			}
			// Multiple blocks
			else if (b instanceof AlternativeBehaviour) {
				// XXX

			} else if (b instanceof ConditionalBehaviour) {
				List<Block> blocks = ((ConditionalBehaviour) b).getBlock();
				boolean first = true;
				for (Block block : blocks) {
					if (!first)
						append(" else ");
					first = false;
					write(block, null, null, thrownExceptions);
				}

			} else if (b instanceof ParallelBehaviour) {
				// XXX run ParallelBehaviour in parallel with separate completion service
				lineComment(b.eClass().getName() + " not supported currently");

			}

		}
		if (writeAfter) {
			writeNotification(b, false);
			writeObjective(b);

			newLine();
			exceptionalBehaviours.forEach(eb -> {
				line("removeExceptionalBehaviour(" + eb + ");");
			});
		}

		if (futures == null) {
			if (!myFutures.isEmpty()) {
				String futureName = getElementName(b) + "_future";
				line("Future<ExecutionResult> " + futureName + " = next();");

				append("try ");
				blockOpen();

				boolean first = true;
				for (FutureInfo f : myFutures) {
					if (!first)
						append(" else ");
					first = false;

					append("if (" + f.varName + " == " + futureName + ")");
					blockOpen();

					line(f.kind + " result = (" + f.kind + ")" + f.varName + ".get();");
					if (f.kind.equals("InteractionResult")) {
						Target localTarget = ((Interaction) b).getTarget().stream()
								.filter(t -> isCurrentComponentInstance(t.getTargetGate().getComponent())).findFirst()
								.get();
						if (b instanceof Message) {
							for (ValueAssignment va : localTarget.getValueAssignment()) {
								lineComment("ValueAssignment");
								Variable var = va.getVariable();
								append(getElementName(var));
								append(" = ");
								append("(" + getElementName(var.getDataType()) + ")");
								line("result.data.getValue();");
							}

						} else {
							// XXX
						}

					} else if (f.kind.equals("TimeoutResult")) {
						line("throw new " + STOP_EXCEPTION + "(\"Timeout\");");
						thrownExceptions.add(STOP_EXCEPTION);

					} else
						throw new RuntimeException("Unknown execution result kind: " + f.kind);

					blockClose();
				}

				line(" else ");
				blockOpen();
				append("ExceptionalBehaviour exceptionalBehaviour = getExceptionalBehaviour(");
				append(futureName);
				line(");");
				append("if (exceptionalBehaviour != null)");
				blockOpen();
				line("exceptionalBehaviour.behaviour.execute();");
				blockClose();
				blockClose();
				thrownExceptions.add(STOP_EXCEPTION);

				blockClose();
				append("finally ");
				blockOpen();

				// Cancel futures
				myFutures.forEach(f -> {
					line("stop(" + f.varName + ");");

					if (f.hasExceptionals) {
						append(f.varName);
						line("_exceptionals.forEach(f -> stop(f));");
					}
				});

				blockClose();

//				MethodCall mc = () -> {
//					return null;
//				};

				newLine();
			}

		} else {
			futures.addAll(myFutures);
			// XXX handle futures in caller (e.g. alt)
		}

	}

	private FutureInfo writeTesterInput(Behaviour b, Map<DataUse, String> dataUseVariables, boolean autoSubmit,
			boolean addExceptionals) {
		if (b instanceof TimeOut || b instanceof Quiescence || b instanceof Message || b instanceof ProcedureCall) {
			FutureInfo future = declareFuture(b, dataUseVariables, autoSubmit);
			if (addExceptionals) {
				future.hasExceptionals = true;
				append("List<Future<ExecutionResult>> ");
				append(future.varName);
				append("_exceptionals = ");
				line("executeExceptionals();");
			}
			return future;
		}
		throw new RuntimeException("Behaviour is not tester-input:" + getMessage(b));
	}

	private void write(Block bl, String exceptionalBehaviourName, List<FutureInfo> futures,
			Set<String> thrownExceptions) {
		// Limit scope of variables with a code block
		append("try");
		blockOpen();

		Map<DataUse, String> dataUseVariables = new Hashtable<DataUse, String>();
		declareDataUses(bl, dataUseVariables, "");
		newLine();

		writeComment(bl);

		Optional<LocalExpression> guard = bl.getGuard().stream()
				.filter(le -> isCurrentComponentInstance(le.getComponentInstance())).findFirst();
		if (guard.isPresent()) {
			DataUse g = guard.get().getExpression();
			initializeDataUse(g, dataUseVariables);

			append("if (");
			write(g, dataUseVariables);
			append(")");
			blockOpen();
		}

		Set<String> myThrownExceptions = new HashSet<String>();
		if (exceptionalBehaviourName != null) {

			// Block in exceptional behaviour:
			// - create callable for initiating behaviour
			// - render other behaviours in inner block

			Map<DataUse, String> innerDataUseVariables = new Hashtable<DataUse, String>();
			boolean triggerWritten = false;
			for (Behaviour b : bl.getBehaviour()) {

				if (!triggerWritten) {
					if (isParticipating(b)) {
						if (!isTesterInput(b))
							throw new RuntimeException(
									"Exceptional block should start with tester input: " + getMessage(b));

						// Initiating behaviour
						lineComment("This gets executed automatically with all interactions");
						FutureInfo callable = writeTesterInput(b, dataUseVariables, false, false);
						line(exceptionalBehaviourName + ".setCallable(" + callable.varName + ");");
						newLine();

						append(exceptionalBehaviourName + ".behaviour = () -> ");
						blockOpen();

						lineComment("Disable while exceptional behaviour is executed");
						line("removeExceptionalBehaviour(" + exceptionalBehaviourName + ");");
						newLine();

						append("try");
						blockOpen();

						// Re-declare data use variables
						declareDataUses(bl, innerDataUseVariables, "exc_");
						newLine();

						triggerWritten = true;

					} else
						continue;

				} else {
					// Other behaviours
					write(b, innerDataUseVariables, null, myThrownExceptions);

				}

				newLine();
			}

			blockClose();
			append("finally");
			blockOpen();

			lineComment("Enable the exceptional behaviour again");
			line("addExceptionalBehaviour(" + exceptionalBehaviourName + ");");
			blockClose();
			blockCloseMethod();

		} else {
			for (Behaviour b : bl.getBehaviour()) {
				write(b, dataUseVariables, futures, myThrownExceptions);
				newLine();
			}
		}

		thrownExceptions.addAll(myThrownExceptions);

		if (guard.isPresent())
			blockClose();

		blockClose();
		if (myThrownExceptions.contains(BREAK_EXCEPTION)) {
			append("catch (" + BREAK_EXCEPTION + " e)");
			blockOpen();
			line("if (\"" + getElementName(bl) + "\".equals(e.getBlockName()))");
			lineInc(bl.container() instanceof CompoundBehaviour ? "return;" : "break;");
			line("else");
			lineInc("throw e;");
			blockClose();
		} else {
			// There must be something...
			line("finally {}");
		}
	}

	private FutureInfo declareFuture(Behaviour b, TimeConstraint tc, Map<DataUse, String> dataUseVariables) {
		String futureName = "timeConstraint_" + getElementName(tc) + "_" + getElementName(b);

		line("Future<ExecutionResult> " + futureName + " = timeConstraint(() -> ");
		blockOpen();

		DataUse exp = tc.getTimeConstraintExpression();
		// TODO some data use variables may not be available in the callback scope
//		lineComment("TODO some data use variables may not be usable in the callback scope");
		declareDataUses(tc, dataUseVariables, "");
		initializeDataUse(exp, dataUseVariables);
		append("return ");
		write(exp, dataUseVariables);
		line(";");

		blockClose();
		line(").execute();");

		return new FutureInfo(futureName, "TimeoutResult", b);
	}

	private FutureInfo declareFuture(Behaviour b, Map<DataUse, String> dataUseVariables, boolean autoSubmit) {
		String typeSignature = autoSubmit ? "Future<ExecutionResult>" : "ExecutionCallable";

		Target localTarget = null;
		if (b instanceof Interaction)
			localTarget = ((Interaction) b).getTarget().stream()
					.filter(t -> isCurrentComponentInstance(t.getTargetGate().getComponent())).findFirst().get();

		if (b instanceof TimeOut) {
			String futureName = "timeout_" + getElementName(b);

			String timerName = getTimerName(((TimerOperation) b).getTimer());
			append(typeSignature + " timeout_" + getElementName(b) + " = timeout(" + timerName + ")");
			if (autoSubmit)
				append(".execute()");
			line(";");

			return new FutureInfo(futureName, "TimeoutResult", b);

		} else if (b instanceof Wait) {
			String futureName = "sleep_" + getElementName(b);

			DataUse period = ((TimeOperation) b).getPeriod();
			initializeDataUse(period, dataUseVariables);

			append(typeSignature + " " + futureName + " = sleep(");
			write(period, dataUseVariables);
			append(")");
			if (autoSubmit)
				append(".execute()");
			line(";");

			return new FutureInfo(futureName, "TimeoutResult", b);

		} else if (b instanceof Quiescence) {
			String futureName = "noInput_" + getElementName(b);

			DataUse period = ((TimeOperation) b).getPeriod();
			initializeDataUse(period, dataUseVariables);

			append(typeSignature + " " + futureName + " = noInput(");
			write(period, dataUseVariables);
			append(", ");
			GateReference gr = ((Quiescence) b).getGateReference();
			if (gr != null)
				writeGetGateReference(gr);
			else
				append("null");
			append(")");
			if (autoSubmit)
				append(".execute()");
			line(";");

			return new FutureInfo(futureName, "TimeoutResult", b);

		} else if (b instanceof Message) {
			Message m = (Message) b;
			String futureName = "receive_" + getElementName(b);

			DataUse arg = m.getArgument();
			initializeDataUse(arg, dataUseVariables);

			append(typeSignature + " " + futureName + " = " + (m.isIsTrigger() ? "trigger" : "receive") + "(");
			convertToData(arg, dataUseVariables);
			append(", ");
			writeGetConnection(localTarget.getTargetGate(), m.getSourceGate());
			append(")");
			if (autoSubmit)
				append(".execute()");
			line(";");

			return new FutureInfo(futureName, "InteractionResult", b);

		} else if (b instanceof ProcedureCall) {
			ProcedureCall pc = (ProcedureCall) b;
			String futureName = "call_" + getElementName(b);

			if (pc.getReplyTo() == null && !isTesterInput(b)) {

				ProcedureCall[] reply = new ProcedureCall[1];
				b.getParentTestDescription().eAllContents().forEachRemaining(e -> {
					if (e instanceof ProcedureCall) {
						if (b.equals(((ProcedureCall) e).getReplyTo())) {
							reply[0] = (ProcedureCall) e;
							return;
						}
					}
				});

				if (reply[0] == null)
					throw new RuntimeException("No reply for: " + getMessage(b));

				List<ParameterBinding> arguments = pc.getArgument();
				List<ParameterBinding> replyArguments = reply[0].getArgument();

				arguments.forEach(arg -> initializeDataUse(arg.getDataUse(), dataUseVariables));
				replyArguments.forEach(arg -> initializeDataUse(arg.getDataUse(), dataUseVariables));

				append(typeSignature + " " + futureName + " = call(");
				writeCallArguments(pc, reply[0], true, dataUseVariables);
				append(", ");
				writeGetConnection(localTarget.getTargetGate(), pc.getSourceGate());
				line(")");
				if (autoSubmit)
					append(".execute()");
				line(";");

				return new FutureInfo(futureName, "InteractionResult", b);

			} else {
				// XXX receive call if reply isTesterInput
			}
		}

		throw new RuntimeException("Don't know how to create future for: " + getMessage(b));
	}

	private void writeCallArguments(ProcedureCall pc, ProcedureCall reply, boolean call,
			Map<DataUse, String> dataUseVariables) {
		// TODO make configurable
		boolean callProceduresDirectly = true;

		List<ParameterBinding> arguments = pc.getArgument();

		if (call) {

			if (callProceduresDirectly) {
				ProcedureSignature signature = pc.getSignature();
				DataElementMapping mapping = getMappingChecked(signature);
				DataResourceMapping resourceMapping = mapping.getDataResourceMapping();
				if (!hasAnnotation(resourceMapping, MAPPING_ANNOTATION_CLASS))
					throw new RuntimeException("Unsupported resource mapping: " + signature.getQualifiedName());

				append("() ->");
				blockOpen();

				String callableRef = null;
				if (hasAnnotation(mapping, MAPPING_ANNOTATION_METHOD)) {
					callableRef = getElementName(signature) + "_adapter";
					append(resourceMapping.getResourceURI() + " " + callableRef + " = ");
					append("(" + resourceMapping.getResourceURI() + ")");
					append("getInstance(" + resourceMapping.getResourceURI() + ".class);");
					newLine();

				} else if (hasAnnotation(mapping, MAPPING_ANNOTATION_STATIC_METHOD))
					callableRef = resourceMapping.getResourceURI();

				else
					throw new RuntimeException("Unsupported resource mapping: " + signature.getQualifiedName());

				append("return " + callableRef + "." + mapping.getElementURI() + "(");

				boolean first = true;
				for (ParameterBinding arg : arguments) {
					if (!first)
						append(", ");
					first = false;
					write(arg.getDataUse(), dataUseVariables);
				}

				line(");");

				blockClose();

			} else {

				String operation = getElementName(pc.getSignature());
				append("\"" + operation + "\", ");

				append("new Argument[] {");
				boolean first = true;
				for (ParameterBinding arg : arguments) {
					if (!first)
						append(", ");
					first = false;
					Parameter p = arg.getParameter();
					String parameterName = getParameterMappingUri(p);
					if (parameterName == null)
						parameterName = getElementName(p);
					convertToArgument(parameterName, arg.getDataUse(), dataUseVariables);
				}
				append("}, ");

				Optional<ParameterBinding> out = reply.getArgument().stream()
						.filter(arg -> ((ProcedureParameter) arg.getParameter()).getKind() == ParameterKind.OUT)
						.findAny();
				if (out.isPresent()) {
					convertToData(out.get().getDataUse(), dataUseVariables);
					append(", null");
				} else {
					Optional<ParameterBinding> ex = reply.getArgument().stream().filter(
							arg -> ((ProcedureParameter) arg.getParameter()).getKind() == ParameterKind.EXCEPTION)
							.findAny();
					append("null, ");
					if (ex.isPresent())
						convertToData(ex.get().getDataUse(), dataUseVariables);
					else
						append("null");
				}
			}

		} else {
			// XXX receive call if reply isTesterInput
		}
	}

	private void declareDataUses(Element b, final Map<DataUse, String> dataUseVariables, String prefix) {

		TreeIterator<EObject> i = b.eAllContents();
		while (i.hasNext()) {
			// We could inline the data uses that are arguments of another data use, but
			// keeping things simple for now
			EObject e = i.next();

			if (e instanceof Block) {
				// Don't go into nested blocks
				i.prune();
				continue;
			}
			if (e instanceof TimeLabelUse) {
				// Time labels are declared in test description scope
				continue;

			}
			if (e instanceof TimeConstraint) {
				// Time constraints have their own block
				continue;

			} else if (e instanceof SpecialValueUse) {
				// Special values cannot be declared in Java
				continue;

			}

			declareDataUse(e, dataUseVariables, prefix);
		}
	}

	private void declareDataUse(EObject e, final Map<DataUse, String> dataUseVariables, String prefix) {

		if (e instanceof VariableUse) {
			dataUseVariables.put((DataUse) e, getElementName(((VariableUse) e).getVariable()));

		} else if (e instanceof DataUse) {
			DataUse d = (DataUse) e;
			String varName = getElementName(d);
			if (varName == null)
				varName = "";
			else
				varName += "_";
			varName += prefix + "datause_" + (dataUseVariables.size() + 1);
			DataType type = d.resolveDataType();
			boolean isTime = false;
			if (type == null && d instanceof PredefinedFunctionCall) {
				// TODO move this to meta-model?
				for (DataUse arg : ((PredefinedFunctionCall) d).getActualParameters()) {
					if (arg instanceof TimeLabelUse) {
						isTime = true;
						break;
					}
					type = arg.resolveDataType();
					if (type != null)
						break;
				}

			}
			if (type == null && !isTime) {
				// XXX meta-model problem
				if (d instanceof LiteralValueUse) {
					LiteralValueUse ld = (LiteralValueUse) d;
					if (ld.getBoolValue() != null)
						line("boolean" + " " + varName + ";");
					else if (ld.getIntValue() != null)
						line("int" + " " + varName + ";");
					else
						line("String" + " " + varName + ";");

				} else if (d instanceof PredefinedFunctionCall) {
					line("// TODO Meta-model problem: DataUse.resolveDataType() return null");
					line(getPredefinedFunctionType(((PredefinedFunctionCall) d).getFunction()) + " " + varName + ";");

				} else {
					line("// TODO Meta-model problem: DataUse.resolveDataType() return null");
					line("String" + " " + varName + ";");
				}

			} else if (type instanceof Time || isTime)
				line("long " + varName + ";");
			else if (type instanceof CollectionDataType)
				line("java.util.List<" + getElementName(((CollectionDataType) type).getItemType()) + "> " + varName + ";");
			else
				line(getElementName(type) + " " + varName + ";");
			dataUseVariables.put(d, varName);
		}

	}

	private Time resolveTimeType(DataUse d) {
		DataType type = d.resolveDataType();
		if (type instanceof Time) {
			return (Time) type;
		}
		List<DataUse> args = getDataUseArgumentValues(d);
		for (DataUse ad : args) {
			type = resolveTimeType(ad);
			if (type != null)
				return (Time) type;
		}
		return null;
	}

	/**
	 * Recursively assigns values to this data use and its arguments, depth-first.
	 */
	private void initializeDataUse(DataUse d, Map<DataUse, String> dataUseVariables) {

		if (d instanceof TimeLabelUse)
			return;
		if (d instanceof SpecialValueUse)
			return;

		for (MemberReference r : d.getReduction()) {
			DataUse idx = r.getCollectionIndex();
			if (idx != null)
				initializeDataUse(idx, dataUseVariables);
		}
		if (d instanceof DataElementUse) {
			for (DataUse item: ((DataElementUse)d).getItem()) {
				initializeDataUse(item, dataUseVariables);
			}
		}
		for (DataUse arg : getDataUseArgumentValues(d)) {
			initializeDataUse(arg, dataUseVariables);
		}

		writeComment(d);

		if (!dataUseVariables.containsKey(d))
			declareDataUse(d, dataUseVariables, "");
		String ref = dataUseVariables.get(d);
		append(ref);
		append(" = ");

		DataInstance dataInstance = null;
		DataType dataType = null;
		FormalParameter formalParameter = null;
		Function function = null;
		PredefinedFunction predefinedFunction = null;
		List<MemberAssignment> memberAssignments = null;
		List<DataUse> collectionItems = null;

		if (d instanceof DataElementUse) {
			NamedElement element = ((DataElementUse) d).getDataElement();
			if (element instanceof DataInstance) {
				dataInstance = (DataInstance) element;
				DataElementMapping m = getMapping(dataInstance);
				if (m == null) {
					// Treat as inline
					dataType = d.resolveDataType();
					if (dataInstance instanceof StructuredDataInstance)
						memberAssignments = ((StructuredDataInstance) dataInstance).getMemberAssignment();
					dataInstance = null;
				}

			} else if (element instanceof DataType)
				dataType = (DataType) element;

			else if (element instanceof FormalParameter)
				formalParameter = (FormalParameter) element;

			else if (element instanceof Function)
				function = (Function) element;

			else if (element instanceof PredefinedFunction)
				predefinedFunction = (PredefinedFunction) element;

			else
				throw new RuntimeException("Unsupported element in DataElementUse " + d.getName());

		} else if (d instanceof DataInstanceUse) {
			dataInstance = ((DataInstanceUse) d).getDataInstance();
			if (dataInstance == null)
				dataType = d.resolveDataType();

		} else if (d instanceof FunctionCall) {
			function = ((FunctionCall) d).getFunction();

		} else if (d instanceof PredefinedFunctionCall) {
			predefinedFunction = ((PredefinedFunctionCall) d).getFunction();
		}

		if (dataInstance != null) {
			DataElementMapping m = getMapping(dataInstance);
			if (m == null) {
				DataType t = dataInstance.getDataType();
				if (t.getQualifiedName().equals("TDL::Verdict")) {
					append("Verdict." + dataInstance.getName());

				} else
					throw new RuntimeException("Mapping missing for: " + dataInstance.getQualifiedName());

			} else {
				// TODO instance function
				String classQName = m.getDataResourceMapping().getResourceURI();
				append(classQName + "." + m.getElementURI());
				if (hasAnnotation(m, MAPPING_ANNOTATION_METHOD) || hasAnnotation(m, MAPPING_ANNOTATION_STATIC_METHOD))
					append("()");
			}

		} else if (dataType != null) {
			boolean isCollection = false;
			if (dataType instanceof CollectionDataType) {
				dataType = ((CollectionDataType) dataType).getItemType();
				isCollection = true;
				if (d instanceof DataElementUse)
					collectionItems = ((DataElementUse) d).getItem();
			}
			DataElementMapping m = getMapping(dataType);
			if (m != null) {
				if (hasAnnotation(m, MAPPING_ANNOTATION_CLASS)) {
					append("new ");
					if (isCollection)
						append("java.util.ArrayList<");
					String classQName = m.getDataResourceMapping().getResourceURI();
					append(classQName + "." + m.getElementURI());
					if (isCollection)
						append(">");
					append("()");
				}
			}

		} else if (d instanceof SpecialValueUse) {
			// XXX

		} else if (d instanceof LiteralValueUse) {
			String str = ((LiteralValueUse) d).getValue();
			BigInteger i = ((LiteralValueUse) d).getIntValue();
			Boolean b = ((LiteralValueUse) d).getBoolValue();
			if (str != null)
				append("\"" + escape(str) + "\"");
			else if (i != null)
				append(Integer.toString(i.intValue()));
			else if (b != null)
				append(Boolean.toString(b));

		} else if (function != null || predefinedFunction != null) {

			if (function != null) {
				// TODO instance function
				DataElementMapping m = getMappingChecked(function);
				String classQName = m.getDataResourceMapping().getResourceURI();
				String functionName = m.getElementURI();

				append(classQName + "." + functionName);

			} else if (predefinedFunction != null) {
				append("this." + FUNCTIONS_FIELD + "." + getPredefinedFunction(predefinedFunction));

			}

			append("(");
			boolean first = true;
			for (DataUse arg : getDataUseArgumentValues(d)) {
				if (!first)
					append(", ");
				first = false;
				if (predefinedFunction != null) {
					String fn = predefinedFunction.getName();
					if (fn.equals("==") || fn.equals("!="))
						convertToData(arg, dataUseVariables);
					else
						write(arg, dataUseVariables);
				} else
					write(arg, dataUseVariables);
			}
			append(")");

		} else if (formalParameter != null) {
			append(getElementName(formalParameter));

		} else if (d instanceof VariableUse) {
			append(HELPER_FIELD + ".clone(" + getElementName(((VariableUse) d).getVariable()) + ")");

		}
		line(";");

		if (!getDataUseArguments(d).isEmpty()) {
			for (ParameterBinding arg : getDataUseArguments(d)) {
				// TODO members of collection type
				append(ref);
				append(".");
				writeMemberAssignment(arg.getParameter(), arg.getDataUse(), dataUseVariables);
				line(";");
			}
		}

		if (memberAssignments != null) {
			for (MemberAssignment ma : memberAssignments) {
				// TODO members of collection type
				append(ref);
				append(".");
				writeMemberAssignment(ma.getMember(), ma.getMemberSpec(), dataUseVariables);
				line(";");
			}
		}
		if (collectionItems != null) {
			for (DataUse item: collectionItems) {
				String itemRef = dataUseVariables.get(item);
				append(ref + ".add(" + itemRef + ")");
				line(";");
				
			}
		}
	}

	private void writeMemberAssignment(Parameter p, DataUse v, Map<DataUse, String> dataUseVariables) {
		ParameterMapping mapping = getParameterMapping(p);
		if (hasAnnotation(mapping, MAPPING_ANNOTATION_SETTER)) {
			append(mapping.getParameterURI());
			append("(");
			write(v, dataUseVariables);
			append(")");

		} else {
			append(mapping.getParameterURI());
			append(" = ");
			write(v, dataUseVariables);
		}
	}

	private String escape(String s) {
		StringBuilder buf = new StringBuilder();
		char[] chars = s.toCharArray();
		for (int i = 0; i < chars.length; i++) {
			if (chars[i] == '\\' || chars[i] == '"')
				buf.append('\\');
			buf.append(chars[i]);
		}
		return buf.toString();
	}

	private void write(DataUse d, Map<DataUse, String> dataUseVariables) {

		// All data uses are variables
		if (d instanceof TimeLabelUse) {
			append(getTimeLabelName(((TimeLabelUse) d).getTimeLabel()));
			append("." + ((TimeLabelUse) d).getKind().getName().toLowerCase() + "()");

		} else {

			DataType type = d.resolveDataType();
			if (type instanceof Time) {
				// Time unit conversion to default (ms)
				append("TimeUnit." + getElementName(type) + ".toSeconds(" + dataUseVariables.get(d) + ")");

			} else {
				append(dataUseVariables.get(d));
			}
		}

		boolean first = true;
		for (MemberReference ref : d.getReduction()) {
			DataUse idx = ref.getCollectionIndex();
			if (first && idx != null) {
				writeCollectionIdnex(d.resolveDataType(), idx, dataUseVariables);

			} else {
				Member prop = ref.getMember();
				ParameterMapping pm = getParameterMapping(prop);

				String mappedName = pm.getParameterURI();
				if (mappedName == null)
					mappedName = getElementName(prop);
				append("." + mappedName);
				if (hasAnnotation(pm, MAPPING_ANNOTATION_GETTER))
					append("()");

				if (idx != null)
					writeCollectionIdnex(prop.getDataType(), idx, dataUseVariables);
			}

			first = false;
		}
	}

	private void writeCollectionIdnex(DataType t, DataUse idx, Map<DataUse, String> dataUseVariables) {
		boolean isArray = true;
		DataElementMapping m = getMapping(t);
		if (m != null) {
			String mappedType = m.getElementURI();
			if (!mappedType.contains("[]"))
				isArray = false;
		} else {
			// Fall back to type array
		}

		Stack<Reference> dataUseVariableRef = new Stack<Reference>();
		dataUseVariableRef.push(new Reference(dataUseVariableRef, dataUseVariables.get(idx)));
		if (isArray) {
			append("[");
			write(idx, dataUseVariables);
			append("]");
		} else {
			append("read");
			append("(");
			write(idx, dataUseVariables);
			line(");");
		}
	}

	private void convertToData(DataUse d, Map<DataUse, String> dataUseVariables) {
		append("new PojoData(");
		write(d, dataUseVariables);
		append(")");
	}

	private void convertToArgument(String parameterName, DataUse d, Map<DataUse, String> dataUseVariables) {
		append("new PojoArgument(");
		write(d, dataUseVariables);
		append(", \"" + parameterName + "\"");
		append(")");
	}

	private String getPredefinedFunction(PredefinedFunction f) {
		switch (f.getName()) {
		case "==":
			return "equals";
		case "!=":
			return "notEquals";
		case "<":
			return "lt";
		case ">":
			return "gt";
		case "<=":
			return "lteq";
		case ">=":
			return "gteq";
		case "+":
			return "plus";
		case "-":
			return "minus";
		case "*":
			return "multiply";
		case "/":
			return "divide";
		default:
			return f.getName();
		}
	}

	private String getPredefinedFunctionType(PredefinedFunction f) {
		switch (f.getName()) {
		case "==":
		case "!=":
		case "<":
		case ">":
		case "<=":
		case ">=":
			return "boolean";
		case "+":
		case "-":
		case "*":
		case "/":
			return "integer";
		default:
			throw new RuntimeException("Unknown predefined function in type resolution.");
		}
	}

	private void writeComment(Element e) {
		// TODO multiline comment
		e.getComment().forEach(c -> {
			append("/* ");
			line(c.getBody());
			line("*/ ");
			line(REPORTER_FIELD + ".comment(\"" + c.getBody() + "\");");
		});
	}

	private void writeObjective(Behaviour b) {
		// TODO multiple URIs
		b.getTestObjective().forEach(to -> {
			append(REPORTER_FIELD + ".testObjectiveReached(");
			append("\"" + to.getObjectiveURI() + "\", ");
			append("\"" + to.getDescription() + "\"");
			line(");");
		});
	}

	private void writeNotification(Behaviour b, boolean started) {
		if (logEvents) {
			append(REPORTER_FIELD + ".");
			append(started ? "behaviourStarted" : "behaviourCompleted");
			append("(");
			if (started)
				append("\"" + b.eClass().getName() + "\", ");
			append("\"" + getQName(b) + "\"");
			line(");");
			// TODO properties
		}
	}

	/**
	 * Provides a descriptive name of the behaviour for informative purposes.
	 * 
	 * @return A descriptive name of the behaviour.
	 */
	private String getQName(Behaviour b) {
		// XXX
		String name = b.getName();
		if (name == null)
			name = getElementName(b);
		return name;
	}

	private String getMessage(Behaviour b) {
		// XXX
		return b.eClass().getName() + ": " + b.getName();
	}

	private boolean isParticipating(Behaviour b) {
		return b.getParticipatingComponents().stream().anyMatch(c -> isCurrentComponentInstance(c));
	}

	private boolean isTesterInput(Behaviour b) {
		if (b instanceof Interaction) {
			return ((Interaction) b).getTarget().stream()
					.anyMatch(t -> isCurrentComponentInstance(t.getTargetGate().getComponent()));
		}
		return b.isTesterInputEvent();
	}

	private boolean isCurrentComponentInstance(ComponentInstance c) {
		if (this.currentTester == null)
			return false;
		return this.currentTester.equals(c);
	}

	private List<DataUse> getDataUseArgumentValues(DataUse d) {
		if (d instanceof PredefinedFunctionCall) {
			return ((PredefinedFunctionCall) d).getActualParameters();
		}

		List<DataUse> arguments = new ArrayList<>();
		List<ParameterBinding> args = getDataUseArguments(d);
		args.stream().forEach(pb -> arguments.add(pb.getDataUse()));

		List<MemberAssignment> memberAssignments = getDataUseMembers(d);
		memberAssignments.stream().forEach(ma -> arguments.add(ma.getMemberSpec()));

		return arguments;
	}

	private List<ParameterBinding> getDataUseArguments(DataUse d) {
		List<ParameterBinding> args = d.getArgument();
		return args;
	}

	private List<MemberAssignment> getDataUseMembers(DataUse d) {

		DataInstance dataInstance = null;
		if (d instanceof DataInstanceUse)
			dataInstance = ((DataInstanceUse) d).getDataInstance();
		else if (d instanceof DataElementUse)
			if (((DataElementUse) d).getDataElement() instanceof DataInstance)
				dataInstance = (DataInstance) ((DataElementUse) d).getDataElement();

		return dataInstance instanceof StructuredDataInstance
				? ((StructuredDataInstance) dataInstance).getMemberAssignment()
				: Collections.EMPTY_LIST;
	}

}

class Reference {
	private Stack<Reference> stack = new Stack<Reference>();

	String ref;

	private String collectionClass;
	private boolean useCollectionVariable = false;
	private String collectionMemberVariable;
	private DataType collectionMemberVariableType;
	private boolean primitiveCollection = false;
	int collectionIdex;
	boolean isCollectionMember = false;

	public Reference(Stack<Reference> reference, String ref) {
		this.ref = ref;
//		this.isCollectionMember = isCollectionMeber;
	}

	private Reference getParent() {
		int index = stack.indexOf(this) - 1;
		if (index >= 0)
			return stack.get(index);
		return null;
	}

	public String getCollectionClass() {
		if (isCollectionMember)
			if (getParent() != null)
				return getParent().collectionClass;
		return collectionClass;
	}

	public boolean isUseCollectionVariable() {
		if (isCollectionMember)
			if (getParent() != null)
				return getParent().useCollectionVariable;
		return useCollectionVariable;
	}

	public boolean isPrimitiveCollection() {
		if (isCollectionMember)
			if (getParent() != null)
				return getParent().primitiveCollection;
		return primitiveCollection;
	}

	public String getCollectionMemberVariable() {
		return collectionMemberVariable;
	}

	public DataType getCollectionMemberVariableType() {
		if (isCollectionMember)
			if (getParent() != null)
				return getParent().collectionMemberVariableType;
		return collectionMemberVariableType;
	}
}