/*******************************************************************************
 * 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.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.eclipse.epsilon.eol.exceptions.EolIllegalPropertyException;
import org.eclipse.epsilon.eol.exceptions.EolRuntimeException;
import org.eclipse.epsilon.eol.execute.context.IEolContext;
import org.eclipse.epsilon.eol.execute.introspection.AbstractPropertyGetter;
import org.eclipse.epsilon.eol.types.EolOrderedSet;
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;
import org.eclipse.hawk.epsilon.emc.EOLQueryEngine.GraphNodeWrapper;
import org.eclipse.hawk.epsilon.emc.tracking.AccessListener;
import org.eclipse.hawk.epsilon.emc.wrappers.GraphEdgeWrapper;
import org.eclipse.hawk.graph.FileNode;
import org.eclipse.hawk.graph.ModelElementNode;
import org.eclipse.hawk.graph.TypeNode;
import org.eclipse.hawk.graph.updater.DirtyDerivedFeaturesListener;
import org.eclipse.hawk.graph.updater.GraphModelInserter;

public class GraphPropertyGetter extends AbstractPropertyGetter {

	public static final String REVERSE_REFNAV_PREFIX = "revRefNav_";

	protected static final int IDX_FLAG_MANY = 1;
	protected static final int IDX_FLAG_ORDERED = 2;
	protected static final int IDX_FLAG_UNIQUE = 3;

	protected static enum PropertyType {
		ATTRIBUTE, DERIVED, REFERENCE, MIXED, INVALID;
		static PropertyType fromCharacter(String s) {
			switch (s) {
			case "d":
				return DERIVED;
			case "r":
				return REFERENCE;
			case "a":
				return ATTRIBUTE;
			case "m":
				return MIXED;
			default:
				return INVALID;
			}
		}
	}

	protected boolean broadcastAccess = false;

	protected IGraphDatabase graph;
	protected EOLQueryEngine model;
	protected AccessListener accessListener = new AccessListener();

	// Cache: type node -> property name -> property type
	protected Map<IGraphNode, Map<String, PropertyType>> propertyTypeCache = new WeakHashMap<>();
	protected Map<IGraphNode, Map<String, String[]>> propertyTypeFlagsCache = new WeakHashMap<>();

	private IGraphNode featureStartingNodeClassNode;

	public GraphPropertyGetter(IGraphDatabase graph, EOLQueryEngine m) {
		this.graph = graph;
		this.model = m;
	}
	
	@Override
	public Object invoke(Object object, final String property, IEolContext context) throws EolRuntimeException {
		return invoke(object, property);
	}

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

		IGraphNode node = ((GraphNodeWrapper) object).getNode();
		Object ret = invokePredefined(property, node);
		if (ret == null) {
			ret = invokeElementProperty(object, property, node);
		}
		if (broadcastAccess)
			broadcastAccess(object, property);

		return ret;
	}

	protected Object invokeElementProperty(final Object obj, final String property, IGraphNode node)
			throws EolRuntimeException, EolIllegalPropertyException {
		PropertyType propertyType = getPropertyType(node, property);
		switch (propertyType) {
		case ATTRIBUTE:
			Object value = node.getProperty(property);
			if (value != null) {
				if (!(isMany(property)))
					return value;
				else
					return new Utils().asList(value);
			} else
				return null;

		case DERIVED:
			Object derivedValue = null;
			for (IGraphEdge r : node.getOutgoingWithType(property)) {
				if (derivedValue != null) {
					throw new EolRuntimeException(String.format(
							"WARNING: a derived property node (arity 1) -- (%s) has more than 1 links in store!",
							property));
				}

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

			if (derivedValue == null) {
				throw new EolRuntimeException("derived attribute lookup failed for: " + node + " # " + property);
			} else if (derivedValue instanceof String
					&& ((String) derivedValue).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());
			}

			return derivedValue;
		case MIXED:
			final Collection<Object> retCollection = getCollectionForProperty(property);
			if (node.getProperty(property) != null) {
				final List<?> values = new Utils().asList(node.getProperty(property));
				retCollection.addAll(values);
			}
			for (IGraphEdge r : node.getOutgoingWithType(property)) {
				retCollection.add(model.wrap(r.getEndNode()));
			}
			return retCollection;

		case REFERENCE:
			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)) {
				if (otherNodes != null)
					otherNodes.add(model.wrap(r.getEndNode()));
				else if (otherNode == null)
					otherNode = model.wrap(r.getEndNode());
				else
					throw new EolRuntimeException(
							"A relationship with arity 1 ( " + property + " ) has more than 1 links");
			}

			return otherNodes != null ? otherNodes : otherNode;
		default:
			return null;
		}
	}

	protected Object invokePredefined(final String property, IGraphNode node) throws EolRuntimeException {
		if (property.startsWith(REVERSE_REFNAV_PREFIX)) {
			final String referenceName = property.substring(REVERSE_REFNAV_PREFIX.length());

			final EolSequence<GraphNodeWrapper> ret = new EolSequence<GraphNodeWrapper>();
			for (IGraphEdge r : node.getIncomingWithType(referenceName)) {
				ret.add(model.wrap(r.getStartNode()));
			}
			for (IGraphEdge r : node.getIncomingWithType(ModelElementNode.DERIVED_EDGE_PREFIX + referenceName)) {
				IGraphNode derivedNode = r.getStartNode();
				IGraphNode elementNode = derivedNode.getIncoming().iterator().next().getStartNode();
				ret.add(model.wrap(elementNode));
			}

			return ret;
		}

		switch (property) {
		case "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 = ";";

			}
			return buff.toString();
		}
		case "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 = ";";

			}
			return buff.toString();
		}
		case "hawkFiles": {
			Set<String> files = new HashSet<>();
			for (IGraphEdge e : node.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE))
				files.add(e.getEndNode().getProperty(IModelIndexer.IDENTIFIER_PROPERTY).toString());

			return files;
		}
		case "hawkRepos": {
			Set<String> repos = new HashSet<>();
			for (IGraphEdge e : node.getOutgoingWithType(ModelElementNode.EDGE_LABEL_FILE))
				repos.add(e.getEndNode().getProperty(FileNode.PROP_REPOSITORY).toString());

			return repos;
		}
		case "eContainer": {
			GraphNodeWrapper ret = null;
			for (IGraphEdge r : node.getIncoming()) {
				if (r.getProperty(ModelElementNode.EDGE_PROPERTY_CONTAINMENT) != null) {
					ret = model.wrap(r.getStartNode());
					break;
				}
			}
			if (ret == null) {
				for (IGraphEdge r : node.getOutgoing()) {
					if (r.getProperty(ModelElementNode.EDGE_PROPERTY_CONTAINER) != null) {
						ret = model.wrap(r.getEndNode());
						break;
					}
				}
			}

			// null if not contained
			return ret;
		}
		case "eContainers": {
			// This version returns a collection - if you don't have a container, it will return
			// an empty collection rather than failing. Useful for closures.
			GraphNodeWrapper ret = null;
			for (IGraphEdge r : node.getIncoming()) {
				if (r.getProperty(ModelElementNode.EDGE_PROPERTY_CONTAINMENT) != null) {
					return Collections.singletonList(model.wrap(r.getStartNode()));
				}
			}
			if (ret == null) {
				for (IGraphEdge r : node.getOutgoing()) {
					if (r.getProperty(ModelElementNode.EDGE_PROPERTY_CONTAINER) != null) {
						return Collections.singletonList(model.wrap(r.getEndNode()));
					}
				}
			}
			return Collections.emptyList();
		}
		case "eContents": {
			return getContents(node);
		}
		case "eAllContents": {
			return addAllContents(node, new ArrayList<>());
		}
		case "hawkIn":
		case "hawkOut": {
			final boolean isIncoming = property.equals("hawkIn");
			final List<GraphNodeWrapper> results = new EolSequence<>();
			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();
				if (r.getProperty(GraphModelInserter.DERIVED_FEATURE_EDGEPROP) != null) {
					final Iterable<IGraphEdge> it = isIncoming ? edgeNode.getIncoming() : edgeNode.getOutgoing();
					for (IGraphEdge derivedEdge : it) {
						final IGraphNode derivedEdgeNode = isIncoming ? derivedEdge.getStartNode()
								: derivedEdge.getEndNode();
						final GraphNodeWrapper edgeNodeWrapper = model.wrap(derivedEdgeNode);
						results.add(edgeNodeWrapper);
					}
				} else {
					final GraphNodeWrapper edgeNodeWrapper = model.wrap(edgeNode);
					results.add(edgeNodeWrapper);
				}
			}
			return results;
		}
		case "hawkInEdges":
		case "hawkOutEdges": {
			final boolean isIncoming = property.equals("hawkInEdges");
			final List<GraphEdgeWrapper> results = new EolSequence<>();
			final Iterable<IGraphEdge> edges = isIncoming ? node.getIncoming() : node.getOutgoing();
			for (IGraphEdge r : edges) {
				if (ModelElementNode.TRANSIENT_EDGE_LABELS.contains(r.getType())) {
					continue;
				}
				if (r.getProperty(GraphModelInserter.DERIVED_FEATURE_EDGEPROP) != null) {
					final IGraphNode derivedNode = isIncoming ? r.getStartNode() : r.getEndNode();
					final Iterable<IGraphEdge> it = isIncoming ? derivedNode.getIncoming() : derivedNode.getOutgoing();
					for (IGraphEdge derivedEdge : it) {
					results.add(new GraphEdgeWrapper(derivedEdge, model));
					}
				} else
					results.add(new GraphEdgeWrapper(r, model));
			}
			return results;
		}
		case "hawkURIFragment":
			return node.getProperty(IModelIndexer.IDENTIFIER_PROPERTY);
		case "hawkProxies": {
			ModelElementNode men = new ModelElementNode(node);
			return men.getProxies();
		}
		default:
			return null;
		}
	}

	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(IGraphNode node) {
		final List<GraphNodeWrapper> results = new ArrayList<>();
		for (IGraphEdge r : node.getOutgoing()) {
			if (r.getProperty(ModelElementNode.EDGE_PROPERTY_CONTAINMENT) != null) {

				// TODO add ability to mark derived edges as containments to
				// be able to use them here

				results.add(model.wrap(r.getEndNode()));
			}
		}
		for (IGraphEdge r : node.getIncoming()) {
			if (r.getProperty(ModelElementNode.EDGE_PROPERTY_CONTAINER) != null) {
				results.add(model.wrap(r.getStartNode()));
			}
		}
		return results;
	}

	protected Collection<Object> getCollectionForProperty(final String property) {
		if (isUnique(property))
			return new EolOrderedSet<Object>();
		else
			return new EolSequence<Object>();
	}

	protected void broadcastAccess(Object object, String property) {
		accessListener.accessed(((GraphNodeWrapper) object).getId() + "", property);
	}

	public void setBroadcastAccess(boolean b) {
		broadcastAccess = b;
	}

	public AccessListener getAccessListener() {
		return accessListener;
	}

	public IGraphDatabase getGraph() {
		return graph;
	}

	public boolean getBroadcastStatus() {
		return broadcastAccess;
	}

	protected boolean canHaveDerivedAttr(IGraphNode node, String property) {
		return getPropertyType(node, property) == PropertyType.DERIVED;
	}

	protected boolean canHaveMixed(IGraphNode node, String property) {
		return getPropertyType(node, property) == PropertyType.MIXED;
	}

	protected boolean canHaveAttr(IGraphNode node, String property) {
		return getPropertyType(node, property) == PropertyType.ATTRIBUTE;
	}

	protected boolean canHaveRef(IGraphNode node, String property) {
		return getPropertyType(node, property) == PropertyType.REFERENCE;
	}

	protected boolean isMany(String ref) {
		return isTypeFlagActive(ref, IDX_FLAG_MANY);
	}

	protected boolean isOrdered(String ref) {
		return isTypeFlagActive(ref, IDX_FLAG_ORDERED);
	}

	protected boolean isUnique(String ref) {
		return isTypeFlagActive(ref, IDX_FLAG_UNIQUE);
	}

	protected PropertyType getPropertyType(IGraphNode node, String property) {
		final Iterator<IGraphEdge> itTypeOf = node.getOutgoingWithType(ModelElementNode.EDGE_LABEL_OFTYPE).iterator();

		if (itTypeOf.hasNext()) {
			featureStartingNodeClassNode = itTypeOf.next().getEndNode();
			Map<String, PropertyType> knownProperties = propertyTypeCache.get(featureStartingNodeClassNode);
			if (knownProperties == null) {
				knownProperties = new HashMap<>();
				propertyTypeCache.put(featureStartingNodeClassNode, knownProperties);
			}

			PropertyType actual = knownProperties.get(property);
			if (actual == null) {
				String value = "_null_hawk_value_error";

				if (featureStartingNodeClassNode.getProperty(property) != null)
					value = ((String[]) featureStartingNodeClassNode.getProperty(property))[0];

				actual = PropertyType.fromCharacter(value);
				if (actual == PropertyType.INVALID) {
					System.err.println("property: " + property + " not found in metamodel for type: "
							+ featureStartingNodeClassNode.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));
				}
				knownProperties.put(property, actual);
			}
			return actual;
		} else {
			System.err.println("type not found for node " + node);
		}

		return PropertyType.INVALID;
	}

	protected boolean isTypeFlagActive(String reference, final int index) {
		if (featureStartingNodeClassNode == null) {
			System.err.println("type not found previously for " + reference);
			return false;
		}

		// Cache type flags
		Map<String, String[]> knownTypeFlags = propertyTypeFlagsCache.get(featureStartingNodeClassNode);
		if (knownTypeFlags == null) {
			knownTypeFlags = new HashMap<>();
			propertyTypeFlagsCache.put(featureStartingNodeClassNode, knownTypeFlags);
		}
		String[] typeFlags = knownTypeFlags.get(reference);
		if (typeFlags == null && featureStartingNodeClassNode.getProperty(reference) != null) {
			typeFlags = (String[]) featureStartingNodeClassNode.getProperty(reference);
			knownTypeFlags.put(reference, typeFlags);
		}
		if (typeFlags != null) {
			return typeFlags[index].equals("t");
		}

		System.err.println("reference: " + reference + " not found in metamodel (isMany) for type: "
				+ featureStartingNodeClassNode.getProperty(IModelIndexer.IDENTIFIER_PROPERTY));
		return false;
	}

	public String debug(GraphNodeWrapper object) {
		IGraphNode node = object.getNode();
		String ret = node.toString();

		for (String p : node.getPropertyKeys()) {
			Object n = node.getProperty(p);
			String temp = "error: " + n.getClass();
			if (n instanceof int[])
				temp = Arrays.toString((int[]) n);
			if (n instanceof long[])
				temp = Arrays.toString((long[]) n);
			if (n instanceof String[])
				temp = Arrays.toString((String[]) n);
			if (n instanceof boolean[])
				temp = Arrays.toString((boolean[]) n);

			ret = ret + ("[" + p + ";" + (((p.equals("class") || p.equals("superclass")))
					? (temp.length() < 1000 ? temp : "[<TOO LONG TO LOG (>1000chars)>]") : (n)) + "] ");
		}

		Collection<String> refs = new HashSet<String>();
		for (IGraphEdge r : node.getOutgoing()) {
			refs.add(r.getType().toString());
		}

		return ret + "\nOF TYPE: "
			+ new TypeNode(node).getTypeName()
			+ "\nWITH OUTGOING REFERENCES: " + refs;
	}

}
