/*
Copyright (c) 2006, Geomatics and Cartographic Research Centre, Carleton 
University All rights reserved.

Redistribution and use in source and binary forms, with or without 
modification, are permitted provided that the following conditions are met:

 - Redistributions of source code must retain the above copyright notice, 
   this list of conditions and the following disclaimer.
 - Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 - Neither the name of the Geomatics and Cartographic Research Centre, 
   Carleton University nor the names of its contributors may be used to 
   endorse or promote products derived from this software without specific 
   prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.

$Id$
*/
package ca.carleton.gcrc.atlas;

import java.lang.IllegalArgumentException;
import java.lang.Number;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.TimerTask;

import ca.carleton.gcrc.atlas.AudioTrack.Debug;

/**
 * <code>FadeTimerTask</code> implements a timer for adjusting numeric scalar
 * settings of the parent (the object creating the timer task) over time.  The scalar
 * being adjusted is a Number (see Number math functions used in this routine for currently
 * implemented subclasses of Number).
 * 
 * <code>FadeTimerTask</code> is initialized with target scale value and increment. 
 * It does not know how many
 * steps that should take.  The calling object figured these out and can recompute
 * the target value and increment on the fly if another fade request is received midway
 * through one fade.
 * 
 * The <code>FadeTimerTask</code> is capable of managing multiple concurrent
 * fade operations, storing active timerIDs and their associated increment and target
 * values in a Map and passing the timerIDs back to the creating object in all interactions.
 * The type used for the timer IDs is parameterized as KeyType.
 * 
 * The timer task's interactions with its creating object are defined by
 * <code>FadeTimerParent</code>.
 * 
 * @author Glenn Brauen
 */
public class FadeTimerTask<KeyType> extends TimerTask {
	/**
	 * The parent object controlling this fade timer.
	 */
	private FadeTimerParent<KeyType> m_parent;
		
	/**
	 * Map that associates timer identifiers and the current target and increment values.
	 * The timerId is passed back to the parent in all FadeTimerParent interface
	 * function calls so that the parent can differentiate the requests.
	 */
	private class TimerData {
		/*
		 * The computed increment and target value for the fade timer.
		 */
		private Number m_targetValue;
		private Number m_increment;  // fade up => +ive while fade down => -ive.
		
		public TimerData(Number targetValue_, Number increment_) {
			update(targetValue_, increment_);
		}
		
		public void update(Number targetValue_, Number increment_) {
			m_targetValue = targetValue_;
			m_increment = increment_;			
		}
		
		public Number getTargetValue() {
			return(m_targetValue);
		}

		public Number getIncrement() {
			return(m_increment);
		}
	} // end class TimerData
	
	/**
	 * Map of running fade requests  Associates timer identifiers to current increment and
	 * target value settings.
	 */
	private HashMap<KeyType,TimerData> m_runningFadeRequests;
	
	/**
	 * Timer task created to fade an integer variable owned by the calling object (parent_)
	 * to the specified targetValue_ in steps of increment_.  The timerId_ is used to identify
	 * this fade request in all interactions between the timer task and its parent object.
	 * 
	 * @param parent_ the calling object.
	 * @param targetValue_ new target value for the variable to be adjusted.
	 * @param increment_ the new increment value for each iteration.
	 * @param timerId_ the timer identifier for all interactions between the parent and timer task.
	 */
	public FadeTimerTask(FadeTimerParent<KeyType> parent_, Number targetValue_, Number increment_, KeyType timerId_) {
		m_parent = parent_;
		m_runningFadeRequests = new HashMap<KeyType, TimerData>();
		m_runningFadeRequests.put(timerId_, new TimerData(targetValue_, increment_));
		DebugLog("FadeTimerTask::const: fade timer task created.");
	}
	
	/**
	 * Reset the target value and increment for a fade timer (if the timerId matches one already
	 * in use) or add a new association to the map of running fade requests.  This approach
	 * allows a running fader to be adjusted on the fly as user interface changes
	 * to the underlying scalar are received in succession.
	 * 
	 * @param targetValue_ new target value for the variable to be adjusted.
	 * @param increment_ the new increment value for each iteration.
	 * @param timerId_ the timer identifier for all interactions between the parent and timer task.
	 */
	synchronized public void setTargetValueAndIncrement(Number targetValue_, Number increment_, KeyType timerId_) {
		if (m_runningFadeRequests.containsKey(timerId_)) {
			// existing timer - update targetValue and incremnt
			m_runningFadeRequests.get(timerId_).update(targetValue_, increment_);
		} else {
			// new timer - add to map of timers
			m_runningFadeRequests.put(timerId_, new TimerData(targetValue_, increment_));
		}
	}
	
	/**
	 * Return true if the map of timerIds is empty.  Otherwise a new map entry has been added since
	 * we thought the fade timer could be destroyed and it will have to be left running.
	 * 
	 * @return true if the map of running fade requests is empty (meaning that all fade requests have
	 *         reached their targets and been removed from the map and no new requests have resulted in
	 *         new or changed fade requests being added to the map).
	 */
	public boolean allTargetsReached() {
		return(m_runningFadeRequests.size() == 0);
	}
	
	/**
	 * Remove the running fade request identified by the input timerId_ if it is indeed running.
	 * @param timerId_ the key for the running fade request.
	 */
	public void remove(KeyType timerId_) {
		m_runningFadeRequests.remove(timerId_);
	} // remove()
	
	/*
	 * N U M B E R   A R I T H M E T I C   R O U T I N E S
	 * 
	 * Number arithmetic functions (for the number types currently implmeneted for this class: Integer
	 * and Float).
	 */
	
	/**
	 * Add a pair of numbers and return their sum.
	 * @param n1_ a number
	 * @param n2_ another number
	 * @return the sum
	 * @throws IllegalArgumentException if the input number is not of a supported type.
	 */
	private Number add(Number n1_, Number n2_) throws IllegalArgumentException {
		Number retVal;
		if (n1_ instanceof Float) { // both are Float!
			retVal = (Float) n1_ + (Float) n2_;
		} else if (n1_ instanceof Integer) { // both are integer!
			retVal = (Integer) n1_ + (Integer) n2_;
		} else {
			throw(new IllegalArgumentException("Unsupported numeric type."));
		}
		return(retVal);
	}// end add()
	
	/**
	 * Determine whether the input number is strictly positve (> 0).
	 * @param n_ a number
	 * @return true if n_ is positive and false if n_ is negative or equal to zero.
	 * @throws IllegalArgumentException if the input number is not of a supported type.
	 */
	private boolean isStrictlyPositive(Number n_) throws IllegalArgumentException {
		boolean retVal = false;
		if (n_ instanceof Float) {
			retVal = ((Float) n_ > 0);
		} else if (n_ instanceof Integer) {
			retVal = ((Integer) n_ > 0);
		} else {
			throw(new IllegalArgumentException("Unsupported numeric type."));
		}
		return(retVal);
	} // isStrictlyPositive()
	
	/**
	 * Determine whether the input number is strictly negative (< 0).
	 * @param n_ a number
	 * @return true if n_ is negative and false if n_ is positive or equal to zero. 
	 * @throws IllegalArgumentException if the input number is not of a supported type.
	 */
	private boolean isStrictlyNegative(Number n_) throws IllegalArgumentException {
		boolean retVal = false;
		if (n_ instanceof Float) {
			retVal = ((Float) n_ < 0);
		} else if (n_ instanceof Integer) {
			retVal = ((Integer) n_ < 0);
		} else {
			throw(new IllegalArgumentException("Unsupported numeric type."));
		}
		return(retVal);
	} // isStrictlyNegative()
	
	/**
	 * Compare two values, returning true if the first is greater than the second.
	 * @param n1_ first number
	 * @param n2_ second number
	 * @return true if n1_ is greater than n2_.
	 * @throws IllegalArgumentException if the input numbers are not of a supported type (only n1_
	 *                                  is checked - both are assumed to be of the same type).
	 */
	private boolean isStrictlyGreaterThan(Number n1_, Number n2_) throws IllegalArgumentException {
		boolean retVal = false;
		if (n1_ instanceof Float) {
			retVal = ((Float) n1_ > (Float) n2_);
		} else if (n1_ instanceof Integer) {
			retVal = ((Integer) n1_ > (Integer) n2_);
		} else {
			throw(new IllegalArgumentException("Unsupported numeric type."));
		}
		return(retVal);
	} // isStrictlyGreaterThan()
	
	/**
	 * Compare two values, returning true if they are numerically equivalent.
	 * @param n1_ first number
	 * @param n2_ second number
	 * @return true if n1_ is greater than n2_.
	 * @throws IllegalArgumentException if the input numbers are not of a supported type (only n1_
	 *                                  is checked - both are assumed to be of the same type).
	 */
	private boolean isNumericallyEqual(Number n1_, Number n2_) throws IllegalArgumentException {
		boolean retVal = false;
		if (n1_ instanceof Float) {
			retVal = (0 ==  (((Float) n1_).compareTo((Float) n2_)));
		} else if (n1_ instanceof Integer) {
			retVal = (0 ==  (((Integer) n1_).compareTo((Integer) n2_)));
		} else {
			throw(new IllegalArgumentException("Unsupported numeric type."));
		}
		return(retVal);
	} // isStrictlyGreaterThan()
	
	/*
	 * S Y N C H R O N I Z E D   M A P   H A N D L I N G   R O U T I N E S
	 */
	
	/**
	 * Make a shallow clone of the map of current fades.  This is done with synchronization so that
	 * run() does not access the original map in unsynchronized mode - that would be a potential
	 * concurrency issue with setTargetValueAndIncrement() which is also synchronized.
	 * @return shalow clone map
	 */
	synchronized private Object makeMapClone() {
		return(m_runningFadeRequests.clone());
	} // makeMapClone()
	
	/**
	 * Clear out completed entries from the map of running fade timers.  Each entry is tested to see if
	 * the fade really looks complete (i.e., no additional update on the target/increment has been received
	 * that will require this fader to remain in operation).  If the entry is to be removed, the parent is
	 * notified of fader completion.
	 * @param fadesToRemove_ list of elements detected as having completed the fade operation.
	 */
	synchronized private void removeMarkedEntriesFromMap(ArrayList<KeyType> fadesToRemove_) {
		for (int index=0; index<fadesToRemove_.size(); index++) {
			KeyType key = fadesToRemove_.get(index); // get key for removal
			Number curr = m_parent.getCurrentFadeTimerValue(key);
			Number target = m_runningFadeRequests.get(key).getTargetValue();
			if (isNumericallyEqual(curr, target)) { // no new update on this fader received since it was marked
				m_runningFadeRequests.remove(key);	// remove it			
				m_parent.fadeCompleted(key);		// tell parent that this fade is complete.
			} else {
				DebugLog("not dropping - curr: " + curr + " target: " + target, Debug.detailLevel);
			}
		}
	} // removeMarkedEntriesFromMap()

	/* (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 * 
	 * Invoked as scheduled to perform one iteration of the fade increments for all running
	 * faders managed by this task.  Completed fades are marked for removal and subsequently
	 * removed in the call to removeMarkedEntriesFromMap().  If all managed faders are complete,
	 * the fader task's parent is notified (to, for instance, trigger a clean-up of the task).
	 */
	public void run() {
		ArrayList<KeyType> fadesToRemove = new ArrayList<KeyType>();
		HashMap<KeyType,TimerData> tempClone = (HashMap) makeMapClone();
		
		for (Iterator iterator = tempClone.entrySet().iterator(); iterator.hasNext();) { 
			Map.Entry entry = (Map.Entry) iterator.next(); 
			KeyType timerId = (KeyType) entry.getKey(); 
			TimerData td = (TimerData) entry.getValue();
			Number targetValue = td.getTargetValue();
			Number increment = td.getIncrement();
			
			boolean stop = true; // assume this will be last pass for this timer.
			
			Number newValue = add(m_parent.getCurrentFadeTimerValue(timerId), increment);
			if (isStrictlyPositive(increment)) { // fading up
				if (isStrictlyGreaterThan(newValue, targetValue)) { //don't overshoot
					newValue = targetValue;
				} else if (isNumericallyEqual(newValue, targetValue)) {
					// exactly hit the target - no adjust but do stop
				} else {
					stop = false;
				}
			} else if (isStrictlyNegative(increment)) { // fading down
				if (isStrictlyGreaterThan(targetValue, newValue)) { // don't undershoot
					newValue = targetValue;
				} else if (isNumericallyEqual(newValue, targetValue)) {
					// exactly hit the target - no adjust but do stop
				} else {
					stop = false;
				}
			}
			m_parent.setCurrentFadeTimerValue(newValue, timerId);
			//DebugLog("FadeTimerTask::run: " + m_timerId + " value: " + newValue);
			
			if (stop) {
				fadesToRemove.add(timerId); 		// mark current mapping for removal from original map.
			}
		} // end for 

		// remove any marked mapping (i.e., completed fades) from the map
		removeMarkedEntriesFromMap(fadesToRemove);
		
		if (allTargetsReached()) {
			// tell parent fade timer operation is complete - synchronized function will clean it up.
			m_parent.allFadeTimersFinished();
			DebugLog("FadeTimerTask::run: all fades finished.");
		}				
	} // end run()
	
	protected final class Debug {
		  protected static final int standardLevel = 0;
		  protected static final int mediumLevel = 1;
		  protected static final int detailLevel = 2;
		  
		  protected static final boolean ON = false;
		  protected static final int LEVEL = standardLevel;
	} 
	
	protected void DebugLog(String str) {
		if (Debug.ON) System.out.println(str);
	}

	protected void DebugLog(String str, int level) {
		if (Debug.ON && level <= Debug.LEVEL) System.out.println(str);
	}
	
} // end of FadeTimerTask
