/*****************************************************
*  
*  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.media
{
	import __AS3__.vec.Vector;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	
	import org.osmf.containers.IMediaContainer;
	import org.osmf.events.ContainerChangeEvent;
	import org.osmf.events.MediaElementEvent;
	import org.osmf.events.MediaErrorEvent;
	import org.osmf.events.MetadataEvent;
	import org.osmf.metadata.Metadata;
	import org.osmf.traits.MediaTraitBase;
	import org.osmf.utils.OSMFStrings;

	/**
	 * Dispatched when a trait is added to the media element.
	 *
	 * @eventType org.osmf.events.MediaElementEvent.TRAIT_ADD
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event(name="traitAdd",type="org.osmf.events.MediaElementEvent")]
	
	/**
	 * Dispatched when a trait is removed from the media element.
	 *
	 * @eventType org.osmf.events.MediaElementEvent.TRAIT_REMOVE
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event(name="traitRemove",type="org.osmf.events.MediaElementEvent")]

	/**
	 * Dispatched when a Metadata object has been added to this media element.
	 * 
	 * @eventType org.osmf.events.MediaElementEvent.METADATA_ADD
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */	
	[Event(name="metadataAdd",type="org.osmf.events.MediaElementEvent")]
	
	/**
	 * Dispatched when a Metadata object has been removed from this media element.
	 * 
	 * @eventType org.osmf.events.MediaElementEvent.METADATA_REMOVE
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */	
	[Event(name="metadataRemove",type="org.osmf.events.MediaElementEvent")]
	
	/**
	 * Dispatched when an error which impacts the operation of the media
	 * element occurs.
	 *
	 * @eventType org.osmf.events.MediaErrorEvent.MEDIA_ERROR
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event(name="mediaError",type="org.osmf.events.MediaErrorEvent")]
	
	/**
	 * Dispatched when the element's container property changed.
	 * 
	 * @eventType org.osmf.events.ContainerChangeEvent.CONTAINER_CHANGE
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */	
	[Event(name="containerChange",type="org.osmf.events.ContainerChangeEvent")]
		
	/**
     * A MediaElement represents a unified media experience.
     * It may consist of a simple media item, such as a video or a sound.
     * Different instances (or subclasses) can represent different types of media.
     * A MediaElement may also represent a complex media experience composed of 
     * multiple items, such as videos, ad banners, SWF overlays, background page branding, etc.
	 * 
     * <p>Programmatically, a media element encapsulates a set of media traits and
     * a state space.
	 * The media traits
	 * represent the capabilities of the media element and are dynamic in
	 * nature.  At one moment in time a media element might be
	 * seekable, at another moment it might not be. For example, this could occur 
     * if the media element is a video sequence containing unskippable ads.</p>
	 * <p>A media element operates on a media resource.  For example, if the
	 * media element represents a video player, the media resource might
	 * encapsulate a URL to a video stream.
     * If the media element represents a complex media composition, 
     * the media resource URL might be a document that references
     * the multiple resources used in the media composition.</p>
     * @see MediaTraitBase
     * @see MediaResourceBase
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	public class MediaElement extends EventDispatcher
	{
		// Public interface
		//
		
		/**
		 * Constructor.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function MediaElement()
		{
			super();
			
			_metadata = createMetadata();
			_metadata.addEventListener(MetadataEvent.VALUE_ADD, onMetadataValueAdd);
			_metadata.addEventListener(MetadataEvent.VALUE_REMOVE, onMetadataValueRemove);
			_metadata.addEventListener(MetadataEvent.VALUE_CHANGE, onMetadataValueChange);
			
			setupTraitResolvers();		
			setupTraits();
			
			addEventListener
				( ContainerChangeEvent.CONTAINER_CHANGE
				, onContainerChange
				, false
				, Number.MAX_VALUE
				);
		}
	
		/**
		 * A Vector of MediaTraitType values representing the trait types on this
		 * media element.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function get traitTypes():Vector.<String>
		{
			// Return a copy of our types vector.
			return _traitTypes.concat();
		}
		
		/**
		 * Determines whether this media element has a media trait of the
		 * specified type.
		 * 
         * @param type The MediaTraitType for the media trait to check for.
		 * 
		 * @throws ArgumentError If the parameter is <code>null</code>.
		 * @return <code>true</code> if this media element has a media
		 * trait of the specified class, <code>false</code> otherwise.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function hasTrait(type:String):Boolean
		{
			if (type == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}
			
			return traits[type] != null;
		}
		
		/**
		 * Returns the media trait of the specified type.
		 * 
         * @param type The MediaTraitType for the media trait to return.
		 * 
         * @throws ArgumentError If the parameter is <code>null</code>.
         * @return The retrieved trait or <code>null</code> if no such trait exists on this
		 * media element.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function getTrait(type:String):MediaTraitBase
		{
			if (type == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}

			return traits[type];
		}
		
		/**
		 * The media resource that this media element operates on.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function get resource():MediaResourceBase
		{
			return _resource;
		}
		
		public function set resource(value:MediaResourceBase):void
		{
			_resource = value;		
		}
		
		/**
		 * The media container that this element uses.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function get container():IMediaContainer
		{
			return _container;
		}
		
		/** 
		 * Adds a Metadata object to this MediaElement under the specified namespace URL.
		 * 
		 * @param namespaceURL The namespace URL used to store the Metadata.
		 * @param metadata The Metadata to add.
		 * 
		 * @throws ArgumentError if namespaceURL or metadata is null.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */ 
		public function addMetadata(namespaceURL:String, metadata:Metadata):void
		{
			if (namespaceURL == null || metadata == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}
			
			this.metadata.addValue(namespaceURL, metadata);
		}
		
		/**
		 * Removes the Metadata object that was stored under this MediaElement with
		 * the specified namespace URL.
		 * 
		 * @param namespaceURL The namespace URL of the Metadata to remove.
		 * 
		 * @returns The removed Metadata, null if no Metadata with the given
		 * namespaceURL exists one this MediaElement.
		 * 
		 * @throws ArgumentError If namespaceURL is null.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */ 
		public function removeMetadata(namespaceURL:String):Metadata
		{
			if (namespaceURL == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}

			return metadata.removeValue(namespaceURL) as Metadata;
		}
		
		/** 
		 * Returns the Metadata object that is stored under this MediaElement with
		 * the specified namespace URL.
		 * 
		 * @param namespaceURL The namespace URL of the Metadata object to retrieve.
		 * 
		 * @returns The Metadata object with the specified namespace URL, null if
		 * no such Metadata object exists on this MediaElement.
		 * 
		 * @throws ArgumentError If namespaceURL is null.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */ 
		public function getMetadata(namespaceURL:String):Metadata
		{
			if (namespaceURL == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}

			return metadata.getValue(namespaceURL) as Metadata;
		}
		
		/**
		 * A Vector containing the namespace URLs for all Metadata
		 * objects stored within this MediaElement.
		 **/
		public function get metadataNamespaceURLs():Vector.<String>
		{
			return metadata.keys;
		}
			
		// Protected
		//
		
		/**
		 * @private
		 * 
		 * Creates the metadata collection class.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */ 
		protected function createMetadata():Metadata
		{
			return new Metadata();
		}
		
		/**
		 * @private
		 **/
		public function get metadata():Metadata
		{
			return _metadata;
		}
		
		/**
		 * Adds a new media trait to this media element.  If successful,
		 * dispatches a MediaElementEvent.
		 * 
         * @param type The MediaTraitType for the media trait to add.
		 * @param trait The media trait to add.
		 * 
         * @throws ArgumentError If either parameter is <code>null</code>, or
		 * if the specified type and the type of the media trait don't match,
		 * or if a different instance of the specific trait class has already
		 * been added.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		protected function addTrait(type:String, instance:MediaTraitBase):void
		{
			if (type == null || instance == null || type != instance.traitType)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}
			
			// If there's a trait resolver for this type, then add the trait
			// to the resolver:
			var traitResolver:MediaTraitResolver = traitResolvers[type];
			if (traitResolver != null)
			{
				traitResolver.addTrait(instance);		
			}
			else
			{
				setLocalTrait(type, instance);
			}
		}
		
		/**
		 * Removes a media trait from this media element.  If successful,
		 * dispatches a MediaElementEvent.
		 * 
         * @param type The MediaTraitType for the media trait to remove.
		 * 
         * @throws ArgumentError If the parameter is <code>null</code>.
         * @return The removed trait or <code>null</code> if no trait was
         * removed.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		protected function removeTrait(type:String):MediaTraitBase
		{
			if (type == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}

			var trait:MediaTraitBase = traits[type];
			var traitResolver:MediaTraitResolver = traitResolvers[type];
			if (traitResolver != null)
			{
				return traitResolver.removeTrait(trait);		
			}
			else
			{
				return setLocalTrait(type, null);
			}
		}
		
		/**
		 * @private
		 * 
		 * Adds a trait resolver object for a given trait type.
		 * 
		 * Only one resolver can be set per trait type.
		 * 
		 * @param type The MediaTraitType for the media trait to add a resolver for.
		 * @param instance The resolver object to add.
		 * 
		 * @throws ArgumentError If type is null, or if instance is null.
		 * @throws ArgumentError If a resolver has already been added for type.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		final protected function addTraitResolver(type:String, instance:MediaTraitResolver):void
		{
			if (instance == null || instance.type != type)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}
			
			if (traitResolvers[type] == null)
			{
				// Store the currently set local trait:
				unresolvedTraits[type] = traits[type];
				
				// Add the resolver to the trait resolvers dictionary:
				traitResolvers[type] = instance;
				
				// If there is a trait instance set for the added resolver's type,
				// then add this trait to the resolver:
				var currentlySetTrait:MediaTraitBase = traits[type];
				if (currentlySetTrait)
				{
					instance.addTrait(currentlySetTrait);
				}
				
				// Run the resolved trait change routine: this will make sure that
				// the local trait gets updated to reflect the resolved trait:
				processResolvedTraitChange(type, instance.resolvedTrait);
				
				// Listen for the resolved trait changing:
				instance.addEventListener(Event.CHANGE, onTraitResolverChange);
			}
			else
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.TRAIT_RESOLVER_ALREADY_ADDED));
			}
		}
		
		/**
		 * @private
		 * 
		 * Removes a trait resolver object for a given trait type.
		 * 
		 * @param type The MediaTraitType for the resolver to remove.
		 * 
		 * @throws ArgumentError If type is null, or if no resolver has been added for type.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		final protected function removeTraitResolver(type:String):MediaTraitResolver
		{
			if (type == null || traitResolvers[type] == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}
			
			// Stop listening for the resolver's resolved trait value changing:
			var instance:MediaTraitResolver = traitResolvers[type];
			instance.removeEventListener(Event.CHANGE, onTraitResolverChange);
			
			// Remove the resolver from the trait resolvers dictionary:
			delete traitResolvers[type];
			
			// Reset the unresolved trait to be the current local trait:
			var unresolvedTrait:MediaTraitBase = unresolvedTraits[type];
			if (unresolvedTrait != traits[type])
			{
				setLocalTrait(type, unresolvedTrait);
			}
			delete unresolvedTraits[type];
			
			return instance;
		}
		
		/**
		 * @private
		 * 
		 * Defines the trait resolver that's set for the specified trait type.
		 * 
		 * @param type The MediaTraitType for the resolver object which should be returned.
		 * @return The requested resolver object, or null if none is set for the specified type.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		final protected function getTraitResolver(type:String):MediaTraitResolver
		{
			return traitResolvers[type];
		}
		
		// Subclass stubs
		//
		
		/**
		 * @private
		 * 
		 * Sets up the trait resolvers for the media elements. Occurs during
		 * construction. Subclasses should override this method and call
		 * addTraitResolver for each trait they wish to add a resolver for.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		protected function setupTraitResolvers():void
		{	
		}
		
		/**
		 * Sets up the traits for this media element.  Occurs during
		 * construction.  Subclasses should override this method and call
		 * addTrait for each trait of their own.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		protected function setupTraits():void
		{
		}		

		// Internals
		//
		
		private function onMediaError(event:MediaErrorEvent):void
		{
			dispatchEvent(event.clone());
		}
		
		private function setLocalTrait(type:String, instance:MediaTraitBase):MediaTraitBase
		{
			var result:MediaTraitBase = traits[type];
			if (instance == null)
			{
				// We're processing a trait removal:
				if (result != null)
				{
					// Stop listening for errors:
					result.removeEventListener(MediaErrorEvent.MEDIA_ERROR, onMediaError);
					
					// Dispose of any resources:
					result.dispose();
	
					// Signal removal is about to occur:
					dispatchEvent(new MediaElementEvent(MediaElementEvent.TRAIT_REMOVE, false, false, type));
					
					_traitTypes.splice(_traitTypes.indexOf(type),1);
					delete traits[type];
				}
			}
			else
			{
				// We're processing a trait addition:
				if (result == null)
				{
					traits[type] = result = instance;
					_traitTypes.push(type);
					
					// Listen for errors:
					result.addEventListener(MediaErrorEvent.MEDIA_ERROR, onMediaError);
					
					// Signal addition:
					dispatchEvent(new MediaElementEvent(MediaElementEvent.TRAIT_ADD, false, false, type));
				}
				else if (result != instance)
				{
					throw new ArgumentError(OSMFStrings.getString(OSMFStrings.TRAIT_INSTANCE_ALREADY_ADDED));
				}
			}
			
			return result;
		}
		
		private function onTraitResolverChange(event:Event):void
		{
			var resolver:MediaTraitResolver = event.target as MediaTraitResolver;
			processResolvedTraitChange(resolver.type, resolver.resolvedTrait);
		}
		
		private function processResolvedTraitChange(type:String, resolvedTrait:MediaTraitBase):void
		{
			if (resolvedTrait != traits[type])
			{
				setLocalTrait(type, resolvedTrait);
			} 
		}

		private function onContainerChange(event:ContainerChangeEvent):void
		{
			if	(	_container == event.oldContainer
				&&	_container != event.newContainer
				)
			{
				_container = event.newContainer;
			}
		}
		
		private function onMetadataValueAdd(event:MetadataEvent):void
		{
			dispatchEvent
				( new MediaElementEvent
					( MediaElementEvent.METADATA_ADD
					, false
					, false
					, null
					, event.key
					, event.value as Metadata
					)
				);
		}
		
		private function onMetadataValueChange(event:MetadataEvent):void
		{
			dispatchEvent
				( new MediaElementEvent
					( MediaElementEvent.METADATA_REMOVE
					, false
					, false
					, null
					, event.key
					, event.oldValue as Metadata
					)
				);

			dispatchEvent
				( new MediaElementEvent
					( MediaElementEvent.METADATA_ADD
					, false
					, false
					, null
					, event.key
					, event.value as Metadata
					)
				);
		}

		private function onMetadataValueRemove(event:MetadataEvent):void
		{
			dispatchEvent
				( new MediaElementEvent
					( MediaElementEvent.METADATA_REMOVE
					, false
					, false
					, null
					, event.key
					, event.value as Metadata
					)
				);
		}
		
		private var traits:Dictionary = new Dictionary();
		private var traitResolvers:Dictionary = new Dictionary();
		private var unresolvedTraits:Dictionary = new Dictionary();

		private var _traitTypes:Vector.<String> = new Vector.<String>();
		private var _resource:MediaResourceBase;
		private var _metadata:Metadata;
		
		private var _container:IMediaContainer;
	}
}