package com.hammurapi.extract;

import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

public abstract class ComparisonPredicate<T, V, C> extends ExtractorBase<T, Boolean, C> implements Predicate<T, C>, Mappable<T,Boolean,C>, BinaryExtractor<T,V,C> {
	
	protected Extractor<T, V, C> leftExtractor;
	protected Extractor<T, V, C> rightExtractor;
	
	protected Set<Integer> parameterIndices = new TreeSet<Integer>();

	protected enum PromotionLevel { 
		INT(0), 
		LONG(1), 
		FLOAT(2), 
		DOUBLE(3);
		
		private PromotionLevel(int level) {
			this.level = level;
		}
		
		int level;
	}
	
	protected ComparisonPredicate(double initialCost, TimeUnit costUnit, Extractor<T, V, C> leftExtractor, Extractor<T, V, C> rightExtractor) {
		super(initialCost, costUnit);
		this.leftExtractor = leftExtractor;
		this.rightExtractor = rightExtractor;
		parameterIndices.addAll(leftExtractor.parameterIndices());
		parameterIndices.addAll(rightExtractor.parameterIndices());
	}

	public Set<Integer> parameterIndices() {
		return Collections.unmodifiableSet(parameterIndices);
	}
	
	protected boolean operandsAreEqual(ComparisonPredicate<T, V, C> other) {
		return other.leftExtractor.equals(leftExtractor) && other.rightExtractor.equals(rightExtractor);
	}

	protected boolean operandsAreReverseEqual(ComparisonPredicate<T, V, C> other) {
		return other.leftExtractor.equals(rightExtractor) && other.rightExtractor.equals(leftExtractor);
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((leftExtractor == null) ? 0 : leftExtractor.hashCode());
		result = prime
				* result
				+ ((parameterIndices == null) ? 0 : parameterIndices.hashCode());
		result = prime * result
				+ ((rightExtractor == null) ? 0 : rightExtractor.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		@SuppressWarnings("rawtypes")
		ComparisonPredicate other = (ComparisonPredicate) obj;
		if (leftExtractor == null) {
			if (other.leftExtractor != null)
				return false;
		} else if (!leftExtractor.equals(other.leftExtractor))
			return false;
		if (parameterIndices == null) {
			if (other.parameterIndices != null)
				return false;
		} else if (!parameterIndices.equals(other.parameterIndices))
			return false;
		if (rightExtractor == null) {
			if (other.rightExtractor != null)
				return false;
		} else if (!rightExtractor.equals(other.rightExtractor))
			return false;
		return true;
	}
	
	public boolean isContextDependent() {
		return leftExtractor.isContextDependent() || rightExtractor.isContextDependent();
	}
	
	@Override
	public String toString() {
		return getClass().getName()+"(cost="+getCost()+", "+leftExtractor+", "+rightExtractor+")";
	}
		
	@SuppressWarnings("unchecked")
	protected Boolean extractInternal(
			C context,
			Map<C, Map<Extractor<T, ? super Boolean, C>, ? super Boolean>> cache,
			T... obj) {
				
		@SuppressWarnings("rawtypes")
		Object o1 = leftExtractor.extract(context, (Map) cache, obj);
		@SuppressWarnings("rawtypes")
		Object o2 = rightExtractor.extract(context, (Map) cache, obj);
		
		if (isPromoteable(o1) && isPromoteable(o2)) {
			Number n1 = (Number) o1;
			Number n2 = (Number) o2;
			switch (promote(n1, n2)) {
			case INT:
				return compare(n1.intValue(), n2.intValue());
			case LONG:
				return compare(n1.longValue(), n2.longValue());
			case FLOAT:
				return compare(n1.floatValue(), n2.floatValue());
			case DOUBLE:
				return compare(n1.doubleValue(), n2.doubleValue());		
			default:
				throw new IllegalArgumentException("Unexpected promotion level "+promote(n1, n2));
			}
		} else {		
			return compare(o1, o2);
		}
	}
	
	private boolean isPromoteable(Object obj) {
		return obj instanceof Integer
			|| obj instanceof Long
			|| obj instanceof Float
			|| obj instanceof Double;
	}

 	protected abstract boolean compare(Object o1, Object o2);
 	
 	protected abstract boolean compare(int n1, int n2);
 	protected abstract boolean compare(long n1, long n2);
 	protected abstract boolean compare(float n1, float n2);
 	protected abstract boolean compare(double n1, double n2);
	
 	private PromotionLevel nLevel(Number n) {
 		if (n instanceof Integer) {
 			return PromotionLevel.INT;
 		}
 		if (n instanceof Long) {
 			return PromotionLevel.LONG;
 		}
 		if (n instanceof Float) {
 			return PromotionLevel.FLOAT;
 		}
 		if (n instanceof Double) {
 			return PromotionLevel.DOUBLE;
 		}
 		throw new IllegalArgumentException(n.getClass()+" is not supported");
 	}
 	
 	protected PromotionLevel promote(Number n1, Number n2) {
 		PromotionLevel pl1 = nLevel(n1);
 		PromotionLevel pl2 = nLevel(n2); 		
 		return pl1.level>=pl2.level ? pl1 : pl2;
 	}

 	@Override
 	public double getCost() {
 		if (cost==0 && costUnit==null) {
 			return leftExtractor.getCost()+rightExtractor.getCost();
 		}
 		return super.getCost();
 	}
 	
	@Override
	public Extractor<T, Boolean, C> map(int[] map) {
		Mappable<T, V, C> leftMappable = ExtractorUtil.toMappable(leftExtractor);
		if (leftMappable!=null) {
			Mappable<T, V, C> rightMappable = ExtractorUtil.toMappable(rightExtractor);			
			if (rightMappable!=null) {
				Extractor<T,V,C> lMapped = leftMappable.map(map);
				if (lMapped!=null) {
					Extractor<T,V,C> rMapped = rightMappable.map(map);
					if (rMapped!=null) {
						return newInstance(lMapped, rMapped);
					}
				}
			}
		}
		return null;
	}

	protected abstract Extractor<T, Boolean, C> newInstance(Extractor<T, V, C> leftExtractor, Extractor<T, V, C> rightExtractor);

	public Extractor<T, V, C> getLeftExtractor() {
		return leftExtractor;
	}
	
	public Extractor<T, V, C> getRightExtractor() {
		return rightExtractor;
	}
}
