/*****************************************************
*  
*  Copyright 2009 Akamai Technologies, Inc.  All Rights Reserved.
*  
*****************************************************
*  The contents of this file are subject to the Mozilla Public License
*  Version 1.1 (the "License"); you may not use this file except in
*  compliance with the License. You may obtain a copy of the License at
*  http://www.mozilla.org/MPL/
*   
*  Software distributed under the License is distributed on an "AS IS"
*  basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
*  License for the specific language governing rights and limitations
*  under the License.
*   
*  
*  The Initial Developer of the Original Code is Akamai Technologies, Inc.
*  Portions created by Akamai Technologies, Inc. are Copyright (C) 2009 Akamai 
*  Technologies, Inc. All Rights Reserved. 
*  
*****************************************************/
package org.osmf.metadata
{
	import __AS3__.vec.Vector;
	
	import flash.events.TimerEvent;
	import flash.utils.Dictionary;
	import flash.utils.Timer;
	
	import org.osmf.events.MediaElementEvent;
	import org.osmf.events.MetadataEvent;
	import org.osmf.events.PlayEvent;
	import org.osmf.events.SeekEvent;
	import org.osmf.events.TimelineMetadataEvent;
	import org.osmf.media.MediaElement;
	import org.osmf.traits.MediaTraitType;
	import org.osmf.traits.PlayState;
	import org.osmf.traits.PlayTrait;
	import org.osmf.traits.SeekTrait;
	import org.osmf.traits.TimeTrait;
	import org.osmf.utils.OSMFStrings;
	
	/**
	 * Dispatched when a TimelineMarker is added to this object.
	 *
	 * @eventType org.osmf.events.TimelineMetadataEvent.MARKER_ADD
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event (name="markerAdd", type="org.osmf.metadata.TimelineMetadataEvent")]

	/**
	 * Dispatched when a TimelineMarker is removed from this object.
	 *
	 * @eventType org.osmf.events.TimelineMetadataEvent.MARKER_REMOVE
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event (name="markerRemove", type="org.osmf.metadata.TimelineMetadataEvent")]

	/**
	 * Dispatched when the currentTime property of the MediaElement associated
	 * with this TimelineMetadata has reached the time value of one of the
	 * TimelineMarkers in this TimelineMetadata.
	 *
	 * @eventType org.osmf.events.TimelineMetadataEvent.MARKER_TIME_REACHED
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event (name="markerTimeReached", type="org.osmf.events.TimelineMetadataEvent")]

	/**
	 * Dispatched when the currentTime property of the MediaElement associated
	 * with this TimelineMetadata has reached the duration offset of one of the
	 * TimelineMarkers in this TimelineMetadata.
	 *
	 * @eventType org.osmf.events.TimelineMetadataEvent.MARKER_TIME_REACHED
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event (name="markerDurationReached", type="org.osmf.events.TimelineMetadataEvent")]

	/**
	 * The TimelineMetadata class encapsulates metadata associated with the
	 * timeline of a MediaElement.
	 * 
	 * <p>TimelineMetadata uses the TimelineMarker class to represent both
	 * keys and values (i.e. a TimelineMarker will be stored as both key and
	 * value).  A TimelineMetadata object dispatches a TimelineMetadataEvent
	 * when the currentTime property of the MediaElement's TimeTrait matches
	 * any of the time values in its collection of TimelineMarker objects.</p>
	 * 
	 *  @includeExample TimelineMetadataExample.as -noswf
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	public class TimelineMetadata extends Metadata
	{
		/**
		 * Constructor.
		 * 
		 * @param media The media element this timeline metadata applies to.
		 * 
		 * @throws ArgumentError If the media argument is null.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function TimelineMetadata(media:MediaElement)
		{
			super();

			if (media == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}

			this.media = media;	
			_enabled = true;
			
			intervalTimer = new Timer(CHECK_INTERVAL);
			intervalTimer.addEventListener(TimerEvent.TIMER, onIntervalTimer);
			
			// Check the media element for traits, if they are null here
			// that's okay we'll manage them in the event handlers.
			timeTrait = media.getTrait(MediaTraitType.TIME) as TimeTrait;
			
			seekTrait = media.getTrait(MediaTraitType.SEEK) as SeekTrait;
			setupTraitEventListener(MediaTraitType.SEEK);
			
			playTrait = media.getTrait(MediaTraitType.PLAY) as PlayTrait;
			setupTraitEventListener(MediaTraitType.PLAY);
			
			media.addEventListener(MediaElementEvent.TRAIT_ADD, onTraitAdd);
			media.addEventListener(MediaElementEvent.TRAIT_REMOVE, onTraitRemove);
		}
		
		/**
		 * The number of TimelineMarker objects contained within this object.
		 **/
		public function get numMarkers():int
		{
			return temporalValueCollection ? temporalValueCollection.length : 0;
		}
		
		/**
		 * Returns the TimelineMarker at the specified index.  Note that the
		 * markers are sorted by time.
		 *  
		 * @param index The index of the marker to return.
		 * 
		 * @returns The marker at that index, null if there is no
		 * such marker at that index.
		 **/
		public function getMarkerAt(index:int):TimelineMarker
		{
			if (index >= 0 && temporalValueCollection != null && index < temporalValueCollection.length)
			{
				return temporalValueCollection[index];
			}
			else
			{
				return null;
			}
		}
		
		/**
		 * Adds the specified TimelineMarker to this object.  This class
		 * maintains the TimelineMarkers in time order.  If another TimelineMarker
		 * with the same time value exists within this object, then the existing
		 * value will be overwritten.
		 * 
		 * @param marker The marker to add.
		 * 
		 * @throws ArgumentError If marker is null or specifies an invalid time.
		 **/
		public function addMarker(marker:TimelineMarker):void
		{
			if (marker == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}

			addValue("" + marker.time, marker);
		}
		
		/**
		 * Removes the specified TimelineMarker from this object.
		 * 
		 * @param marker The marker to remove.
		 * 
		 * @returns The removed marker, null if the specified marker is not
		 * contained within this object.
		 * 
		 * @throws ArgumentError If marker is null.
		 **/
		public function removeMarker(marker:TimelineMarker):TimelineMarker
		{
			if (marker == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}

			return removeValue("" + marker.time);
		}
		
		/**
		 * @private
		 */
		override public function addValue(key:String, value:Object):void
		{
			var time:Number = new Number(key);
			var marker:TimelineMarker = value as TimelineMarker;
			
			if (key == null || isNaN(time) || time < 0 || marker == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}
			
			if (temporalValueCollection == null)
			{
				temporalKeyCollection = new Vector.<Number>();
				temporalKeyCollection.push(time);

				temporalValueCollection = new Vector.<TimelineMarker>();
				temporalValueCollection.push(value);
			}
			else
			{
				// Find the index where we should insert this value
				var index:int = findTemporalMetadata(0, temporalValueCollection.length - 1, time);
				
				// A negative index value means it doesn't exist in the array and the absolute value is the
				// index where it should be inserted.  A positive index means a value exists and in this
				// case we'll overwrite the existing value rather than insert a duplicate.
				if (index < 0) 
				{
					index *= -1;
					temporalKeyCollection.splice(index, 0, time);
					temporalValueCollection.splice(index, 0, marker);
				}
				
				// Make sure we don't insert a dup at index 0
				else if ((index == 0) && (time != temporalKeyCollection[0])) 
				{
					temporalKeyCollection.splice(index, 0, time);
					temporalValueCollection.splice(index, 0, marker);
				}
				else 
				{
					temporalKeyCollection[index] = time;
					temporalValueCollection[index] = marker;
				}
			}
			
			enabled = true;
			
			dispatchEvent(new MetadataEvent(MetadataEvent.VALUE_ADD, false, false, key, marker));
			dispatchEvent(new TimelineMetadataEvent(TimelineMetadataEvent.MARKER_ADD, false, false, marker));
		}
		
		/**
		 * @private
		 **/
		override public function removeValue(key:String):*
		{
			if (key == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}

			var time:Number = new Number(key);
			
			var result:* = null;
			
			// Also remove from our collections.
			var index:int = temporalValueCollection ? findTemporalMetadata(0, temporalValueCollection.length - 1, time) : -1;
			if (index >= 0)
			{
				temporalKeyCollection.splice(index, 1);
				result = temporalValueCollection.splice(index, 1)[0];
				
				dispatchEvent(new MetadataEvent(MetadataEvent.VALUE_REMOVE, false, false, key, result));
				dispatchEvent(new TimelineMetadataEvent(TimelineMetadataEvent.MARKER_REMOVE, false, false, result as TimelineMarker));
			}
			
			return result;
		}
		
		/**
		 * @private
		 */
		override public function getValue(key:String):*
		{
			if (key == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}

			var time:Number = new Number(key);
			if (!isNaN(time))
			{
				for (var i:int = 0; i < temporalKeyCollection.length; i++)
				{
					var keyTime:Number = temporalKeyCollection[i];
					if (keyTime == time)
					{
						return temporalValueCollection[i];
					}
				}
			}
			
			return null;
		}
		
		/**
		 * @private
		 * 
		 * Enables/disables this metadata object (enabled by default). If enabled, the
		 * class will dispatch events of type TimelineMetadataEvent. Setting
		 * this property to <code>false</code> will cause the class to stop
		 * dispatching events.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function get enabled():Boolean
		{
			return _enabled;
		}
		
		/**
		 * @private
		 **/ 
		public function set enabled(value:Boolean):void 
		{
			_enabled = value;
			reset(value);
		}
		
		/**
		 * Starts / stops the interval timer.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		private function startTimer(start:Boolean=true):void
		{
			if (!start)
			{
				intervalTimer.stop();
			}
			else if (	timeTrait != null
					 && temporalValueCollection != null
					 && temporalValueCollection.length > 0 
					 && restartTimer
					 && enabled
					 && !intervalTimer.running
					) 
			{
				// If there is a PlayTrait and the media isn't playing, there is no reason to 
				// start the timer.
				if (playTrait != null && playTrait.playState == PlayState.PLAYING)
				{
					intervalTimer.start();
				}
			}
		}
						
		/**
		 * Perform a reset on the class' internal state.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		private function reset(startTimer:Boolean):void 
		{
			lastFiredTemporalMetadataIndex = -1;
			restartTimer = true;
			intervalTimer.reset();
			intervalTimer.delay = CHECK_INTERVAL;
			
			if (startTimer)
			{
				this.startTimer();
			}
		}
		
		/**
		 * The interval timer callback. Checks for temporal metadata 
		 * around the current TimeTrait.currentTime and dispatches an event
		 * if found. 
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
   		private function checkForTemporalMetadata():void 
   		{
			var now:Number = timeTrait.currentTime;
			
			// Start looking one index past the last one we found
			var index:int = findTemporalMetadata(lastFiredTemporalMetadataIndex + 1, temporalValueCollection.length - 1, now);
			
			// A negative index value means it doesn't exist in the collection and the absolute value is the
			// index where it should be inserted.  Therefore, to get the closest match, we'll look at the index
			// before this one.  A positive index means an exact match was found.
			if (index <= 0) 
			{
				index *= -1;
				index = (index > 0) ? (index - 1) : 0;
			}
			
			// See if the value at this index is within our tolerance
			if (!checkTemporalMetadata(index, now) && ((index + 1) < temporalValueCollection.length)) 
			{
				// Look at the next one, see if it is close enough to fire
				checkTemporalMetadata(index+1, now);
			}
   		}
   		
   		private function setupTraitEventListener(traitType:String, add:Boolean=true):void
   		{
   			if (add)
   			{
	   			if (traitType == MediaTraitType.SEEK && seekTrait != null)
	   			{
					seekTrait.addEventListener(SeekEvent.SEEKING_CHANGE, onSeekingChange);
	   			}
	   			
	   			else if (traitType == MediaTraitType.PLAY && playTrait != null)
	   			{
	   				playTrait.addEventListener(PlayEvent.PLAY_STATE_CHANGE, onPlayStateChange);
	   				
	   				// We need to check the playing state, if the media is already playing, we won't 
	   				// get the play state change event and the interval timer will never start.
	   				if (playTrait.playState == PlayState.PLAYING)
	   				{
	   					var event:PlayEvent = new PlayEvent(PlayEvent.PLAY_STATE_CHANGE, false, false, PlayState.PLAYING);
	   					onPlayStateChange(event);
	   				}
	   			}
	   		}
	   		else
	   		{
	   			if (traitType == MediaTraitType.SEEK && seekTrait != null)
	   			{
					seekTrait.removeEventListener(SeekEvent.SEEKING_CHANGE, onSeekingChange);
	   			}
	   			
	   			else if (traitType == MediaTraitType.PLAY && playTrait != null)
	   			{
	   				playTrait.removeEventListener(PlayEvent.PLAY_STATE_CHANGE, onPlayStateChange);
	   			}
	   		}
   		}
   		
   		private function onSeekingChange(event:SeekEvent):void
   		{
   			if (event.seeking)
   			{
   				reset(true);
   			}
   		}
   		
   		private function onPlayStateChange(event:PlayEvent):void
   		{
   			var timer:Timer;
   			if (event.playState == PlayState.PLAYING)
   			{
   				// Start any duration timers.
   				if (durationTimers != null)
   				{
   					for each (timer in durationTimers)
   					{
   						timer.start();
   					}
   				}
   				startTimer();
   			}
   			else
   			{
  				// Pause any duration timers.
   				if (durationTimers != null)
   				{
   					for each (timer in durationTimers)
   					{
   						timer.stop();
   					}
   				}
 
   				startTimer(false);
   			}
   		}
   		
		/**
		 * Returns the index of the temporal metadata object matching the time. If no match is found, returns
		 * the index where the value should be inserted as a negative number.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		private function findTemporalMetadata(firstIndex:int, lastIndex:int, time:Number):int 
		{
			if (firstIndex <= lastIndex) 
			{
				var mid:int = (firstIndex + lastIndex) / 2;	// divide and conquer
				if (time == temporalKeyCollection[mid]) 
				{
					return mid;
				}
				else if (time < temporalKeyCollection[mid]) 
				{
					// search the lower part
					return findTemporalMetadata(firstIndex, mid - 1, time);
				}
				else 
				{
					// search the upper part
					return findTemporalMetadata(mid + 1, lastIndex, time);
				}
			}
			return -(firstIndex);
		}   		
		
		/**
		 * Dispatch the events for this temporal value. If there is a duration
		 * property on the value, dispatch a duration reached event after the 
		 * proper amount of time has passed.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		private function dispatchTemporalEvents(index:int):void
		{
			var marker:TimelineMarker = temporalValueCollection[index];
			dispatchEvent(new TimelineMetadataEvent(TimelineMetadataEvent.MARKER_TIME_REACHED, false, false, marker));
			
			if (marker.duration > 0)
			{
				var timer:Timer = new Timer(CHECK_INTERVAL);
				var endTime:Number = marker.time + marker.duration;
				
				// Add it to the dictionary of duration timers so we can pause it 
				// if the media pauses.
				if (durationTimers == null)
				{
					durationTimers = new Dictionary();
				}
				durationTimers[marker] = timer;

				timer.addEventListener(TimerEvent.TIMER, onDurationTimer);
				timer.start();
				
				function onDurationTimer(event:TimerEvent):void
				{
					if (timeTrait && timeTrait.currentTime >= endTime)
					{
						timer.removeEventListener(TimerEvent.TIMER, onDurationTimer);
						delete durationTimers[marker];
						dispatchEvent(new TimelineMetadataEvent(TimelineMetadataEvent.MARKER_DURATION_REACHED, false, false, marker));
					}
				}
			}
		}
		
   		/**
   		 * Checks the item at the index passed in with the time passed in.
   		 * If the item time is within the class' tolerance, an event is dispatched.
   		 * 
   		 * Returns True if a match was found, otherwise False.
   		 *  
   		 *  @langversion 3.0
   		 *  @playerversion Flash 10
   		 *  @playerversion AIR 1.5
   		 *  @productversion OSMF 1.0
   		 */
   		private function checkTemporalMetadata(index:int, now:Number):Boolean 
   		{ 		
			if (!temporalValueCollection || !temporalValueCollection.length) 
			{
				return false;
			}
			
			var result:Boolean = false;																				
		
			if ( 	(temporalValueCollection[index].time >= (now - TOLERANCE))
				&& 	(temporalValueCollection[index].time <= (now + TOLERANCE))
				&&	(index != lastFiredTemporalMetadataIndex)
			   ) 
			{
				lastFiredTemporalMetadataIndex = index;
				
				dispatchTemporalEvents(index);
				
				// Adjust the timer interval if necessary
				var thisTime:Number = temporalKeyCollection[index];
				// Get the next time value after this one so we can decide to adjust the timer interval
				var nextTime:Number = calcNextTime(index);
				
				var newDelay:Number = ((nextTime - thisTime)*1000)/4;
				newDelay = (newDelay > CHECK_INTERVAL) ? newDelay : CHECK_INTERVAL;
								
				// If no more data, stop the timer
				if (thisTime == nextTime) 
				{
					startTimer(false);
					restartTimer = false;
				}
				else if (newDelay != intervalTimer.delay) 
				{
					intervalTimer.reset();
					intervalTimer.delay = newDelay;
					startTimer();
				}
				result = true;
			}
			
			// If we've optimized the interval time by reseting the delay, we could miss a data point
			//    if it happens to fall between this check and next one.
			// See if we are going to miss a data point (meaning there is one between now and the 
			//    next interval timer event).  If so, drop back down to the default check interval.
			else if ((intervalTimer.delay != CHECK_INTERVAL) && ((now + (intervalTimer.delay/1000)) > calcNextTime(index))) 
			{
				this.intervalTimer.reset();
				this.intervalTimer.delay = CHECK_INTERVAL;
				startTimer();
			}
			return result;				
   		}		

		private function calcNextTime(index:int):Number
		{
			return temporalValueCollection [ index + 1 < temporalKeyCollection.length
										  	 ? index + 1
					  		  				 : temporalKeyCollection.length - 1
					 						].time;
		}
		
		/**
		 * The interval timer event handler.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		private function onIntervalTimer(event:TimerEvent):void 
		{
			checkForTemporalMetadata();
		}
		
		/**
		 * Called when traits are added to the media element.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		private function onTraitAdd(event:MediaElementEvent):void
		{
			switch (event.traitType)
			{
				case MediaTraitType.TIME:
					timeTrait = media.getTrait(MediaTraitType.TIME) as TimeTrait;
					startTimer();
					break;
				case MediaTraitType.SEEK:
					seekTrait = media.getTrait(MediaTraitType.SEEK) as SeekTrait;
					break;
				case MediaTraitType.PLAY:
					playTrait = media.getTrait(MediaTraitType.PLAY) as PlayTrait;
					break;
			}
			
			setupTraitEventListener(event.traitType);
		}
		
		/**
		 * Called when traits are removed from the media element.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		private function onTraitRemove(event:MediaElementEvent):void
		{
			// Remove any event listeners
			setupTraitEventListener(event.traitType, false);

			switch (event.traitType)
			{
				case MediaTraitType.TIME:
					timeTrait = null;
					// This is a work around for FM-171. Traits are added and removed for
					// each child in a composition element when transitioning between child
					// elements. So don't stop the timer if the MediaElement is a composition
					// (which we determine by looking for the numChildren property, since
					// we don't want to link the composition classes in by default).
					if (media.hasOwnProperty("numChildren") == false)
					{
						startTimer(false);
					}
					break;
				case MediaTraitType.SEEK:
					seekTrait = null;
					break;
				case MediaTraitType.PLAY:
					playTrait = null;
					break;
			}
		}
			
		private static const CHECK_INTERVAL:Number = 100;	// The default interval (in milliseconds) the 
															// class will check for temporal metadata
		private static const TOLERANCE:Number = 0.25;	// A value must be within this tolerence to trigger
														//	a timeReached event.				
		private var temporalKeyCollection:Vector.<Number>;
		private var temporalValueCollection:Vector.<TimelineMarker>;
		private var media:MediaElement;
		private var timeTrait:TimeTrait;
		private var seekTrait:SeekTrait;
		private var playTrait:PlayTrait;
		private var lastFiredTemporalMetadataIndex:int;
		private var intervalTimer:Timer;
		private var restartTimer:Boolean;
		private var _enabled:Boolean;
		private var durationTimers:Dictionary;
	}
}
