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

import java.util.ArrayList;
import java.util.List;

import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.henshin.interpreter.Assignment;
import org.eclipse.emf.henshin.interpreter.Match;
import org.eclipse.emf.henshin.interpreter.util.InterpreterUtil;
import org.eclipse.emf.henshin.model.Edge;
import org.eclipse.emf.henshin.model.Node;
import org.eclipse.emf.henshin.model.Parameter;
import org.eclipse.emf.henshin.model.Rule;
import org.eclipse.emf.henshin.model.Unit;

/**
 * Default {@link Match} implementation.
 */
public class MatchImpl extends AssignmentImpl implements Match {
	
	// Nodes to be matched:
	protected List<Node> nodes;

	/**
	 * Default constructor.
	 * @param rule Rule to be matched.
	 */
	public MatchImpl(Rule rule) {
		this (rule, false);
	}

	/**
	 * Constructor.
	 * @param rule The rule that this match is used for.
	 * @param isResultMatch Determines whether this is a result match.
	 */
	public MatchImpl(Rule rule, boolean isResultMatch) {
		super(rule, isResultMatch);
	}

	/**
	 * Constructor.
	 * @param assignment The assignment or match to be copied.
	 * @param isResultMatch Determines whether this is a result match.
	 */
	public MatchImpl(Assignment assignment, boolean isResultMatch) {
		super(assignment.getUnit(), isResultMatch);
		copyParameterValues(assignment);
		if (assignment instanceof Match) {
			Match match = (Match) assignment;
			for (Node node : nodes) {
				setNodeTarget(node, match.getNodeTarget(node));
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.impl.AssignmentImpl#setUnit(org.eclipse.emf.henshin.model.Unit)
	 */
	@Override
	protected void setUnit(Unit unit) {
		if (!(unit instanceof Rule)) {
			throw new IllegalArgumentException("Unit must be a rule");
		}
		this.unit = unit;
		// LHS or RHS nodes?
		this.nodes = isResult ? ((Rule) unit).getRhs().getNodes() : ((Rule) unit).getLhs().getNodes(); 
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.Match#getRule()
	 */
	@Override
	public Rule getRule() {
		return (Rule) unit;
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.Match#getNodeTarget(org.eclipse.emf.henshin.model.Node)
	 */
	@Override
	public EObject getNodeTarget(Node node) {
		return (EObject) values.get(node);
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.Match#setNodeTarget(org.eclipse.emf.henshin.model.Node, org.eclipse.emf.ecore.EObject)
	 */
	@Override
	public void setNodeTarget(Node node, EObject target) {
		setValue(node, target);
		// Check whether there is a parameter with the same name:
		if (unit!=null && node.getName()!=null) {
			Parameter param = unit.getParameter(node.getName());
			if (param!=null) {
				setValue(param, target);
			}
		}
	}
	
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.impl.AssignmentImpl#setParameterValue(org.eclipse.emf.henshin.model.Parameter, java.lang.Object)
	 */
	@Override
	public void setParameterValue(Parameter param, Object value) {
		setValue(param, value);
		// Check whether there are nodes with the same name as the parameter:
		String name = param.getName();
		if (name!=null && nodes!=null && (value==null || value instanceof EObject)) {
			for (Node node : nodes) {
				if (name.equals(node.getName())) {
					setValue(node, value);
				}
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.Match#getAllNodeTargets()
	 */
	@Override
	public List<EObject> getNodeTargets() {
		List<EObject> targets = new ArrayList<EObject>();
		if (nodes!=null) {
			for (Node node : nodes) {
				EObject tar = (EObject) values.get(node);
				if (tar!=null) {
					targets.add(tar);
				}
			}
		}
		return targets;
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.Match#getMultiMatches(org.eclipse.emf.henshin.model.Rule)
	 */
	@Override
	public List<Match> getMultiMatches(Rule multiRule) {
		@SuppressWarnings("unchecked")
		List<Match> nested = (List<Match>) values.get(multiRule);
		if (nested==null) {
			nested = new ArrayList<Match>();
			values.put(multiRule, nested);
		}
		return nested;
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.Match#overlapsWith(org.eclipse.emf.henshin.interpreter.Match)
	 */
	@Override
	public boolean overlapsWith(Match match) {
		List<EObject> common = getNodeTargets();
		common.retainAll(match.getNodeTargets());
		return !common.isEmpty();
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.Match#isComplete()
	 */
	@Override
	public boolean isComplete() {
		if (nodes==null || getRule()==null) {
			return false;
		}
		if (!values.keySet().containsAll(nodes)) {
			return false;
		}
		for (Rule multiRule : getRule().getMultiRules()) {
			for (Match nestedMatch : getMultiMatches(multiRule)) {
				if (!nestedMatch.isComplete()) {
					return false;
				}
			}
		}
		return true;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.Match#isValid()
	 */
	@Override
	public boolean isValid() {
		// Check completeness:
		if (!isComplete()) {
			return false;
		}
		// Check object types:
		for (Node node : nodes) {
			EClass nodeType = node.getType();
			EObject eObject = (EObject) values.get(node);
			EClass eObjectType = eObject.eClass();
			if (!nodeType.isSuperTypeOf(eObjectType)) {
				return false;
			}
		}
		// Check if all edges are present:
		for (Node node : nodes) {
			EObject source = (EObject) values.get(node);
			for (Edge edge : node.getOutgoing()) {
				EReference edgeType = edge.getType();
				EObject target = (EObject) values.get(edge.getTarget());
				if (edgeType.isMany()) {
					@SuppressWarnings("unchecked")
					List<EObject> targetObjects = (List<EObject>) source.eGet(edgeType);
					if (!targetObjects.contains(target)) {
						return false;
					}
				} else {
					if (source.eGet(edgeType)!=target) {
						return false;
					}
				}
			}
		}
		return true;
	}
	
	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (!super.equals(obj) || !(obj instanceof Match)) {
			return false;
		}
		Match match = (Match) obj;
		if (nodes!=null) {
			for (Node node : nodes) {
				if (values.get(node)!=match.getNodeTarget(node)) {
					return false;
				}
			}
		}
		return true;
	}
	
	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		if (isResult) {
			return "Result match for rule '" + unit.getName() + "':\n" + toStringWithIndent("");
		} else {
			return "Match for rule '" + unit.getName() + "':\n" + toStringWithIndent("");			
		}
	}

	/*
	 * toString helper.
	 */
	@Override
	protected String toStringWithIndent(String indent) {
		String result = super.toStringWithIndent(indent);
		String matchType = isResult ? "Result match" : "Match";
		int index = 1;
		if (nodes.isEmpty()) {
			result = result + indent + "- no nodes\n";
		} else {
			for (Node node : nodes) {
				String name = node.getName()!=null ? "'" + node.getName() + "'" : "#" + index;
				EObject target = getNodeTarget(node);
				if (target!=null) {
					result = result + indent + "- node " + name + " => " + 
							InterpreterUtil.objectToString(target) + "\n";
				}
				index++;
			}
		}
		index = 1;
		for (Rule multiRule : ((Rule) unit).getMultiRules()) {
			String name = multiRule.getName()!=null ? "'" + multiRule.getName() + "'" : "#" + index;
			result = result + "\n" + indent + "  Multi-rule " + name + ":\n";
			List<Match> matches = getMultiMatches(multiRule);
			for (int i=0; i<matches.size(); i++) {
				result = result + "\n" + indent + "  " + matchType + " #" + i + ":\n";
				Match match = matches.get(i);
				if (match instanceof MatchImpl) {
					result = result + ((MatchImpl) match).toStringWithIndent(indent + "  "); 
				} else {
					result = result + indent + "  " + match.toString();
				}
			}
			index++;
		}
		return result;
		
	}

}
