package com.hammurapi.extract;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import com.hammurapi.extract.ComparisonResult.Type;

/**
 * Base class for compiled java extractors/predicates.
 * @author Pavel Vlasov
 *
 * @param <T>
 * @param <V>
 * @param <C>
 */
@SuppressWarnings("unchecked")
public abstract class CompiledExtractorBase<T, V, C> extends AbstractExtractor<T, V, C> implements Mappable<T, V, C> {
	
	protected int[] map;
	
	protected CompiledExtractorBase(int[] map, boolean contextDependent, int... parameterIndices) {
		super(0, null, false, parameterIndices); 
		this.map = map;
	}
	
	protected abstract CompiledExtractorBase<T, V, C> newInstance(int[] map);
	
	protected abstract List<Object> getIdentity();
	
	/**
	 * Maps argument indices for invocation.
	 * @param idx
	 * @return
	 */
	protected int mapArg(int idx) {
		if (map==null) {
			return idx;
		}
		
		for (int i=0; i<map.length; ++i) {
			if (map[i]==idx) {
				return i;
			}
		}
		
		throw new IllegalArgumentException("Cannot map index "+idx+" using map "+Arrays.toString(map));
	}

	@Override
	public Extractor<T,V,C> map(int[] map) {
		if (ComparisonResult.isOneToOneMapping(map)) {
			return this;
		}
		if (this.map==null) {
			return newInstance(map);
		}
		
		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;
		}
		
		return newInstance(newMap);		
		
	}
		
	@Override
	public ComparisonResult compareTo(Extractor<T, V, C> other) {
		if (other==this || other instanceof CompiledExtractorBase && getIdentity().equals(((CompiledExtractorBase<T,V,C>) other).getIdentity())) {
			return ComparisonResult.EQUAL_NM;
		}
		
		if (other instanceof CompiledExtractorBase) {
			Object oi = ((CompiledExtractorBase<T,V,C>) other).getIdentity();
			if (oi instanceof List) {
				Map<Integer,Integer> mapping = new HashMap<Integer,Integer>();
				if (map(getIdentity(), (List<Object>) oi, mapping)) {
					int maxIdx = -1;
					for (Integer idx: mapping.keySet()) {
						if (idx>maxIdx) {
							maxIdx = idx;
						}
					}
					int[] retMap = new int[maxIdx+1];
					Arrays.fill(retMap, -1);
					for (Map.Entry<Integer, Integer> e: mapping.entrySet()) {
						retMap[e.getKey()] = e.getValue();
					}
					return new ComparisonResult(Type.EQUAL, retMap);
				}
			}
		}
		return super.compareTo(other);
	}
	
	private static boolean map(List<Object> thisIdentity, List<Object> otherIdentity, Map<Integer,Integer> mapping) {
		if (thisIdentity.size()!=otherIdentity.size()) {
			return false;
		}
		Iterator<Object> tit = thisIdentity.iterator();
		Iterator<Object> oit = otherIdentity.iterator();
		if (!(tit.hasNext() && tit.next().equals(oit.next()))) {
			return false;
		}
		
		Object thisName = tit.next();
		Object otherName = oit.next();
		if (thisName instanceof Integer && otherName instanceof Integer) {
			Integer existingMapping = mapping.get(thisName);
			if (existingMapping==null) {
				mapping.put((Integer) thisName, (Integer) otherName);
			} else if (!existingMapping.equals(otherName)) {
				return false;
			}			
		} else if (!cmp(thisName, otherName)) {
			return false;
		}
		
		// At this point types are equal, values are equal or mapped.
		if (tit.hasNext()) {
			if (!oit.hasNext()) {
				return false;
			}
			List<List<Object>> tChildren = (List<List<Object>>) tit.next();
			List<List<Object>> oChildren = (List<List<Object>>) oit.next();
			if (tChildren.size()!=oChildren.size()) {
				return false;
			}
			Iterator<List<Object>> tcit = tChildren.iterator();
			Iterator<List<Object>> ocit = oChildren.iterator();
			while (tcit.hasNext()) {
				if (!map(tcit.next(), ocit.next(), mapping)) {
					return false;
				}
			}
		}
		return true;
	}
	
	private static boolean cmp(Object o1, Object o2) {
		if (o1==null) {
			return o2==null;
		}
		
		return o1.equals(o2);
	}
	
	@Override
	public Set<Integer> parameterIndices() {
		if (map==null) {
			return super.parameterIndices();
		}
		
		Set<Integer> original = super.parameterIndices();
		Set<Integer> ret = new TreeSet<Integer>();
		for (int i=0; i<map.length; ++i) {
			if (map[i]!=-1 && original.contains(map[i])) {
				ret.add(i);
			}
		}
		
		return ret;
	}

}
