/*****************************************************
*  
*  Copyright 2009 Adobe Systems Incorporated.  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 Adobe Systems Incorporated.
*  Portions created by Adobe Systems Incorporated are Copyright (C) 2009 Adobe Systems 
*  Incorporated. All Rights Reserved. 
*  
*****************************************************/
package org.osmf.elements.compositeClasses
{
	import org.osmf.events.LoadEvent;
	import org.osmf.media.MediaResourceBase;
	import org.osmf.traits.LoadState;
	import org.osmf.traits.LoadTrait;
	import org.osmf.traits.MediaTraitBase;
	import org.osmf.traits.MediaTraitType;
	
	/**
	 * Implementation of LoadTrait which can be a composite media trait.
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	internal class CompositeLoadTrait extends LoadTrait
	{
		/**
		 * Constructor.
		 * 
		 * @param traitAggregator The object which is aggregating all instances
		 * of the ILoadable trait within this composite trait.
		 * @param mode The composition mode to which this composite trait
		 * should adhere.  See CompositionMode for valid values.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function CompositeLoadTrait(traitAggregator:TraitAggregator, mode:String)
		{
			super(null, null);
			
			this.traitAggregator = traitAggregator;
			this.mode = mode;
			traitAggregationHelper = new TraitAggregationHelper
				( traitType
				, traitAggregator
				, processAggregatedChild
				, processUnaggregatedChild
				);
		}
		
		override public function dispose():void
		{
			traitAggregationHelper.detach();
			traitAggregationHelper = null;
			
			super.dispose();
		}
		
		/**
		 * @private
		 */
		override public function get bytesLoaded():Number
		{
			var compositeBytesLoaded:Number;
			
			if (mode == CompositionMode.SERIAL)
			{
				var emptyUnitSeen:Boolean = false;
				traitAggregator.forEachChildTrait
					(
						function (mediaTrait:MediaTraitBase):void
						{
							if (!emptyUnitSeen)
							{
						  		var loadTrait:LoadTrait = LoadTrait(mediaTrait);
						  
						  		if (!isNaN(loadTrait.bytesLoaded))
						  		{
						  			// The last contributor to bytesLoaded is
						  			// the first non-fully-downloaded child.
						    		emptyUnitSeen = loadTrait.bytesLoaded < loadTrait.bytesTotal;
					  	    		if (isNaN(compositeBytesLoaded))
					  	    		{
					  	  	  			compositeBytesLoaded = 0;
					  	    		}
						    		compositeBytesLoaded += loadTrait.bytesLoaded;
						  		}
							}
					  	},
					  	MediaTraitType.LOAD
					);
			}
			else // PARALLEL
			{						
				traitAggregator.forEachChildTrait
					(
						function (mediaTrait:MediaTraitBase):void
					  	{
					  		var loadTrait:LoadTrait = LoadTrait(mediaTrait);
					  		if (!isNaN(loadTrait.bytesLoaded))
					  		{
					  	  		if (isNaN(compositeBytesLoaded))
					  	  		{
					  	  			compositeBytesLoaded = 0;
					  	  		}
						  		compositeBytesLoaded += loadTrait.bytesLoaded;
							}						
					  	},
					  	MediaTraitType.LOAD
					);
			}
			return compositeBytesLoaded;
		}
		
		/**
		 * @private
		 */
		override public function get resource():MediaResourceBase
		{
			var value:MediaResourceBase = null;
			
			// For serial compositions, expose the resource of the current
			// child.  For parallel compositions, no return value makes
			// sense.
			if (mode == CompositionMode.SERIAL)
			{
				if (traitAggregator.listenedChild != null)
				{
					value = traitAggregator.listenedChild.resource;
				}
			}
			
			return value;
		}

		/**
		 * @private
		 */
		override public function load():void
		{
			if (mode == CompositionMode.PARALLEL)
			{
				// Call load() on all not-yet-loaded children.
				traitAggregator.forEachChildTrait
					(
					  function(mediaTrait:MediaTraitBase):void
					  {
					     var loadTrait:LoadTrait = LoadTrait(mediaTrait);
					     if (loadTrait.loadState != LoadState.LOADING &&
					     	 loadTrait.loadState != LoadState.READY)
					     {
					     	loadTrait.load();
					     }
					  }
					, MediaTraitType.LOAD
					);
			}
			else // SERIAL
			{
				// Call load() on the current child only.
				var currentLoadTrait:LoadTrait = traitOfCurrentChild;
				if (currentLoadTrait != null &&
					currentLoadTrait.loadState != LoadState.LOADING &&
				    currentLoadTrait.loadState != LoadState.READY)
				{
					currentLoadTrait.load();
				}
			}
		}

		/**
         * @private
		 */
		override public function unload():void
		{
			if (mode == CompositionMode.PARALLEL)
			{
				// Call unload() on all not-yet-unloaded children.
				traitAggregator.forEachChildTrait
					(
					  function(mediaTrait:MediaTraitBase):void
					  {
					     var loadTrait:LoadTrait = LoadTrait(mediaTrait);
					     if (loadTrait.loadState == LoadState.LOADING ||
					     	 loadTrait.loadState == LoadState.READY)
					     {
					     	loadTrait.unload();
					     }
					  }
					, MediaTraitType.LOAD
					);
			}
			else // SERIAL
			{
				// Call unload() on the current child only.
				var currentLoadTrait:LoadTrait = traitOfCurrentChild;
				if (currentLoadTrait != null &&
					currentLoadTrait.loadState == LoadState.LOADING ||
					currentLoadTrait.loadState == LoadState.READY)
				{
					currentLoadTrait.unload();
				}
			}
		}
		
		// Internals
		//
		
		private function processAggregatedChild(child:MediaTraitBase):void
		{
			child.addEventListener(LoadEvent.LOAD_STATE_CHANGE, onLoadStateChange, false, 0, true);
			child.addEventListener(LoadEvent.BYTES_TOTAL_CHANGE, onBytesTotalChange, false, 0, true);
			
			if (mode == CompositionMode.PARALLEL)
			{
				if (traitAggregator.getNumTraits(MediaTraitType.LOAD) == 1)
				{
					// The first added child's properties are applied to the
					// composite trait.
					syncToLoadState((child as LoadTrait).loadState);
				}
				else
				{
					// All subsequently added children inherit their properties
					// from the composite trait.
					syncToLoadState(loadState);
				}
			}
			else if (child == traitOfCurrentChild)
			{
				// The first added child's properties are applied to the
				// composite trait.
				syncToLoadState((child as LoadTrait).loadState);
			}
			
			updateBytesTotal();
		}

		private function processUnaggregatedChild(child:MediaTraitBase):void
		{
			child.removeEventListener(LoadEvent.LOAD_STATE_CHANGE, onLoadStateChange);
			child.removeEventListener(LoadEvent.BYTES_TOTAL_CHANGE, onBytesTotalChange);
			
			updateBytesTotal();
		}
		
		private function onLoadStateChange(event:LoadEvent):void
		{
			// For parallel compositions and for the current child in a serial
			// composition, changes from the child propagate to the composite
			// trait.
			if (mode == CompositionMode.PARALLEL ||
				event.target == traitOfCurrentChild)
			{
				syncToLoadState(event.loadState);
			}
		}
		
		private function syncToLoadState(newLoadState:String):void
		{
			// If the state to apply is READY or LOADING, then we load the
			// composition as a whole.  The already-loaded parts will be
			// ignored.
			if (newLoadState == LoadState.LOADING ||
				newLoadState == LoadState.READY)
			{
				load();
			}
			// If the state to apply is UNINITIALIZED or UNLOADING, then we
			// unload the composition as a whole.  The already-unloaded parts
			// will be ignored.
			else if (newLoadState == LoadState.UNINITIALIZED ||
					 newLoadState == LoadState.UNLOADING)
			{
				unload();
			}
			
			updateLoadState();
		}
		
		private function onBytesTotalChange(event:LoadEvent):void
		{
			updateBytesTotal();
		}

		private function updateLoadState():void
		{
			var newLoadState:String;
			
			if (mode == CompositionMode.PARALLEL)
			{
				// Examine all child traits to find out the state that best
				// represents the composite trait.  This state is based on some
				// simple rules about the precedence of states in relation to each
				// other.
				var loadStateInt:int = int.MAX_VALUE;
				traitAggregator.forEachChildTrait
					(
					  function(mediaTrait:MediaTraitBase):void
					  {
					  	 // Find the state with the lowest value.
					     loadStateInt
					     	= Math.min
					     		( loadStateInt
					     		, getIntegerForLoadState(LoadTrait(mediaTrait).loadState)
					     		);
					  }
					, MediaTraitType.LOAD
					);
				
				// Convert the integer back to the composite state.
				newLoadState = 
					   getLoadStateForInteger(loadStateInt)
					|| LoadState.UNINITIALIZED;
			}
			else // SERIAL
			{
				var currentLoadTrait:LoadTrait = traitOfCurrentChild;
				newLoadState = currentLoadTrait
						 	 ? currentLoadTrait.loadState
						 	 : LoadState.UNINITIALIZED;
			}
			
			setLoadState(newLoadState);
		}
		
		private function updateBytesTotal():void
		{
			var compositeBytesTotal:Number;
								
			traitAggregator.forEachChildTrait
				(
					function (mediaTrait:MediaTraitBase):void
				  	{
				  		var loadTrait:LoadTrait = LoadTrait(mediaTrait);
				  		if (!isNaN(loadTrait.bytesTotal))
				  		{
				  	  		if (isNaN(compositeBytesTotal))
				  	  		{
				  	  			compositeBytesTotal = 0;
				  	  		}

					  		compositeBytesTotal += loadTrait.bytesTotal;
						}	
				  	},
				  	MediaTraitType.LOAD
				);
		
			setBytesTotal(compositeBytesTotal);
		}
		
		private function getIntegerForLoadState(loadState:String):int
		{
			if (loadState == LoadState.UNINITIALIZED) 	return UNINITIALIZED_INT;
			if (loadState == LoadState.LOADING)  		return LOADING_INT;
			if (loadState == LoadState.UNLOADING)   	return UNLOADING_INT;
			if (loadState == LoadState.READY)		 	return READY_INT;
			/*  loadState == LoadState.LOAD_ERROR*/ 	return LOAD_ERROR_INT;
		}
		
		private function getLoadStateForInteger(i:int):String
		{
			if (i == UNINITIALIZED_INT) return LoadState.UNINITIALIZED;
			if (i == LOADING_INT)	  return LoadState.LOADING;
			if (i == UNLOADING_INT)	  return LoadState.UNLOADING;
			if (i == READY_INT)	  return LoadState.READY;
			if (i == LOAD_ERROR_INT) return LoadState.LOAD_ERROR;
			/* i out of range */	  return null;
		}
		
		private function get traitOfCurrentChild():LoadTrait
		{
			return   traitAggregator.listenedChild
				   ? traitAggregator.listenedChild.getTrait(MediaTraitType.LOAD) as LoadTrait
				   : null;
		}
		
		// Ordered such that the lowest one takes precedence.  For example,
		// if we have two child traits with states LOAD_ERROR and UNINITIALIZED,
		// then the composite trait has state LOAD_ERROR.
		private static const LOAD_ERROR_INT:int 	= 0;
		private static const UNLOADING_INT:int 		= 1;
		private static const LOADING_INT:int 		= 2;
		private static const UNINITIALIZED_INT:int 	= 3;
		private static const READY_INT:int 			= 4;

		private var traitAggregator:TraitAggregator;
		private var traitAggregationHelper:TraitAggregationHelper;
		private var mode:String;
	}
}