/*******************************************************************************
 * 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
 *     Jonathan Co - update for Epsilon 2.0 compatibility
 ******************************************************************************/
package org.eclipse.hawk.epsilon.emc.pgetters;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.epsilon.eol.exceptions.EolIllegalPropertyException;
import org.eclipse.epsilon.eol.exceptions.EolRuntimeException;
import org.eclipse.epsilon.eol.types.EolSequence;
import org.eclipse.hawk.core.IModelIndexer;
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.util.Utils;
import org.eclipse.hawk.epsilon.emc.EOLQueryEngine.GraphNodeWrapper;
import org.eclipse.hawk.epsilon.emc.contextful.CEOLQueryEngine;
import org.eclipse.hawk.graph.FileNode;
import org.eclipse.hawk.graph.ModelElementNode;
import org.eclipse.hawk.graph.updater.DirtyDerivedFeaturesListener;

public class CGraphPropertyGetter extends GraphPropertyGetter {

	private final CEOLQueryEngine engine;

	public CGraphPropertyGetter(IGraphDatabase graph, CEOLQueryEngine m) {
		super(graph, m);
		engine = m;
	}

	@SuppressWarnings("unchecked")
	@Override
	public Object invoke(Object object, final String property) throws EolRuntimeException {
		Object ret = null;
		if (!(object instanceof GraphNodeWrapper))
			throw new EolRuntimeException("a non GraphNodeWrapper object passed to GraphPropertyGetter!");

		final IGraphNode node = ((GraphNodeWrapper) object).getNode();

		// avoid using switch (in the outer-most structure) to allow partial
		// matches and method calls in alternatives
		if (property.equals("hawkFile")) {

			String sep = "";
			StringBuilder buff = new StringBuilder(32);
			for (IGraphEdge e : node.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE)) {
				buff.append(sep);
				buff.append(e.getEndNode().getProperty(IModelIndexer.IDENTIFIER_PROPERTY).toString());
				sep = ";";

			}
			ret = buff.toString();

		} else if (property.equals("hawkRepo")) {

			String sep = "";
			StringBuilder buff = new StringBuilder(32);
			for (IGraphEdge e : node.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE)) {
				buff.append(sep);
				buff.append(e.getEndNode().getProperty(FileNode.PROP_REPOSITORY).toString());
				sep = ";";

			}
			ret = buff.toString();

		} else if (property.equals("hawkFiles")) {

			Set<String> files = new HashSet<>();
			for (IGraphEdge e : node.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE))
				files.add(e.getEndNode().getProperty(IModelIndexer.IDENTIFIER_PROPERTY).toString());

			ret = files;

		} else if (property.equals("hawkRepos")) {

			Set<String> repos = new HashSet<>();
			for (IGraphEdge e : node.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE))
				repos.add(e.getEndNode().getProperty(FileNode.PROP_REPOSITORY).toString());

			ret = repos;

		} else if (property.startsWith(REVERSE_REFNAV_PREFIX)) {

			List<GraphNodeWrapper> nodes = new EolSequence<GraphNodeWrapper>();
			final String referenceName = property.substring(REVERSE_REFNAV_PREFIX.length());
			for (IGraphEdge r : node.getIncomingWithType(referenceName)) {
				GraphNodeWrapper n = wrapIfInScope(r.getStartNode());
				if (n != null)
					nodes.add(n);
			}
			for (IGraphEdge r : node.getIncomingWithType(ModelElementNode.DERIVED_EDGE_PREFIX + referenceName)) {
				IGraphNode derivedNode = r.getStartNode();
				IGraphNode elementNode = derivedNode.getIncoming().iterator().next().getStartNode();
				GraphNodeWrapper n = wrapIfInScope(elementNode);
				if (n != null) {
					nodes.add(n);
				}
			}

			ret = nodes;

		} else if (property.equals("eContainer")) {

			Iterable<IGraphEdge> inc = node.getIncoming();

			for (IGraphEdge r : inc) {

				if (r.getProperty(ModelElementNode.EDGE_PROPERTY_CONTAINMENT) != null) {

					ret = wrapIfInScope(r.getStartNode());

					break;

				}

			}

			if (ret == null)
				throw new EolRuntimeException("eContainer failed,\n" + object + "\nis not contained in current scope");

		}

		else if (property.equals("eContents")) {
			ret = getContents(node);
		}
		else if (property.equals("eAllContents")) {
			ret = addAllContents(node, new ArrayList<>());
		}
		else if (property.equals("eContainers")) {
			for (IGraphEdge r : node.getIncoming()) {
				if (r.getProperty(ModelElementNode.EDGE_PROPERTY_CONTAINMENT) != null) {
					ret = wrapIfInScope(r.getStartNode());
				}
			}
			if (ret == null) {
				for (IGraphEdge r : node.getOutgoing()) {
					if (r.getProperty(ModelElementNode.EDGE_PROPERTY_CONTAINER) != null) {
						ret = wrapIfInScope(r.getEndNode());
						break;
					}
				}
			}

			if (ret == null) {
				ret = Collections.emptyList();
			} else {
				ret = Collections.singletonList(ret);
			}
		}
		else if (property.equals("hawkIn") || property.equals("hawkOut")) {
			final boolean isIncoming = property.equals("hawkIn");
			final List<GraphNodeWrapper> results = new ArrayList<>();
			final Iterable<IGraphEdge> edges = isIncoming ? node.getIncoming() : node.getOutgoing();
			for (IGraphEdge r : edges) {
				if (ModelElementNode.TRANSIENT_EDGE_LABELS.contains(r.getType())) {
					continue;
				}
				final IGraphNode edgeNode = isIncoming ? r.getStartNode() : r.getEndNode();
				final GraphNodeWrapper edgeNodeWrapper = wrapIfInScope(edgeNode);
				results.add(edgeNodeWrapper);
			}
			ret = results;
		}
		else if (property.equals("hawkProxies")) {
			// TODO perform context filtering here as well (we'd need to do our own pattern matching)
			return new ModelElementNode(node).getProxies();
		}
		else if (canHaveDerivedAttr(node, property)) {

			for (IGraphEdge r : node.getOutgoingWithType(property)) {
				if (ret == null) {
					final IGraphNode derivedNode = r.getEndNode();
					ret = derivedNode.getProperty(property);
					if (ret == null) {
						List<GraphNodeWrapper> derivedTargets = new EolSequence<>();
						for (IGraphEdge edge : derivedNode.getOutgoingWithType(ModelElementNode.DERIVED_EDGE_PREFIX + property)) {
							derivedTargets.add(model.wrap(edge.getEndNode()));
							ret = derivedTargets;
						}
					}

					ret = retainScoped(ret);

				} else {
					throw new EolRuntimeException("WARNING: a derived property (arity 1) -- ( " + property
							+ " ) has more than 1 links in store!");
				}
			}
			if (ret == null) {
				throw new EolRuntimeException("derived attribute lookup failed for: " + object + " # " + property);
			} else if (ret instanceof String && ((String) ret).startsWith(DirtyDerivedFeaturesListener.NOT_YET_DERIVED_PREFIX)) {
				// XXX IDEA: dynamically derive on the spot on access
				System.err.println("attribute: " + property + " is NYD for node: " + node.getId());
			}

		}

		else if (canHaveMixed(node, property)) {

			ret = getCollectionForProperty(property);

			final Collection<Object> retCollection = (Collection<Object>) ret;

			if (node.getProperty(property) != null) {

				final List<?> values = new Utils().asList(node.getProperty(property));
				retCollection.addAll(values);

			}

			for (IGraphEdge r : node.getOutgoingWithType(property)) {
				GraphNodeWrapper o = wrapIfInScope(r.getEndNode());
				if (o != null)
					retCollection.add(o);
			}
		}

		else if (canHaveAttr(node, property)) {

			if (node.getProperty(property) != null) {
				// FIXMEdone handle collections / ordered etc
				if (!(isMany(property)))
					ret = node.getProperty(property);
				else {

					ret = getCollectionForProperty(property);

					Object[] array = ((Object[]) node.getProperty(property));

					for (int i = 0; i < array.length; i++)
						((Collection<Object>) ret).add(array[i]);

				}
			} else
			// return new NeoIdWrapper(0L, m);
			{
				// ret = "UNSET";
				ret = null;
				// throw new EolRuntimeException("unset property");
			}

		}

		else if (canHaveRef(node, property)) {

			GraphNodeWrapper otherNode = null;
			Collection<Object> otherNodes = null;

			// FIXMEdone arity etc in metamodel
			// otherNodes = new EolBag<NeoIdWrapperDebuggable>();
			if (isMany(property)) {
				otherNodes = getCollectionForProperty(property);
			}

			for (IGraphEdge r : node.getOutgoingWithType(property)) {
				IGraphNode n = r.getEndNode();
				GraphNodeWrapper o = wrapIfInScope(n);
				if (otherNodes != null) {
					if (o != null)
						otherNodes.add(o);
				} else if (otherNode == null)
					otherNode = o;
				else
					throw new EolRuntimeException(
							"A relationship with arity 1 ( " + property + " ) has more than 1 links");
			}

			ret = otherNodes != null ? otherNodes : otherNode;

		} else {
			throw new EolIllegalPropertyException(object, property, null);
		}

		if (broadcastAccess)
			broadcastAccess(object, property);

		return ret;

	}

	private List<GraphNodeWrapper> addAllContents(IGraphNode node, List<GraphNodeWrapper> results) {
		for (GraphNodeWrapper child : getContents(node)) {
			results.add(child);
			addAllContents(child.getNode(), results);
		}
		return results;
	}

	private List<GraphNodeWrapper> getContents(final IGraphNode node) {
		final List<GraphNodeWrapper> results = new ArrayList<>();
		Iterable<IGraphEdge> out = node.getOutgoing();
		for (IGraphEdge r : out) {
			if (r.getProperty(ModelElementNode.EDGE_PROPERTY_CONTAINMENT) != null) {
				final GraphNodeWrapper endNode = wrapIfInScope(r.getEndNode());
				if (endNode != null) {
					results.add(endNode);
				}
			}
		}
		return results;
	}

	private Object retainScoped(Object ret) {

		Collection<?> cRet = null;
		if (ret instanceof Collection<?>)
			cRet = (Collection<?>) ret;

		if (cRet == null) {
			if (ret instanceof GraphNodeWrapper)
				ret = retainScoped((GraphNodeWrapper) ret);
		} else {
			Iterator<?> it = cRet.iterator();
			while (it.hasNext()) {
				Object r = it.next();
				if (r instanceof GraphNodeWrapper)
					if (retainScoped((GraphNodeWrapper) r) == null)
						it.remove();
			}
		}
		return cRet == null ? ret : cRet;
	}

	private Object retainScoped(GraphNodeWrapper ret) {
		if (!engine.isTraversalScopingEnabled())
			return ret;

		// capture multiple file containment (ie for singleton nodes)
		for (IGraphEdge e : graph.getNodeById(ret.getId()).getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE)) {
			if (engine.getRawFileNodes().contains(e.getEndNode())) {
				return ret;
			}
		}

		return null;
	}

	private GraphNodeWrapper wrapIfInScope(IGraphNode node) {
		if (!engine.isTraversalScopingEnabled())
			return model.wrap(node);

		// capture multiple file containment (ie for singleton nodes)
		for (IGraphEdge e : node.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE)) {
			if (engine.getRawFileNodes().contains(e.getEndNode())) {
				return model.wrap(node);
			}
		}

		return null;
	}
}
