/**
 * <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.info;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.henshin.interpreter.impl.EngineImpl;
import org.eclipse.emf.henshin.interpreter.matching.constraints.AttributeConstraint;
import org.eclipse.emf.henshin.interpreter.matching.constraints.BinaryConstraint;
import org.eclipse.emf.henshin.interpreter.matching.constraints.ContainmentConstraint;
import org.eclipse.emf.henshin.interpreter.matching.constraints.DanglingConstraint;
import org.eclipse.emf.henshin.interpreter.matching.constraints.PathConstraint;
import org.eclipse.emf.henshin.interpreter.matching.constraints.ReferenceConstraint;
import org.eclipse.emf.henshin.interpreter.matching.constraints.UnaryConstraint;
import org.eclipse.emf.henshin.interpreter.matching.constraints.Variable;
import org.eclipse.emf.henshin.model.Attribute;
import org.eclipse.emf.henshin.model.BinaryFormula;
import org.eclipse.emf.henshin.model.Edge;
import org.eclipse.emf.henshin.model.Formula;
import org.eclipse.emf.henshin.model.Graph;
import org.eclipse.emf.henshin.model.Mapping;
import org.eclipse.emf.henshin.model.NestedCondition;
import org.eclipse.emf.henshin.model.Node;
import org.eclipse.emf.henshin.model.Rule;
import org.eclipse.emf.henshin.model.UnaryFormula;
import org.eclipse.emf.henshin.model.staticanalysis.PathFinder;

public class VariableInfo {

	// variables which represent nodes when they are first introduced, e.g. if a
	// mapped node occurs in the LHS and in one condition, only the variable
	// representing the LHS node will be in this collection
	private Collection<Variable> mainVariables;

	// node-variable pair
	private Map<Node, Variable> node2variable;

	// variable-node pair
	private Map<Variable, Node> variable2node;

	// map between a graph and all variables corresponding to nodes of that
	// graph
	private Map<Graph, List<Variable>> graph2variables;

	// map between a key variable and its main variable, e.g. there is a mapping
	// chain from the node belonging to the main variable to the key variable
	private Map<Variable, Variable> variable2mainVariable;

	private Rule rule;
	private EngineImpl engine;

	public VariableInfo(RuleInfo ruleInfo, EngineImpl engine) {
		this.rule = ruleInfo.getRule();
		this.engine = engine;

		this.node2variable = new HashMap<Node, Variable>();
		this.variable2node = new HashMap<Variable, Node>();

		this.graph2variables = new HashMap<Graph, List<Variable>>();
		this.variable2mainVariable = new HashMap<Variable, Variable>();

		createVariables(rule.getLhs(), null);

		for (Node node : rule.getLhs().getNodes()) {
			if (rule.getMappings().getImage(node, rule.getRhs()) == null) {
				boolean postpone = ruleInfo.getPostponed().contains(node);
				createDanglingConstraints(node, postpone);
			}
		}

		mainVariables = variable2mainVariable.values();
	}

	private void createVariables(Graph g, Collection<Mapping> mappings) {
		List<Variable> variables = new ArrayList<Variable>();

		for (Node node : g.getNodes()) {
			EClass type = node.getType();
			Variable var = new Variable(type);
			var.name = node.getName();
			variables.add(var);
			node2variable.put(node, var);
			variable2node.put(var, node);

			Variable mainVariable = var;
			if (mappings != null) {
				for (Mapping mapping : mappings) {
					if (node == mapping.getImage()) {
						mainVariable = variable2mainVariable.get(node2variable.get(mapping.getOrigin()));
					}
				}
			}

			variable2mainVariable.put(var, mainVariable);
		}

		for (Node node : g.getNodes()) {
			createConstraints(node);
		}

		graph2variables.put(g, variables);

		createVariables(g.getFormula());
	}

	private void createVariables(Formula formula) {
		if (formula instanceof BinaryFormula) {
			createVariables(((BinaryFormula) formula).getLeft());
			createVariables(((BinaryFormula) formula).getRight());
		} else if (formula instanceof UnaryFormula) {
			createVariables(((UnaryFormula) formula).getChild());
		} else if (formula instanceof NestedCondition) {
			NestedCondition nc = (NestedCondition) formula;
			createVariables(nc.getConclusion(), nc.getMappings());
		}
	}

	private void createConstraints(Node node) {
		Variable var = node2variable.get(node);

		// User-defined node constraints:
		UnaryConstraint userConstraint = engine.createUserConstraints(node);
		if (userConstraint != null) {
			var.userConstraints.add(userConstraint);
		}

		// Outgoing edges:
		for (Edge edge : node.getOutgoing()) {
			Variable target = node2variable.get(edge.getTarget());
			ReferenceConstraint constraint;
			String index = edge.getIndex();
			if (index != null && index.length() > 0) {
				if (rule.getParameter(index) != null) {
					constraint = new ReferenceConstraint(target, edge.getType(), index, false);
					var.requiresFinalCheck = true;
				} else {
					try {
						Number constant = (Number) engine.getScriptEngine().eval(index);
						constraint = new ReferenceConstraint(target, edge.getType(), constant, true);
					} catch (Exception e) {
						throw new RuntimeException("Error evaluating index expression: " + index);
					}
				}
			} else {
				constraint = new ReferenceConstraint(target, edge.getType(), null, true);
			}
			var.referenceConstraints.add(constraint);
			BinaryConstraint binaryUserConstraint = engine.createUserConstraints(edge);
			if (binaryUserConstraint != null) {
				var.binaryUserConstraints.put(constraint, binaryUserConstraint);
			}

		}

		// Incoming edges:
		for (Edge edge : node.getIncoming()) {
			if (edge.getType().isContainment()) {
				Variable target = node2variable.get(edge.getSource());
				ContainmentConstraint constraint = new ContainmentConstraint(target);
				var.containmentConstraints.add(constraint);
			} else if (edge.getType().getEOpposite() != null) {
				Variable target = node2variable.get(edge.getSource());
				ReferenceConstraint constraint = new ReferenceConstraint(target, edge.getType().getEOpposite(), null,
						true);
				var.referenceConstraints.add(constraint);
			}
		}

		// Attributes:
		createAttributeConstraints(node, var);

		// Path constraints:
		if (node.getGraph() == rule.getLhs() && !rule.getLhs().getPACs().isEmpty()) {
			for (Node target : node.getGraph().getNodes()) {
				if (node == target) {
					continue;
				}
				for (Entry<List<EReference>, Integer> entry : PathFinder.findReferencePaths(node, target, true, true)
						.entrySet()) {
					Variable targetVar = node2variable.get(target);
					PathConstraint constraint = new PathConstraint(targetVar, entry.getKey(), entry.getValue());
					var.pathConstraints.add(constraint);
				}
			}
		}

	}

	private void createAttributeConstraints(Node node, Variable var) {
		for (Attribute attribute : node.getAttributes()) {
			String value = attribute.getValue();
			AttributeConstraint constraint;
			if (rule.getParameter(value) != null) {
				constraint = new AttributeConstraint(attribute.getType(), value, false);
			} else {
				Object constant = engine.evalAttributeExpression(attribute, rule);
				constraint = new AttributeConstraint(attribute.getType(), constant, true);
			}
			var.attributeConstraints.add(constraint);
			UnaryConstraint unaryUserConstraint = engine.createUserConstraints(attribute);
			if (unaryUserConstraint != null) {
				var.attributeUserConstraints.put(constraint, unaryUserConstraint);
			}
		}
	}

	private void createDanglingConstraints(Node node, boolean postpone) {
		Variable var = node2variable.get(node);
		
		DanglingConstraint constraint = new DanglingConstraint(getEdgeCounts(node, false), getEdgeCounts(node, true), postpone);
		
		var.danglingConstraints.add(constraint);
	}

	public static Map<EReference, Integer> getEdgeCounts(Node node, boolean incoming) {

		Collection<Edge> edges = incoming ? node.getIncoming() : node.getOutgoing();
		Collection<Edge> oppositeEdges = incoming ? node.getOutgoing() : node.getIncoming();

		if (edges.size() == 0) {
			return null;
		}

		Map<EReference, Integer> edgeCount = new HashMap<EReference, Integer>();
		for (Edge edge : edges) {
			EReference type = edge.getType();
			Integer count = edgeCount.get(type);
			if (count == null) {
				count = ONE;
			} else {
				count = count + 1;
			}
			edgeCount.put(type, count);
		}

		for (Edge edge : oppositeEdges) {
			if (edge.getType().getEOpposite() == null)
				continue;

			EReference oppType = edge.getType().getEOpposite();

			if (incoming) {
				// opposite edges are outgoing from the PoV of the node
				Node remoteNode = edge.getTarget();
				if (remoteNode.getOutgoing(oppType, node) == null) {
					Integer count = edgeCount.get(oppType);
					if (count == null) {
						count = ONE;
					} else {
						count = count + 1;
					}
					edgeCount.put(oppType, count);
				}
			} else {
				// opposite edges are incoming from the PoV of the node
				Node remoteNode = edge.getSource();
				if (node.getOutgoing(oppType, remoteNode) == null) {
					Integer count = edgeCount.get(oppType);
					if (count == null) {
						count = ONE;
					} else {
						count = count + 1;
					}
					edgeCount.put(oppType, count);
				}
			}
		}

		return edgeCount;
	}

	public Node getVariableForNode(Variable variable) {
		return variable2node.get(variable);
	}

	public Collection<Variable> getDependendVariables(Variable mainVariable) {
		Collection<Variable> dependendVariables = new HashSet<Variable>();
		for (Variable var : variable2mainVariable.keySet()) {
			if (variable2mainVariable.get(var) == mainVariable) {
				dependendVariables.add(var);
			}
		}

		return dependendVariables;
	}

	public Collection<Variable> getMainVariables() {
		return mainVariables;
	}

	/**
	 * @return the graph2variables
	 */
	public Map<Graph, List<Variable>> getGraph2variables() {
		return graph2variables;
	}

	/**
	 * @return the node2variable
	 */
	public Map<Node, Variable> getNode2variable() {
		// TODO: change Solution to get rid of this method
		return node2variable;
	}

	private static final Integer ONE = new Integer(1);

	public void updateCached() {
		for (Entry<Variable, Node> entry : variable2node.entrySet()) {
			entry.getKey().attributeConstraints.clear();
			createAttributeConstraints(entry.getValue(), entry.getKey());
		}
	}

}
