/*******************************************************************************
 * Copyright (c) 2011-2017 The University of York, Aston University.
 * 
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License, v. 2.0 are satisfied: GNU General Public License, version 3.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-3.0
 *
 * Contributors:
 *     Konstantinos Barmpis - initial API and implementation
 *     Antonio Garcia-Dominguez - cleanup and bug fixes, refactor into two levels
 ******************************************************************************/
package org.eclipse.hawk.epsilon.emc;

import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.Consumer;

import org.eclipse.epsilon.common.parse.problem.ParseProblem;
import org.eclipse.epsilon.eol.EolModule;
import org.eclipse.epsilon.eol.IEolModule;
import org.eclipse.epsilon.eol.exceptions.EolInternalException;
import org.eclipse.epsilon.eol.exceptions.EolRuntimeException;
import org.eclipse.epsilon.eol.exceptions.EolUndefinedVariableException;
import org.eclipse.epsilon.eol.exceptions.models.EolModelElementTypeNotFoundException;
import org.eclipse.epsilon.eol.exceptions.models.EolModelLoadingException;
import org.eclipse.epsilon.eol.exceptions.models.EolNotInstantiableModelElementTypeException;
import org.eclipse.epsilon.eol.execute.context.Variable;
import org.eclipse.epsilon.eol.execute.control.DefaultExecutionController;
import org.eclipse.epsilon.eol.execute.introspection.IPropertyGetter;
import org.eclipse.epsilon.eol.execute.introspection.IPropertySetter;
import org.eclipse.epsilon.eol.types.EolAnyType;
import org.eclipse.epsilon.eol.types.EolSequence;
import org.eclipse.hawk.core.IModelIndexer;
import org.eclipse.hawk.core.IStateListener.HawkState;
import org.eclipse.hawk.core.graph.IGraphDatabase;
import org.eclipse.hawk.core.graph.IGraphEdge;
import org.eclipse.hawk.core.graph.IGraphIterable;
import org.eclipse.hawk.core.graph.IGraphNode;
import org.eclipse.hawk.core.graph.IGraphNodeIndex;
import org.eclipse.hawk.core.graph.IGraphNodeReference;
import org.eclipse.hawk.core.graph.IGraphTransaction;
import org.eclipse.hawk.core.graph.timeaware.ITimeAwareGraphNodeIndex;
import org.eclipse.hawk.core.query.IQueryEngine;
import org.eclipse.hawk.core.query.InvalidQueryException;
import org.eclipse.hawk.core.query.QueryExecutionException;
import org.eclipse.hawk.core.util.Utils;
import org.eclipse.hawk.epsilon.emc.contextful.CEOLQueryEngine;
import org.eclipse.hawk.epsilon.emc.optimisation.OptimisableCollection;
import org.eclipse.hawk.epsilon.emc.pgetters.GraphPropertyGetter;
import org.eclipse.hawk.epsilon.emc.tracking.AccessListener;
import org.eclipse.hawk.epsilon.emc.wrappers.FileNodeWrapper;
import org.eclipse.hawk.epsilon.emc.wrappers.MetamodelNodeWrapper;
import org.eclipse.hawk.epsilon.emc.wrappers.TypeNodeWrapper;
import org.eclipse.hawk.graph.FileNode;
import org.eclipse.hawk.graph.GraphWrapper;
import org.eclipse.hawk.graph.MetamodelNode;
import org.eclipse.hawk.graph.ModelElementNode;
import org.eclipse.hawk.graph.ProxyReferenceList;
import org.eclipse.hawk.graph.Slot;
import org.eclipse.hawk.graph.TypeNode;
import org.eclipse.hawk.graph.updater.DirtyDerivedFeaturesListener;
import org.eclipse.hawk.graph.updater.GraphModelBatchInjector;
import org.eclipse.hawk.graph.updater.GraphModelInserter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Exposes a Hawk instance as an Epsilon model and adds support for EOL queries to Hawk.
 * Other Epsilon languages may inherit from this class, redefining the {@link #createModule()}
 * method and being careful with derived attributes (if they support them at all).
 *
 * TODO: investigate how this class could be broken up into model + proper query engine.
 * Right now the two seem to be deeply intertwined and separating them would break backwards
 * compatibility.
 */
public class EOLQueryEngine extends AbstractHawkModel implements IQueryEngine {

	/**
	 * Weak ID-based reference to a node. In cases of heavy memory usage, it can GC the node
	 * itself and retrieve it again a later time.
	 *
	 * This class should only be instantiated from the query engine. Subclasses of EOLQueryEngine
	 * may want to provide their own wrapper subclasses.
	 */
	public class GraphNodeWrapper implements IGraphNodeReference {

		protected String id;
		protected EOLQueryEngine containerModel;
		protected WeakReference<IGraphNode> node;

		protected GraphNodeWrapper(IGraphNode n, EOLQueryEngine containerModel) {
			node = new WeakReference<IGraphNode>(n);
			this.id = n.getId().toString();
			this.containerModel = containerModel;
		}

		@Override
		public IGraphNode getNode() {
			IGraphNode ret = node.get();
			if (ret == null) {
				ret = containerModel.getBackend().getNodeById(id);
				node = new WeakReference<IGraphNode>(ret);
			}
			return ret;
		}

		@Override
		public String getId() {
			return id;
		}

		@Override
		public EOLQueryEngine getContainerModel() {
			return containerModel;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			GraphNodeWrapper other = (GraphNodeWrapper) obj;
			return Objects.equals(containerModel, other.containerModel) && Objects.equals(id, other.id);
		}

		public Object getFeature(String name) throws EolRuntimeException {
			return containerModel.getPropertyGetter().invoke(this, name);
		}

		/**
		 * Returns true if this model element is contained directly or indirectly
		 * within the specified path at the specified repository.
		 */
		public boolean isContainedWithin(String repository, String path) {
			try (IGraphTransaction t = containerModel.getBackend().beginTransaction()) {
				final ModelElementNode men = new ModelElementNode(getNode());
				return men.isContainedWithin(repository, path);
			} catch (Exception e) {
				LOGGER.error(e.getMessage(), e);
			}
			return false;
		}

		@Override
		public String getTypeName() {
			String type = "";

			try (IGraphTransaction t = containerModel.getBackend().beginTransaction()) {
				IGraphNode n = getNode();

				final Iterator<IGraphEdge> itTypeNode = n.getOutgoingWithType(ModelElementNode.EDGE_LABEL_OFTYPE).iterator();
				if (itTypeNode.hasNext()) {
					final IGraphNode typeNode = itTypeNode.next().getEndNode();
					type = typeNode.getProperty(IModelIndexer.IDENTIFIER_PROPERTY).toString();
				} else {
					LOGGER.error("No type node found for node {}", n);
				}

				t.success();
			} catch (Exception e) {
				LOGGER.error(e.getMessage(), e);
			}

			return type;
		}

		@Override
		public String toString() {
			String info = "";

			try (IGraphTransaction t = containerModel.getBackend().beginTransaction()) {
				info += "type:" + getTypeName();
				info += "";
				t.success();
			} catch (Exception e) {
				LOGGER.error(e.getMessage(), e);
			}
			return "GNW|id:" + id + "|" + (info.equals("") ? "[no meta-info]" : info) + "";
		}

		@Override
		public int hashCode() {
			return Objects.hash(containerModel, id);
		}
	}
	

	protected class IGraphIterableCollection implements Collection<GraphNodeWrapper> {
		private final IGraphIterable<? extends IGraphNode> iterableNodes;

		protected IGraphIterableCollection(IGraphIterable<? extends IGraphNode> iterableNodes) {
			this.iterableNodes = iterableNodes;
		}

		@Override
		public int size() {
			return iterableNodes.size();
		}

		@Override
		public boolean isEmpty() {
			return iterableNodes.size() > 0;
		}

		@Override
		public boolean contains(Object o) {
			for (final Iterator<GraphNodeWrapper> itN = iterator(); itN.hasNext(); ) {
				if (itN.next().equals(o)) {
					return true;
				}
			}
			return false;
		}

		@Override
		public Iterator<GraphNodeWrapper> iterator() {
			final Iterator<? extends IGraphNode> itNodes = iterableNodes.iterator();
			return new Iterator<GraphNodeWrapper>() {
				@Override
				public boolean hasNext() {
					return itNodes.hasNext();
				}

				@Override
				public GraphNodeWrapper next() {
					return wrap(itNodes.next());
				}
			};
		}

		@Override
		public Object[] toArray() {
			return toArray(new Object[size()]);
		}

		@SuppressWarnings("unchecked")
		@Override
		public <T> T[] toArray(T[] a) {
			if (a.length < size()) {
				final T[] result = (T[]) Array.newInstance(a.getClass().getComponentType(), size());
				return toArray(result);
			}

			int i = 0;
			for (final Iterator<GraphNodeWrapper> itN = iterator(); itN.hasNext(); i++) {
				final GraphNodeWrapper n = itN.next();
				a[i] = (T) n;
			}
			while (i < a.length) {
				a[i++] = null;
			}

			return a;
		}

		@Override
		public boolean add(GraphNodeWrapper e) {
			throw new UnsupportedOperationException();
		}

		@Override
		public boolean remove(Object o) {
			throw new UnsupportedOperationException();
		}

		@Override
		public boolean containsAll(Collection<?> c) {
			for (Object e : c) {
				if (!contains(e)) {
					return false;
				}
			}
			return true;
		}

		@Override
		public boolean addAll(Collection<? extends GraphNodeWrapper> c) {
			throw new UnsupportedOperationException();
		}

		@Override
		public boolean removeAll(Collection<?> c) {
			throw new UnsupportedOperationException();
		}

		@Override
		public boolean retainAll(Collection<?> c) {
			throw new UnsupportedOperationException();
		}

		@Override
		public void clear() {
			throw new UnsupportedOperationException();
		}
	}

	/**
	 * Allows a query to be cancelled by the user according to an arbitrary binary flag.
	 * This could be used e.g. from an Eclipse job listener.
	 */
	public static class SettableExecutionController extends DefaultExecutionController {
		private volatile boolean isTerminated = false;

		@Override
		public boolean isTerminated() {
			return isTerminated;
		}

		public void setTerminated(boolean terminated) {
			this.isTerminated = terminated;
		}
	}

	interface Function3<T, U, V> {
		V apply(T t, U u);
	}

	public static final String TYPE = "org.eclipse.hawk.epsilon.emc.EOLQueryEngine";

	private static final Logger LOGGER = LoggerFactory.getLogger(EOLQueryEngine.class);
	private static final String MMURI_TYPE_SEPARATOR = "::";
	private static final String ANY_TYPE = new EolAnyType().getName();
	
	/* TODO: these two should not have to be static.*/
	protected IModelIndexer indexer = null;
	protected IGraphDatabase graph = null;

	protected IGraphNodeIndex metamodeldictionary;
	protected Set<String> defaultNamespaces = null;
	protected GraphPropertyGetter propertyGetter;

	/** Speeds up repeated queries for type nodes. */
	private Map<String, List<IGraphNode>> typeNodesCache = new WeakHashMap<>();

	/** Do not use OptimisedCollection unless we can benefit from it. */
	private boolean useOptimisableCollection;
	
	/**
	 * Returns all of the contents of the database in the form of lightweight
	 * {@link GraphNodeWrapper} objects.
	 */
	@Override
	public Collection<?> allContents() {
		final IGraphIterable<? extends IGraphNode> iterableNodes = graph.allNodes(ModelElementNode.OBJECT_VERTEX_LABEL);
		final Collection<GraphNodeWrapper> allContents = new IGraphIterableCollection(iterableNodes);
		broadcastAllOfXAccess(allContents);
		return allContents;
	}

	@Override
	public Object createInstance(String metaClassName)
			throws EolModelElementTypeNotFoundException, EolNotInstantiableModelElementTypeException {
		throw new EolNotInstantiableModelElementTypeException("Hawk Model Index", metaClassName);
	}

	@Override
	public void deleteElement(Object arg0) throws EolRuntimeException {
		throw new EolRuntimeException("Hawk Model Index: this type of model cannot create/delete elements");
	}

	public Collection<Object> getAllOf(String typeName, final String typeorkind) throws EolModelElementTypeNotFoundException, EolInternalException {
		try {
			final List<IGraphNode> typeNodes = getTypeNodes(typeName);
			if (typeNodes.size() == 1) {
				return getAllOf(typeNodes.get(0), typeorkind);
			}
		} catch (Exception e) {
			throw new EolInternalException(e);
		}

		throw new EolModelElementTypeNotFoundException(this.getName(), typeName);
	}

	/**
	 * Convenience version of
	 * {@link #getAllOf(String, String, String, String, String)} that looks in all
	 * repositories.
	 */
	public Collection<GraphNodeWrapper> getAllOf(final String metamodelURI, final String typeName, final String filePatterns)
			throws EolInternalException, EolModelElementTypeNotFoundException {
		return getAllOf(metamodelURI, typeName, "*", filePatterns);
	}

	/**
	 * Finds all the instances of a certain type (including subtypes) contained in
	 * the specified repository and file patterns. This version assumes we will provide
	 * a limited number of files and that these will be small - we start from the files.
	 *
	 * @param metamodelURI
	 *            URI of the metamodel, e.g.
	 *            <code>http://eclipse.org/example</code>.
	 * @param typeName
	 *            Name of the type within the metamodel, e.g.
	 *            <code>Subsystem</code>.
	 * @param repoPattern
	 *            Pattern of the repository or repositories to use, e.g.
	 *            <code>file:/*</code> or <code>project:/resource</code>.
	 * @param filePatterns
	 *            Comma-separated list of file patterns, such as
	 *            <code>/a/b/c.xmi</code> or <code>/d/*</code>.
	 *            If URI-invalid characters are found in the pattern,
	 *            they will be URI-encoded.
	 * @throws EolInternalException
	 *             Error while retrieving the type/file nodes.
	 * @throws EolModelElementTypeNotFoundException
	 *             Could not find the specified type.
	 */
	public Collection<GraphNodeWrapper> getAllOf(final String metamodelURI, final String typeName, final String repoPattern, final String filePatterns)
		throws EolInternalException, EolModelElementTypeNotFoundException
	{
		try {
			List<IGraphNode> typeNodes = getTypeNodes(metamodelURI, typeName);
			if (typeNodes.size() == 1) {
				final TypeNode targetTypeNode = new TypeNode(typeNodes.get(0));

				final List<GraphNodeWrapper> results = new ArrayList<>();
				final Set<FileNode> fileNodes = new GraphWrapper(graph).getFileNodes(
					Collections.singleton(repoPattern),
					Arrays.asList(filePatterns.split(",")));

				for (FileNode fn : fileNodes) {
					for (ModelElementNode me : fn.getModelElements()) {
						if (me.isOfKind(targetTypeNode)) {
							results.add(wrap(me.getNode()));
						}
					}
				}

				return results;
			}

		} catch (Exception e) {
			throw new EolInternalException(e);
		}

		throw new EolModelElementTypeNotFoundException(this.getName(), typeName);
	}

	public Collection<Object> getAllOf(IGraphNode typeNode, final String typeorkind) {
		Collection<Object> nodes = createAllOfCollection(typeNode);

		for (IGraphEdge n : typeNode.getIncomingWithType(typeorkind)) {
			nodes.add(wrap(n.getStartNode()));
		}

		broadcastAllOfXAccess(nodes);
		return nodes;
	}

	protected Collection<Object> createAllOfCollection(IGraphNode typeNode) {
		Collection<Object> nodes = useOptimisableCollection
			? new OptimisableCollection(this, wrap(typeNode)) : new EolSequence<>();
		return nodes;
	}

	public List<IGraphNode> getTypeNodes(String typeName) {
		List<IGraphNode> typeNodes = typeNodesCache.get(typeName);
		if (typeNodes == null) {
			typeNodes = computeTypeNodes(typeName);
			typeNodesCache.put(typeName, typeNodes);
		}
		return typeNodes;
	}

	protected List<IGraphNode> computeTypeNodes(String typeName) {
		final int idxColon = typeName.lastIndexOf(MMURI_TYPE_SEPARATOR);
		if (idxColon != -1) {
			final String epackage = typeName.substring(0, idxColon);
			final String type = typeName.substring(idxColon + MMURI_TYPE_SEPARATOR.length()); 
			return getTypeNodes(epackage, type);
		} else {
			final IGraphIterable<? extends IGraphNode> allMetamodels = metamodeldictionary.query("id", "*");

			final List<IGraphNode> candidates = new LinkedList<IGraphNode>();
			for (final Iterator<? extends IGraphNode> packs = allMetamodels.iterator(); packs.hasNext(); ) {
				IGraphNode pack = packs.next();
				for (IGraphEdge n : pack.getIncomingWithType("epackage")) {
	
					final IGraphNode othernode = n.getStartNode();
					final Object id = othernode.getProperty(IModelIndexer.IDENTIFIER_PROPERTY);
					if (id.equals(typeName)) {
						candidates.add(othernode);
					}
				}
			}

			if (candidates.size() == 1) {
				return candidates;
			} else if (candidates.size() > 1) {
				// use default namespaces to limit types
				for (Iterator<IGraphNode> it = candidates.iterator(); it.hasNext();) {
					IGraphNode n = it.next();
					String metamodel = new TypeNode(n).getMetamodelURI();
					if (defaultNamespaces != null && !defaultNamespaces.isEmpty() && !defaultNamespaces.contains(metamodel)) {
						it.remove();
					}
				}
	
				return candidates;
			}
		}

		return Collections.emptyList();
	}

	protected List<IGraphNode> getTypeNodes(String mmURI, String type) {
		Iterator<? extends IGraphNode> itPack = metamodeldictionary.get("id", mmURI).iterator();
		IGraphNode packageNode = null;
		if (itPack.hasNext()) {
			packageNode = itPack.next();
		} else {
			// Could not find by metamodel URI - might be the name of the metamodel
			for (IGraphNode candidatePackageNode : metamodeldictionary.query("*", "*")) {
				if (mmURI.equals(candidatePackageNode.getProperty(IModelIndexer.METAMODEL_NAME_PROPERTY))) {
					packageNode = candidatePackageNode;
					break;
				}
			}
			if (packageNode == null) {
				throw new NoSuchElementException("Could not find the metamodel node for " + mmURI);
			}
		}

		for (IGraphEdge r : packageNode.getIncomingWithType("epackage")) {
			IGraphNode othernode = r.getStartNode();
			if (othernode.getProperty(IModelIndexer.IDENTIFIER_PROPERTY).equals(type)) {
				return Collections.singletonList(othernode);
			}
		}
		return Collections.emptyList();
	}

	protected void broadcastAllOfXAccess(Iterable<?> ret) {
		/*
		 * TODO can optimise by not keeping all the nodes of type/kind but the
		 * concept of type/kind and only update the attr if a new node is added
		 * or one is removed.
		 */
		if (((GraphPropertyGetter) propertyGetter).getBroadcastStatus()) {
			for (Object n : ret) {
				final AccessListener accessListener = ((GraphPropertyGetter) propertyGetter).getAccessListener();
				final String sID = ((GraphNodeWrapper) n).getId() + "";
				accessListener.accessed(sID, "property_unused_type_or_kind");
			}
		}
	}

	@Override
	public Object getElementById(String id) {
		IGraphNode ret = graph.getNodeById(id);
		return ret == null ? null : wrap(ret);
	}

	@Override
	public String getElementId(Object arg0) {
		if (arg0 instanceof GraphNodeWrapper)
			return ((GraphNodeWrapper) arg0).getId() + "";
		else
			return null;
	}

	@Override
	public String getTypeNameOf(Object arg0) {
		String ret = null;
		try {
			Object type = getTypeOf(arg0);
			IGraphNode typeNode = ((GraphNodeWrapper) type).getNode();

			ret = typeNode.getProperty(IModelIndexer.IDENTIFIER_PROPERTY).toString();
		} catch (Exception e) {
			LOGGER.error(e.getMessage(), e);
		}

		return ret;
	}

	@Override
	public TypeNodeWrapper getTypeOf(Object arg0) {
		IGraphNode objectNode = ((GraphNodeWrapper) arg0).getNode();
		TypeNode typeNode = new ModelElementNode(objectNode).getTypeNode();
		if (typeNode != null) {
			return new TypeNodeWrapper(typeNode, this);
		} else {
			return null;
		}
	}

	@Override
	public FileNodeWrapper getFileOf(Object arg0) {
		IGraphNode objectNode = ((GraphNodeWrapper) arg0).getNode();
		FileNode fileNode = new ModelElementNode(objectNode).getFileNode();
		if (fileNode != null) {
			return new FileNodeWrapper(fileNode, this);
		} else {
			return null;
		}
	}

	@Override
	public List<FileNodeWrapper> getFilesOf(Object arg0) {
		List<FileNodeWrapper> ret = new ArrayList<>();
		IGraphNode objectNode = ((GraphNodeWrapper) arg0).getNode();
		for (FileNode fn : new ModelElementNode(objectNode).getFileNodes()) {
			ret.add(new FileNodeWrapper(fn, this));
		}
		return ret;
	}

	@Override
	public boolean hasType(String type) {
		return getTypeNodes(type).size() == 1;
	}

	@Override
	public boolean isModelElement(Object arg0) {
		return arg0 instanceof GraphNodeWrapper;
	}

	@Override
	public void load() throws EolModelLoadingException {
		if (graph != null)
			load((IModelIndexer) null);
		else
			throw new EolModelLoadingException(new Exception("load called with no graph store initialized"), this);
	}

	public void load(IModelIndexer m) throws EolModelLoadingException {
		if (m != null) {
			indexer = m;
			graph = m.getGraph();
			aliases.add(m.getName());
		}

		if (name == null) {
			name = "Model";
		}

		if (propertyGetter == null || propertyGetter.getGraph() != graph) {
			propertyGetter = createContextlessPropertyGetter();
		}

		if (graph != null) {
			try (IGraphTransaction tx = graph.beginTransaction()) {
				metamodeldictionary = graph.getMetamodelIndex();
				useOptimisableCollection = !indexer.getIndexedAttributes().isEmpty() || !indexer.getDerivedAttributes().isEmpty();

				tx.success();
			} catch (Exception e) {
				LOGGER.error("Could not retrieve the metamodel index", e);
			}
		} else {
			throw new EolModelLoadingException(new Exception("Attempt to load a model from an invalid graph: " + graph), this);
		}
	}

	@Override
	public boolean owns(Object arg0) {
		if (arg0 instanceof GraphNodeWrapper) {
			final GraphNodeWrapper gnw = (GraphNodeWrapper) arg0;
			if (gnw.getContainerModel() == null || gnw.getContainerModel() == this) {
				return true;
			} else {
				LOGGER.warn("owns failed on {} with getContainerModel(): {} and 'this': {}", arg0, gnw.getContainerModel(), this);
			}
		}

		return false;
	}

	@Override
	public boolean isOfKind(Object instance, String metaClass) throws EolModelElementTypeNotFoundException {
		return isOf(instance, metaClass, ModelElementNode::isOfKind);
	}

	@Override
	public boolean isOfType(Object instance, String metaClass) throws EolModelElementTypeNotFoundException {
		return isOf(instance, metaClass, ModelElementNode::isOfType);
	}

	private boolean isOf(Object instance, String metaClass, Function3<ModelElementNode, IGraphNode, Boolean> isOfCheck) {
		if (!(instance instanceof GraphNodeWrapper)) {
			return false;
		}
		if (ANY_TYPE.equals(metaClass)) {
			// Needed to support the 'Any' supertype in Epsilon (useful in EPL)
			return true;
		}

		final List<IGraphNode> typeNodes = getTypeNodes(metaClass);
		if (typeNodes.isEmpty()) {
			return false;
		} else {
			final GraphNodeWrapper gnw = (GraphNodeWrapper) instance;
			final ModelElementNode men = new ModelElementNode(gnw.getNode());
			return isOfCheck.apply(men, typeNodes.get(0));
		}
	}

	@Override
	public boolean knowsAboutProperty(Object instance, String property) {
		if (!owns(instance))
			return false;

		if (instance instanceof GraphNodeWrapper)
			return true;

		return false;

	}

	@Override
	public IPropertyGetter getPropertyGetter() {
		if (propertyGetter == null) {
			LOGGER.warn("null property getter, was load() called?");
		}

		return propertyGetter;
	}

	@Override
	public IPropertySetter getPropertySetter() {
		return null;
	}

	public IGraphDatabase getBackend() {
		return graph;
	}

	public List<ProxyReferenceList> getProxies() throws Exception {
		return ProxyReferenceList.getLists(graph);
	}

	public List<ProxyReferenceList> getProxies(String repositoryPrefix) throws Exception {
		return ProxyReferenceList.getLists(graph, repositoryPrefix);
	}

	public List<TypeNodeWrapper> getTypes() {
		final List<TypeNodeWrapper> nodes = new ArrayList<>();
		for (IGraphNode n : graph.getMetamodelIndex().query("*", "*")) {
			final MetamodelNode pn = new MetamodelNode(n);
			for (TypeNode tn : pn.getTypes()) {
				nodes.add(new TypeNodeWrapper(tn, this));
			}
		}
		return nodes;
	}

	public List<MetamodelNodeWrapper> getMetamodels() {
		final List<MetamodelNodeWrapper> nodes = new ArrayList<>();
		for (IGraphNode n : graph.getMetamodelIndex().query("*", "*")) {
			final MetamodelNode pn = new MetamodelNode(n);
			nodes.add(new MetamodelNodeWrapper(pn, this));
		}
		return nodes;
	}

	/**
	 * Returns the collection of all the files indexed in the graph.
	 */
	public Set<FileNodeWrapper> getFiles() {
		Set<FileNodeWrapper> allFNW = new HashSet<>();
		for (IGraphNode n : graph.allNodes(FileNode.FILE_NODE_LABEL)) {
			allFNW.add(new FileNodeWrapper(new FileNode(n), this));
		}
		return allFNW;
	}

	// deriving attributes
	@Override
	public AccessListener calculateDerivedAttributes(IModelIndexer m, Iterable<IGraphNode> nodes) {
		final boolean enableDebug = false;

		indexer = m;
		graph = m.getGraph();
		if (propertyGetter == null) {
			propertyGetter = createContextlessPropertyGetter();
		}

		try {
			load(m);
		} catch (EolModelLoadingException e2) {
			e2.printStackTrace();
		}

		// listen to accesses
		GraphPropertyGetter pg = null;
		if (!enableDebug) {
			pg = (GraphPropertyGetter) getPropertyGetter();
			pg.setBroadcastAccess(true);
		}

		// Compute the derived attributes
		final Map<String, EolModule> cachedModules = new HashMap<String, EolModule>();
		try (IGraphTransaction t = graph.beginTransaction()) {
			for (IGraphNode n : nodes) {
				final String attrType = n.getProperty(GraphModelInserter.DERIVED_ATTR_TYPE) + "";
				if (Slot.ATTR_TYPE_TIMEANNOTATION.equals(attrType)) {
					calculateTimelineAnnotation(cachedModules, n);
				} else {				
					calculateDerivedAttributes(cachedModules, n);
				}
			}
			t.success();
		} catch (Exception e) {
			LOGGER.error("Failed to compute the derived attributes", e);
		}

		if (!enableDebug) {
			pg.setBroadcastAccess(false);
			return pg.getAccessListener();
		}
		return null;
	}

	private void calculateTimelineAnnotation(Map<String, EolModule> cachedModules, IGraphNode n) {
		// TODO Auto-generated method stub
		final String derivationLogic = n.getProperty(GraphModelInserter.DERIVED_ATTR_LOGIC) + "";
		final String name = n.getIncoming().iterator().next().getType();
		try {
			Object value = new DeriveFeature().deriveTimelineAnnotation(cachedModules, indexer, n, this, derivationLogic);
			if (value instanceof Boolean && ((Boolean)value).booleanValue()) {
				final IGraphNode elementNode = n.getIncoming().iterator().next().getStartNode();
				final String idxName = n.getProperty(GraphModelInserter.DERIVED_IDXNAME_NODEPROP).toString();
				final IGraphNodeIndex idxNodeByDerivedValue = graph.getOrCreateNodeIndex(idxName);

				if (idxNodeByDerivedValue instanceof ITimeAwareGraphNodeIndex) {
					ITimeAwareGraphNodeIndex taNI = (ITimeAwareGraphNodeIndex) idxNodeByDerivedValue;
					taNI.annotate(elementNode, name);
				} else {
					idxNodeByDerivedValue.remove(elementNode, name, null);
					idxNodeByDerivedValue.add(elementNode, name, true);
				}

				// Note: this is needed in order to mark the node as "dirty" to some time-aware backends.
				elementNode.setProperty(GraphModelInserter.LAST_DERIVED_TSTAMP_NODEPROP, System.nanoTime());
			}
		} catch (Exception e) {
			LOGGER.error("Error while deriving timeline annotation " + derivationLogic, e);
		}
	}

	protected GraphPropertyGetter createContextlessPropertyGetter() {
		return new GraphPropertyGetter(graph, this);
	}

	protected void calculateDerivedAttributes(Map<String, EolModule> cachedModules, IGraphNode n) {
		for (String propKey : n.getPropertyKeys()) {
			String propValue = n.getProperty(propKey).toString();

			if (propValue.startsWith(DirtyDerivedFeaturesListener.NOT_YET_DERIVED_PREFIX)) {
				Object derived = "DERIVATION_EXCEPTION";
				try {
					derived = new DeriveFeature().deriveFeature(cachedModules, indexer, n, this, propValue);
				} catch (Exception e) {
					LOGGER.error("Error while deriving feature " + propValue, e);
				}

				// Unset the current value (if there is any)
				final String derivedEdgeLabel = ModelElementNode.DERIVED_EDGE_PREFIX + propKey;
				for (IGraphEdge edge : n.getOutgoingWithType(derivedEdgeLabel)) {
					LOGGER.debug("Clearing edge {}", edge.getType());
					edge.delete();
				}
				n.removeProperty(propKey);

				// Set the new value
				if (derived instanceof Object[] && ((Object[])derived).length > 0 && ((Object[])derived)[0] instanceof GraphNodeWrapper) {
					GraphNodeWrapper[] nodes = (GraphNodeWrapper[])derived;

					// Replace existing edges with new ones
					for (GraphNodeWrapper gw : nodes) {
						graph.createRelationship(n, gw.getNode(), derivedEdgeLabel);
					}
				} else if (derived instanceof GraphNodeWrapper) {
					GraphNodeWrapper gw = (GraphNodeWrapper) derived;
					graph.createRelationship(n, gw.getNode(), derivedEdgeLabel);
				} else if (derived != null) {
					n.setProperty(propKey, derived);
				} else {
					n.setProperty(propKey, new String[0]);
				}

				final IGraphNode elementNode = n.getIncoming().iterator().next().getStartNode();
				final String idxName = n.getProperty(GraphModelInserter.DERIVED_IDXNAME_NODEPROP).toString();
				final IGraphNodeIndex idxNodeByDerivedValue = graph.getOrCreateNodeIndex(idxName);

				// flatten multi-valued derived features for indexing
				if (derived != null) {
					if (derived.getClass().getComponentType() != null || derived instanceof Collection<?>) {
						derived = new Utils().toString(derived);
					}

					// TODO: need to test how this works with derived edges
					idxNodeByDerivedValue.remove(elementNode, propKey, null);
					idxNodeByDerivedValue.add(elementNode, propKey, derived);

					// Note: this is needed in order to mark the node as "dirty" to some time-aware backends.
					elementNode.setProperty(GraphModelInserter.LAST_DERIVED_TSTAMP_NODEPROP, System.nanoTime());
				}
			}
		}

		IGraphNodeIndex derivedProxyDictionary = graph.getOrCreateNodeIndex(GraphModelBatchInjector.DERIVED_PROXY_DICT_NAME);
		derivedProxyDictionary.remove(n);
	}

	@Override
	public String getType() {
		/*
		 * This *must* be overridden in subclasses, otherwise Hawk may pick the wrong
		 * query engine! It happened in GH issue #78.
		 */
		return TYPE;
	}

	@Override
	public List<String> validate(String derivationlogic) {
		final IEolModule module = createModule();

		final List<String> ret = new LinkedList<>();
		try {
			module.parse(derivationlogic);
			for (ParseProblem p : module.getParseProblems()) {
				ret.add(p.toString());
			}
		} catch (Exception e) {
			LOGGER.error("Error while parsing EOL", e);
		}
		return ret;
	}

	@Override
	public Object query(IModelIndexer m, String query, Map<String, Object> context)
			throws InvalidQueryException, QueryExecutionException {
		/*
		 * Check if we're in the right state: we should not use {@link
		 * IModelIndexer#waitFor} here, as that would introduce unwanted
		 * synchronisation (reducing peak throughput for some cases).
		 */
		final HawkState currentState = m.getCompositeStateListener().getCurrentState();
		if (currentState != HawkState.RUNNING) {
			throw new QueryExecutionException(
					String.format("Cannot run the query, as the indexer is not in the RUNNING state: it is %s instead.",
							currentState));
		}

		if (context != null && (context.containsKey(PROPERTY_REPOSITORYCONTEXT) || context.containsKey(PROPERTY_FILECONTEXT) || context.containsKey(PROPERTY_SUBTREECONTEXT))) {
			return contextfulQuery(m, query, context);
		} else {
			return contextlessQuery(m, query, context);
		}
	}

	protected Object contextlessQuery(IModelIndexer m, String query, Map<String, Object> context)
			throws QueryExecutionException, InvalidQueryException {
		String defaultnamespaces = null;
		if (context != null)
			defaultnamespaces = (String) context.get(PROPERTY_DEFAULTNAMESPACES);

		/*
		 * We need to always create a new engine for every query: reusing the
		 * same engine would be thread-unsafe.
		 */
		final EOLQueryEngine q = new EOLQueryEngine();
		try {
			q.load(m);
			q.setDefaultNamespaces(defaultnamespaces);
		} catch (EolModelLoadingException e) {
			throw new QueryExecutionException("Loading of EOLQueryEngine failed");
		}

		final IEolModule module = createModule();
		parseQuery(query, context, q, module);
		return q.runQuery(module);
	}

	protected Object contextfulQuery(IModelIndexer m, String query, Map<String, Object> context)
			throws QueryExecutionException, InvalidQueryException {
		CEOLQueryEngine q = new CEOLQueryEngine();
		try {
			q.load(m);
			q.setContext(context);
		} catch (EolModelLoadingException e) {
			throw new QueryExecutionException("Loading of EOLQueryEngine failed");
		}
		LOGGER.debug("Graph path: {}", graph.getPath());

		final IEolModule module = createModule();
		parseQuery(query, context, q, module);
		return q.runQuery(module);
	}

	// IQueryEngine part //////////////////////////////////////////////////////
	
	/**
	 * Query engines that add support for other Epsilon languages should redefine this method.
	 */
	protected IEolModule createModule() {
		return new EolModule();
	}

	protected void parseQuery(String query, Map<String, Object> context, final EOLQueryEngine model,
			final IEolModule module) throws InvalidQueryException {
		try {
			module.parse(query);
			if (!module.getParseProblems().isEmpty()) {
				StringBuilder sb = new StringBuilder("Query failed to parse correctly:");
				for (ParseProblem problem : module.getParseProblems()) {
					sb.append("\n");
					sb.append(problem.toString());
				}
				throw new InvalidQueryException(sb.toString());
			}
		} catch (Exception ex) {
			throw new InvalidQueryException(ex);
		}

		module.getContext().getModelRepository().addModel(model);
		addQueryArguments(context, module);
		if (context != null && context.containsKey(IQueryEngine.PROPERTY_CANCEL_CONSUMER)) {
			@SuppressWarnings("unchecked")
			final Consumer<Runnable> cancelProvider = (Consumer<Runnable>) context.get(IQueryEngine.PROPERTY_CANCEL_CONSUMER);

			final SettableExecutionController controller = new SettableExecutionController();
			module.getContext().getExecutorFactory().setExecutionController(controller);
			cancelProvider.accept(() -> {
				controller.setTerminated(true);
			});
		}
	}

	protected Object runQuery(final IEolModule module) throws QueryExecutionException {
		Object ret = null;
		try (IGraphTransaction tx = graph.beginTransaction()) {
			ret = module.execute();
			tx.success();
		} catch (EolUndefinedVariableException ex) {
			// Provide more details than Epsilon about ambiguous intra-model type references
			try (IGraphTransaction tx = graph.beginTransaction()) {
				/*
				 * Use the same EOLQueryEngine as in the model - if using a separate object
				 * (e.g. the time-aware one) this EOLQueryEngine might not be loaded.
				 */
				final EOLQueryEngine eolQuery = (EOLQueryEngine) module.getContext().getModelRepository().getModels().get(0);
				final List<IGraphNode> typeNodes = eolQuery.getTypeNodes(ex.getVariableName());

				final StringBuilder sb = new StringBuilder("Ambiguous type reference '" + ex.getVariableName() + "' across these metamodels:\n");
				for (IGraphNode typeNode : typeNodes) {
					sb.append("\n* ");
					sb.append(new TypeNode(typeNode).getMetamodelURI());
				}
				sb.append("\n\nSpecify the desired one in the default namespaces to resolve the ambiguity.");
				tx.success();

				if (typeNodes.size() > 1) {
					throw new QueryExecutionException(sb.toString());
				} else {
					throw new QueryExecutionException(ex);
				}
			} catch (QueryExecutionException e) {
				throw e;
			} catch (Exception e) {
				throw new QueryExecutionException(e);
			}
		} catch (Exception e) {
			throw new QueryExecutionException(e);
		} finally {
			module.getContext().getModelRepository().dispose();
			module.getContext().dispose();
			module.getContext().setOperationFactory(null);
			typeNodesCache.clear();

			propertyGetter = null;
			metamodeldictionary = null;
		}

		return ret;
	}

	@SuppressWarnings("unchecked")
	protected void addQueryArguments(Map<String, Object> context, final IEolModule module) {
		if (context != null) {
			final Map<String, Object> args = (Map<String, Object>) context.get(PROPERTY_ARGUMENTS);
			if (args != null) {
				for (Entry<String, Object> entry : args.entrySet()) {
					module.getContext().getFrameStack().putGlobal(new Variable(entry.getKey(), entry.getValue(), null));
				}
			}
		}
	}

	@Override
	public void setDefaultNamespaces(String namespaces) {
		// set default packages if applicable
		try {
			defaultNamespaces = new HashSet<String>();

			if (namespaces != null && !namespaces.trim().equals("")) {
				String[] eps = ((String) namespaces).split(",");
				for (String s : eps) {
					defaultNamespaces.add(s.trim());
				}
			}

		} catch (Throwable t) {
			LOGGER.error("Setting default namespaces failed, malformed property: " + namespaces, t);
		}
	}

	@Override
	public String getHumanReadableName() {
		return "EOL Query Engine";
	}

	/**
	 * Wraps the provided node for its use in EOL.
	 *
	 * TODO change return type to {@link IGraphNodeReference}
	 */
	public GraphNodeWrapper wrap(IGraphNode node) {
		return new GraphNodeWrapper(node, this);
	}
}
