/**
 * <copyright>
 * Copyright (c) 2010-2014 Henshin developers. All rights reserved. 
 * This program and the accompanying materials are made available 
 * under the terms of the Eclipse Public License v1.0 which 
 * accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * </copyright>
 */
package org.eclipse.emf.henshin.interpreter.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EFactory;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.henshin.interpreter.EGraph;
import org.eclipse.emf.henshin.interpreter.impl.EGraphImpl;
import org.eclipse.emf.henshin.model.Attribute;
import org.eclipse.emf.henshin.model.Edge;
import org.eclipse.emf.henshin.model.Graph;
import org.eclipse.emf.henshin.model.HenshinFactory;
import org.eclipse.emf.henshin.model.Node;

/**
 * {@link Graph}-to-{@link EGraph} converter.
 */
public class HenshinEGraph extends EGraphImpl implements Adapter {
	
	// Generated serial ID
	private static final long serialVersionUID = -2408288653525326829L;

	// The Henshin graph:
	protected final Graph henshinGraph;
	
	// Correspondence maps: 
	protected Map<Node, EObject> node2object;
	protected Map<EObject, Node> object2node;
	
	/**
	 * Default constructor.
	 * @param graph The Henshin graph.
	 */
	public HenshinEGraph(Graph graph) {
		henshinGraph = graph;
		node2object = new HashMap<Node, EObject>();
		object2node = new HashMap<EObject, Node>();
		henshin2emfGraph();
	}
	
	/**
	 * Translates the Henshin {@link Graph} in {@link #henshinGraph} into an
	 * ordinary EMF graph with {@link EObject}s, {@link EReference}s and so on.
	 * Afterwards, corresponding {@link EObject}s can be found in
	 * <code>eObjects</code> and correspondences in <code>eObject2nodeMap</code>
	 * and <code>node2eObjectMap</code>.
	 * <p>
	 * <strong>Remark:</strong> Currently, edges typed over <i>derived</i>
	 * {@link EReference}s (see {@link EReference#isDerived()}) are omitted
	 * during translation. Furthermore, if a typing {@link EReference}s is
	 * non-changeable (see {@link EReference#isChangeable()}) but its opposite
	 * (see {@link EReference#getEOpposite()}) is changeable, the reference is
	 * translated into its opposite.
	 */
	@SuppressWarnings("unchecked")
	private void henshin2emfGraph() {
		
		clear();
		// henshinGraph.eAdapters().clear();
		object2node.clear();
		node2object.clear();
		
		for (Node node : henshinGraph.getNodes()) {
			EObject eObject = node2object.get(node);
			
			if (eObject == null) {
				EClass nodeType = node.getType();
				EFactory factory = nodeType.getEPackage().getEFactoryInstance();
				eObject = factory.create(nodeType);
				addSynchronizedPair(node, eObject);
				add(eObject);
			}
			
			for (Attribute attr : node.getAttributes()) {
				// don't notify me about changes that I made
				eObject.eAdapters().remove(this);
				EAttribute attrType = attr.getType();
				String attrValue = attr.getValue();
				attrValue = attrValue.replaceAll("\"", "");
				
				if (attrType.isMany()) {
					List<Object> attrValues = (List<Object>) eObject.eGet(attrType);
					attrValues.add(attrValue);
				} else {
					eObject.eSet(attrType,
							EcoreUtil.createFromString(attrType.getEAttributeType(), attrValue));
				}
				
				eObject.eAdapters().add(this);
			}
		}
		
		for (Edge edge : new ArrayList<Edge>(henshinGraph.getEdges())) {
			EReference edgeType = edge.getType();
			/*
			 * If reference <code>edgeType</code> is derived it is available
			 * implicitly and does not need to be set. Furthermore, if a
			 * reference is not changeable it is omitted as well.
			 */
			if (edgeType.isDerived())
				continue;
			
			EObject ownerObject = node2object.get(edge.getSource());
			EObject targetObject = node2object.get(edge.getTarget());
			
			/*
			 * If the edgeType is not changeable but its opposite is, then we
			 * switch to the opposite.
			 */
			if (!edgeType.isChangeable()) {
				if (edgeType.getEOpposite() != null && edgeType.getEOpposite().isChangeable()) {
					edgeType = edgeType.getEOpposite();
					// switch source and target
					EObject temp = ownerObject;
					ownerObject = targetObject;
					targetObject = temp;
				} else
					/*
					 * Otherwise we cannot handle the edge and omit it (or
					 * better: shall throw an exception)
					 */
					continue;
			}// if
			
			// don't notify me about changes that I made
			ownerObject.eAdapters().remove(this);
			targetObject.eAdapters().remove(this);

			if (edgeType.isMany()) {
				List<Object> edgeValues = (List<Object>) ownerObject.eGet(edgeType);
				edgeValues.add(targetObject);
			} else {
				ownerObject.eSet(edgeType, targetObject);
			}
			
			ownerObject.eAdapters().add(this);
			targetObject.eAdapters().add(this);
		}
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.impl.EGraphImpl#didAdd(org.eclipse.emf.ecore.EObject)
	 */
	@Override
	public void didAdd(EObject eObject) {
		super.didAdd(eObject);			
		Node node = object2node.get(eObject);
		if (node == null) {
			node = createNode();
			node.setType(eObject.eClass());
			henshinGraph.getNodes().add(node);
			addSynchronizedPair(node, eObject);
		} else {
			if (!henshinGraph.getNodes().contains(node))
				henshinGraph.getNodes().add(node);
		}
	}
	
	protected Attribute createAttribute() {
		return HenshinFactory.eINSTANCE.createAttribute();
	}

	protected Edge createEdge() {
		return HenshinFactory.eINSTANCE.createEdge();
	}

	protected Node createNode() {
		return HenshinFactory.eINSTANCE.createNode();
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.impl.EGraphImpl#didRemove(org.eclipse.emf.ecore.EObject)
	 */
	@Override
	public void didRemove(EObject object) {
		super.didRemove(object);
		Node node = object2node.get(object);
		if (node != null) {
			henshinGraph.getNodes().remove(node);
			List<Edge> list = new ArrayList<Edge>(node.getIncoming());
			list.addAll(node.getOutgoing());
			for (Edge edge : list) {
				edge.setSource(null);
				edge.setTarget(null);
				edge.setGraph(null);
			}
			removeSynchronizedPair(node, (EObject) object);
		}
	}
	
	/**
	 * This methods will update the EMF representation of the Henshin-Graph.
	 */
	public void updateEGraph() {
		henshin2emfGraph();
	}
	
	/*
	 * Add a synchronized pair of a node and an object.
	 */
	private void addSynchronizedPair(Node node, EObject eObject) {
		node2object.put(node, eObject);
		object2node.put(eObject, node);
		eObject.eAdapters().add(this);
	}
	
	/*
	 * Remove a synchronized pair of a node and an object.
	 */
	private void removeSynchronizedPair(Node node, EObject eObject) {
		// node2eObjectMap.remove(node);
		// eObject2nodeMap.remove(eObject);
		// eObject.eAdapters().remove(this);
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.common.notify.Adapter#getTarget()
	 */
	@Override
	public Notifier getTarget() {
		return null;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.common.notify.Adapter#isAdapterForType(java.lang.Object)
	 */
	@Override
	public boolean isAdapterForType(Object type) {
		return false;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.common.notify.Adapter#notifyChanged(org.eclipse.emf.common.notify.Notification)
	 */
	@Override
	public void notifyChanged(Notification notification) {
		EObject owner = (EObject) notification.getNotifier();
		Node ownerNode = object2node.get(owner);
		Object feature = notification.getFeature();
		Object oldValue = notification.getOldValue();
		Object newValue = notification.getNewValue();
		if (feature instanceof EStructuralFeature && ownerNode != null) {
			// remove all deleted structures from the henshin graph
			if (oldValue != null && newValue != oldValue) {
				removeFromHenshinGraph(ownerNode, (EStructuralFeature) feature, oldValue);
			}
			// add new structures to henshin graph
			if (newValue != null) {
				addToHenshinGraph(owner, (EStructuralFeature) feature, newValue);
			}
		}
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.common.notify.Adapter#setTarget(org.eclipse.emf.common.notify.Notifier)
	 */
	@Override
	public void setTarget(Notifier newTarget) {
	}
	
	/*
	 * Remove an object from the Henshin graph.
	 */
	private void removeFromHenshinGraph(Node owner, EStructuralFeature feature, Object value) {
		if (feature instanceof EAttribute) {
			Attribute attribute = null;
			for (Attribute nodeAttribute : owner.getAttributes()) {
				if (nodeAttribute.getType() == feature) {
					attribute = nodeAttribute;
					break;
				}
			}
			if (attribute != null) {
				attribute.setNode(null);
			}
		} else if (feature instanceof EReference) {
			Edge edge = null;
			if (value instanceof EObject) {
				Node targetNode = object2node.get(value);
				for (Edge outgoingEdge : owner.getOutgoing()) {
					if (outgoingEdge.getTarget() == targetNode && outgoingEdge.getType() == feature) {
						edge = outgoingEdge;
						break;
					}
				}
				if (edge != null) {
					edge.setSource(null);
					edge.setTarget(null);
					edge.setGraph(null);
				}
			}
		}
	}
	
	/*
	 * Add an object to the Henshin graph.
	 */
	private void addToHenshinGraph(EObject owner, EStructuralFeature feature, Object value) {
		Node node = object2node.get(owner);
		if (node != null && value != null) {
			if (feature instanceof EAttribute) {
				Attribute attribute = null;
				for (Attribute nodeAttribute : node.getAttributes()) {
					if (nodeAttribute.getType() == feature) {
						attribute = nodeAttribute;
						break;
					}
				}
				if (attribute == null) {
					attribute = createAttribute();
					attribute.setType((EAttribute) feature);
					attribute.setNode(node);
				}
				attribute.setValue(value.toString());
				
			} else if (feature instanceof EReference) {
				Edge edge = null;
				if (value instanceof EObject) {
					Node targetNode = object2node.get(value);
					for (Edge outgoingEdge : node.getOutgoing()) {
						if (outgoingEdge.getTarget() == targetNode
								&& outgoingEdge.getType() == feature) {
							edge = outgoingEdge;
							break;
						}
					}
					if (edge == null) {
						edge = createEdge();
						edge.setSource(node);
						edge.setTarget(targetNode);
						edge.setGraph(henshinGraph);
						edge.setType((EReference) feature);
					}
				}
			}
		}
	}
	
	public Map<Node, EObject> getNode2ObjectMap() {
		return node2object;
	}
	
	public void setNode2ObjectMap(Map<Node,EObject> node2object) {
		this.node2object = node2object;
	}
	
	public Map<EObject, Node> getObject2NodeMap() {
		return object2node;
	}
	
	public void setObject2NodeMap(Map<EObject, Node> object2node) {
		this.object2node = object2node;
	}

	public Graph getHenshinGraph() {
		return henshinGraph;
	}

}
