/*******************************************************************************
 * Copyright (c) 2011-2020 The University of York.
 * 
 * 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 - protect against null EPackage nsURIs
 ******************************************************************************/
package org.eclipse.hawk.graph.updater;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.eclipse.hawk.core.IMetaModelResourceFactory;
import org.eclipse.hawk.core.IModelIndexer;
import org.eclipse.hawk.core.graph.IGraphChangeListener;
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.IGraphTransaction;
import org.eclipse.hawk.core.graph.timeaware.ITimeAwareGraphDatabase;
import org.eclipse.hawk.core.model.IHawkAttribute;
import org.eclipse.hawk.core.model.IHawkClass;
import org.eclipse.hawk.core.model.IHawkClassifier;
import org.eclipse.hawk.core.model.IHawkDataType;
import org.eclipse.hawk.core.model.IHawkMetaModelResource;
import org.eclipse.hawk.core.model.IHawkObject;
import org.eclipse.hawk.core.model.IHawkPackage;
import org.eclipse.hawk.core.model.IHawkReference;
import org.eclipse.hawk.core.model.IHawkStructuralFeature;
import org.eclipse.hawk.core.runtime.CompositeGraphChangeListener;
import org.eclipse.hawk.core.util.CompositeException;
import org.eclipse.hawk.graph.FileNode;
import org.eclipse.hawk.graph.ModelElementNode;
import org.eclipse.hawk.graph.Slot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GraphMetaModelResourceInjector {
	private static final Logger LOGGER = LoggerFactory.getLogger(GraphMetaModelResourceInjector.class);

	/**
	 * Represents the case when the insertion of a metamodel fails.
	 */
	public static class FailedMetamodelRegistrationException extends Exception {
		private static final long serialVersionUID = 1L;

		public FailedMetamodelRegistrationException(String metamodelURI, Throwable reason) {
			super(String.format("Failed to register metamodel '%s': %s", metamodelURI, reason.getMessage()), reason);
		}
	}

	/**
	 * Represents the situation when a particular metamodel is missing, so a certain
	 * EClass cannot be registered.
	 */
	public static class MissingMetamodelException extends Exception {
		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;

		public MissingMetamodelException(String metamodelURI) {
			super(String.format("Missing metamodel with URI '%s'", metamodelURI));
		}
	}

	// integer array containing the current number of added elements:
	// (element,((ofType)M->MM)reference,((ofKind)M->MM)reference,(unset(M->M))reference)
	private int objectCount = 0;

	private IModelIndexer hawk;
	private IGraphDatabase graph;

	private final long startTime = System.nanoTime();
	private final CompositeGraphChangeListener listener;

	public GraphMetaModelResourceInjector(IModelIndexer hawk, Set<IHawkMetaModelResource> set, CompositeGraphChangeListener listener) throws Exception {
		this.hawk = hawk;
		this.graph = hawk.getGraph();
		this.listener = listener;

		LOGGER.info("ADDING METAMODELS: ");
		LOGGER.info("ADDING: ");

		int nodes;
		if (graph instanceof ITimeAwareGraphDatabase) {
			// For time-aware databases, we have to go to timepoint 0 so the metamodels
			// always exist, then return to the current timepoint.
			final ITimeAwareGraphDatabase tadb = (ITimeAwareGraphDatabase)graph;
			final long currentTime = tadb.getTime();
			try {
				tadb.setTime(0);
				nodes = insertMetamodels(set);
			} finally {
				tadb.setTime(currentTime);
			}
		} else {
			nodes = insertMetamodels(set);
		}

		LOGGER.info("{} METAMODEL NODES! (took ~{} sec)", nodes, (System.nanoTime() - startTime) / 1_000_000_000);
	}

	public GraphMetaModelResourceInjector(IModelIndexer hawk, CompositeGraphChangeListener listener) {
		this.hawk = hawk;
		this.graph = hawk.getGraph();
		this.listener = listener;
	}

	/**
	 * Removes all the metamodel resources and their dependent model elements from the index.
	 */
	public void removeMetamodels(Set<IHawkMetaModelResource> set) {

		try (IGraphTransaction t = graph.beginTransaction()) {
			listener.changeStart();
			IGraphNodeIndex ePackageDictionary = graph.getMetamodelIndex();
			Set<IGraphNode> epns = new HashSet<>();

			for (IHawkMetaModelResource metamodelResource : set) {
				// if (resourceset == null)
				// resourceset = metamodelResource.getResourceSet();

				Set<IHawkObject> children = metamodelResource.getAllContents();

				for (IHawkObject child : children) {

					// if (child.eIsProxy()) {
					// throw new Exception("FAILED. PROXY UNRESOLVED: "
					// + ((InternalEObject) child).eProxyURI()
					// .fragment());
					// }

					// add the element
					if (child instanceof IHawkPackage) {

						Iterator<? extends IGraphNode> it = ePackageDictionary.get("id", ((IHawkPackage) child).getNsURI()).iterator();

						if (!it.hasNext()) {
							LOGGER.warn("Metamodel: {} with uri: {} not indexed. Nothing happened.",
									((IHawkPackage) child).getName(), ((IHawkPackage) child).getNsURI());
						} else {
							IGraphNode epn = it.next();
							LOGGER.info("Removing metamodel: {} with uri: {}", ((IHawkPackage) child).getName(), ((IHawkPackage) child).getNsURI());
							epns.add(epn);
						}
					}
				}
			}

			removeAll(epns);

			t.success();
			listener.changeSuccess();

		} catch (Exception e1) {
			listener.changeFailure();
			LOGGER.error("Error in removing metamodels (all removal changes reverted)", e1);
		}

	}

	/**
	 * Removes all the metamodels and models dependent on the <code>epsn</code> metamodel nodes.
	 * 
	 * @return URIs of the repositories impacted by the removal.
	 */
	private Set<String> removeAll(Set<IGraphNode> epns) throws Exception {

		Set<String> affectedRepositories = new HashSet<>();

		DeletionUtils del = new DeletionUtils(graph);

		for (IGraphNode epn : epns)
			for (IGraphEdge rel : epn.getIncomingWithType(IModelIndexer.METAMODEL_DEPENDENCY_EDGE)) {
				LOGGER.debug("dependency from {} to {}",
						rel.getStartNode().getProperty(IModelIndexer.IDENTIFIER_PROPERTY),
						epn.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));

				// delete all dependent metamodels and models
				IGraphNode depmm = rel.getStartNode();
				del.delete(rel);
				epns.add(depmm);
			}

		Set<IGraphNode> files = new HashSet<>();

		for (IGraphNode n : epns)
			files.addAll(remove(n));

		for (IGraphNode file : files)
			affectedRepositories.add(file.getProperty(FileNode.PROP_REPOSITORY).toString());

		for (IGraphNode file : files)
			del.delete(file);

		return affectedRepositories;

	}

	private Set<IGraphNode> remove(IGraphNode epn) throws Exception {

		long start = System.currentTimeMillis();

		HashSet<IGraphNode> fileNodes = new HashSet<IGraphNode>();

		try (IGraphTransaction transaction = graph.beginTransaction()) {
			LOGGER.info("Deleting nodes from metamodel: {}", epn.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));

			HashSet<IGraphNode> metaModelElements = new HashSet<IGraphNode>();
			HashSet<IGraphNode> modelElements = new HashSet<IGraphNode>();

			DeletionUtils del = new DeletionUtils(graph);

			for (IGraphEdge rel : epn.getIncomingWithType("epackage")) {
				metaModelElements.add(rel.getStartNode());
				del.delete(rel);
			}

			for (IGraphEdge rel : epn.getOutgoingWithType(IModelIndexer.METAMODEL_DEPENDENCY_EDGE)) {
				del.delete(rel);
			}

			del.delete(epn);

			for (IGraphNode metamodelelement : metaModelElements) {
				for (IGraphEdge rel : metamodelelement.getIncomingWithType(ModelElementNode.EDGE_LABEL_OFTYPE)) {
					modelElements.add(rel.getStartNode());
					del.delete(rel);
				}
			}
			for (IGraphNode metamodelelement : metaModelElements) {
				for (IGraphEdge rel : metamodelelement.getIncomingWithType(ModelElementNode.EDGE_LABEL_OFKIND)) {
					modelElements.add(rel.getStartNode());
					del.delete(rel);
				}
			}

			for (IGraphNode metaModelElement : metaModelElements)
				del.delete(metaModelElement);

			// remove model elements and update derived attributes

			if (modelElements.size() > 0) {
				LOGGER.info("Deleting nodes from relevant models...");

				Set<IGraphNode> toBeUpdated = new HashSet<>();
				final DirtyDerivedFeaturesListener l = new DirtyDerivedFeaturesListener(graph);
				if (!hawk.getDerivedAttributes().isEmpty()) {
					hawk.addGraphChangeListener(l);
				}

				for (IGraphNode modelElement : modelElements) {
					Iterator<IGraphEdge> it = modelElement.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE)
							.iterator();
					if (it.hasNext()) {
						IGraphEdge e = it.next();
						fileNodes.add(e.getEndNode());
						del.delete(e);

					}

					del.dereference(modelElement, listener, null);
				}

				for (IGraphNode modelElement : modelElements) {

					del.delete(modelElement);

				}

				toBeUpdated.addAll(l.getNodesToBeUpdated());
				listener.remove(l);

				LOGGER.info("Updating any relevant derived attributes...");
				try {
					new GraphModelInserter(hawk, () -> del, new TypeCache(hawk))
							.updateDerivedAttributes(hawk.getDerivedAttributeExecutionEngine(), toBeUpdated);
					toBeUpdated = new HashSet<>();
				} catch (Exception e) {
					toBeUpdated = new HashSet<>();
					LOGGER.error("Exception while updating derived attributes", e);
				}

			}

			transaction.success();
		}

		LOGGER.info("deleted all, took: {}s", (System.currentTimeMillis() - start) / 1000.0);
		return fileNodes;
	}

	/**
	 * Inserts a number of metamodels into the graph.
	 * 
	 * @param metamodels Metamodel resources to be registered.
	 * @return Number of added objects.
	 * @throws A single exception, or a {@link CompositeException} with multiple
	 *           exceptions that have happened for this set of metamodels.
	 */
	private int insertMetamodels(Set<IHawkMetaModelResource> metamodels) throws Exception {
		// Expand set with dependencies if needed
		Map<String, IHawkPackage> expanded = new HashMap<>();
		expandWithDependencies(metamodels, expanded);

		// TODO: simplify rest of code after testing early dependency detection

		// Create the EPackage nodes
		Set<IHawkPackage> addedPackages = new HashSet<>();
		try (IGraphTransaction t = graph.beginTransaction()) {
			listener.changeStart();
			IGraphNodeIndex ePackageDictionary = graph.getMetamodelIndex();

			for (IHawkPackage pkg : expanded.values()) {
				if (addEPackage(pkg, ePackageDictionary)) {
					addedPackages.add(pkg);
				}
			}

			t.success();
			listener.changeSuccess();
		} catch (Exception ex) {
			listener.changeFailure();
			throw ex;
		}

		// Create the EClass nodes inside each EPackage
		List<Exception> exceptions = new ArrayList<>();
		Iterator<IHawkPackage> itPackage = addedPackages.iterator();
		while (itPackage.hasNext()) {
			IHawkPackage epackage = itPackage.next();

			boolean revert = false;
			final IGraphTransaction t = graph.beginTransaction();
			try {
				listener.changeStart();
				addEClasses(epackage);
				t.success();
				listener.changeSuccess();
			} catch (Exception eAddPackage) {
				LOGGER.error(eAddPackage.getMessage(), eAddPackage);
				t.failure();
				listener.changeFailure();
				exceptions.add(new FailedMetamodelRegistrationException(epackage.getNsURI(), eAddPackage));
				itPackage.remove();
				revert = true;
			} finally {
				t.close();
			}

			if (revert) {
				// Revert the addition of the metamodel node
				try (IGraphTransaction t2 = graph.beginTransaction()) {
					IGraphNode ePackageNode = graph.getMetamodelIndex().get("id", epackage.getNsURI()).iterator().next();
					new DeletionUtils(graph).delete(ePackageNode);
					t2.success();
				} catch (Exception eRevertAddition) {
					LOGGER.error(eRevertAddition.getMessage(), eRevertAddition);
					exceptions.add(eRevertAddition);
				}
			}
		}

		for (IHawkPackage ePackage : addedPackages) {
			try (IGraphTransaction t = graph.beginTransaction()) {
				final IGraphNode epackagenode = graph.getMetamodelIndex().get("id", ePackage.getNsURI()).getSingle();

				// add resource to package
				final Optional<String> s = ePackage.getResource().getMetaModelResourceFactory().dumpPackageToString(ePackage);
				if (s.isPresent()) {
					epackagenode.setProperty(IModelIndexer.METAMODEL_RESOURCE_PROPERTY, s.get());
				}
				t.success();
			} catch (Exception eSaveResource) {
				LOGGER.error("Cannot save package into the metamodel node", eSaveResource);
				exceptions.add(eSaveResource);
			}
		}

		// Throw any collected exceptions if we have them
		if (!exceptions.isEmpty()) {
			throw new CompositeException(exceptions);
		}

		return objectCount;
	}

	/**
	 * Performs dependency resolution on a set of metamodels, by applying a
	 * fixed-point algorithm where we keep adding metamodels as we find new
	 * dependencies, until we do not have any pending metamodels or we run into a
	 * metamodel that we cannot obtain.
	 * 
	 * If we successfully exit from this method, it means that the value set of
	 * {@code expanded} is complete, perhaps reusing some packages that are already
	 * in the graph.
	 * 
	 * @throws MissingMetamodelException A metamodel that this set depends upon
	 *                                   could not be found.
	 * @throws Exception                 Another type of exception occurred during
	 *                                   the dependency resolution. This will most
	 *                                   likely be an issue with the node indices.
	 */
	private void expandWithDependencies(Set<IHawkMetaModelResource> metamodels, Map<String, IHawkPackage> expanded) throws Exception {
		final Set<String> alreadyInGraph = new HashSet<>();

		boolean firstBatch = false;
		do {
			for (Iterator<IHawkMetaModelResource> itM = metamodels.iterator(); itM.hasNext();) {
				IHawkMetaModelResource m = itM.next();
				for (IHawkObject child : m.getAllContents()) {
					if (child instanceof IHawkPackage) {
						final IHawkPackage pkg = (IHawkPackage) child;
						if (pkg.getNsURI() == null) {
							LOGGER.info("Package {} has null URI, ignoring", pkg.getName());
						} else if (firstBatch && isMetamodelRegistered(pkg.getNsURI())) {
							// First round: the graph may already have some of the provided metamodels 
							alreadyInGraph.add(pkg.getNsURI());
							itM.remove();
							LOGGER.info("Package {} already in the graph, ignoring", pkg.getNsURI());
						} else {
							expanded.put(pkg.getNsURI(), pkg);
						}
					}
				}
			}
			firstBatch = false;

			// Find out which metamodels this batch requires that we are missing so far
			final Set<String> missingURIs = new HashSet<>();
			addPendingDependencies(metamodels, expanded, missingURIs, alreadyInGraph);

			// Try to locate the missing metamodels, or abort
			metamodels.clear();
			missingMetamodels:
			for (String uri : missingURIs) {
				for (IMetaModelResourceFactory mp : hawk.getMetaModelParsers()) {
					IHawkMetaModelResource mr = mp.getMetamodel(uri);
					if (mr != null) {
						metamodels.add(mr);
						continue missingMetamodels;
					}
				}
				throw new MissingMetamodelException(uri);
			}

		} while (!metamodels.isEmpty());

	}

	private void addPendingDependencies(Set<IHawkMetaModelResource> metamodels, Map<String, IHawkPackage> expanded, final Set<String> missingURIs, final Set<String> alreadyInGraph) throws Exception {
		for (IHawkMetaModelResource m : metamodels) {
			for (IHawkObject child : m.getAllContents()) {
				if (child instanceof IHawkPackage) {
					final IHawkPackage pkg = (IHawkPackage) child;
					for (IHawkClassifier pkgClassifier : pkg.getClasses()) {
						if (pkgClassifier instanceof IHawkClass) {
							IHawkClass eClass = (IHawkClass) pkgClassifier;

							for (IHawkClass st : eClass.getAllSuperTypes()) {
								addPendingDependencies(expanded, missingURIs, alreadyInGraph, st);
							}
							for (IHawkAttribute e : eClass.getAllAttributes()) {
								if (e.getType() != null) {
									addPendingDependencies(expanded, missingURIs, alreadyInGraph, e.getType());
								}
							}
							for (IHawkReference r : eClass.getAllReferences()) {
								addPendingDependencies(expanded, missingURIs, alreadyInGraph, r.getType());
							}							
						}
					}
				}
			}
		}
	}

	private void addPendingDependencies(Map<String, IHawkPackage> toBeRegistered, final Set<String> missingURIs, final Set<String> alreadyInGraph, IHawkClassifier targetType) throws Exception {
		final String nsURI = targetType.getPackageNSURI();
		if (nsURI != null && !toBeRegistered.containsKey(nsURI) && !missingURIs.contains(nsURI) && !alreadyInGraph.contains(nsURI)) {
			if (!isMetamodelRegistered(nsURI)) {
				// Not in the set to be registered, not pending yet, and not in the graph: mark as missing
				missingURIs.add(nsURI);
			} else {
				// Not in the set to be registered, but it is already in the graph
				alreadyInGraph.add(nsURI);
			}
		}
	}

	private boolean isMetamodelRegistered(String nsURI) throws Exception {
		boolean ret = false;
		try (IGraphTransaction tx = graph.beginTransaction()) {
			final IGraphIterable<? extends IGraphNode> iterMatches = graph.getMetamodelIndex().get("id", nsURI);
			ret = iterMatches.iterator().hasNext();
			tx.success();
		}
		return ret;
	}

	private void addEClasses(IHawkPackage ePackage) throws MissingMetamodelException {
		for (IHawkClassifier child : ePackage.getClasses()) {
			if (child instanceof IHawkClass) {
				addMetaClass((IHawkClass) child);
			} else if (child instanceof IHawkDataType) {
				// FIXME need to handle datatypes?
				if (child.getInstanceType() != null) {
					LOGGER.warn(
						"Hawk does not support custom data types yet: {}::{} will be handled as its instance type {}",
						ePackage.getNsURI(), child.getName(), child.getInstanceType());
				} else {
					LOGGER.warn(
						"Hawk does not support custom data types yet: {}::{} will be handled as a string",
						ePackage.getNsURI(), child.getName());
				}
			} else {
				LOGGER.error("Unknown classifier: ({}): {}", child.getName(), child.getClass());
			}
		}
	}

	/**
	 * Returns {@code true} iff we added a new package node to the graph.
	 */
	private boolean addEPackage(IHawkPackage ePackage, IGraphNodeIndex ePackageDictionary) throws IOException {
		final String uri = ePackage.getNsURI();
		if (uri == null) {
			LOGGER.warn("ePackage {} has null nsURI, ignoring", ePackage);
			return false;
		}

		if (ePackageDictionary.get("id", uri).iterator().hasNext() == false) {
			Map<String, Object> fields = new HashMap<>();
			fields.put(IModelIndexer.IDENTIFIER_PROPERTY, uri);
			fields.put(IModelIndexer.METAMODEL_NAME_PROPERTY, ePackage.getName());
			fields.put(IModelIndexer.METAMODEL_TYPE_PROPERTY, ePackage.getResource().getMetaModelResourceFactory().getType());
			IGraphNode epackagenode = graph.createNode(fields, "epackage");

			ePackageDictionary.add(epackagenode, "id", uri);
			listener.metamodelAddition(ePackage, epackagenode);
			return true;
		} else {
			LOGGER.warn(
				"metamodel: {} ({}) already in store, updating it instead NYI -- doing nothing!",
				(ePackage).getName(), uri);
			return false;
		}
	}

	/**
	 * 
	 * @param eClass
	 * @return the URI ID of an eClass
	 */
	public String getEObjectId(IHawkClass eClass) {
		return eClass.getPackageNSURI() + "/" + eClass.getName();
	}

	/**
	 * Creates a node in the graph database with the given eClass's attributes
	 * in it.
	 * 
	 * @param eObject
	 * @param id
	 * @return the Node
	 */
	private void createEClassNode(IHawkClass eClass, String id) throws MissingMetamodelException {
		final Map<String, Object> map = new HashMap<>();
		map.put(IModelIndexer.IDENTIFIER_PROPERTY, id);

		final IGraphNode node = graph.createNode(new HashMap<String, Object>(), "eclass");
		final IGraphNode metamodelNode = graph.getMetamodelIndex().get("id", eClass.getPackageNSURI()).getSingle();
		graph.createRelationship(node, metamodelNode, "epackage");
		
		for (IHawkClass superType : eClass.getAllSuperTypes()) {
			addMetamodelDependency(eClass, metamodelNode, superType);
		}

		for (IHawkAttribute e : eClass.getAllAttributes()) {
			if (e.getType() != null) {
				addMetamodelDependency(eClass, metamodelNode, e.getType());
			}

			String[] metadata = new Slot.MetadataBuilder()
					.attribute()
					.many(e.isMany())
					.ordered(e.isOrdered())
					.unique(e.isUnique())
					.type(getPropertyType(e))
					.build();

			map.put(e.getName(), metadata);
		}

		for (IHawkReference r : eClass.getAllReferences()) {
			addMetamodelDependency(eClass, metamodelNode, r.getType());

			String[] metadata = new Slot.MetadataBuilder()
					.reference()
					.many(r.isMany())
					.ordered(r.isOrdered())
					.unique(r.isUnique())
					.type(getPropertyType(r))
					.build();

			map.put(r.getName(), metadata);
		}

		for (String s : map.keySet()) {
			node.setProperty(s, map.get(s));
		}

		listener.classAddition(eClass, node);
	}

	private String getPropertyType(IHawkStructuralFeature e) {
		String propType = "unknown";
		if (e.getType() != null) {
			if (e.getType().getName() != null) {
				// System.err.println(e.getType().getName());
				propType = e.getType().getInstanceType();
			} else {
				LOGGER.warn(
					"warning: unknown (null) type NAME found in metamodel parsing into db for attribute: {} of type: {}",
					e.getName(), e.getType());
			}
		} else {
			LOGGER.warn("warning: unknown (null) type found in metamodel parsing into db for attribute: {}", e.getName());
		}
		return propType;
	}

	private void addMetamodelDependency(IHawkClass sourceType, final IGraphNode metamodelNode, IHawkClassifier targetType) throws MissingMetamodelException {
		final String targetTypeMetamodelURI = targetType.getPackageNSURI();
		final IGraphIterable<? extends IGraphNode> metamodels = graph.getMetamodelIndex().get("id", targetTypeMetamodelURI);

		if (metamodels.iterator().hasNext() == false) {
			LOGGER.error(
					"EClass {} has supertype {} which is in a package not registered yet, "
							+ "reverting all changes to this package registration, please register "
							+ "package with URI {} first",
					sourceType.getName(), targetType.getName() == null ? targetType.getUri() : targetType.getName(), targetTypeMetamodelURI);

			throw new MissingMetamodelException(targetTypeMetamodelURI);
		} else if (!targetTypeMetamodelURI.equals(sourceType.getPackageNSURI())) {
			final IGraphNode superTypeEPackage = metamodels.getSingle();
			if (!metamodelDependencyExists(metamodelNode, superTypeEPackage)) {
				LOGGER.debug("supertype dependency from {} to {}", sourceType.getPackageNSURI(), targetTypeMetamodelURI);
				graph.createRelationship(metamodelNode, superTypeEPackage, IModelIndexer.METAMODEL_DEPENDENCY_EDGE);
			}
		}
	}

	private boolean metamodelDependencyExists(final IGraphNode metamodelNode, final IGraphNode superTypeEPackage) {
		boolean alreadythere = false;
		Iterable<IGraphEdge> depEdges = metamodelNode.getOutgoingWithType(IModelIndexer.METAMODEL_DEPENDENCY_EDGE);
		for (IGraphEdge r : depEdges) {
			if (r.getEndNode().equals(superTypeEPackage)) {
				alreadythere = true;
				break;
			}
		}
		return alreadythere;
	}

	/**
	 * Turns an {@link IHawkClass} into a graph node.
	 * 
	 * @param eClass Class to be added.
	 * @throws MissingMetamodelException A required metamodel is missing from the graph.
	 */
	private void addMetaClass(IHawkClass eClass) throws MissingMetamodelException {
		String id = eClass.getName();
		objectCount++;
		createEClassNode(eClass, id);
	}

	/**
	 * Adds a new derived attribute to a type. Returns whether propagation to
	 * current instances of this type is necessary.
	 * 
	 * @param metamodeluri
	 * @param typename
	 * @param attributename
	 * @param isMany
	 * @param isOrdered
	 * @param isUnique
	 * @param attributetype
	 * @param derivationlanguage
	 * @param derivationlogic
	 * @param graph
	 * @return
	 */
	public static boolean addDerivedAttribute(String metamodeluri, String typename, String attributename,
			boolean isMany, boolean isOrdered, boolean isUnique, String attributetype, String derivationlanguage,
			String derivationlogic, IGraphDatabase graph, IGraphChangeListener listener) {

		boolean requiresPropagationToInstances = false;

		try (IGraphTransaction t = graph.beginTransaction()) {
			listener.changeStart();
			IGraphIterable<? extends IGraphNode> ep = graph.getMetamodelIndex().get("id", metamodeluri);

			IGraphNode packagenode = null;

			if (ep.size() == 1) {
				packagenode = ep.getSingle();
			} else {
				throw new Exception("metamodel not found:" + metamodeluri);
			}

			IGraphNode typenode = null;

			for (IGraphEdge e : packagenode.getIncomingWithType("epackage")) {
				if (e.getStartNode().getProperty(IModelIndexer.IDENTIFIER_PROPERTY).equals(typename)) {
					typenode = e.getStartNode();
					break;
				}
			}

			if (typenode == null) {
				LOGGER.error("type: {} in: {} does not exist, aborting operation: addDerivedAttribute", typename, metamodeluri);
			} else {
				/*
				 * at least one instance already present so derived attribute
				 * needs to be reconfigured for each element already present
				 * (whether the derived attribute is new or existed already and
				 * is being updated)
				 */
				if (typenode.getIncomingWithType(ModelElementNode.EDGE_LABEL_OFTYPE).iterator().hasNext()
						|| typenode.getIncomingWithType(ModelElementNode.EDGE_LABEL_OFKIND).iterator().hasNext())
					requiresPropagationToInstances = true;

				String[] metadata = new String[7];
				metadata[0] = "d";
				metadata[1] = (isMany ? "t" : "f");
				metadata[2] = (isOrdered ? "t" : "f");
				metadata[3] = (isUnique ? "t" : "f");
				metadata[4] = attributetype;
				metadata[5] = derivationlanguage;
				metadata[6] = derivationlogic;

				if (typenode.getProperty(attributename) != null) {
					LOGGER.warn("Attribute already derived, nothing happened!");
					requiresPropagationToInstances = false;
				} else {
					typenode.setProperty(attributename, metadata);
					if (LOGGER.isInfoEnabled()) {
						final String logic = (derivationlogic.length() > 100
								? derivationlogic.substring(0, 100) + "\n[! long script, snipped !]"
								: derivationlogic);

						LOGGER.info(
								"Derived attribute added: {}::{}#{} (isMany={}|isOrdered={}|isUnique={}|type={}) {}#\n{}",
								metamodeluri, typename, attributename,
								isMany, isOrdered, isUnique, attributetype, derivationlanguage,
								logic);
					}
				}

				/** Must create the empty index so it will be known by the {@link ModelIndexerImpl} */
				graph.getOrCreateNodeIndex(metamodeluri + "##" + typename + "##" + attributename);
			}
			t.success();
			listener.changeSuccess();
		} catch (Exception e1) {
			LOGGER.error("error in adding a derived attribute to the metamodel", e1);
			listener.changeFailure();
		}

		return requiresPropagationToInstances;
	}

	/**
	 * Adds a new indexed attribute to a type. Returns whether propagation to
	 * current instances of this type is necessary.
	 * 
	 * @param metamodeluri
	 * @param typename
	 * @param attributename
	 * @param graph
	 * @param listener2
	 * @return
	 */
	public static boolean addIndexedAttribute(String metamodeluri, String typename, String attributename,
			IGraphDatabase graph, IGraphChangeListener listener) {

		boolean requiresPropagationToInstances = false;

		try (IGraphTransaction t = graph.beginTransaction()) {
			listener.changeStart();
			IGraphNode packagenode = graph.getMetamodelIndex().get("id", metamodeluri).getSingle();

			IGraphNode typenode = null;

			for (IGraphEdge e : packagenode.getIncomingWithType("epackage")) {
				if (e.getStartNode().getProperty(IModelIndexer.IDENTIFIER_PROPERTY).equals(typename)) {
					typenode = e.getStartNode();
					break;
				}
			}

			if (typenode == null) {
				LOGGER.error("type: {} in: {} does not exist, aborting operation: addIndexedAttribute", typename, metamodeluri);
			} else {

				// at least one instance already present so indexed attribute
				// needs
				// to be reconfigured for each element already present (whether
				// the
				// indexed attribute is new or existed already and is being
				// updated)

				String[] metadata = (String[]) typenode.getProperty(attributename);

				if (metadata == null) {
					LOGGER.error("attribute: {} in: {}#{} does not exist, aborting operation: addIndexedAttribute", attributename, metamodeluri, typename);
				} else if (!metadata[0].equals("a")) {
					LOGGER.error("{}#{} is not an attribute, aborting operation: addIndexedAttribute", metamodeluri, typename);
				} else {
					if (typenode.getIncomingWithType(ModelElementNode.EDGE_LABEL_OFTYPE).iterator().hasNext()
							|| typenode.getIncomingWithType(ModelElementNode.EDGE_LABEL_OFKIND).iterator().hasNext())
						requiresPropagationToInstances = true;

					if (metadata.length == 6) {
						if ("t".equals(metadata[5])) {
							LOGGER.warn("attribute already indexed, nothing happened!");
							requiresPropagationToInstances = false;
						} else {
							metadata[5] = "t";
							typenode.setProperty(attributename, metadata);
							//
							graph.getOrCreateNodeIndex(metamodeluri + "##" + typename + "##" + attributename);
							//
							LOGGER.info("indexed attribute added: {}::{}#{}", metamodeluri, typename, attributename);
						}
					} else if (metadata.length == 7) {
						LOGGER.warn("derived attributes are already indexed, nothing happened.");
					} else {
						// FIXME log actual exception
						LOGGER.error("unknown exception in addIndexedAttribute of GraphMetamodelResourceInjector");
					}
				}
			}
			t.success();
			listener.changeSuccess();
		} catch (Exception e) {
			LOGGER.error("Error in adding an indexed attribute", e);
			listener.changeFailure();
		}

		return requiresPropagationToInstances;

	}

	/**
	 * Removes all the metamodels with the specified URIs.
	 * 
	 * @return URIs of the repositories impacted by the removal.
	 */
	public Set<String> removeMetamodels(String[] mmuris) {

		Set<String> ret = new HashSet<>();

		try (IGraphTransaction t = graph.beginTransaction()) {

			Set<String> indexNames = graph.getNodeIndexNames();
			Set<IGraphNodeIndex> markedForRemoval = new HashSet<>();

			listener.changeStart();
			IGraphNodeIndex ePackageDictionary = graph.getMetamodelIndex();

			Set<IGraphNode> epns = new HashSet<>();
			for (String mmuri : mmuris) {
				try {

					for (Iterator<String> it = indexNames.iterator(); it.hasNext();) {
						String s = it.next();
						if (s.startsWith(mmuri + "##")) {
							IGraphNodeIndex i = graph.getOrCreateNodeIndex(s);
							markedForRemoval.add(i);
							it.remove();
						}
					}

					epns.add(ePackageDictionary.get("id", mmuri).getSingle());
				} catch (Exception e) {
					LOGGER.error("Metamodel with URI " + mmuri + " not indexed. Nothing happened.", e);
				}
			}
			if (epns.size() > 0) {
				LOGGER.info("Removing metamodels with URIs {}", Arrays.toString(mmuris));
				ret = removeAll(epns);
			}

			for (IGraphNodeIndex i : markedForRemoval) {
				LOGGER.info("Deleting index {} as its metamodel was removed.", i.getName());
				i.delete();
			}

			t.success();
			listener.changeSuccess();
		} catch (Exception e) {
			listener.changeFailure();
			LOGGER.error("Error in removing metamodels " + Arrays.toString(mmuris) + "\n(ALL removal changes reverted)", e);
		}
		return ret;
	}

	public static boolean removeIndexedAttribute(String metamodelUri, String typename, String attributename,
			IGraphDatabase graph, CompositeGraphChangeListener listener) {

		boolean found = false;

		try (IGraphTransaction t = graph.beginTransaction()) {
			listener.changeStart();

			IGraphNode packagenode = null;
			IGraphIterable<? extends IGraphNode> ep = graph.getMetamodelIndex().get("id", metamodelUri);

			if (ep.size() == 1) {
				packagenode = ep.getSingle();
			} else {
				throw new Exception("metamodel not found:" + metamodelUri);
			}

			IGraphNode typenode = null;

			for (IGraphEdge e : packagenode.getIncomingWithType("epackage")) {
				if (e.getStartNode().getProperty(IModelIndexer.IDENTIFIER_PROPERTY).equals(typename)) {
					typenode = e.getStartNode();
					break;
				}
			}

			if (typenode == null) {
				LOGGER.error(
					"type: {} in: {} does not exist, aborting operation: removeIndexedAttribute",
					typename, metamodelUri
				);
				listener.changeFailure();
			} else {
				String[] metadata = (String[]) typenode.getProperty(attributename);

				if (metadata == null) {
					LOGGER.error("attribute: {} in: {}::{} does not exist, aborting operation: removeIndexedAttribute",
							attributename, metamodelUri, typename);
					listener.changeFailure();
				} else if (!metadata[0].equals("a")) {
					// System.err.println(Arrays.toString(metadata));
					LOGGER.error("{}::{} is a reference not an attribute, aborting operation: removeIndexedAttribute",
							metamodelUri, typename);
					listener.changeFailure();
				} else {

					if (metadata.length == 6) {
						if ("t".equals(metadata[5])) {
							metadata[5] = "f";
							typenode.setProperty(attributename, metadata);
							//
							String indexname = metamodelUri + "##" + typename + "##" + attributename;

							if (graph.nodeIndexExists(indexname)) {
								graph.getOrCreateNodeIndex(indexname).delete();
								found = true;
							}

							t.success();
							listener.changeSuccess();
						} else {
							LOGGER.error("attribute was not indexed, nothing happened!");
							listener.changeFailure();
						}

					} else {
						LOGGER.error("error in removeIndexedAttribute (metadata.length!=6), nothing happened!");
						listener.changeFailure();
					}
				}

			}

		} catch (Exception e) {
			LOGGER.error("Error while removing an indexed attribute", e);
			listener.changeFailure();
		}
		return found;

	}

	public static boolean removeDerivedAttribute(String metamodelUri, String typeName, String attributeName,
			IGraphDatabase graph, CompositeGraphChangeListener listener) {

		boolean found = false;

		try (IGraphTransaction t = graph.beginTransaction()) {
			listener.changeStart();
			IGraphIterable<? extends IGraphNode> ep = graph.getMetamodelIndex().get("id", metamodelUri);

			IGraphNode packagenode = null;

			if (ep.size() == 1) {
				packagenode = ep.getSingle();
			} else {
				throw new Exception("metamodel not found:" + metamodelUri);
			}

			IGraphNode typenode = null;

			for (IGraphEdge e : packagenode.getIncomingWithType("epackage")) {
				if (e.getStartNode().getProperty(IModelIndexer.IDENTIFIER_PROPERTY).equals(typeName)) {
					typenode = e.getStartNode();
					break;
				}
			}

			if (typenode == null) {
				LOGGER.error(
					"type: {} in: {} does not exist, aborting operation: removeDerivedAttribute",
					typeName, metamodelUri);
				listener.changeFailure();
			} else {

				String[] metadata = (String[]) typenode.getProperty(attributeName);
				if (metadata != null) {

					if (metadata.length == 7 && metadata[0].equals("d")) {
						LOGGER.info("derived attribute removed: {}::{}", metamodelUri, typeName);
						IGraphNodeIndex derivedAccessDictionary = graph.getOrCreateNodeIndex(GraphModelInserter.DERIVED_ACCESS_IDXNAME);
						IGraphNodeIndex derivedProxyDictionary = graph.getOrCreateNodeIndex(GraphModelBatchInjector.DERIVED_PROXY_DICT_NAME);

						typenode.removeProperty(attributeName);
						graph.getOrCreateNodeIndex(metamodelUri + "##" + typeName + "##" + attributeName).delete();

						boolean noerror = true;

						for (IGraphEdge e : (typenode.getIncomingWithType(ModelElementNode.EDGE_LABEL_OFTYPE)))
							noerror = noerror && removeDerivedAttribute(derivedAccessDictionary, derivedProxyDictionary,
									attributeName, e);

						for (IGraphEdge e : (typenode.getIncomingWithType(ModelElementNode.EDGE_LABEL_OFKIND)))
							noerror = noerror && removeDerivedAttribute(derivedAccessDictionary, derivedProxyDictionary,
									attributeName, e);

						if (noerror)
							found = true;

						t.success();
						listener.changeSuccess();
					} else {
						LOGGER.error("Error in removeDerivedAttribute, attribute metadata not valid");
						listener.changeFailure();
					}
				} else {
					LOGGER.error("Attribute was not already derived, nothing happened!");
					listener.changeFailure();
				}
			}

		} catch (Exception e1) {
			LOGGER.error("Error in removing a derived attribute to the metamodel", e1);
			listener.changeFailure();
		}

		return found;

	}

	/**
	 * Internal, to remove instance derived attributes
	 */
	private static boolean removeDerivedAttribute(IGraphNodeIndex derivedAccessDictionary,
			IGraphNodeIndex derivedProxyDictionary, String attributeName, IGraphEdge instanceToTypeEdge) {

		boolean error = true;

		IGraphNode n = instanceToTypeEdge.getStartNode();

		IGraphEdge dae = null;

		for (IGraphEdge ed : n.getOutgoingWithType(attributeName)) {
			if (dae == null)
				dae = ed;
			else {
				LOGGER.error("multiple edges found for derived attribute: {} in node {}", attributeName, n);
				dae = null;
				break;
			}
		}

		if (dae == null) {
			LOGGER.error("derived attribute ({}) not found for node {}", attributeName, n);
		} else {
			IGraphNode dan = dae.getEndNode();
			dae.delete();
			derivedAccessDictionary.remove(dan);
			derivedProxyDictionary.remove(dan);
			dan.delete();
			error = false;
		}

		return !error;

	}
}
