/*******************************************************************************
 * Copyright (c) 2011-2018 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 - separate inserter and deletion utils creation
 *       for time-aware version
 ******************************************************************************/
package org.eclipse.hawk.graph.updater;

import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import org.eclipse.hawk.core.IConsole;
import org.eclipse.hawk.core.IModelIndexer;
import org.eclipse.hawk.core.IModelUpdater;
import org.eclipse.hawk.core.IVcsManager;
import org.eclipse.hawk.core.VcsChangeType;
import org.eclipse.hawk.core.VcsCommit;
import org.eclipse.hawk.core.VcsCommitItem;
import org.eclipse.hawk.core.VcsRepositoryDelta;
import org.eclipse.hawk.core.graph.IGraphChangeListener;
import org.eclipse.hawk.core.graph.IGraphDatabase;
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.model.IHawkModelResource;

public class GraphModelUpdater implements IModelUpdater {

	public static final String FILEINDEX_REPO_SEPARATOR = "||||";
	public static final String PROXY_REFERENCE_PREFIX = "hawkProxyRef:";
	public static final boolean CARES_ABOUT_RESOURCES = true;

	/**
	 * Used in URIs of targets when we only know the unique fragment and we
	 * don't know the file.
	 */
	public static final String PROXY_FILE_WILDCARD = "*";

	protected IModelIndexer indexer;
	protected IConsole console;
	private boolean isActive = false;
	protected Set<IGraphNode> toBeUpdated = new HashSet<>();

	@Override
	public void run(IConsole c, IModelIndexer hawk) throws Exception {
		this.indexer = hawk;
		this.console = c;
	}

	@Override
	public boolean updateStore(VcsCommitItem f, IHawkModelResource res) {
		final long start = System.currentTimeMillis();
		boolean success = true;

		/*
		 * We register this listener only for this particular updater and during
		 * this method call. It is used to collect information about which nodes
		 * should have their derived attributes updated, rather than collecting
		 * all changes and working from there (which can use a lot more memory).
		 * Ideally, if we do not have any derived attributes, this shouldn't
		 * require any more memory, and the CPU cost should be the same.
		 */
		final IGraphDatabase g = indexer.getGraph();
		final DirtyDerivedFeaturesListener l = new DirtyDerivedFeaturesListener(g);
		if (!indexer.getDerivedAttributes().isEmpty()) {
			indexer.addGraphChangeListener(l);
		}

		/*
		 * Print more information when we have few commit items: normally this
		 * means we have a few big files instead of many small files.
		 */
		final boolean verbose = hasFewItems(f);

		try {
			try {
				if (res == null) {
					/*
					 * File failed to update to latest version so remove it to maintain consistency.
					 */
					if (!deleteAll(f)) {
						console.printerrln("warning: failed to delete item: "
								+ f
								+ "\nafter its resource failed to be loaded");
						success = false;
					}
				} else if (!createInserter().run(res, f, verbose)) {
					console.printerrln("warning: failed to update item: " + f
							+ "\nmodel resource: " + res);
					success = false;
				}
			} catch (Exception ex) {
				console.printerrln(ex);
				success = false;
			}

			if (verbose) {
				long end = System.currentTimeMillis();
				console.println((end - start) / 1000 + "s" + (end - start) % 1000 + "ms [pure insertion]");
			}
		} finally {
			final long s = System.currentTimeMillis();
			if (verbose) {
				console.print("marking any relevant derived attributes for update...");
				indexer.getCompositeStateListener().info("Marking relevant derived attributes for update...");
			}
			toBeUpdated.addAll(l.getNodesToBeUpdated());
			indexer.removeGraphChangeListener(l);

			if (verbose) {
				final long end = System.currentTimeMillis();
				console.println((end - s) / 1000 + "s" + (end - s) % 1000 + "ms");
				indexer.getCompositeStateListener().info("Marked relevant derived attributes for update. "
						+ (end - s) / 1000 + "s" + (end - s) % 1000 + "ms");
			}
		}
		return success;
	}

	private boolean hasFewItems(VcsCommitItem f){
		return f.getCommit().getItems().size() < 100;
	}

	@Override
	public void updateProxies() {
		final long start = System.currentTimeMillis();

		console.println("attempting to resolve any leftover cross-file references...");
		try {
			indexer.getCompositeStateListener().info(
					"Resolving any leftover cross-file references...");
			createInserter().resolveProxies(indexer.getGraph());
		} catch (Exception e) {
			console.printerrln("Exception in updateStore - resolving proxies, returning 0:");
			console.printerrln(e);
		}

		console.println("attempting to resolve any uninitialized derived attributes...");
		try {
			indexer.getCompositeStateListener().info(
					"Resolving any uninitialized derived attributes...");
			createInserter().resolveDerivedAttributeProxies(
					indexer.getDerivedAttributeExecutionEngine());
		} catch (Exception e) {
			console.printerrln("Exception in updateStore - resolving DERIVED proxies, returning 0:");
			console.printerrln(e);
		}

		console.println("attempting to update any relevant derived attributes...");
		try {
			indexer.getCompositeStateListener().info(
					"Updating any affected derived attributes...");
			createInserter().updateDerivedAttributes(
					indexer.getDerivedAttributeExecutionEngine(), toBeUpdated);
			toBeUpdated = new HashSet<>();
		} catch (Exception e) {
			toBeUpdated = new HashSet<>();
			console.printerrln("Exception in updateStore - UPDATING DERIVED attributes");
			console.printerrln(e);
		}
		long end = System.currentTimeMillis();
		console.println((end - start) / 1000 + "s" + (end - start) % 1000
				+ "ms [proxy update]");
	}

	public boolean isActive() {
		return isActive;
	}

	@Override
	public void shutdown() {
		//
	}

	@Override
	public boolean caresAboutResources() {
		return CARES_ABOUT_RESOURCES;
	}

	@Override
	public boolean deleteAll(IVcsManager c) throws Exception {

		boolean ret = false;

		IGraphDatabase graph = indexer.getGraph();

		try (IGraphTransaction t = graph.beginTransaction()) {
			IGraphNodeIndex filedictionary = graph.getFileIndex();
			IGraphIterable<? extends IGraphNode> fileNodes = filedictionary.query("id", c.getLocation() + "*");

			// Construct a simulated VcsRepositoryDelta with a "-deleted" revision
			final VcsCommit fakeCommit = new VcsCommit();
			final VcsRepositoryDelta delta = new VcsRepositoryDelta(Collections.singleton(fakeCommit));
			delta.setManager(c);
			fakeCommit.setAuthor("hawk");
			fakeCommit.setJavaDate(new Date());
			fakeCommit.setRevision(c.getCurrentRevision() + "-deleted");
			fakeCommit.setMessage("stopped indexing");

			final IGraphChangeListener changeListener = indexer.getCompositeGraphChangeListener();

			for (IGraphNode fileNode : fileNodes) {
				VcsCommitItem item = new VcsCommitItem();
				item.setChangeType(VcsChangeType.DELETED);
				item.setCommit(fakeCommit);
				String path = fileNode.getProperty(
						IModelIndexer.IDENTIFIER_PROPERTY).toString();
				item.setPath(path.startsWith("/") ? path : "/" + path);

				fakeCommit.getItems().add(item);
				createDeletionUtils().deleteAll(fileNode, item, changeListener);
			}

			t.success();
		}

		return ret;
	}

	@Override
	public boolean deleteAll(VcsCommitItem c) throws Exception {
		indexer.getCompositeStateListener().info("Deleting all contents of file: " + c.getPath() + "...");
		boolean ret = false;

		IGraphNode n = new Utils().getFileNodeFromVCSCommitItem(indexer.getGraph(), c);
		if (n != null) {

			try (IGraphTransaction t = indexer.getGraph().beginTransaction()) {
				ret = createDeletionUtils().deleteAll(n, c, indexer.getCompositeGraphChangeListener());
				t.success();
			} catch (Exception e) {
				console.printerrln(e);
				ret = false;
			}
		} else {
			return true;
		}

		indexer.getCompositeStateListener().info("Deleted all contents of file: " + c.getPath() + ".");

		return ret;
	}

	@Override
	public void updateDerivedAttribute(String metamodeluri, String typename,
			String attributename, String attributetype, boolean isMany,
			boolean isOrdered, boolean isUnique, String derivationlanguage,
			String derivationlogic) {
		createInserter()
			.updateDerivedAttribute(metamodeluri,
				typename, attributename, attributetype, isMany, isOrdered,
				isUnique, derivationlanguage, derivationlogic);
	}

	@Override
	public void updateIndexedAttribute(String metamodeluri, String typename,
			String attributename) {
		createInserter().updateIndexedAttribute(metamodeluri,
				typename, attributename);
	}

	@Override
	public String getName() {
		return "Default Hawk Graph Model Updater (v1.0)";
	}

	@Override
	public Set<VcsCommitItem> compareWithLocalFiles(
			Set<VcsCommitItem> reposItems) {
		if (reposItems.isEmpty()) {
			return reposItems;
		}

		final VcsCommitItem firstItem = reposItems.iterator().next();
		final String repositoryURL = firstItem.getCommit().getDelta()
				.getManager().getLocation();
		final Set<VcsCommitItem> changed = new HashSet<VcsCommitItem>();
		changed.addAll(reposItems);

		IGraphDatabase graph = indexer.getGraph();

		if (graph != null) {

			try (IGraphTransaction tx = graph.beginTransaction()) {
				// operations on the graph
				// ...

				IGraphNodeIndex filedictionary = graph.getFileIndex();

				if (filedictionary != null
						&& filedictionary
								.query("id",
										repositoryURL
												+ FILEINDEX_REPO_SEPARATOR
												+ "*").iterator().hasNext()) {
					for (VcsCommitItem r : reposItems) {
						String rev = "-2";
						try {
							IGraphIterable<? extends IGraphNode> ret = filedictionary
									.get("id",
											repositoryURL
													+ FILEINDEX_REPO_SEPARATOR
													+ r.getPath());

							if (ret.iterator().hasNext()) {
								IGraphNode n = ret.getSingle();
								rev = (String) n.getProperty("revision");
							}

						} catch (Exception e) {
							console.printerrln(e);
						}
						if (r.getCommit().getRevision().equals(rev))
							changed.remove(r);

						if (IModelIndexer.VERBOSE)
							console.println("comparing revisions of: "
									+ r.getPath() + " | "
									+ r.getCommit().getRevision() + " | " + rev);

					}
				}

				tx.success();
			} catch (Exception e) {
				console.printerrln(e);
			}

		}

		return changed;
	}

	public GraphModelInserter createInserter() {
		return new GraphModelInserter(indexer, this::createDeletionUtils,  new TypeCache(this.indexer));
	}

	protected DeletionUtils createDeletionUtils() {
		return new DeletionUtils(indexer.getGraph());
	}

}
