package org.eclipse.emf.henshin.interpreter.matching.constraints;

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

import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;

/**
 * Path constraint class.
 * @author Christian Krause
 */
public class PathConstraint implements BinaryConstraint {
	
	// Integers:
	static final Integer[] INTEGERS = new Integer[64];
	static {
		for (int i=0; i<INTEGERS.length; i++) {
			INTEGERS[i] = i;
		}
	}
	
	// Target variable:
	final Variable targetVariable;
	
	// References list representing a path:
	final List<EReference> references;

	// Number of paths:
	final int numPaths;
	
	/**
	 * Default constructor.
	 * @param target Target variable.
	 * @param references List of reference used for the path.
	 * @param numPaths Number of paths.
	 */
	public PathConstraint(Variable target, List<EReference> references, int numPaths) {
		this.targetVariable = target;
		this.references = references;
		this.numPaths = numPaths;
		if (references.isEmpty()) {
			throw new IllegalArgumentException("Cannot create path constraint for empty paths");
		}
	}

	private static Integer inc(Integer integer) {
		if (integer==null) return INTEGERS[1];
		int succ = integer.intValue() + 1;
		if (succ < INTEGERS.length) {
			return INTEGERS[succ];
		}
		return succ;
	}
	
	/*
	 * Get the targets for a list o sources and a reference.
	 */
	@SuppressWarnings("unchecked")
	private static Map<EObject,Integer> getTargetObjects(Map<EObject,Integer> sources, EReference reference) {
		Map<EObject,Integer> targets = new LinkedHashMap<EObject,Integer>();
		for (Entry<EObject,Integer> source : sources.entrySet()) {
			EObject src = source.getKey();
			if (src.eClass().getEAllReferences().contains(reference)) {
				if (reference.isMany()) {
					for (EObject trg : (List<EObject>) src.eGet(reference)) {
						targets.put(trg, inc(targets.get(trg)));
					}
				} else {
					EObject trg = (EObject) src.eGet(reference);
					if (trg!=null) {
						targets.put(trg, inc(targets.get(trg)));
					}
				}
			}
		}
		return targets;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.matching.constraints.BinaryConstraint#check(org.eclipse.emf.henshin.interpreter.matching.constraints.DomainSlot, org.eclipse.emf.henshin.interpreter.matching.constraints.DomainSlot)
	 */
	@Override
	public boolean check(DomainSlot source, DomainSlot target) {

		// Source variable must be locked:
		if (!source.locked) {
			return false;
		}

		// Follow all paths and get the target objects:
		Map<EObject,Integer> targetObjects = new HashMap<EObject,Integer>();
		targetObjects.put(source.value, INTEGERS[1]);
		for (EReference reference : references) {
			targetObjects = getTargetObjects(targetObjects, reference);
		}
		
		// Target domain slot locked?
		if (target.locked) {
			Integer numTargets = targetObjects.get(target.value);
			return (numTargets != null && numTargets >= numPaths);
			
		} else {
			
			// Target not locked yet. Create a domain change to restrict the target domain:
			DomainChange change = new DomainChange(target, target.temporaryDomain);
			source.remoteChangeMap.put(this, change);
			
			// Calculated temporary domain:
			target.temporaryDomain = new ArrayList<EObject>();
			for (Entry<EObject,Integer> entry : targetObjects.entrySet()) {
				if (entry.getValue() >= numPaths) {
					target.temporaryDomain.add(entry.getKey());
				}
			}
			
			// TODO: why does domain restriction not work for path constraints?
			//if (change.originalValues!=null) {
			//	target.temporaryDomain.retainAll(change.originalValues);
			//}
			
			// Temporary domain must not be empty:
			return !target.temporaryDomain.isEmpty();
			
		}

	}
	
	public Variable getTargetVariable() {
		return targetVariable;
	}
	
	public List<EReference> getReferences() {
		return references;
	}

}