package com.hammurapi.store.local;

import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import com.hammurapi.common.Observable;
import com.hammurapi.common.Observer;
import com.hammurapi.common.Util;
import com.hammurapi.extract.Extractor;
import com.hammurapi.extract.Predicate;
import com.hammurapi.store.Store;
import com.hammurapi.store.Store.Handle;
import com.hammurapi.store.StoreException;

public class LocalHandle<T,PK, S extends Store<T,PK,S>> implements Handle<T, PK, S> {
	
	private final Observer<T> observer = new Observer<T>() {

		@Override
		public void update(T obj, Object... args) {
			LocalHandle.this.update();
		}
		
	};

	public enum HandleStrength {
		STRONG, SOFT, WEAK
	}
	
	@Override
	public int hashCode() {
		if (isValid()) {
			return get().hashCode();
		}
		return 0;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		
		@SuppressWarnings("unchecked")
		LocalHandle<T,PK,S> other = (LocalHandle<T,PK,S>) obj;
		if (isValid() && other.isValid()) {
			return get().equals(other.get());
		}
		return !other.isValid();
	}

	private Reference<T> ref;
	private T obj;
	private AtomicBoolean valid = new AtomicBoolean(true);
	private PK primaryKey;
	
	private Map<S, Map<Extractor<T, ? super PK, S>, ? super PK>> cache;
	private Reference<Map<S, Map<Extractor<T, ? super PK, S>, ? super PK>>> cacheRef;
	private HandleStrength handleStrength;
	private LocalStoreBase<T,PK,S> store;
	private boolean cacheExtracted;
	private Predicate<T, S>[] validators;
	
	public LocalHandle(
			LocalStoreBase<T,PK,S> store,
			T obj, 
			PK primaryKey, 
			Map<S, Map<Extractor<T, ? super PK, S>, ? super PK>> cache,	
			Predicate<T, S>[] validators, 			
			HandleStrength handleStrength, 
			boolean cacheExtracted) {

		this.store = store;
		this.handleStrength = handleStrength;
		this.cacheExtracted = cacheExtracted;
		this.validators = validators;
		
		com.hammurapi.common.Observable<T> observable = toObservable(obj);
		if (observable!=null) {
			observable.addObserver(observer);
		}
		
		switch (handleStrength) {
		case STRONG:
			this.obj = obj; 
			this.cache = cache;
			break;
		case SOFT:
			ref = new SoftReference<T>(obj);
			if (cache!=null) {
				cacheRef = new SoftReference<Map<S,Map<Extractor<T,? super PK,S>,? super PK>>>(cache);
			}
			break;
		case WEAK:
			ref = new WeakReference<T>(obj);
			if (cache!=null) {
				cacheRef = new WeakReference<Map<S,Map<Extractor<T,? super PK,S>,? super PK>>>(cache);
			}
			break;
		}
		this.primaryKey = primaryKey;
		valid.set(true);
	}
		
	@Override
	public void update(T obj) {			
		Map<S, Map<Extractor<T, ? super PK, S>, ? super PK>> theCache = getCache();
		if (cacheExtracted) {
			if (theCache!=null) {
				theCache.clear();
			}
		}
		
		if (store.getPrimaryKeyExtractor()!=null) {
			@SuppressWarnings("unchecked")
			PK newPrimaryKey = store.getPrimaryKeyExtractor().extract((S) store, theCache, Util.wrap(obj));
			if (!primaryKey.equals(newPrimaryKey)) {
				throw new StoreException("Primary key of the update object is different from the original primary key, use Store.put() instead of Handle.update()");
			}
		}
		
		store.updateIndices(this, false);
		
		switch (handleStrength) {
		case STRONG:
			deleteObserver(this.obj);
			this.obj = obj; 
			break;
		case SOFT:
			deleteObserver(ref==null ? null : ref.get());
			ref = new SoftReference<T>(obj);			
			break;
		case WEAK:
			deleteObserver(ref==null ? null : ref.get());
			ref = new WeakReference<T>(obj);
			break;
		}
		
		com.hammurapi.common.Observable observable = toObservable(obj);
		if (observable!=null) {
			observable.addObserver(observer);
		}
		
		valid.set(true);
	}

	@Override
	public void update() {			
		if (cacheExtracted) {
			getCache().clear();
		}
		
		if (store.getPrimaryKeyExtractor()!=null) {
			@SuppressWarnings("unchecked")
			PK newPrimaryKey = store.getPrimaryKeyExtractor().extract((S) store, getCache(), Util.wrap(obj));
			if (!primaryKey.equals(newPrimaryKey)) {
				throw new StoreException("Primary key of the updated object is different from the original primary key, use Store.put() instead of Handle.update()");
			}
		}
		
		store.updateIndices(this, false);
		valid.set(true);
	}
	
	@Override
	public void remove() {
		if (HandleStrength.STRONG.equals(handleStrength)) {
			deleteObserver(this.obj);
		} else {
			deleteObserver(ref==null ? null : ref.get());
		}
		
		valid.set(false);
		obj = null;
		ref = null;		
		cache = null;
		cacheRef = null;
	}
	
	private void deleteObserver(T obj) {
		com.hammurapi.common.Observable<T> observable = toObservable(obj);
		if (observable!=null) {
			observable.deleteObserver(observer);
		}		
	}

	protected Map<S, Map<Extractor<T, ? super PK, S>, ? super PK>> getCache() {
		if (valid.get()) {
			if (handleStrength==HandleStrength.STRONG) {
				return cache;
			}
			Map<S, Map<Extractor<T, ? super PK, S>, ? super PK>> ret = cacheRef==null ? null : cacheRef.get();
			if (ret==null && cacheExtracted) {
				ret = store.createCache();
				switch (handleStrength) {
				case SOFT:
					cacheRef = new SoftReference<Map<S,Map<Extractor<T,? super PK,S>,? super PK>>>(ret);
					break;
				case WEAK:
					cacheRef = new WeakReference<Map<S,Map<Extractor<T,? super PK,S>,? super PK>>>(ret);
					break;
				}
			}
			
			return ret; 
		}
		return null;
	}
	
	@Override
	public T get() {
		if (isValid()) {
			if (HandleStrength.STRONG.equals(handleStrength)) {
				return obj;
			}
			return ref==null ? null : ref.get();
		}
		return null;
	}

	@Override
	public PK getPrimaryKey() {
		return primaryKey;
	}
	
	public boolean isValid() {		
		if (!valid.get()) {
			return false;
		}
		
		T theObj;
		if (HandleStrength.STRONG.equals(handleStrength)) {
			theObj = obj;
		} else {
			theObj = ref==null ? null : ref.get();
		}
		if (theObj==null) {
			return false;
		}
		if (validators!=null && validators.length>0) {
//			S deputy = store.createDeputy(); // Do we need deputy here???
			for (Predicate<T, S> v: validators) {
				if (!v.extract((S) store /* deputy */, null, Util.wrap(theObj))) {
					return false;
				}
			}
		}
		return true;
	}

	@Override
	public <V> V extract(Extractor<T, V, S> extractor) {
		if (isValid()) {
			// Rude cast because cache is intentionally of wrong value type (PK) instead of Object for convenience. 
			Map<S, Map<Extractor<T, ? super V, S>, ? super V>> theCache = (Map) getCache(); 
			return extractor.extract(store.createDeputy(), theCache, Util.wrap(get()));
		} 
		
		return null;
	}

	@Override
	public String toString() {
		return "HandleImpl [get()=" + get() + ", getPrimaryKey()="
				+ getPrimaryKey() + ", isValid()=" + isValid() + "]";
	}
	
	// TODO equals() and hashCode() for unique stores.		
	
	private Observable<T> toObservable(T obj) {
		return store.config.getObservableConverter()==null ? null : store.config.getObservableConverter().convert(obj);
	}
		
}
