package cz.cuni.amis.utils.flag;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import cz.cuni.amis.utils.collections.MyCollections;

/**
 * This class is implementing the waiting on some flag value.
 * <BR><BR>
 * Note that you may call only one AWAIT() at time.
 * <BR><BR>
 * Typical usage:
 * <BR>
 * boolean flagValue = new WaitForFlagChange&lt;Boolean&gt;(booleanFlag, Boolean.TRUE).await();
 * <BR>
 * int flagValue = new WaitForFlagChange&lt;Integer&gt;(integerFlag, new Integer[]{1,3}).await();
 * 
 * @author Jimmy
 *
 * @param <TYPE>
 */
public class WaitForFlagChange<TYPE> {
	
	public static interface IAccept<TYPE> {
		
		public boolean accept(TYPE flagValue);
		
	}
	
	private class ListAccept implements IAccept<TYPE> {
		
		private List<TYPE> waitingFor;
		
		public ListAccept(Collection<TYPE> list) {
			waitingFor = new ArrayList<TYPE>(list.size());
			waitingFor.addAll(list);
		}
		
		public ListAccept(TYPE... accept) {
			waitingFor = new ArrayList(accept.length);
			waitingFor.addAll(MyCollections.asList(accept));
		}

		@Override
		public boolean accept(TYPE flagValue) {
			return waitingFor.contains(flagValue);
		}
		
	}
	
	private IAccept<TYPE> accept;
	private Flag<TYPE> flag;
	
	private Object latchAccessMutex = new Object();
	private Object mutex = new Object();
	
	private CountDownLatch latch = null;
	
	private boolean isResult = false;
	private TYPE result = null;
	
	private class Listener implements FlagListener<TYPE> {
		
		public Listener(Flag<TYPE> flag) {
			WaitForFlagChange.this.result = flag.getFlag();			
			if (!(isResult = isDesiredValue(WaitForFlagChange.this.result))) {
				flag.addListener(this);
			}
			// must do second check because of the flag that has been just attached
			if (!isResult) {
				TYPE result = flag.getFlag();
				if (isDesiredValue(result)) { 
					flag.removeListener(this);
					isResult = true;
					WaitForFlagChange.this.result = result; 
				}
			}
		}

		@Override
		public void flagChanged(TYPE changedValue) {
			if (!isResult && isDesiredValue(changedValue)) {
				flag.removeListener(this);
				synchronized(latchAccessMutex) {
					if (latch != null) {
						latch.countDown();
					}
				}
				isResult = true;
				result = changedValue;				
			}
		}
		
	}
	
	public WaitForFlagChange(Flag<TYPE> flag, IAccept<TYPE> waitingFor) {
		this.flag = flag;
		this.accept = waitingFor;
	}
	
	public WaitForFlagChange(Flag<TYPE> flag, TYPE waitingFor) {
		this.flag = flag;
		this.accept = new ListAccept(waitingFor);
	}
	
	public WaitForFlagChange(Flag<TYPE> flag, TYPE[] waitingFor) {
		this.flag = flag;
		this.accept = new ListAccept(waitingFor);		
	}
	
	public WaitForFlagChange(Flag<TYPE> flag, Collection<TYPE> waitingFor) {
		this.flag = flag;
		this.accept = new ListAccept(waitingFor);		
	}
		
	private boolean isDesiredValue(TYPE value) {
		return accept.accept(value);
	}
	
	/**
	 * Note that you may call only await() from one thread! If the instance is already in used
	 * it may produce unwanted behavior (e.g. dead-lock).
	 * 
	 * @return value from the flag that raised the latch
	 * @throws InterruptedException
	 */
	public TYPE await() throws InterruptedException {
		synchronized(mutex) {
			synchronized(latchAccessMutex) {
				latch = new CountDownLatch(1);
			}
			// instantiation checks whether we doesn't have desired result already, 
			// if not adds itself as a listener to a flag
			Listener listener = new Listener(flag); 
			if (isResult) return result;			
			latch.await(); // the latch is raised whenever a listener receive a correct result
			synchronized(latchAccessMutex) {
				flag.removeListener(listener);
				latch = null;
			}
			return result;
		}
	}
	
	/**
	 * Note that you may call only await() from one thread! If the instance is already in used
	 * it may produce unwanted behavior.
	 * <p><p>
	 * Returns null if desired value hasn't been set at the flag.
	 * 
	 * @param timeout
	 * @param timeUnit
	 * @return value of the flag
	 * @throws InterruptedException
	 */
	public TYPE await(long timeout, TimeUnit timeUnit) throws InterruptedException {
		synchronized(mutex) {
			synchronized(latchAccessMutex) {
				latch = new CountDownLatch(1);
			}
			// instantiation checks whether we doesn't have desired result already, 
			// if not adds itself as a listener to a flag
			Listener listener = new Listener(flag); 
			if (isResult) return result;			
			latch.await(timeout, timeUnit); // the latch is raised whenever a listener receive a correct result
			synchronized(latchAccessMutex) {
				flag.removeListener(listener);
				latch = null;
			}
			return result;
		}
	}

}
