package com.hammurapi.extract;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import com.hammurapi.extract.ComparisonResult.Type;


/**
 * Base class for extractors, implements value caching.
 * @author Pavel Vlasov.
 *
 * @param <T>
 * @param <V>
 */
public abstract class ExtractorBase<T, V, C> implements Extractor<T, V, C> {
	
	protected TimeUnit costUnit;
	protected long invocations;
	protected double totalCost;
	protected boolean nanos;
	protected double cost;
	protected double initialCost;

	/**
	 * @param initialCost Initial cost. If positive, this value is added to the calculated cost. If costUnit is null,
	 * then cost is always equal to the initial cost. If negative and time unit is not null, then absolute value is used as first measurement of cost.
	 * @param costUnit If this argument is not null, then extractor cost is 
	 * calculated as average time to execute extractInternal() method in specified time unit.
	 * If time unit is nanoseconds or microseconds, then System.nanoTime() method is used for 
	 * measurement, otherwise Systsm.currentTimeMillis() is used.
	 */
	protected ExtractorBase(double initialCost, TimeUnit costUnit) {		
		this.costUnit = costUnit;
		this.initialCost = initialCost;
		if (initialCost<0 && costUnit!=null) {
			invocations = 1;
			totalCost = initialCost;
		} else {
			this.cost = initialCost;			
		}
		nanos = costUnit!=null && TimeUnit.MILLISECONDS.compareTo(costUnit)>0;
	}
	
	@SuppressWarnings("unchecked")
	public V extract(C context, Map<C, Map<Extractor<T, ? super V, C>, ? super V>> cache, T... obj) {
		long start = 0;
		if (costUnit!=null) {
			start = nanos ? System.nanoTime() : System.currentTimeMillis();
		}
		try {
			if (cache==null) {
				return extractInternal(context, cache, obj);
			}
			
			synchronized (cache) {
				Map<Extractor<T, ? super V, C>, ? super V> ce = cache.get(context);
				if (ce==null) {
					ce = createCacheEntry();
					cache.put(context, ce);
				}
				
				V ret = (V) ce.get(this);
				if (ret==null) {
					ret = extractInternal(context, cache, obj);
					ce.put(this, ret);
				}
				return ret;
			}
		} finally {
			if (costUnit!=null) {
				long end = nanos ? System.nanoTime() : System.currentTimeMillis();
				totalCost += costUnit.convert(end-start, nanos ? TimeUnit.NANOSECONDS : TimeUnit.MICROSECONDS);
				++invocations;
			}
		}
	}
	
	@Override
	public double getCost() {
		
		if (invocations==0) {
			return cost;
		}
		
		return cost + totalCost/invocations;
	}

	/**
	 * Basic comparisons with True and False.
	 * @param otherPredicate
	 * @return
	 */
	public ComparisonResult compareTo(Extractor<T, V, C> other) {
		if (this==other) {
			return ComparisonResult.EQUAL_NM;
		}
		
		// True is less restrictive than anything but self.
		if (other instanceof True) {
			return ComparisonResult.MORE_RESTRICTIVE_NM;
		}
		
		// False is more restrictive than anything but self.
		if (other instanceof False) {
			return ComparisonResult.LESS_RESTRICTIVE_NM;
		}
		
		if (other instanceof FacadeExtractor && !(this instanceof FacadeExtractor)) {
			ComparisonResult cr = other.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;
	}
	

	protected abstract V extractInternal(C context, Map<C, Map<Extractor<T, ? super V, C>, ? super V>> cache, T... obj);
	
	protected Map<Extractor<T, ? super V, C>, ? super V> createCacheEntry() {
		return new HashMap<Extractor<T,? super V,C>, V>();
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj instanceof Extractor) {
			ComparisonResult cr = compareTo((Extractor) obj);
			if (ComparisonResult.Type.EQUAL.equals(cr.getType()) && cr.isOneToOneMapping()) {
				return true;
			}
		}
		return false;
	}
	
}
