package com.hammurapi.extract;

import java.util.Map;
import java.util.Set;

import com.hammurapi.extract.ComparisonResult.Type;


/**
 * This predicate evaluates to true if argument is of a particular type.
 * @author Pavel Vlasov.
 *
 * @param <T>
 */
public class InstanceOfPredicate<T, C> implements Predicate<T, C>, Mappable<T,Boolean,C> {
	
	private Class<?> instanceType;
	private Extractor<T, Object, C> extractor;

	@SuppressWarnings("unchecked")
	public InstanceOfPredicate(Extractor<T, Object, C> extractor, Class<?> instanceType) {
		this.extractor = extractor;
		if (instanceType.isPrimitive()) {
			if (boolean.class.equals(instanceType)) {
				this.instanceType = (Class<? extends T>) Boolean.class;
			} else if (char.class.equals(instanceType)) {
				this.instanceType = (Class<? extends T>) Character.class;
			} else if (byte.class.equals(instanceType)) {
				this.instanceType = (Class<? extends T>) Byte.class;
			} else if (short.class.equals(instanceType)) {
				this.instanceType = (Class<? extends T>) Short.class;
			} else if (int.class.equals(instanceType)) {
				this.instanceType = (Class<? extends T>) Integer.class;
			} else if (long.class.equals(instanceType)) {
				this.instanceType = (Class<? extends T>) Long.class;
			} else if (float.class.equals(instanceType)) {
				this.instanceType = (Class<? extends T>) Float.class;
			} else if (double.class.equals(instanceType)) {
				this.instanceType = (Class<? extends T>) Double.class;
			} else if (void.class.equals(instanceType)) {
				this.instanceType = null;
			}
		} else {
			this.instanceType = instanceType;
		}
	}

	@SuppressWarnings("unchecked")
	public ComparisonResult compareTo(Extractor<T, Boolean, C> otherPredicate) {
		if (instanceType==null) {
			return False.getInstance().compareTo((Extractor<Object, Boolean, Object>) otherPredicate);
		}
		
		if (otherPredicate instanceof InstanceOfPredicate) {
			ComparisonResult ecr = extractor.compareTo(((InstanceOfPredicate<T, C>) otherPredicate).extractor);
			if (ecr == null || ecr.getType()!=ComparisonResult.Type.EQUAL) {
				return ComparisonResult.NOT_EQUAL_NM;
			}
			
			Class<?> otherInstanceType = ((InstanceOfPredicate<T, C>) otherPredicate).instanceType;
			if (otherInstanceType.equals(instanceType)) {
				return new ComparisonResult(Type.EQUAL, ecr.getIndexMap());
			}
			if (instanceType.isAssignableFrom(otherInstanceType)) {
				return new ComparisonResult(Type.LESS_RESTRICTIVE, ecr.getIndexMap());
			}
			if (otherInstanceType.isAssignableFrom(instanceType)) {
				return new ComparisonResult(Type.MORE_RESTRICTIVE, ecr.getIndexMap());
			}
			
			// No multiple inheritance in Java - if both types are classes, then they
			// are mutually exclusive - i.e. a instanceof String is opposite less restrictive than
			// a instanceof Integer.
			if (!instanceType.isInterface() && !otherInstanceType.isInterface()) {
				return new ComparisonResult(Type.OPPOSITE_LESS_RESTRICTIVE, ecr.getIndexMap());
			}
			
		}
		
		// True is less restrictive than anything but self.
		if (otherPredicate instanceof True) {
			return ComparisonResult.MORE_RESTRICTIVE_NM;
		}
		
		// False is more restrictive than anything but self.
		if (otherPredicate instanceof False) {
			return ComparisonResult.LESS_RESTRICTIVE_NM;
		}
		
		if (otherPredicate instanceof FacadeExtractor) {
			ComparisonResult cr = otherPredicate.compareTo(this);
			if (cr==null) {
				return cr;
			}
			switch (cr.getType()) {
			case EQUAL:
			case NOT_EQUAL:
			case OPPOSITE:
				return cr;
			case LESS_RESTRICTIVE:
				return new ComparisonResult(Type.MORE_RESTRICTIVE, ComparisonResult.inverse(cr.getIndexMap(), parameterIndices()));
			case MORE_RESTRICTIVE:
				return new ComparisonResult(Type.LESS_RESTRICTIVE, ComparisonResult.inverse(cr.getIndexMap(), parameterIndices()));
			case OPPOSITE_LESS_RESTRICTIVE:
				return new ComparisonResult(Type.OPPOSITE_LESS_RESTRICTIVE, ComparisonResult.inverse(cr.getIndexMap(), parameterIndices()));
			case OPPOSITE_MORE_RESTRICTIVE:
				return new ComparisonResult(Type.OPPOSITE_MORE_RESTRICTIVE, ComparisonResult.inverse(cr.getIndexMap(), parameterIndices()));
			}
		}
		
		return ComparisonResult.NOT_EQUAL_NM;
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public Boolean extract(C context, Map<C, Map<Extractor<T, ? super Boolean, C>, ? super Boolean>> cache, T... obj) {
		return instanceType==null ? false : instanceType.isInstance(extractor.extract(context, (Map) cache, obj));
	}

	public Set<Integer> parameterIndices() {
		return extractor.parameterIndices();
	}

	public boolean isContextDependent() {
		return false;
	}

	@Override
	public String toString() {
		return "InstanceOfPredicate [instanceType=" + instanceType + ", extractor=" + extractor + "]";
	}

	@Override
	public double getCost() {
		return 0;
	}

	@Override
	public Extractor<T, Boolean, C> map(int[] map) {
		if (extractor instanceof Mappable) {
			Extractor<T,Object,C> mapped = ((Mappable<T,Object,C>) extractor).map(map);
			if (mapped!=null) {
				return new InstanceOfPredicate<T, C>(mapped, instanceType);
			}
		}
		return null;
	}
	
	/**
	 * @return Extractor which return value is checked for instanceof.
	 */
	public Extractor<T, Object, C> getExtractor() {
		return extractor;
	}
	
	public Class<?> getInstanceType() {
		return instanceType;
	}
}
