package com.hammurapi.extract;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
 * Extractor which maps arguments to the target extractor.
 * @author Pavel Vlasov
 *
 * @param <T>
 * @param <V>
 * @param <C>
 */
public class MappedExtractor<T, V, C> implements FacadeExtractor<T, V, C>, Mappable<T,V,C> {

	private Extractor<T, V, C> target;
	private int[] map;
	private TreeSet<Integer> parameterIndices;
	
	/**
	 * @param target Target extractor
	 * @param map Index map, e.g. if it is {5, 3} then the first argument
	 * of this extractor becomes the sixth argument of the target extractor
	 * and the second argument of this extractor becomes the 4th argument
	 * of the target extractor. 
	 */
	protected MappedExtractor(Extractor<T, V, C> target, int[] map) {
		super();
		this.target = target;
		this.map = map;
		
		parameterIndices = new TreeSet<Integer>();
		
		Set<Integer> tpIdx = target.parameterIndices();
		for (int i=0; i<map.length; ++i) {
			if (tpIdx.contains(map[i])) {
				parameterIndices.add(i);
			}
		}		
	}

	@Override
	public V extract(C context,	Map<C, Map<Extractor<T, ? super V, C>, ? super V>> cache, T... obj) {
		int maxTargetIndex = -1;
		for (Integer tIdx: target.parameterIndices()) {
			if (tIdx>maxTargetIndex) {
				maxTargetIndex=tIdx;
			}
		}
		
		@SuppressWarnings("unchecked")
		T[] tObj = (T[]) Array.newInstance(obj.getClass().getComponentType(), maxTargetIndex+1);
		for (int i=0; i<map.length; ++i) {
			if (parameterIndices.contains(i)) {
				tObj[map[i]] = obj[i];
			}
		}
		
		return target.extract(context, cache, tObj);
	}

	@Override
	public Set<Integer> parameterIndices() {		
		return parameterIndices;
	}

	@Override
	public boolean isContextDependent() {
		return target.isContextDependent();
	}

	@Override
	public ComparisonResult compareTo(Extractor<T, V, C> other) {
		ComparisonResult tcr = target.compareTo(other);
		if (tcr==null) {
			return null;
		}
		if (tcr.isOneToOneMapping()) {
			// Inverse mapping.
			int dim = -1;
			for (int i=0; i<map.length; ++i) {
				if (parameterIndices.contains(i) && map[i]>dim) {
					dim = map[i];
				}
			}
			int[] rmap = new int[dim+1];
			Arrays.fill(rmap, -1);
			for (int i=0; i<map.length; ++i) {
				if (parameterIndices.contains(i)) {
					rmap[map[i]] = i;
				}
			}
			return new ComparisonResult(tcr.getType(), rmap); 
		}
		
		int[] tcrim = tcr.getIndexMap();
		int[] cmap = new int[map.length];
		Arrays.fill(cmap, -1);
		for (int i=0; i<map.length; ++i) {
			if (parameterIndices.contains(i)) {
				if (tcrim.length>map[i]) {
					cmap[i] = tcrim[map[i]];
				} else {
					return null;
				}
			}
		}
		return new ComparisonResult(tcr.getType(), cmap); 
	}

	@Override
	public String toString() {
		return "MappedExtractor [target=" + target + ", map="
				+ Arrays.toString(map) + "]";
	}
	
	@Override
	public double getCost() {
		return target.getCost();
	}
	
	
	public static <T,V,C> Extractor<T,V,C> mapExtractor(Extractor<T,V,C> target, int[] map) {
		if (ComparisonResult.isOneToOneMapping(map)) {
			return target;
		}
		
		// Target is not index-dependent (e.g. constant).
		if (target.parameterIndices().isEmpty()) {
			return target;
		}
		if (target instanceof Mappable) {
			@SuppressWarnings("unchecked")
			Extractor<T, V, C> ret = ((Mappable<T,V,C>) target).map(map);
			if (ret!=null) {
				return ret;
			}
		}
		
		return new MappedExtractor<T, V, C>(target, map);
	}

	@SuppressWarnings("unchecked")
	@Override
	public Extractor<T, V, C> map(int[] map) {
		if (ComparisonResult.isOneToOneMapping(map)) {
			return this;
		}
		
		int[] newMap = new int[map.length];
		Arrays.fill(newMap, -1);
		
		Set<Integer> pi = new HashSet<Integer>(parameterIndices);
		
		for (int i=0; i<newMap.length; ++i) {
			if (map[i]!=-1) {
				if (pi.remove(map[i])) {
					newMap[i]=this.map[map[i]];
				}
			} 
		}
		
		if (!pi.isEmpty()) { // Not all indexes were mapped.
			return null;
		}
		
		if (ComparisonResult.isOneToOneMapping(newMap)) {
			return target;
		}
		
		if (this instanceof MappedPredicate) {
			return (Extractor<T, V, C>) new MappedPredicate<T, C>((Predicate<T, C>) target, newMap);
		}
		return new MappedExtractor<T, V, C>(target, newMap);
	}

	public Extractor<T, V, C> getTarget() {
		return target;
	}
	
	public int[] getMap() {
		return map;
	}
}
