/*******************************************************************************
 * 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 - extract import to interface, cleanup,
 *       use revision in imports, divide into super + subclass
 ******************************************************************************/
package org.eclipse.hawk.core.runtime;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.nio.file.Files;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Set;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.eclipse.hawk.core.IConsole;
import org.eclipse.hawk.core.ICredentialsStore;
import org.eclipse.hawk.core.IFileImporter;
import org.eclipse.hawk.core.IMetaModelResourceFactory;
import org.eclipse.hawk.core.IMetaModelUpdater;
import org.eclipse.hawk.core.IModelIndexer;
import org.eclipse.hawk.core.IModelResourceFactory;
import org.eclipse.hawk.core.IModelUpdater;
import org.eclipse.hawk.core.IStateListener;
import org.eclipse.hawk.core.IStateListener.HawkState;
import org.eclipse.hawk.core.IVcsManager;
import org.eclipse.hawk.core.VcsChangeType;
import org.eclipse.hawk.core.VcsCommitItem;
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.IGraphNode;
import org.eclipse.hawk.core.graph.IGraphTransaction;
import org.eclipse.hawk.core.model.IHawkMetaModelResource;
import org.eclipse.hawk.core.model.IHawkModelResource;
import org.eclipse.hawk.core.query.IQueryEngine;
import org.eclipse.hawk.core.util.DerivedAttributeParameters;
import org.eclipse.hawk.core.util.FileOperations;
import org.eclipse.hawk.core.util.HawkProperties;
import org.eclipse.hawk.core.util.IndexedAttributeParameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

public abstract class BaseModelIndexer implements IModelIndexer {

	private static final String DEFAULT_DERIVED_QUERY_ENGINE = "org.eclipse.hawk.epsilon.emc.EOLQueryEngine";

	private static final class DummyScheduledFuture<T> implements ScheduledFuture<T> {
		private final T t2;
		private final Exception ex2;

		private DummyScheduledFuture(T t2, Exception ex2) {
			this.t2 = t2;
			this.ex2 = ex2;
		}

		@Override
		public long getDelay(TimeUnit unit) {
			return 0;
		}

		@Override
		public int compareTo(Delayed o) {
			return 0;
		}

		@Override
		public boolean cancel(boolean mayInterruptIfRunning) {
			return false;
		}

		@Override
		public boolean isCancelled() {
			return false;
		}

		@Override
		public boolean isDone() {
			return true;
		}

		@Override
		public T get() throws ExecutionException {
			if (ex2 == null) {
				return t2;
			} else {
				throw new ExecutionException(ex2);
			}
		}

		@Override
		public T get(long timeout, TimeUnit unit) throws ExecutionException {
			return get();
		}
	}

	private final class RunUpdateTask extends TimerTask {

		private boolean periodic;

		/**
		 * Creates a new update task.
		 *
		 * @param periodic {@code true} if this is an automatically scheduled task,
		 *                 {@code false} if this is a manually requested task (e.g. via
		 *                 {@link BaseModelIndexer#requestImmediateSync()}).
		 */
		private RunUpdateTask(boolean periodic) {
			this.periodic = periodic;
		}

		@Override
		public void run() {
			stateListener.info("Updating Hawk...");
			long start = System.currentTimeMillis();

			boolean synchronised = true;
			console.println("updating indexer: ");

			try {
				synchronised = synchronise();
			} catch (Throwable e) {
				LOGGER.error("Error during synchronisation", e);
			}

			if (!synchronised) {
				console.println("SYNCHRONISATION ERROR");
				stateListener.error("Update FAILED!");
			}

			// Timer only enabled if the delay is non-zero
			if (maxDelay > 0) {
				if (!latestUpdateFoundChanges) {
					final int oldDelay = currentDelay;
					currentDelay = Math.min(currentDelay * 2, maxDelay);
					console.println(
						String.format("same revision, incrementing check timer: %s s -> %s s (max: %s s)",
							oldDelay / 1000, currentDelay / 1000, maxDelay / 1000));
				} else if (!synchronised) {
					int oldDelay = currentDelay;
					currentDelay = Math.min(currentDelay * 2, Math.min(CEILING_DELAY_FAILED_UPDATES, maxDelay));
					console.println(
						String.format("update failed, incrementing check timer: %s s -> %s s (max: %s s with %s s ceiling)",
							oldDelay / 1000, currentDelay / 1000, maxDelay / 1000, CEILING_DELAY_FAILED_UPDATES / 1000));
				} else {
					console.println("different revisions, resetting check timer and propagating changes!");
					currentDelay = minDelay;
				}

				/*
				 * Automatically scheduled update tasks will schedule the next repetition,
				 * but manually scheduled tasks will not. Otherwise, repeated manual syncs
				 * would make the Hawk instance unusable.
				 */
				if (periodic) {
					updateTimer.schedule(new RunUpdateTask(true), currentDelay, TimeUnit.MILLISECONDS);
				}
			}

			final long time = (System.currentTimeMillis() - start);
			stateListener.info(String.format(
				"Updated Hawk instance %s (%s). %d s %d ms",
				getName(), synchronised ? "success" : "failure", time / 1_000, time % 1_000
			));
		}
	}

	public static class DefaultFileImporter implements IFileImporter {
		private final Map<String, File> cachedImports = new HashMap<>();
		private final IVcsManager vcs;
		private final String revision;
		private final File tempDir;
	
		public DefaultFileImporter(IVcsManager vcs, String revision, File tempDir) {
			this.vcs = vcs;
			this.revision = revision;
			this.tempDir = tempDir;
		}
	
		@Override
		public File importFile(String commitPath) {
			/*
			 * We cache the results as some files may get imported repeatedly
			 * (e.g. mmversion.dat for Modelio).
			 */
			if (!cachedImports.containsKey(commitPath)) {
				String[] commitPathSplit = commitPath.split("/");
	
				// Resolves the commit path as a relative path from the
				// temporary directory
				File destination = tempDir;
				if (commitPathSplit.length > 1) {
					for (String pathComponent : commitPathSplit) {
						destination = new File(destination, pathComponent);
					}
					destination.getParentFile().mkdirs();
				} else {
					destination = new File(destination, commitPath);
				}
	
				File result = vcs.importFile(revision, commitPath, destination);
				cachedImports.put(commitPath, result);
				return result;
			} else {
				return cachedImports.get(commitPath);
			}
		}
	}

	private static final Logger LOGGER = LoggerFactory.getLogger(BaseModelIndexer.class);

	private static final int CEILING_DELAY_FAILED_UPDATES = 60_000;

	public static final int FILECOUNT_PROGRESS_THRESHOLD = 100;
	public static final int SHUTDOWN_WAIT_SECONDS = 120;
	public static final int DEFAULT_MAXDELAY = 1000 * 512;
	public static final int DEFAULT_MINDELAY = 5000;

	protected static final boolean IS_DERIVED = true;
	protected static final boolean IS_INDEXED = false;

	protected boolean isSyncMetricsEnabled = false;
	protected Map<VcsCommitItem, IHawkModelResource> fileToResourceMap = new HashMap<>();

	protected int deletedFiles, interestingFiles, currchangeditems, loadedResources;
	private long synctime;

	protected boolean latestUpdateFoundChanges = false;
	protected String name;
	protected File parentfolder = null;
	protected boolean running = false;
	protected final ICredentialsStore credStore;

	protected IGraphDatabase graph = null;
	protected Queue<IVcsManager> monitors = new ConcurrentLinkedQueue<>();
	protected Queue<IModelUpdater> updaters = new ConcurrentLinkedQueue<>();
	protected IMetaModelUpdater metamodelupdater = null;
	protected Map<String, IModelResourceFactory> modelParsers = new ConcurrentHashMap<>();
	protected Map<String, IMetaModelResourceFactory> metamodelParsers = new ConcurrentHashMap<>();
	protected Map<String, IQueryEngine> knownQueryLanguages = new ConcurrentHashMap<>();
	protected IConsole console;

	private int maxDelay = DEFAULT_MAXDELAY;
	private int minDelay = DEFAULT_MINDELAY;
	private int currentDelay = minDelay;
	private ScheduledThreadPoolExecutor updateTimer = null;
	private ThreadLocal<Boolean> insideUpdateThread = new ThreadLocal<Boolean>() {
		@Override
		protected Boolean initialValue() {
			return false;
		}
	};

	private final CompositeGraphChangeListener listener = new CompositeGraphChangeListener();
	protected final CompositeStateListener stateListener = new CompositeStateListener();

	/**
	 * We cache the list of derived attributes to avoid switching back and forth between
	 * batch and transactional mode when inserting large numbers of files (since we check
	 * if there are any derived attrs before we insert the derived attribute listener).
	 *
	 * Every method that changes the derived attributes in some form should set this to
	 * null, so it is recomputed in the next call to {@link #getDerivedAttributes()}.
	 */
	private Collection<IndexedAttributeParameters> cachedDerivedAttributes = null;

	/**
	 * Creates an indexer.
	 * 
	 * @param name
	 *            Name of the new indexer.
	 * @param parentFolder
	 *            Folder where the contents of the index should be stored.
	 * @param credStore
	 *            Credentials store for the VCS usernames and passwords.
	 * @param c
	 *            Console for printing user-facing informational and error messages.
	 *            More detailed information is sent to the SLF4J logs.
	 */
	public BaseModelIndexer(String name, File parentFolder, ICredentialsStore credStore, IConsole c) {
		this.name = name;
		this.console = c;
		this.credStore = credStore;
		this.parentfolder = parentFolder;
	}

	/**
	 * Tells the indexer that this repository may need to be re-indexed.
	 */
	protected abstract void resetRepository(String repoURL);

	/**
	 * Adds a {@link IVcsManager} to be immediately indexed by Hawk. If
	 * <code>persist</code> is <code>true</code>, the new manager should be added to
	 * the Hawk index configuration file.
	 */
	public void addVCSManager(IVcsManager vcs, boolean persist) {
		monitors.add(vcs);

		try {
			if (persist) {
				saveIndexer();
			}
		} catch (Exception e) {
			LOGGER.error("addVCSManager tried to saveIndexer but failed", e);
		}
	
		requestImmediateSync();
	}

	/**
	 * Removes a {@link IVcsManager} from Hawk, including any contained models
	 * indexed.
	 */
	public void removeVCSManager(IVcsManager vcs) throws Exception {
		stateListener.state(HawkState.UPDATING);
		stateListener.info("Removing vcs...");
		monitors.remove(vcs);

		try {
			saveIndexer();
		} catch (Exception e) {
			System.err.println("removeVCS tried to saveIndexer but failed");
			e.printStackTrace();
		}
	
		for (IModelUpdater u : updaters)
			try {
				u.deleteAll(vcs);
			} catch (Exception e) {
				System.err.println("removeVCS tried to delete index contents but failed");
				e.printStackTrace();
			}
	
		stateListener.state(HawkState.RUNNING);
		stateListener.info("Removed vcs.");
	}

	/**
	 * Updates the Hawk index based on the current contents of the specified {@link IVcsManager}.
	 * Returns <code>true</code> if successful, <code>false</code> otherwise.
	 */
	protected abstract boolean synchronise(IVcsManager vcsManager) throws Exception;

	@Override
	public void requestImmediateSync() {
		if (running) {
			updateTimer.submit(new RunUpdateTask(false));
		}
	}

	protected boolean synchronise() throws Exception {
		listener.synchroniseStart();
		stateListener.state(HawkState.UPDATING);
	
		try {
			long start = System.currentTimeMillis();
			boolean allSync = true;
			fileToResourceMap = new HashMap<>();
			deletedFiles = 0;
			interestingFiles = 0;
			currchangeditems = 0;
			loadedResources = 0;
			synctime = 0;
	
			latestUpdateFoundChanges = false;
	
			if (monitors.size() > 0) {
				for (IVcsManager m : monitors) {
					if (!m.isFrozen()) {
						if (m.isActive()) {
							allSync = allSync && synchronise(m);
						} else {
							console.printerrln("Warning, monitor is inactive, synchronisation failed!");
							allSync = false;
						}
					} else {
						console.printerrln("Monitor is frozen, skipping it.");
						// frozen do nothing
					}
				}
			}
	
			synctime = (System.currentTimeMillis() - start);
	
			return allSync;
		} finally {
			stateListener.info("Performing optional post-sync operations.");
			stateListener.state(HawkState.RUNNING);
			listener.synchroniseEnd();
		}
	}


	protected boolean deleteRemovedModels(boolean success, IModelUpdater u, final Set<VcsCommitItem> deleteditems) {
		stateListener.info("Deleting models removed from repository...");

		try {
			listener.changeStart();
			for (VcsCommitItem c : deleteditems) {
				success = success && u.deleteAll(c);
			}
			listener.changeSuccess();
		} catch (Exception e) {
			success = false;
			LOGGER.error("Error while deleting removed files from store", e);
			listener.changeFailure();
		}

		return success;
	}

	protected void importFiles(IFileImporter importer, final Set<VcsCommitItem> changedItems, final Map<String, File> pathToImported) {
		int iImported = 0;

		for (VcsCommitItem s : changedItems) {
			final String commitPath = s.getPath();
	
			if (VERBOSE) {
				console.println("-->" + commitPath + " HAS CHANGED (" + s.getChangeType()
						+ "), PROPAGATING CHANGES");
			}
	
			if (!pathToImported.containsKey(commitPath)) {
				final File imported = importer.importFile(commitPath);
				pathToImported.put(commitPath, imported);
			}

			++iImported;
			if (iImported % 10 == 0) {
				stateListener.info(String.format("Imported %d/%d files from %s",
					iImported, changedItems.size(), s.getCommit().getDelta().getManager().getLocation()));
			}
		}
	}

	@Override
	public Collection<IModelUpdater> getModelUpdaters() {
		return updaters;
	}

	@Override
	public void shutdown(ShutdownRequestType type) throws Exception {
		// The type is ignored for local instances of Hawk: they
		// must always be safely shut down, to preserve consistency.
		stateListener.info("Hawk shutting down...");
		preShutdown();
	
		if (graph != null) {
			graph.shutdown();
			graph = null;
		}
		if (credStore != null) {
			credStore.shutdown();
		}
		stateListener.info("Hawk shut down.");
	}

	@Override
	public void delete() throws Exception {
		preShutdown();

		if (graph != null) {
			graph.delete();
		}
		graph = null;
	}

	private void preShutdown() {
		try {
			saveIndexer();
		} catch (Exception e) {
			LOGGER.error("Error while saving the indexer during shut down", e);
		}
	
		try {
			LOGGER.info("Waiting {}s for all scheduled tasks to complete", SHUTDOWN_WAIT_SECONDS);
			updateTimer.shutdownNow();
			updateTimer.awaitTermination(SHUTDOWN_WAIT_SECONDS, TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			LOGGER.error("Wait for scheduled tasks was interrupted", e);
		}

		for (IVcsManager monitor : monitors) {
			monitor.shutdown();
		}

		monitors.clear();
		running = false;
		stateListener.state(HawkState.STOPPED);
	}

	@Override
	public IGraphDatabase getGraph() {
		return graph;
	}

	@Override
	public Set<String> getKnownMMUris() {
		return graph.getKnownMMUris();
	}

	@Override
	public Set<IVcsManager> getRunningVCSManagers() {
		return new HashSet<>(monitors);
	}

	@Override
	public String getId() {
		return getClass().getCanonicalName();
	}

	private void registerMetamodelFiles() throws Exception {
		stateListener.info("Registering metamodels...");
	
		try (IGraphTransaction t = graph.beginTransaction()) {
			for (IGraphNode epackage : graph.getMetamodelIndex().query("id", "*")) {
				// Some metamodel nodes may not have a resource associated with them (e.g. static metamodels) 
				final String s = (String) epackage.getProperty(IModelIndexer.METAMODEL_RESOURCE_PROPERTY);

				if (s != null) {
					final String type = epackage.getProperty(IModelIndexer.METAMODEL_TYPE_PROPERTY) + "";
					final String ep = epackage.getProperty(IModelIndexer.IDENTIFIER_PROPERTY) + "";
					final IMetaModelResourceFactory p = getMetaModelParser(type);

					if (p != null) {
						p.parseFromString(IMetaModelResourceFactory.DUMPED_PKG_PREFIX + ep, s);
					} else {
						console.printerrln("cannot register metamodel in graph, named: " + ep + ", with type: " + type + ", as no relevant parser is registered");
					}
				}
			}
	
			t.success();
		}
		stateListener.info("Registered metamodels.");
	}

	@Override
	public void removeMetamodels(String... mm) throws Exception {
		stateListener.state(HawkState.UPDATING);
		stateListener.info("Removing metamodels...");
		for (String repoURL : metamodelupdater.removeMetamodels(this, mm)) {
			resetRepository(repoURL);
		}
		stateListener.state(HawkState.RUNNING);
		stateListener.info("Removed metamodels.");
	}

	@Override
	public void registerMetamodels(File... metamodel) throws Exception {
		stateListener.info("Adding metamodel(s)...");
		HashSet<IHawkMetaModelResource> set = new HashSet<IHawkMetaModelResource>();
	
		IMetaModelResourceFactory parser = null;
		String previousParserType = null;
	
		for (File mm : metamodel) {
			parser = getMetaModelParserFromFilename(mm.getPath());
	
			if (parser == null) {
				console.printerrln("metamodel regstration failed, no relevant factory found");
			} else {
				String parserType = parser.getType();
				IHawkMetaModelResource metamodelResource = parser.parse(mm);
	
				if (previousParserType != null && !previousParserType.equals(parserType)) {
					console.printerrln(
							"cannot add heterogeneous metamodels concurrently, plase add one metamodel type at a time");
					set.clear();
					break;
				} else {
					if (metamodelResource != null) {
						console.println("Adding metamodels in: " + mm + " to store");
						set.add(metamodelResource);
						previousParserType = parserType;
					}
				}
			}
	
		}

		metamodelupdater.insertMetamodels(set, this);

		// reset repositories as models may be parsable in them
		for (IVcsManager s : monitors) {
			resetRepository(s.getLocation());
		}

		stateListener.info("Added metamodel(s).");
	}

	@Override
	public IConsole getConsole() {
		return console;
	}

	public void addModelResourceFactory(IModelResourceFactory p) {
		modelParsers.put(p.getType(), p);
	}

	public void addMetaModelResourceFactory(IMetaModelResourceFactory p) {
		metamodelParsers.put(p.getType(), p);
	}

	public void addQueryEngine(IQueryEngine q) {
		knownQueryLanguages.put(q.getType(), q);
	}

	@Override
	public Set<String> getKnownMetaModelParserTypes() {
		return metamodelParsers.keySet();
	}

	@Override
	public Collection<IMetaModelResourceFactory> getMetaModelParsers() {
		return metamodelParsers.values();
	}

	@Override
	public IMetaModelResourceFactory getMetaModelParser(String type) {
		if (type == null) {
			console.printerrln("null type given to getMetaModelParser(type), returning null");
		} else {
			return metamodelParsers.get(type);
		}
	
		return null;
	}

	@Override
	public Collection<IModelResourceFactory> getModelParsers() {
		return modelParsers.values();
	}

	public IMetaModelResourceFactory getMetaModelParserFromFilename(String name) {
		if (name == null)
			console.printerrln("null extension given to getMetaModelParserFromFilename(extension), returning null");
		else
			for (String p : metamodelParsers.keySet()) {
				IMetaModelResourceFactory parser = metamodelParsers.get(p);
				for (String ext : parser.getMetaModelExtensions()) {
					if (name.endsWith(ext)) {
						return parser;
					}
				}
			}
		return null;
	}

	public IModelResourceFactory getModelParserFromFilename(String name) {
		if (name == null)
			console.printerrln("null extension given to getModelParserFromFilename(extension), returning null");
		else
			for (String p : modelParsers.keySet()) {
				IModelResourceFactory parser = modelParsers.get(p);
				for (String ext : parser.getModelExtensions()) {
					if (name.endsWith(ext)) {
						return parser;
					}
				}
			}
		return null;
	}

	@Override
	public Map<String, IQueryEngine> getKnownQueryLanguages() {
		return knownQueryLanguages;
	}

	@Override
	public Set<String> getKnownMetamodelFileExtensions() {
		Set<String> exts = new HashSet<>();
		for (IMetaModelResourceFactory mm : metamodelParsers.values()) {
			exts.addAll(mm.getMetaModelExtensions());
		}
		return exts;
	}

	@Override
	public void addModelUpdater(IModelUpdater u) {
		try {
			u.run(console, this);
			updaters.add(u);
		} catch (Exception e) {
			LOGGER.error("Error while adding model updater", e);
		}
	}

	@Override
	public IMetaModelUpdater getMetaModelUpdater() {
		return metamodelupdater;
	}

	@Override
	public void setMetaModelUpdater(IMetaModelUpdater u) {
		if (metamodelupdater == null) {
			u.run();
			metamodelupdater = u;
		} else {
			LOGGER.warn("metamodel updater alredy registered, cannot have more than one");
		}
	}

	public void saveIndexer() throws Exception {
		stateListener.info("Saving Hawk metadata...");
		XStream stream = new XStream(new DomDriver());
		stream.processAnnotations(HawkProperties.class);
	
		HashSet<String[]> set = new HashSet<String[]>();
		for (IVcsManager s : getRunningVCSManagers()) {
			String[] meta = new String[] { s.getLocation(), s.getType(), s.isFrozen() + "" };
			console.println("adding: " + meta[0] + ":" + meta[1] + ":" + meta[2]);
			set.add(meta);
		}
		HawkProperties hp = new HawkProperties(graph.getType(), set, minDelay, maxDelay);
	
		Files.createDirectories(getParentFolder().toPath());
		String out = stream.toXML(hp);
		try (BufferedWriter b = new BufferedWriter(
				new FileWriter(getParentFolder() + File.separator + "properties.xml"))) {
			b.write(out);
			b.flush();
		}
		stateListener.info("Saved Hawk metadata.");
	}

	@Override
	public void setDB(IGraphDatabase db, boolean persist) {
		graph = db;
		try {
			if (persist) {
				saveIndexer();
			}
		} catch (Exception e) {
			System.err.println("setDB tried to saveIndexer but failed");
			e.printStackTrace();
		}
	}

	@Override
	public void init(int minDelay, int maxDelay) throws Exception {
		stateListener.info("Initializing Hawk...");
		this.maxDelay = maxDelay;
		this.minDelay = minDelay;
		this.currentDelay = minDelay;
	
		// register all static metamodels
		console.println("inserting static metamodels of registered metamodel factories to graph:");
		for (String factoryNames : metamodelParsers.keySet()) {
			IMetaModelResourceFactory f = metamodelParsers.get(factoryNames);
			console.println(f.getType());
			metamodelupdater.insertMetamodels(f.getStaticMetamodels(), this);
		}
		console.println("inserting static metamodels complete");
	
		// register all metamodels in graph to their factories
		registerMetamodelFiles();
	
		// begin scheduled updates from vcs
		updateTimer = new ScheduledThreadPoolExecutor(1) {
			@Override
			protected void beforeExecute(Thread t, Runnable r) {
				insideUpdateThread.set(true);
				super.beforeExecute(t, r);
			}

			@Override
			protected void afterExecute(Runnable r, Throwable t) {
				super.afterExecute(r, t);
				insideUpdateThread.set(false);
			}
		};
		insideUpdateThread.set(false);
		updateTimer.submit(new RunUpdateTask(true));
	
		running = true;
		stateListener.state(HawkState.RUNNING);
		stateListener.info("Initialized Hawk.");
	}

	@Override
	public File getParentFolder() {
		return parentfolder;
	}

	@Override
	public void addDerivedAttribute(String metamodeluri, String typename, String attributename, String attributetype,
			boolean isMany, boolean isOrdered, boolean isUnique, String derivationlanguage, String derivationlogic) {
		stateListener
				.info(String.format("Adding derived attribute %s::%s::%s...", metamodeluri, typename, attributename));

		final boolean success = metamodelupdater.addDerivedAttribute(metamodeluri, typename, attributename,
				attributetype, isMany, isOrdered, isUnique, derivationlanguage, derivationlogic, this);
		if (success) {
			for (IModelUpdater u : getModelUpdaters())
				u.updateDerivedAttribute(metamodeluri, typename, attributename, attributetype, isMany, isOrdered,
						isUnique, derivationlanguage, derivationlogic);
		}

		cachedDerivedAttributes = null;
		stateListener.info(String.format("Added derived attribute %s::%s::%s.", metamodeluri, typename, attributename));
	}

	@Override
	public Collection<IndexedAttributeParameters> getDerivedAttributes() {
		if (cachedDerivedAttributes == null) {
			cachedDerivedAttributes = getExtraAttributes(IS_DERIVED);
		}
		return cachedDerivedAttributes;
	}

	@Override
	public boolean removeDerivedAttribute(String metamodelUri, String typeName, String attributeName) {
		stateListener.info(String.format(
			"Removing derived attribute %s::%s::%s...",
			metamodelUri, typeName, attributeName));
	
		final boolean removed = metamodelupdater.removeDerivedAttribute(metamodelUri, typeName, attributeName, this);
		cachedDerivedAttributes = null;
	
		stateListener.info(
				removed ? String.format("Removed derived attribute %s::%s::%s.", metamodelUri, typeName, attributeName)
						: String.format("Derived attribute %s::%s::%s did not exist so nothing happened.", metamodelUri,
								typeName, attributeName));
	
		return removed;
	}

	@Override
	public void addIndexedAttribute(String metamodeluri, String typename, String attributename) {
		stateListener.info(String.format(
			"Adding indexed attribute %s::%s::%s...",
			metamodeluri, typename, attributename));
	
		if (metamodelupdater.addIndexedAttribute(metamodeluri, typename, attributename, this))
			for (IModelUpdater u : getModelUpdaters())
				u.updateIndexedAttribute(metamodeluri, typename, attributename);
	
		stateListener.info(String.format(
			"Added indexed attribute %s::%s::%s.",
			metamodeluri, typename, attributename));
	}

	@Override
	public Collection<IndexedAttributeParameters> getIndexedAttributes() {
		return getExtraAttributes(IS_INDEXED);
	}

	@Override
	public boolean removeIndexedAttribute(String metamodelUri, String typename, String attributename) {
		stateListener.info(String.format(
			"Removing indexed attribute %s::%s::%s...",
			metamodelUri, typename, attributename));
	
		boolean removed = metamodelupdater.removeIndexedAttribute(metamodelUri, typename, attributename, this);
	
		stateListener.info(
				removed ? String.format("Removed indexed attribute %s::%s::%s.", metamodelUri, typename, attributename)
						: String.format("Indexed attribute %s::%s::%s did not exist so nothing happened.", metamodelUri,
								typename, attributename));
	
		return removed;
	
	}

	/**
	 * Returns {@link #IS_DERIVED} if the attribute is derived or
	 * {@link #IS_INDEXED} if not.
	 */
	private boolean isDerivedAttribute(IGraphNode typenode, final String attrName) {
		final String[] propertyInfo = (String[]) typenode.getProperty(attrName);
		if (propertyInfo == null) {
			LOGGER.warn("Information on derived attribute {} in type node {} is missing", typenode.getId(), attrName);
			return false;
		} else {
			return propertyInfo[0].equals("d");
		}
	}

	private Collection<IndexedAttributeParameters> getExtraAttributes(final boolean isDerived) {
		Set<IndexedAttributeParameters> paramsSet = new HashSet<IndexedAttributeParameters>();
	
		try (IGraphTransaction t = graph.beginTransaction()) {
			Set<String> ret = getAttributeIndexNames();
	
			Iterator<String> it = ret.iterator();
			while (it.hasNext()) {
	
				String s = it.next();
				String[] split = s.split("##"); 
				
				final String mmURI = split[0];
				final String typeName = split[1];
				final String attrName = split[2];
				
				IGraphNode typenode = getTypeNode(mmURI, typeName);
	
				if (isDerivedAttribute(typenode, attrName) == isDerived) {
					String[] metadata = (String[]) typenode.getProperty(attrName);
					IndexedAttributeParameters params = getAttributeParametersfromMetadata(mmURI, typeName, attrName, metadata);
					paramsSet.add(params);
				}
			}
	
			t.success();
		} catch (Exception e) {
			System.err.println("error in getExtraAttributes");
			e.printStackTrace();
		}
		return paramsSet;
	}

	private IGraphNode getTypeNode(String mmURI, String typeName) {
		final Iterator<? extends IGraphNode> itMetamodelNode = graph.getMetamodelIndex().get("id", mmURI).iterator();
		if (!itMetamodelNode.hasNext()) {
			throw new NoSuchElementException(String.format("Metamodel %s could not be found", mmURI));
		}
		final IGraphNode epackagenode = itMetamodelNode.next();
		IGraphNode typenode = null;

		for (IGraphEdge e : epackagenode.getIncomingWithType("epackage")) {
			IGraphNode temp = e.getStartNode();
			if (temp.getProperty(IModelIndexer.IDENTIFIER_PROPERTY).equals(typeName)) {
				if (typenode == null) {
					typenode = temp;
				} else {
					System.err.println("error in getExtraAttributes, typenode had more than 1 type found");
				}
			}
		}
	
		return typenode;
	}

	private Set<String> getAttributeIndexNames() {
		Set<String> ret = new HashSet<String>();
	
		ret.addAll(graph.getNodeIndexNames());
		Iterator<String> it = ret.iterator();
		while (it.hasNext()) {
			String s = it.next();
			if (!s.matches("(.*)##(.*)##(.*)"))
				it.remove();
		}
		
		return ret;
	}

	private IndexedAttributeParameters getAttributeParametersfromMetadata(String mmURI, String typeName, String attrName, String[] metadata) {
		IndexedAttributeParameters params;
	
		if (metadata[0].equals("d")) {
			params = new DerivedAttributeParameters(mmURI, typeName, attrName, metadata[4],
					metadata[1].equals("t"), metadata[2].equals("t"), metadata[3].equals("t"),
					metadata[5], metadata[6]);
		} else {
			params = new IndexedAttributeParameters(mmURI, typeName, attrName);
		}
	
		return params;
	}

	@Override
	public Collection<String> getIndexes() {
		Set<String> ret = new HashSet<String>();
		try (IGraphTransaction t = graph.beginTransaction()) {
			ret.addAll(graph.getNodeIndexNames());
	
			t.success();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return ret;
	
	}

	@Override
	public List<String> validateExpression(String derivationlanguage, String derivationlogic) {
		IQueryEngine q = knownQueryLanguages.get(derivationlanguage);
		return q.validate(derivationlogic);
	}

	@Override
	public String getName() {
		return name;
	}

	@Override
	public boolean isRunning() {
		return running;
	}

	@Override
	public boolean addGraphChangeListener(IGraphChangeListener changeListener) {
		final boolean add = listener.add(changeListener);
		changeListener.setModelIndexer(this);
		return add;
	}

	@Override
	public boolean removeGraphChangeListener(IGraphChangeListener changeListener) {
		final boolean remove = listener.remove(changeListener);
		changeListener.setModelIndexer(null);
		return remove;
	}

	@Override
	public CompositeGraphChangeListener getCompositeGraphChangeListener() {
		return listener;
	}

	@Override
	public void setSyncMetricsEnabled(Boolean enable) {
		isSyncMetricsEnabled = enable;
	}

	public Map<VcsCommitItem, IHawkModelResource> getFileToResourceMap() {
		if (!isSyncMetricsEnabled) {
			LOGGER.warn("isSyncMetricsEnabled == false, this method will return an empty Map");
		}
		return fileToResourceMap;
	}

	public int getDeletedFiles() {
		// works even if metrics disabled
		return deletedFiles;
	}

	public int getInterestingFiles() {
		// works even if metrics disabled
		return interestingFiles;
	}

	public int getCurrChangedItems() {
		// works even if metrics disabled
		return currchangeditems;
	}

	public int getLoadedResources() {
		// works even if metrics disabled
		return loadedResources;
	}

	public long getLatestSynctime() {
		// works even if metrics disabled
		return synctime;
	}

	@Override
	public ICredentialsStore getCredentialsStore() {
		return credStore;
	}

	@Override
	public String getDerivedAttributeExecutionEngine() {
		if (!getKnownQueryLanguages().keySet().contains(DEFAULT_DERIVED_QUERY_ENGINE)) {
			LOGGER.warn("Default derived attribute engine ({}) disabled - will not be able to derive features");
		}
		return DEFAULT_DERIVED_QUERY_ENGINE;
	}

	@Override
	public boolean addStateListener(IStateListener listener) {
		return stateListener.add(listener);
	}

	@Override
	public boolean removeStateListener(IStateListener listener) {
		return stateListener.remove(listener);
	}

	@Override
	public CompositeStateListener getCompositeStateListener() {
		return stateListener;
	}

	@Override
	public void setPolling(int base, int max) {
		minDelay = base;
		maxDelay = max;
	}

	@Override
	public void waitFor(HawkState targetState) throws InterruptedException {
		waitFor(targetState, 0);
	}

	@Override
	public void waitFor(HawkState targetState, long timeoutMillis) throws InterruptedException {
		synchronized (stateListener) {
			final long end = System.currentTimeMillis() + timeoutMillis;
			for (HawkState s = stateListener.getCurrentState(); s != targetState; s = stateListener.getCurrentState()) {
				if (s == HawkState.STOPPED) {
					throw new IllegalStateException("The selected Hawk is stopped");
				}
	
				if (timeoutMillis == 0) {
					stateListener.wait();
				} else {
					final long remaining = end - System.currentTimeMillis();
					if (remaining > 0) {
						// Wait for the remaining time
						stateListener.wait(remaining);
					} else {
						// Exit the loop due to timeout
						break;
					}
				}
			}
		}
	}

	@Override
	public <T> ScheduledFuture<T> scheduleTask(Callable<T> task, long delayMillis) {
		if (insideUpdateThread.get()) {
			/*
			 * We're trying to schedule a task from within a task - that would cause
			 * deadlock due to thread starvation, so just run the task from within here.
			 */
			T t = null;
			Exception ex = null;
			try {
				t = task.call();
			} catch (Exception e) {
				ex = e;
			}
			return new DummyScheduledFuture<T>(t, ex);
		} else {
			return updateTimer.schedule(task, delayMillis, TimeUnit.MILLISECONDS);
		}
	}

	protected void inspectChanges(Iterable<VcsCommitItem> files, Set<VcsCommitItem> deleteditems, Set<VcsCommitItem> interestingfiles) {
		stateListener.info("Calculating relevant changed model files...");

		fileLoop:
		for (VcsCommitItem r : files) {
			for (IModelResourceFactory parser : modelParsers.values()) {
				for (String ext : parser.getModelExtensions()) {
					if (r.getPath().toLowerCase().endsWith(ext)) {
						interestingfiles.add(r);
						continue fileLoop;
					}
				}
			}
		}

		Iterator<VcsCommitItem> it = interestingfiles.iterator();
		while (it.hasNext()) {
			VcsCommitItem c = it.next();
	
			if (c.getChangeType().equals(VcsChangeType.DELETED)) {
				if (VERBOSE) {
					console.println(String.format(
						"--> %s HAS CHANGED (%S), PROPAGATING CHANGES",
						c.getPath(), c.getChangeType()));
				}

				deleteditems.add(c);
				it.remove();
			}
		}
	}

	protected boolean internalSynchronise(final String currentRevision, final IVcsManager m,
			final IModelUpdater u, final Set<VcsCommitItem> deletedItems, final Set<VcsCommitItem> interestingfiles,
			final String monitorTempDir) {
		boolean success = true;

		// enters transaction mode!
		Set<VcsCommitItem> currReposChangedItems = u.compareWithLocalFiles(interestingfiles);

		// metadata about synchronise
		final int totalFiles = currReposChangedItems.size();
		currchangeditems = currchangeditems + totalFiles;

		// create temp files with changed repos files
		final Map<String, File> pathToImported = new HashMap<>();
		final IFileImporter importer = new DefaultFileImporter(m, currentRevision, new File(monitorTempDir));
		importFiles(importer, currReposChangedItems, pathToImported);

		// delete all removed files
		success = deleteRemovedModels(success, u, deletedItems);

		stateListener.info("Updating models to the new version...");

		// prepare for mass inserts if needed
		graph.enterBatchMode();

		final boolean fileCountProgress = totalFiles > FILECOUNT_PROGRESS_THRESHOLD;
		final long millisSinceStart = System.currentTimeMillis();
		int totalProcessedFiles = 0, filesProcessedSinceLastPrint = 0;
		long millisSinceLastPrint = millisSinceStart;

		for (VcsCommitItem v : currReposChangedItems) {
			try {
				// Place before the actual update so we print the 0/X message as well
				if (fileCountProgress && (totalProcessedFiles == 0 && filesProcessedSinceLastPrint == 0
						|| filesProcessedSinceLastPrint == FILECOUNT_PROGRESS_THRESHOLD)) {
					totalProcessedFiles += filesProcessedSinceLastPrint;

					final long millisPrint = System.currentTimeMillis();
					stateListener.info(String.format("Processed %d/%d files in repo %s (%s sec, %s sec total)",
							totalProcessedFiles, totalFiles, m.getLocation(),
							(millisPrint - millisSinceLastPrint) / 1000, (millisPrint - millisSinceStart) / 1000));

					filesProcessedSinceLastPrint = 0;
					millisSinceLastPrint = millisPrint;
				}

				IHawkModelResource r = null;
				if (u.caresAboutResources()) {
					final File file = pathToImported.get(v.getPath());
					if (file == null || !file.exists()) {
						console.printerrln("warning, cannot find file: " + file + ", ignoring changes");
					} else {
						IModelResourceFactory mrf = getModelParserFromFilename(file.getName().toLowerCase());
						if (mrf.canParse(file)) {
							r = mrf.parse(importer, file);
						}
					}
				}
				success = u.updateStore(v, r) && success;

				if (r != null) {
					if (!isSyncMetricsEnabled) {
						r.unload();
					} else {
						fileToResourceMap.put(v, r);
					}
					loadedResources++;
				}

				filesProcessedSinceLastPrint++;

			} catch (Exception e) {
				console.printerrln("updater: " + u + "failed to update store");
				console.printerrln(e);
				success = false;
			}
		}

		// Print the final message
		if (fileCountProgress) {
			totalProcessedFiles += filesProcessedSinceLastPrint;
			final long millisPrint = System.currentTimeMillis();
			stateListener.info(String.format("Processed %d/%d files in repo %s (%s sec, %s sec total)",
					totalProcessedFiles, totalFiles, m.getLocation(), (millisPrint - millisSinceLastPrint) / 1000,
					(millisPrint - millisSinceStart) / 1000));
		}

		stateListener.info("Updating proxies...");

		// update proxies
		u.updateProxies();

		// leave batch mode
		graph.exitBatchMode();

		stateListener.info("Updated proxies.");

		return success;
	}

	protected boolean synchroniseFiles(String revision, IVcsManager vcsManager, final Collection<VcsCommitItem> files) {
		final Set<VcsCommitItem> deleteditems = new HashSet<VcsCommitItem>();
		final Set<VcsCommitItem> interestingfiles = new HashSet<VcsCommitItem>();
		inspectChanges(files, deleteditems, interestingfiles);
		deletedFiles = deletedFiles + deleteditems.size();
		interestingFiles = interestingFiles + interestingfiles.size();

		final String monitorTempDir = graph.getTempDir();
		File temp = new File(monitorTempDir);
		temp.mkdir();

		// for each registered updater
		boolean updatersOK = true;
		for (IModelUpdater updater : getModelUpdaters()) {
			updatersOK = updatersOK && internalSynchronise(revision, vcsManager, updater, deleteditems, interestingfiles, monitorTempDir);
		}

		// delete temporary files
		if (!FileOperations.deleteFiles(new File(monitorTempDir), true))
			console.printerrln("error in deleting temporary local vcs files");
		return updatersOK;
	}
	
	@Override
	public void removeMetaModelResourceFactory(IMetaModelResourceFactory metaModelParser) {
		metamodelParsers.remove(metaModelParser.getType());
		requestImmediateSync();
	}

	@Override
	public void removeModelResourceFactory(IModelResourceFactory modelParser) {
		modelParsers.remove(modelParser.getType());
		requestImmediateSync();
	}

	@Override
	public void removeModelUpdater(IModelUpdater updater) throws Exception {
		if (updaters.size()>1) {
			updaters.remove(updater);
			requestImmediateSync();
		} else {
			throw new Exception("Cannot remove updater, as hawk requires at least one");
		}
	}

}