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

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.NoSuchElementException;
import java.util.Set;

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.timeaware.ITimeAwareGraphNode;
import org.eclipse.hawk.graph.updater.GraphModelUpdater;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Wraps an {@link IGraphDatabase} that has been updated by this plugin. This is
 * the starting point for the read-only abstraction of the graph populated by
 * this updater. Users of this class and any other classes of this package are
 * expected to manage their own transactions with
 * {@link IGraphDatabase#beginTransaction()}.
 *
 * TODO This is an incomplete WIP abstraction. More methods will be called as
 * the existing queries are moved into this API.
 */
public class GraphWrapper {

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

	protected final IGraphDatabase graph;

	public GraphWrapper(IGraphDatabase graph) {
		this.graph = graph;
	}

	/**
	 * Convenience version of
	 * {@link #getFileNodes(IGraphNodeIndex, Iterable, Iterable)} which passes the
	 * database's default file index configuration.
	 */
	public Set<FileNode> getFileNodes(Iterable<String> repoPatterns, Iterable<String> filePatterns) {
		final IGraphNodeIndex fileIndex = graph.getFileIndex();
		return getFileNodes(fileIndex, repoPatterns, filePatterns);
	}

	/**
	 * Returns all the file nodes with repository URLs within the specified file
	 * index (usually from {@link IGraphDatabase#getFileIndex()}) that match the
	 * <code>repositoryPattern</code> and at least one of the <code>patterns</code>.
	 *
	 * @param fileIndex    File index that can retrieve all available files through
	 *                     glob patterns.
	 * @param rplist       Pattern for the repository URL. <code>null</code> or
	 *                     <code>"*"</code> will access all repositories.
	 * @param filePatterns Patterns for the files. Having <code>"*"</code> as an
	 *                     element or passing a null or empty {@link Iterable} will
	 *                     return all files in the selected repository or
	 *                     repositories. If a file pattern has URI-invalid
	 *                     characters (e.g. spaces), it will be URI-encoded first.
	 */
	public Set<FileNode> getFileNodes(final IGraphNodeIndex fileIndex, Iterable<String> repoPatterns,
			Iterable<String> filePatterns) {
		if (repoPatterns == null || !repoPatterns.iterator().hasNext()) {
			repoPatterns = Arrays.asList("*");
		}
		if (filePatterns == null || !filePatterns.iterator().hasNext()) {
			filePatterns = Arrays.asList("*");
		}

		final Set<FileNode> files = new LinkedHashSet<>();
		for (String repo : repoPatterns) {
			for (String file : filePatterns) {
				String fullPattern;
				if ("*".equals(repo) && "*".equals(file)) {
					fullPattern = "*";
				} else {
					try {
						// Is this a valid URI?
						new URI(file);
					} catch (URISyntaxException ex) {
						try {
							// No, encode it (but keep slashes and use %20 instead of + for spaces)
							file = URLEncoder.encode(file, StandardCharsets.UTF_8.toString())
									.replaceAll("%2F", "/").replaceAll("[+]", "%20");
						} catch (UnsupportedEncodingException e) {
							LOGGER.error(e.getMessage(), e);
						}
					}

					fullPattern = repo.trim() + GraphModelUpdater.FILEINDEX_REPO_SEPARATOR + file.trim();
				}

				for (IGraphNode n : fileIndex.query("id", fullPattern)) {
					files.add(new FileNode(n));
				}
			}
		}
		return files;
	}

	/**
	 * Returns the graph wrapped by this instance.
	 */
	public IGraphDatabase getGraph() {
		return graph;
	}

	/**
	 * Retrieves a {@link ModelElementNode} by identifier.
	 *
	 * @throws NoSuchElementException
	 *             No node with that identifier exists.
	 */
	public ModelElementNode getModelElementNodeById(Object id) {
		final IGraphNode rawNode = graph.getNodeById(id);
		if (rawNode == null) {
			throw new NoSuchElementException("No node exists with id " + id);
		}
		return new ModelElementNode(rawNode);
	}

	/**
	 * Retrieves a {@link ModelElementNode} referenced from another node. Will
	 * make sure to reuse the same context, i.e. same world/timepoint if we are
	 * working within a time-aware graph.
	 */
	public ModelElementNode getModelElementNodeById(ModelElementNode ctx, Object id) {
		final IGraphNode ctxNode = ctx.getNode();
		final IGraphNode rawNode = graph.getNodeById(id);
		if (rawNode == null) {
			throw new NoSuchElementException("No node exists with id " + id);
		}

		if (ctxNode instanceof ITimeAwareGraphNode) {
			ITimeAwareGraphNode ctxTANode = (ITimeAwareGraphNode) ctxNode;
			ITimeAwareGraphNode rawTANode = (ITimeAwareGraphNode) rawNode;
			return new ModelElementNode(rawTANode.travelInTime(ctxTANode.getTime()));
		}
		return new ModelElementNode(rawNode);
	}

	/**
	 * Retrieves a {@link TypeNode} by identifier.
	 *
	 * @throws NoSuchElementException
	 *             No node with that identifier exists.
	 */
	public TypeNode getTypeNodeById(String id) {
		final IGraphNode rawNode = graph.getNodeById(id);
		if (rawNode == null) {
			throw new NoSuchElementException("No type node exists with id " + id);
		}
		return new TypeNode(rawNode);
	}

	/**
	 * Retrieves a metamodel node by namespace URI.
	 *
	 * @throws NoSuchElementException
	 *             No metamodel node with that namespace URI exists.
	 */
	public MetamodelNode getMetamodelNodeByNsURI(String nsURI) {
		final Iterator<? extends IGraphNode> metamodelNode = graph.getMetamodelIndex().query("id", nsURI).iterator();
		if (!metamodelNode.hasNext()) {
			throw new NoSuchElementException("No metamodel node exists with URI " + nsURI);
		}
		return new MetamodelNode(metamodelNode.next());
	}

	/**
	 * Returns an iterable of all metamodel nodes.
	 */
	public Iterable<MetamodelNode> getMetamodelNodes() {
		final IGraphIterable<? extends IGraphNode> metamodelNodes = graph.getMetamodelIndex().query("*", "*");
		return new Iterable<MetamodelNode>() {

			@Override
			public Iterator<MetamodelNode> iterator() {
				final Iterator<? extends IGraphNode> itMN = metamodelNodes.iterator();
				return new Iterator<MetamodelNode>(){

					@Override
					public boolean hasNext() {
						return itMN.hasNext();
					}

					@Override
					public MetamodelNode next() {
						return new MetamodelNode(itMN.next());
					}

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

				};
			}

		};
	}
}
