/*******************************************************************************
 * Copyright (c) 2018-2019 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:
 *     Antonio Garcia-Dominguez - initial API and implementation
 ******************************************************************************/
package org.eclipse.hawk.timeaware.queries;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import org.eclipse.epsilon.eol.IEolModule;
import org.eclipse.epsilon.eol.exceptions.models.EolModelLoadingException;
import org.eclipse.epsilon.eol.execute.operations.contributors.OperationContributorRegistry;
import org.eclipse.hawk.core.IModelIndexer;
import org.eclipse.hawk.core.IStateListener.HawkState;
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.IGraphTransaction;
import org.eclipse.hawk.core.graph.timeaware.ITimeAwareGraphDatabase;
import org.eclipse.hawk.core.graph.timeaware.ITimeAwareGraphNode;
import org.eclipse.hawk.core.graph.timeaware.ITimeAwareGraphNodeIndex;
import org.eclipse.hawk.core.query.InvalidQueryException;
import org.eclipse.hawk.core.query.QueryExecutionException;
import org.eclipse.hawk.epsilon.emc.EOLQueryEngine;
import org.eclipse.hawk.epsilon.emc.contextful.AllOf;
import org.eclipse.hawk.epsilon.emc.contextful.TypeFirstAllOf;
import org.eclipse.hawk.epsilon.emc.pgetters.GraphPropertyGetter;
import org.eclipse.hawk.epsilon.emc.wrappers.FileNodeWrapper;
import org.eclipse.hawk.graph.FileNode;
import org.eclipse.hawk.graph.GraphWrapper;
import org.eclipse.hawk.graph.ModelElementNode;
import org.eclipse.hawk.graph.ProxyReferenceList;
import org.eclipse.hawk.graph.updater.GraphModelBatchInjector;
import org.eclipse.hawk.graph.updater.GraphModelUpdater;
import org.eclipse.hawk.timeaware.graph.VCSManagerIndex;
import org.eclipse.hawk.timeaware.graph.VCSManagerIndex.RepositoryNode;
import org.eclipse.hawk.timeaware.queries.operations.reflective.TimeAwareNodeHistoryOperationContributor;
import org.eclipse.hawk.timeaware.queries.operations.reflective.TypeHistoryOperationContributor;
import org.eclipse.hawk.timeaware.queries.operations.scopes.IScopingTimeAwareGraphNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Variant of {@link EOLQueryEngine} that exposes the time-aware nature of the
 * index through EOL. Both types and model elements have the same set of
 * time-aware properties and references: <code>.time</code>,
 * <code>.earliest</code>, <code>.latest</code>, <code>.versions</code>.
 *
 * TODO: integrate a "human time to seconds since epoch UTC" library.
 *
 * TODO: allow for revision-based times as well.
 */
public class TimeAwareEOLQueryEngine extends EOLQueryEngine {

	/**
	 * Wraps a standalone node while remembering its original time, so we can
	 * retrieve the exact same node later if the weak reference has been GC'ed.
	 */
	public class TimeAwareGraphNodeWrapper extends GraphNodeWrapper {
		private long time;

		protected TimeAwareGraphNodeWrapper(IGraphNode n, TimeAwareEOLQueryEngine containerModel) {
			super(n, containerModel);
			this.time = ((ITimeAwareGraphNode) n).getTime();
		}

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

	/**
	 * Variant of {@link GraphNodeWrapper} which keeps a strong reference, in case
	 * we cannot be sure that we can simply retrieve the exact original node through
	 * ID and optionally time. This is needed for the scoping wrappers in the
	 * time-aware query DSL, as recreating those would essentially require
	 * re-running the query itself.
	 */
	public class StrongGraphNodeWrapper extends GraphNodeWrapper {
		private IGraphNode node;

		protected StrongGraphNodeWrapper(IGraphNode n, EOLQueryEngine containerModel) {
			super(n, containerModel);
			this.node = n;
		}

		@Override
		public IGraphNode getNode() {
			return node;
		}
	}

	/**
	 * This is the default 'all of' provider. It will go to the type node and follow
	 * all 'is of type' and/or 'is of kind' edges in reverse, with no filtering.
	 */
	protected class ContextlessAllOf implements AllOf {
		public void addAllOf(IGraphNode typeNode, final String typeorkind, Collection<Object> nodes) {
			for (IGraphEdge n : typeNode.getIncomingWithType(typeorkind)) {
				nodes.add(new TimeAwareGraphNodeWrapper(n.getStartNode(), TimeAwareEOLQueryEngine.this));
			}
			broadcastAllOfXAccess(nodes);
		}
	}

	/**
	 * This is the default 'all files' provider. Without a source node, it will
	 * return all available file nodes just like the regular EOL query engine. With
	 * a source node, it will return the file nodes on the same instant as that
	 * node.
	 */
	protected class ContextlessTimeAwareAllFiles implements Function<IGraphNode, Iterable<? extends IGraphNode>> {
		public Iterable<? extends ITimeAwareGraphNode> apply(IGraphNode sourceNode) {
			final ITimeAwareGraphDatabase taGraph = (ITimeAwareGraphDatabase) graph;
			if (sourceNode == null) {
				return taGraph.allNodes(FileNode.FILE_NODE_LABEL);
			} else {
				final ITimeAwareGraphNode taNode = (ITimeAwareGraphNode) sourceNode;
				return taGraph.allNodes(FileNode.FILE_NODE_LABEL, taNode.getTime());
			}
		}
	}

	/**
	 * This is a contextful 'all files' provider, which will use glob patterns to
	 * filter the files to be returned. If there is a context node, the files to be
	 * filtered will be those that existed at the same instant.
	 */
	protected class GlobPatternTimeAwareAllFiles implements Function<IGraphNode, Iterable<? extends IGraphNode>> {
		private final List<String> rplist;
		private final List<String> fplist;

		protected GlobPatternTimeAwareAllFiles(List<String> rplist, List<String> fplist) {
			this.rplist = rplist;
			this.fplist = fplist;
		}

		@Override
		public Iterable<? extends ITimeAwareGraphNode> apply(IGraphNode sourceNode) {
			final ITimeAwareGraphNode taNode = (ITimeAwareGraphNode) sourceNode;
			final ITimeAwareGraphDatabase tadb = (ITimeAwareGraphDatabase) graph;
			final GraphWrapper gw = new GraphWrapper(tadb);

			try (IGraphTransaction tx = tadb.beginTransaction()) {
				Set<FileNode> fileNodes;
				if (sourceNode == null) {
					fileNodes = gw.getFileNodes(rplist, fplist);
				} else {
					final ITimeAwareGraphNodeIndex fileIndex = tadb.getFileIndex().travelInTime(taNode.getTime());
					fileNodes = gw.getFileNodes(fileIndex, rplist, fplist);
				}

				final Set<ITimeAwareGraphNode> rawFileNodes = new HashSet<>();
				for (FileNode fn : fileNodes) {
					rawFileNodes.add((ITimeAwareGraphNode) fn.getNode());
				}
				tx.success();

				return rawFileNodes;
			} catch (Exception e) {
				LOGGER.error("Failed to find matching files", e);
				return Collections.emptySet();
			}
		}
	}

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

	/**
	 * Provides a set of file nodes, based on some other node. {@link #getFiles()}
	 * and other similar methods rely on this, so we may use composition rather than
	 * inheritance for this type of behaviour.
	 */
	private Function<IGraphNode, Iterable<? extends IGraphNode>> allFiles;

	/**
	 * Provides on demand a list of instances of a type.
	 */
	private AllOf allOf;

	public TimeAwareEOLQueryEngine() {
		this.allFiles = new ContextlessTimeAwareAllFiles();
		this.allOf = new ContextlessAllOf();
	}

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

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

	/**
	 * Extends "allInstances" with the concept of time. It is likely to be used
	 * through the {@link #allInstancesNow()} convenience function.
	 * 
	 * The regular "allInstances" can still be used in timeline queries, where the
	 * global timepoint is changed.
	 */
	public Collection<?> allInstancesAt(long timepoint) {
		final Set<Object> allContents = new HashSet<Object>();
		final ITimeAwareGraphDatabase taGraph = (ITimeAwareGraphDatabase) graph;
		for (IGraphNode node : taGraph.allNodes(ModelElementNode.OBJECT_VERTEX_LABEL, timepoint)) {
			GraphNodeWrapper wrapper = new TimeAwareGraphNodeWrapper(node, this);
			allContents.add(wrapper);
		}
		return allContents;
	}

	/**
	 * Version of {@link #allInstancesAt(long)} required to work around a limitation
	 * in Epsilon's reflective operation lookup, which does not seem to consider the
	 * promotion of integer values into long values.
	 */
	public Collection<?> allInstancesAt(int timepoint) {
		return allInstancesAt((long) timepoint);
	}

	/**
	 * Returns all the instances in the model at this current moment, by visiting
	 * the type nodes at the current point in time.
	 */
	public Collection<?> allInstancesNow() {
		return allInstancesAt(System.currentTimeMillis());
	}

	/**
	 * Returns the proxy reference lists active at a certain point in time, within
	 * repositories whose URL matches the specified prefix.
	 *
	 * @param repositoryPrefix URL prefix to filter repositories with.
	 * @param timepoint Timepoint in milliseconds at which proxies should be listed.
	 * @throws Exception There was a problem querying the graph.
	 */
	public List<ProxyReferenceList> proxiesAt(String repositoryPrefix, long timepoint) throws Exception {
		final Set<Object> visitedIDs = new HashSet<>();
		final List<ProxyReferenceList> proxyReferenceLists = new ArrayList<>();
		final ITimeAwareGraphDatabase taGraph = (ITimeAwareGraphDatabase) graph;

		try (IGraphTransaction tx = graph.beginTransaction()) {
			ITimeAwareGraphNodeIndex idxProxies = taGraph
				.getOrCreateNodeIndex(GraphModelBatchInjector.PROXY_DICT_NAME)
				.travelInTime(timepoint);
			IGraphIterable<? extends IGraphNode> proxies = idxProxies
				.query(GraphModelUpdater.PROXY_REFERENCE_PREFIX, repositoryPrefix + "*");

			for (IGraphNode n : proxies) {
				if (visitedIDs.add(n.getId())) {
					ModelElementNode men = new ModelElementNode(n);
					proxyReferenceLists.addAll(men.getProxies());
				}
			}

			tx.success();
		}

		return proxyReferenceLists;
	}

	/**
	 * Convenience version of {@link #proxiesAt(String, long)} focused on the
	 * current timepoint, while allowing the user to specify a repository URL
	 * prefix.
	 */
	public List<ProxyReferenceList> proxiesNow(String repositoryPrefix) throws Exception {
		return proxiesAt(repositoryPrefix, System.currentTimeMillis());
	}

	/**
	 * Convenience version of {@link #proxiesAt(String, long)} which goes through
	 * all repositories at the current timepoint.
	 */
	public List<ProxyReferenceList> proxiesNow() throws Exception {
		return proxiesNow("");
	}

	/**
	 * Returns all the files indexed at a specific point in time.
	 *
	 * @param timepoint Timepoint whose files we want to list.
	 */
	public Set<FileNodeWrapper> getFilesAt(long timepoint) {
		final ITimeAwareGraphDatabase taGraph = (ITimeAwareGraphDatabase) graph;

		Set<FileNodeWrapper> allFNW = new HashSet<>();
		for (IGraphNode n : taGraph.allNodes(FileNode.FILE_NODE_LABEL, timepoint)) {
			allFNW.add(new FileNodeWrapper(new FileNode(n), this));
		}
		return allFNW;
	}

	/**
	 * Returns all the files indexed in the latest version of the graph.
	 */
	public Set<FileNodeWrapper> getFilesNow() {
		return getFilesAt(System.currentTimeMillis());
	}

	@Override
	public Object query(IModelIndexer m, String query, Map<String, Object> context)
			throws InvalidQueryException, QueryExecutionException {
		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 (!(m.getGraph() instanceof ITimeAwareGraphDatabase)) {
			throw new QueryExecutionException(getClass().getName() + " can only be used with time-aware backends");
		}

		String defaultnamespaces = null;
		if (context != null) {
			defaultnamespaces = (String) context.get(PROPERTY_DEFAULTNAMESPACES);
		}

		try (TimeAwareEOLQueryEngine q = new TimeAwareEOLQueryEngine()) {
			q.load(m);
			q.setDefaultNamespaces(defaultnamespaces);
			if (context == null || context.isEmpty()) {
				// nothing to do!
			} else {
				q.setContext(context);
			}

			final IEolModule module = createModule();
			module.getContext().setOperationFactory(new TimeAwareEOLOperationFactory(q));

			final OperationContributorRegistry opcRegistry = module.getContext().getOperationContributorRegistry();
			opcRegistry.add(new TimeAwareNodeHistoryOperationContributor(q));
			opcRegistry.add(new TypeHistoryOperationContributor(q));
			parseQuery(query, context, q, module);
			return q.runQuery(module);
		} catch (EolModelLoadingException e) {
			throw new QueryExecutionException("Loading of EOLQueryEngine failed");
		}
	}

	@Override
	public String getHumanReadableName() {
		return "Time Aware " + super.getHumanReadableName();
	}

	@Override
	public Collection<Object> getAllOf(IGraphNode typeNode, String typeorkind) {
		final Collection<Object> nodes = createAllOfCollection(typeNode);
		allOf.addAllOf(typeNode, typeorkind, nodes);
		return nodes;
	}

	@Override
	public Set<FileNodeWrapper> getFiles() {
		Set<FileNodeWrapper> allFNW = new HashSet<>();
		for (IGraphNode rawNode : allFiles.apply(null)) {
			allFNW.add(new FileNodeWrapper(new FileNode(rawNode), this));
		}
		return allFNW;
	}

	
	@Override
	public ITimeAwareGraphDatabase getBackend() {
		return (ITimeAwareGraphDatabase) super.getBackend();
	}

	/**
	 * Returns the {@link RepositoryNode} associated with this instance node.
	 *
	 * @throws Exception Error while travelling in time to the matching instant on
	 *                   the repository node.
	 */
	public RepositoryNode getRepository(Object oInstanceNode) throws Exception {
		if (oInstanceNode instanceof GraphNodeWrapper) {
			GraphNodeWrapper gnw = (GraphNodeWrapper) oInstanceNode;
			if (gnw.getNode() instanceof ITimeAwareGraphNode) {
				ITimeAwareGraphNode taNode = (ITimeAwareGraphNode) gnw.getNode();
				ModelElementNode me = new ModelElementNode(taNode);
				String repoURL = me.getFileNode().getRepositoryURL();

				final ITimeAwareGraphDatabase taGraph = (ITimeAwareGraphDatabase) graph;
				final RepositoryNode repoNode = new VCSManagerIndex(taGraph).getOrCreateRepositoryNode(repoURL);

				return repoNode.travelInTime(taNode.getTime());
			}
		}
		return null;
	}

	private void setAllFiles(Function<IGraphNode, Iterable<? extends IGraphNode>> allFiles) {
		this.allFiles = allFiles;
	}

	private void setAllOf(AllOf allOf) {
		this.allOf = allOf;
	}

	/**
	 * Loads up the specified context into this object, changing its 'all of' and
	 * 'all files' providers.
	 */
	private void setContext(Map<String, Object> context) {
		// TODO use composition over existing property getter for context restriction -
		// needed for traversal scoping

		if (context.containsKey(PROPERTY_FILECONTEXT) || context.containsKey(PROPERTY_REPOSITORYCONTEXT)) {
			// Set up file/repository pattern lists
			final String sFilePatterns = (String) context.get(PROPERTY_FILECONTEXT);
			final String[] filePatterns = (sFilePatterns != null && sFilePatterns.trim().length() != 0)
					? sFilePatterns.split(",")
					: null;
			final List<String> fplist = (filePatterns != null) ? Arrays.asList(filePatterns) : null;

			final String sRepoPatterns = (String) context.get(PROPERTY_REPOSITORYCONTEXT);
			final String[] repoPatterns = (sRepoPatterns != null && sRepoPatterns.trim().length() != 0)
					? sRepoPatterns.split(",")
					: null;
			final List<String> rplist = (repoPatterns != null) ? Arrays.asList(repoPatterns) : null;

			/*
			 * Create suppliers for allFiles and allOf, so we may support file/repository
			 * contexts.
			 *
			 * TODO - currently limited to file first (no subtree, no derived allOf support
			 * yet).
			 */
			final GlobPatternTimeAwareAllFiles innerAllFiles = new GlobPatternTimeAwareAllFiles(rplist, fplist);
			setAllFiles(innerAllFiles);
			setAllOf(new TypeFirstAllOf(innerAllFiles, this));
		}
	}

	@Override
	public GraphNodeWrapper wrap(IGraphNode node) {
		/*
		 * If we're wrapping a scoping node, then it has to be a strong reference-based one.
		 * Otherwise, use a weak reference-based one to save memory.
		 */
		if (node instanceof IScopingTimeAwareGraphNode) {
			return new StrongGraphNodeWrapper(node, this);
		} else {
			return new TimeAwareGraphNodeWrapper(node, this);
		}
	}

}
