/*******************************************************************************
 * Copyright 2014 See AUTHORS file.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.badlogic.gdx.ai.btree.utils;

import java.io.InputStream;
import java.io.Reader;

import com.badlogic.gdx.ai.GdxAI;
import com.badlogic.gdx.ai.btree.BehaviorTree;
import com.badlogic.gdx.ai.btree.Task;
import com.badlogic.gdx.ai.btree.annotation.TaskAttribute;
import com.badlogic.gdx.ai.btree.annotation.TaskConstraint;
import com.badlogic.gdx.ai.btree.branch.DynamicGuardSelector;
import com.badlogic.gdx.ai.btree.branch.Parallel;
import com.badlogic.gdx.ai.btree.branch.RandomSelector;
import com.badlogic.gdx.ai.btree.branch.RandomSequence;
import com.badlogic.gdx.ai.btree.branch.Selector;
import com.badlogic.gdx.ai.btree.branch.Sequence;
import com.badlogic.gdx.ai.btree.decorator.AlwaysFail;
import com.badlogic.gdx.ai.btree.decorator.AlwaysSucceed;
import com.badlogic.gdx.ai.btree.decorator.Include;
import com.badlogic.gdx.ai.btree.decorator.Invert;
import com.badlogic.gdx.ai.btree.decorator.Random;
import com.badlogic.gdx.ai.btree.decorator.Repeat;
import com.badlogic.gdx.ai.btree.decorator.SemaphoreGuard;
import com.badlogic.gdx.ai.btree.decorator.UntilFail;
import com.badlogic.gdx.ai.btree.decorator.UntilSuccess;
import com.badlogic.gdx.ai.btree.leaf.Failure;
import com.badlogic.gdx.ai.btree.leaf.Success;
import com.badlogic.gdx.ai.btree.leaf.Wait;
import com.badlogic.gdx.ai.utils.random.Distribution;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.ObjectMap.Entries;
import com.badlogic.gdx.utils.ObjectMap.Entry;
import com.badlogic.gdx.utils.ObjectSet;
import com.badlogic.gdx.utils.SerializationException;
import com.badlogic.gdx.utils.reflect.Annotation;
import com.badlogic.gdx.utils.reflect.ClassReflection;
import com.badlogic.gdx.utils.reflect.Field;
import com.badlogic.gdx.utils.reflect.ReflectionException;

/** A {@link BehaviorTree} parser.
 * 
 * @author davebaol */
public class BehaviorTreeParser<E> {

	public static final int DEBUG_NONE = 0;
	public static final int DEBUG_LOW = 1;
	public static final int DEBUG_HIGH = 2;

	private static final String TAG = "BehaviorTreeParser";

	public int debugLevel;
	public DistributionAdapters distributionAdapters;

	private DefaultBehaviorTreeReader<E> btReader;

	public BehaviorTreeParser () {
		this(DEBUG_NONE);
	}

	public BehaviorTreeParser (DistributionAdapters distributionAdapters) {
		this(distributionAdapters, DEBUG_NONE);
	}

	public BehaviorTreeParser (int debugLevel) {
		this(new DistributionAdapters(), debugLevel);
	}

	public BehaviorTreeParser (DistributionAdapters distributionAdapters, int debugLevel) {
		this(distributionAdapters, debugLevel, null);
	}

	public BehaviorTreeParser (DistributionAdapters distributionAdapters, int debugLevel, DefaultBehaviorTreeReader<E> reader) {
		this.distributionAdapters = distributionAdapters;
		this.debugLevel = debugLevel;
		btReader = reader == null ? new DefaultBehaviorTreeReader<E>() : reader;
		btReader.setParser(this);
	}

	/** Parses the given string.
	 * @param string the string to parse
	 * @param object the blackboard object. It can be {@code null}.
	 * @return the behavior tree
	 * @throws SerializationException if the string cannot be successfully parsed. */
	public BehaviorTree<E> parse (String string, E object) {
		btReader.parse(string);
		return createBehaviorTree(btReader.root, object);
	}

	/** Parses the given input stream.
	 * @param input the input stream to parse
	 * @param object the blackboard object. It can be {@code null}.
	 * @return the behavior tree
	 * @throws SerializationException if the input stream cannot be successfully parsed. */
	public BehaviorTree<E> parse (InputStream input, E object) {
		btReader.parse(input);
		return createBehaviorTree(btReader.root, object);
	}

	/** Parses the given file.
	 * @param file the file to parse
	 * @param object the blackboard object. It can be {@code null}.
	 * @return the behavior tree
	 * @throws SerializationException if the file cannot be successfully parsed. */
	public BehaviorTree<E> parse (FileHandle file, E object) {
		btReader.parse(file);
		return createBehaviorTree(btReader.root, object);
	}

	/** Parses the given reader.
	 * @param reader the reader to parse
	 * @param object the blackboard object. It can be {@code null}.
	 * @return the behavior tree
	 * @throws SerializationException if the reader cannot be successfully parsed. */
	public BehaviorTree<E> parse (Reader reader, E object) {
		btReader.parse(reader);
		return createBehaviorTree(btReader.root, object);
	}

	protected BehaviorTree<E> createBehaviorTree (Task<E> root, E object) {
		if (debugLevel > BehaviorTreeParser.DEBUG_LOW) printTree(root, 0);
		return new BehaviorTree<E>(root, object);
	}

	protected static <E> void printTree (Task<E> task, int indent) {
		for (int i = 0; i < indent; i++)
			System.out.print(' ');
		if (task.getGuard() != null) {
			System.out.println("Guard");
			indent = indent + 2;
			printTree(task.getGuard(), indent);
			for (int i = 0; i < indent; i++)
				System.out.print(' ');
		}
		System.out.println(task.getClass().getSimpleName());
		for (int i = 0; i < task.getChildCount(); i++) {
			printTree(task.getChild(i), indent + 2);
		}
	}

	public static class DefaultBehaviorTreeReader<E> extends BehaviorTreeReader {

		private static final ObjectMap<String, String> DEFAULT_IMPORTS = new ObjectMap<String, String>();
		static {
			Class<?>[] classes = new Class<?>[] {// @off - disable libgdx formatter
				AlwaysFail.class,
				AlwaysSucceed.class,
				DynamicGuardSelector.class,
				Failure.class,
				Include.class,
				Invert.class,
				Parallel.class,
				Random.class,
				RandomSelector.class,
				RandomSequence.class,
				Repeat.class,
				Selector.class,
				SemaphoreGuard.class,
				Sequence.class,
				Success.class,
				UntilFail.class,
				UntilSuccess.class,
				Wait.class
			}; // @on - enable libgdx formatter
			for (Class<?> c : classes) {
				String fqcn = c.getName();
				String cn = c.getSimpleName();
				String alias = Character.toLowerCase(cn.charAt(0)) + (cn.length() > 1 ? cn.substring(1) : "");
				DEFAULT_IMPORTS.put(alias, fqcn);
			}
		}

		enum Statement {
			Import("import") {
				@Override
				protected <E> void enter (DefaultBehaviorTreeReader<E> reader, String name, boolean isGuard) {
				}

				@Override
				protected <E> boolean attribute (DefaultBehaviorTreeReader<E> reader, String name, Object value) {
					if (!(value instanceof String)) reader.throwAttributeTypeException(this.name, name, "String");
					reader.addImport(name, (String)value);
					return true;
				}

				@Override
				protected <E> void exit (DefaultBehaviorTreeReader<E> reader) {
					return;
				}
			},
			Subtree("subtree") {
				@Override
				protected <E> void enter (DefaultBehaviorTreeReader<E> reader, String name, boolean isGuard) {
				}

				@Override
				protected <E> boolean attribute (DefaultBehaviorTreeReader<E> reader, String name, Object value) {
					if (!name.equals("name")) reader.throwAttributeNameException(this.name, name, "name");
					if (!(value instanceof String)) reader.throwAttributeTypeException(this.name, name, "String");
					if ("".equals(value)) throw new GdxRuntimeException(this.name + ": the name connot be empty");
					if (reader.subtreeName != null)
						throw new GdxRuntimeException(this.name + ": the name has been already specified");
					reader.subtreeName = (String)value;
					return true;
				}

				@Override
				protected <E> void exit (DefaultBehaviorTreeReader<E> reader) {
					if (reader.subtreeName == null)
						throw new GdxRuntimeException(this.name + ": the name has not been specified");
					reader.switchToNewTree(reader.subtreeName);
					reader.subtreeName = null;
				}
			},
			Root("root") {
				@Override
				protected <E> void enter (DefaultBehaviorTreeReader<E> reader, String name, boolean isGuard) {
					reader.subtreeName = ""; // the root tree has empty name
				}

				@Override
				protected <E> boolean attribute (DefaultBehaviorTreeReader<E> reader, String name, Object value) {
					reader.throwAttributeTypeException(this.name, name, null);
					return true;
				}

				@Override
				protected <E> void exit (DefaultBehaviorTreeReader<E> reader) {
					reader.switchToNewTree(reader.subtreeName);
					reader.subtreeName = null;
				}
			},
			TreeTask(null) {
				@Override
				protected <E> void enter (DefaultBehaviorTreeReader<E> reader, String name, boolean isGuard) {
					// Root tree is the default one
					if (reader.currentTree == null) {
						reader.switchToNewTree("");
						reader.subtreeName = null;
					}

					reader.openTask(name, isGuard);
				}

				@Override
				protected <E> boolean attribute (DefaultBehaviorTreeReader<E> reader, String name, Object value) {
					StackedTask<E> stackedTask = reader.getCurrentTask();
					AttrInfo ai = stackedTask.metadata.attributes.get(name);
					if (ai == null) return false;
					boolean isNew = reader.encounteredAttributes.add(name);
					if (!isNew) throw reader.stackedTaskException(stackedTask, "attribute '" + name + "' specified more than once");
					Field attributeField = reader.getField(stackedTask.task.getClass(), ai.fieldName);
					reader.setField(attributeField, stackedTask.task, value);
					return true;
				}

				@Override
				protected <E> void exit (DefaultBehaviorTreeReader<E> reader) {
					if (!reader.isSubtreeRef) {
						reader.checkRequiredAttributes(reader.getCurrentTask());
						reader.encounteredAttributes.clear();
					}
				}
			};
			
			String name;
			
			Statement(String name) {
				this.name = name;
			}
			
			protected abstract <E> void enter (DefaultBehaviorTreeReader<E> reader, String name, boolean isGuard);
			protected abstract <E> boolean attribute (DefaultBehaviorTreeReader<E> reader, String name, Object value);
			protected abstract <E> void exit (DefaultBehaviorTreeReader<E> reader);

		} 

		protected BehaviorTreeParser<E> btParser;

		ObjectMap<Class<?>, Metadata> metadataCache = new ObjectMap<Class<?>, Metadata>();

		Task<E> root;
		String subtreeName;
		Statement statement;
		private int indent;

		public DefaultBehaviorTreeReader () {
			this(false);
		}

		public DefaultBehaviorTreeReader (boolean reportsComments) {
			super(reportsComments);
		}

		public BehaviorTreeParser<E> getParser () {
			return btParser;
		}

		public void setParser (BehaviorTreeParser<E> parser) {
			this.btParser = parser;
		}

		@Override
		public void parse (char[] data, int offset, int length) {
			debug = btParser.debugLevel > BehaviorTreeParser.DEBUG_NONE;
			root = null;
			clear();
			super.parse(data, offset, length);

			// Pop all task from the stack and check their minimum number of children
			popAndCheckMinChildren(0);

			Subtree<E> rootTree = subtrees.get("");
			if (rootTree == null) throw new GdxRuntimeException("Missing root tree");
			root = rootTree.rootTask;
			if (root == null) throw new GdxRuntimeException("The tree must have at least the root task");

			clear();
		}
		
		@Override
		protected void startLine (int indent) {
			if (btParser.debugLevel > BehaviorTreeParser.DEBUG_LOW)
				GdxAI.getLogger().debug(TAG, lineNumber + ": <" + indent + ">");
			this.indent = indent;
		}

		private Statement checkStatement (String name) {
			if (name.equals(Statement.Import.name)) return Statement.Import;
			if (name.equals(Statement.Subtree.name)) return Statement.Subtree;
			if (name.equals(Statement.Root.name)) return Statement.Root;
			return Statement.TreeTask;
		}
		
		@Override
		protected void startStatement (String name, boolean isSubtreeReference, boolean isGuard) {
			if (btParser.debugLevel > BehaviorTreeParser.DEBUG_LOW)
				GdxAI.getLogger().debug(TAG, (isGuard? " guard" : " task") + " name '" + name + "'");
			
			this.isSubtreeRef = isSubtreeReference;
			
			this.statement = isSubtreeReference ? Statement.TreeTask : checkStatement(name);
			if (isGuard) {
				if (statement != Statement.TreeTask)
					throw new GdxRuntimeException(name + ": only tree's tasks can be guarded");
			}

			statement.enter(this, name, isGuard);
		}

		@Override
		protected void attribute (String name, Object value) {
			if (btParser.debugLevel > BehaviorTreeParser.DEBUG_LOW)
				GdxAI.getLogger().debug(TAG, lineNumber + ": attribute '" + name + " : " + value + "'");
			
			boolean validAttribute = statement.attribute(this, name, value);
			if (!validAttribute) {
				if (statement == Statement.TreeTask) {
					throw stackedTaskException(getCurrentTask(), "unknown attribute '" + name + "'");
				} else {
					throw new GdxRuntimeException(statement.name + ": unknown attribute '" + name + "'");
				}
			}
		}

		private Field getField (Class<?> clazz, String name) {
			try {
				return ClassReflection.getField(clazz, name);
			} catch (ReflectionException e) {
				throw new GdxRuntimeException(e);
			}
		}

		private void setField (Field field, Task<E> task, Object value) {
			field.setAccessible(true);
			Object valueObject = castValue(field, value);
			if (valueObject == null) throwAttributeTypeException(getCurrentTask().name, field.getName(), field.getType().getSimpleName());
			try {
				field.set(task, valueObject);
			} catch (ReflectionException e) {
				throw new GdxRuntimeException(e);
			}
		}

		/**
		 * Convert serialized value to java value.
		 * Parsed value must be assignable to field argument.
		 * Subclasses may override this method to parse unsupported types.
		 * @param field task attribute field
		 * @param value unparsed value (can be Number, String or Boolean)
		 * @return parsed value or null if field type is not supported.
		 */
		protected Object castValue (Field field, Object value) {
			Class<?> type = field.getType();
			Object ret = null;
			if (value instanceof Number) {
				Number numberValue = (Number)value;
				if (type == int.class || type == Integer.class)
					ret = numberValue.intValue();
				else if (type == float.class || type == Float.class)
					ret = numberValue.floatValue();
				else if (type == long.class || type == Long.class)
					ret = numberValue.longValue();
				else if (type == double.class || type == Double.class)
					ret = numberValue.doubleValue();
				else if (type == short.class || type == Short.class)
					ret = numberValue.shortValue();
				else if (type == byte.class || type == Byte.class)
					ret = numberValue.byteValue();
				else if (ClassReflection.isAssignableFrom(Distribution.class, type)) {
					@SuppressWarnings("unchecked")
					Class<Distribution> distributionType = (Class<Distribution>)type;
					ret = btParser.distributionAdapters.toDistribution("constant," + numberValue, distributionType);
				}
			} else if (value instanceof Boolean) {
				if (type == boolean.class || type == Boolean.class) ret = value;
			} else if (value instanceof String) {
				String stringValue = (String)value;
				if (type == String.class)
					ret = value;
				else if (type == char.class || type == Character.class) {
					if (stringValue.length() != 1) throw new GdxRuntimeException("Invalid character '" + value + "'");
					ret = Character.valueOf(stringValue.charAt(0));
				} else if (ClassReflection.isAssignableFrom(Distribution.class, type)) {
					@SuppressWarnings("unchecked")
					Class<Distribution> distributionType = (Class<Distribution>)type;
					ret = btParser.distributionAdapters.toDistribution(stringValue, distributionType);
				} else if (ClassReflection.isAssignableFrom(Enum.class, type)) {
					Enum<?>[] constants = (Enum<?>[])type.getEnumConstants();
					for (int i = 0, n = constants.length; i < n; i++) {
						Enum<?> e = constants[i];
						if (e.name().equalsIgnoreCase(stringValue)) {
							ret = e;
							break;
						}
					}
				}
			}
			return ret;
		}

		private void throwAttributeNameException (String statement, String name, String expectedName) {
			String expected = " no attribute expected";
			if (expectedName != null)
				expected = "expected '" + expectedName + "' instead";
			throw new GdxRuntimeException(statement + ": attribute '" + name + "' unknown; " + expected);
		}

		private void throwAttributeTypeException (String statement, String name, String expectedType) {
			throw new GdxRuntimeException(statement + ": attribute '" + name + "' must be of type " + expectedType);
		}

		@Override
		protected void endLine () {
		}

		@Override
		protected void endStatement () {
			statement.exit(this);
		}

		private void openTask (String name, boolean isGuard) {
			try {
				Task<E> task;
				if (isSubtreeRef) {
					task = subtreeRootTaskInstance(name);
				}
				else {
					String className = getImport(name);
					if (className == null) className = name;
					@SuppressWarnings("unchecked")
					Task<E> tmpTask = (Task<E>)ClassReflection.newInstance(ClassReflection.forName(className));
					task = tmpTask;
				}
				
				if (!currentTree.inited()) {
					initCurrentTree(task, indent);
					indent = 0;
				} else if (!isGuard) {
					StackedTask<E> stackedTask = getPrevTask();

					indent -= currentTreeStartIndent;
					if (stackedTask.task == currentTree.rootTask) {
						step = indent;
					}
					if (indent > currentDepth) {
						stack.add(stackedTask); // push
					} else if (indent <= currentDepth) {
						// Pop tasks from the stack based on indentation
						// and check their minimum number of children
						int i = (currentDepth - indent) / step;
						popAndCheckMinChildren(stack.size - i);
					}

					// Check the max number of children of the parent
					StackedTask<E> stackedParent = stack.peek();
					int maxChildren = stackedParent.metadata.maxChildren;
					if (stackedParent.task.getChildCount() >= maxChildren)
						throw stackedTaskException(stackedParent, "max number of children exceeded ("
							+ (stackedParent.task.getChildCount() + 1) + " > " + maxChildren + ")");

					// Add child task to the parent
					stackedParent.task.addChild(task);
				}
				updateCurrentTask(createStackedTask(name, task), indent, isGuard);
			} catch (ReflectionException e) {
				throw new GdxRuntimeException("Cannot parse behavior tree!!!", e);
			}
		}
		
		private StackedTask<E> createStackedTask (String name, Task<E> task) {
			Metadata metadata = findMetadata(task.getClass());
			if (metadata == null)
				throw new GdxRuntimeException(name + ": @TaskConstraint annotation not found in '" + task.getClass().getSimpleName()
					+ "' class hierarchy");
			return new StackedTask<E>(lineNumber, name, task, metadata);
		}

		private Metadata findMetadata (Class<?> clazz) {
			Metadata metadata = metadataCache.get(clazz);
			if (metadata == null) {
				Annotation tca = ClassReflection.getAnnotation(clazz, TaskConstraint.class);
				if (tca != null) {
					TaskConstraint taskConstraint = tca.getAnnotation(TaskConstraint.class);
					ObjectMap<String, AttrInfo> taskAttributes = new ObjectMap<String, AttrInfo>();
					Field[] fields = ClassReflection.getFields(clazz);
					for (Field f : fields) {
						Annotation a = f.getDeclaredAnnotation(TaskAttribute.class);
						if (a != null) {
							AttrInfo ai = new AttrInfo(f.getName(), a.getAnnotation(TaskAttribute.class));
							taskAttributes.put(ai.name, ai);
						}
					}
					metadata = new Metadata(taskConstraint.minChildren(), taskConstraint.maxChildren(), taskAttributes);
					metadataCache.put(clazz, metadata);
				}
			}
			return metadata;
		}

		protected static class StackedTask<E> {
			public int lineNumber;
			public String name;
			public Task<E> task;
			public Metadata metadata;

			StackedTask (int lineNumber, String name, Task<E> task, Metadata metadata) {
				this.lineNumber = lineNumber;
				this.name = name;
				this.task = task;
				this.metadata = metadata;
			}
		}

		private static class Metadata {
			int minChildren;
			int maxChildren;
			ObjectMap<String, AttrInfo> attributes;

			/** Creates a {@code Metadata} for a task accepting from {@code minChildren} to {@code maxChildren} children and the given
			 * attributes.
			 * @param minChildren the minimum number of children (defaults to 0 if negative)
			 * @param maxChildren the maximum number of children (defaults to {@link Integer.MAX_VALUE} if negative)
			 * @param attributes the attributes */
			Metadata (int minChildren, int maxChildren, ObjectMap<String, AttrInfo> attributes) {
				this.minChildren = minChildren < 0 ? 0 : minChildren;
				this.maxChildren = maxChildren < 0 ? Integer.MAX_VALUE : maxChildren;
				this.attributes = attributes;
			}
		}

		private static class AttrInfo {
			String name;
			String fieldName;
			boolean required;

			AttrInfo (String fieldName, TaskAttribute annotation) {
				this(annotation.name(), fieldName, annotation.required());
			}

			AttrInfo (String name, String fieldName, boolean required) {
				this.name = name == null || name.length() == 0 ? fieldName : name;
				this.fieldName = fieldName;
				this.required = required;
			}
		}

		protected static class Subtree<E> {
			String name;  // root tree must have no name
			Task<E> rootTask;
			int referenceCount;

			Subtree() {
				this(null);
			}

			Subtree(String name) {
				this.name = name;
				this.rootTask = null;
				this.referenceCount = 0;
			}

			public void init(Task<E> rootTask) {
				this.rootTask = rootTask;
			}

			public boolean inited() {
				return rootTask != null;
			}

			public boolean isRootTree() {
				return name == null || "".equals(name);
			}

			public Task<E> rootTaskInstance () {
				if (referenceCount++ == 0) {
					return rootTask;
				}
				return rootTask.cloneTask();
			}
		}

		ObjectMap<String, String> userImports = new ObjectMap<String, String>();

		ObjectMap<String, Subtree<E>> subtrees = new ObjectMap<String, Subtree<E>>();
		Subtree<E> currentTree;

		int currentTreeStartIndent;
		int currentDepth;
		int step;
		boolean isSubtreeRef;
		protected StackedTask<E> prevTask;
		protected StackedTask<E> guardChain;
		protected Array<StackedTask<E>> stack = new Array<StackedTask<E>>();
		ObjectSet<String> encounteredAttributes = new ObjectSet<String>();			
		boolean isGuard;
		
		StackedTask<E> getLastStackedTask() {
			return stack.peek();
		}
		
		StackedTask<E> getPrevTask() {
			return prevTask;
		}
		
		StackedTask<E> getCurrentTask() {
			return isGuard? guardChain : prevTask;
		}
		
		void updateCurrentTask(StackedTask<E> stackedTask, int indent, boolean isGuard) {
			this.isGuard = isGuard;
			stackedTask.task.setGuard(guardChain == null ? null : guardChain.task);
			if (isGuard) {
				guardChain = stackedTask;
			}
			else {
				prevTask = stackedTask;
				guardChain = null;
				currentDepth = indent;
			}
		}
		
		void clear() {
			prevTask = null;
			guardChain = null;
			currentTree = null;
			userImports.clear();
			subtrees.clear();
			stack.clear();
			encounteredAttributes.clear();
		}
		
		//
		// Subtree
		//
		
		void switchToNewTree(String name) {
			// Pop all task from the stack and check their minimum number of children
			popAndCheckMinChildren(0);

			this.currentTree = new Subtree<E>(name);
			Subtree<E> oldTree = subtrees.put(name, currentTree);
			if (oldTree != null)
				throw new GdxRuntimeException("A subtree named '" + name + "' is already defined");
		}
		
		void initCurrentTree(Task<E> rootTask, int startIndent) {
			currentDepth = -1;
			step = 1;
			currentTreeStartIndent = startIndent;
			this.currentTree.init(rootTask);
			prevTask = null;
		}
		
		Task<E> subtreeRootTaskInstance(String name) {
			Subtree<E> tree = subtrees.get(name);
			if (tree == null)
				throw new GdxRuntimeException("Undefined subtree with name '" + name + "'");
			return tree.rootTaskInstance();
		}
		
		//
		// Import
		//

		void addImport (String alias, String task) {
			if (task == null) throw new GdxRuntimeException("import: missing task class name.");
			if (alias == null) {
				Class<?> clazz = null;
				try {
					clazz = ClassReflection.forName(task);
				} catch (ReflectionException e) {
					throw new GdxRuntimeException("import: class not found '" + task + "'");
				}
				alias = clazz.getSimpleName();
			}
			String className = getImport(alias);
			if (className != null) throw new GdxRuntimeException("import: alias '" + alias + "' previously defined already.");
			userImports.put(alias, task);
		}

		String getImport (String as) {
			String className = DEFAULT_IMPORTS.get(as);
			return className != null ? className : userImports.get(as);
		}
		
		//
		// Integrity checks
		//

		private void popAndCheckMinChildren (int upToFloor) {
			// Check the minimum number of children in prevTask
			if (prevTask != null) checkMinChildren(prevTask);

			// Check the minimum number of children while popping up to the specified floor
			while (stack.size > upToFloor) {
				StackedTask<E> stackedTask = stack.pop();
				checkMinChildren(stackedTask);
			}
		}

		private void checkMinChildren (StackedTask<E> stackedTask) {
			// Check the minimum number of children
			int minChildren = stackedTask.metadata.minChildren;
			if (stackedTask.task.getChildCount() < minChildren)
				throw stackedTaskException(stackedTask, "not enough children (" + stackedTask.task.getChildCount() + " < " + minChildren
					+ ")");
		}

		private void checkRequiredAttributes (StackedTask<E> stackedTask) {
			// Check the minimum number of children
			Entries<String, AttrInfo> entries = stackedTask.metadata.attributes.iterator();
			while (entries.hasNext()) {
				Entry<String, AttrInfo> entry = entries.next();
				if (entry.value.required && !encounteredAttributes.contains(entry.key))
					throw stackedTaskException(stackedTask, "missing required attribute '" + entry.key + "'");
			}
		}

		private GdxRuntimeException stackedTaskException(StackedTask<E> stackedTask, String message) {
			return new GdxRuntimeException(stackedTask.name + " at line " + stackedTask.lineNumber + ": " + message);
		}

	}
}