/*****************************************************
*  
*  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.EventDispatcher;
	import flash.utils.Dictionary;
	
	import org.osmf.elements.ProxyElement;
	import org.osmf.events.MediaFactoryEvent;
	import org.osmf.events.PluginManagerEvent;
	import org.osmf.media.pluginClasses.PluginManager;
	import org.osmf.utils.OSMFStrings;
	
	/**
	 * Dispatched when the MediaFactory has successfully loaded a plugin.
	 *
	 * @eventType org.osmf.events.MediaFactoryEvent.PLUGIN_LOAD
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event(name="pluginLoad", type="org.osmf.events.MediaFactoryEvent")]

	/**
	 * Dispatched when the MediaFactory has failed to load a plugin due to an error.
	 *
	 * @eventType org.osmf.events.MediaFactoryEvent.PLUGIN_LOAD_ERROR
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event(name="pluginLoadError", type="org.osmf.events.MediaFactoryEvent")]

	/**
	 * Dispatched when the MediaFactory has created a MediaElement.
	 *
	 * @eventType org.osmf.events.MediaFactoryEvent.MEDIA_ELEMENT_CREATE
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event(name="mediaElementCreate", type="org.osmf.events.MediaFactoryEvent")]

	/**
	 * MediaFactory represents a factory class for media elements.
	 * 
	 * <p>The factory operation takes a MediaResourceBase as input and produces a MediaElement
	 * as output.</p>
	 * <p>The MediaFactory maintains a list of MediaFactoryItem objects,
	 * each of which encapsulates all the information necessary to create 
	 * a specific MediaElement. The MediaFactory relies on
	 * the canHandleResourceFunction method of each MediaFactoryItem to find a
	 * MediaFactoryItem object that can handle the specified MediaResourceBase.</p>
	 *
	 * <p>The factory interface also exposes methods for querying for specific MediaFactoryItem 
	 * objects, and for loading plugins (which hold MediaFactoryItem objects).</p>
	 * 
	 * @see MediaFactoryItem
	 * @see MediaResourceBase
	 * @see MediaElement
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 * 
	 *  @includeExample MediaFactoryExample.as -noswf
	 */	
	public class MediaFactory extends EventDispatcher
	{
		/**
		 * Constructor.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function MediaFactory()
		{
			super();
			
			allItems = new Dictionary();
		}
		
		/**
		 * Adds the specified MediaFactoryItem to the factory.
		 * After the MediaFactoryItem has been added, for any MediaResourceBase
		 * that this MediaFactoryItem can handle, the factory will be able to create
		 * the corresponding media element.
		 * 
		 * If a MediaFactoryItem with the same ID already exists in this
		 * factory, the new MediaFactoryItem object replaces it.
		 * 
		 * @param item The MediaFactoryItem to add.
		 * 
		 * @throws ArgumentError If the argument is <code>null</code> or if the argument
		 * has a <code>null</code> ID field.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function addItem(item:MediaFactoryItem):void
		{
			if (item == null || item.id == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}
			
			var items:Vector.<MediaFactoryItem> = findOrCreateItems(item.type);
			
			// Make sure to overwrite any duplicate.
			var existingIndex:int = getIndexOfItem(item.id, items);
			if (existingIndex != -1)
			{
				items[existingIndex] = item;
			}
			else
			{
				items.push(item);		
			}
		}
		
		/**
		 * Removes the specified MediaFactoryItem from the factory.
		 * 
		 * If no such MediaFactoryItem exists in this factory, does nothing.
		 * 
		 * @param item The MediaFactoryItem to remove.
		 * 
		 * @throws ArgumentError If the argument is <code>null</code> or if the argument
		 * has a <code>null</code> ID field.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function removeItem(item:MediaFactoryItem):void
		{
			if (item == null || item.id == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}
			
			var items:Vector.<MediaFactoryItem> = allItems[item.type];
			if (items != null)
			{
				var existingIndex:int = items.indexOf(item);
				if (existingIndex != -1)
				{
					items.splice(existingIndex, 1);
				}
			}
		}

		/**
		 * The number of MediaFactoryItems managed by the factory.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function get numItems():int
		{
			var numItems:int = 0;
			
			for each (var type:String in MediaFactoryItemType.ALL_TYPES)
			{
				var items:Vector.<MediaFactoryItem> = allItems[type];
				if (items != null)
				{
					numItems += items.length;
				}
			}
			
			return numItems;
		}
		
		/**
		 * Gets the MediaFactoryItem at the specified index.
		 * 
		 * @param index The index in the list from which to retrieve the MediaFactoryItem.
		 * 
		 * @return The MediaFactoryItem at that index or <code>null</code> if there is none.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function getItemAt(index:int):MediaFactoryItem
		{
			var result:MediaFactoryItem = null;
			
			if (index >= 0)
			{
				for each (var type:String in MediaFactoryItemType.ALL_TYPES)
				{
					var items:Vector.<MediaFactoryItem> = allItems[type];
					if (items != null)
					{
						if (index < items.length)
						{
							result = items[index];
							break;
						}
						else
						{
							// Not in this list, try the next.
							index -= items.length;
						}
					}
				}
			}
			
			return result;
		}

		/**
		 * Returns the MediaFactoryItem with the specified ID or <code>null</code> if the
		 * specified MediaFactoryItem does not exist in this factory.
		 * 
		 * @param The ID of the MediaFactoryItem to retrieve.
		 * 
		 * @return The MediaFactoryItem with the specified ID or <code>null</code> if the specified
		 * MediaFactoryItem does not exist in this factory. 
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function getItemById(id:String):MediaFactoryItem
		{
			var result:MediaFactoryItem = null;
			
			for each (var type:String in MediaFactoryItemType.ALL_TYPES)
			{
				var items:Vector.<MediaFactoryItem> = allItems[type];
				if (items != null)
				{
					var existingIndex:int = getIndexOfItem(id, items);
					if (existingIndex != -1)
					{
						result = items[existingIndex];
						break;
					}
				}
			}
			
			return result;
		}
		
		/**
		 * Load a plugin identified by the specified resource. The MediaFactory will not
		 * reload the plugin if it has already been loaded. Upon successful loading, the
		 * MediaFactoryItems within the plugin's PluginInfo property will be added to
		 * this MediaFactory, and a MediaFactoryEvent.PLUGIN_LOAD event will be dispatched.
		 * OtherwiseIf the load fails, a MediaFactoryEvent.PLUGIN_LOAD_ERROR event will be
		 * dispatched.
		 *
		 * @param resource MediaResourceBase representing the plugin.  For remote (dynamic)
		 * plugins, use an URLResource pointing to the remote SWF to load.  For local
		 * (static) plugins, use a PluginInfoResource.  
		 *
		 * @throws ArgumentError If resource is null or resource is neither an URLResource
		 * nor a PluginInfoResource. 
		 *
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function loadPlugin(resource:MediaResourceBase):void
		{
			createPluginManager();
			
			pluginManager.loadPlugin(resource);
		}

		/**
		 * Returns a MediaElement that can be created based on the specified
		 * MediaResourceBase.
		 * <p>Returns <code>null</code> if the factory cannot         
		 * find a MediaFactoryItem object
		 * capable of creating such a MediaElement in this factory.</p>
		 * 
		 * @param resource The MediaResourceBase for which a corresponding
		 * MediaElement should be created.
		 * 
		 * @return The MediaElement that was created or <code>null</code> if no such
		 * MediaElement could be created from the resource.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function createMediaElement(resource:MediaResourceBase):MediaElement
		{
			// Make sure we have a plugin manager before creating a MediaElement, so
			// that it will catch the mediaElementCreate event.
			createPluginManager();
			
			// Note that proxies are resolved before creation callbacks are called:
			// if a media element is proxied, then the creation callback will be invoked
			// with the root proxy, not the wrapped media element.
			// 

			// We attempt to create a MediaElement of the STANDARD type.
			var mediaElement:MediaElement = createMediaElementByResource(resource, MediaFactoryItemType.STANDARD);
			if (mediaElement != null)
			{
				var proxyElement:MediaElement = 
						createMediaElementByResource
							( mediaElement.resource
							, MediaFactoryItemType.PROXY
							, mediaElement			/* element to wrap */
							);
				
				// If we have a corresponding ProxyElement, then instead of
				// returning the STANDARD MediaElement, we instead return that
				// PROXY element as the wrapper for the STANDARD element.
				mediaElement = (proxyElement != null ? proxyElement : mediaElement);
				
				// Inform clients of the creation.
				dispatchEvent
					( new MediaFactoryEvent
						( MediaFactoryEvent.MEDIA_ELEMENT_CREATE
						, false
						, false
						, null
						, mediaElement
						)
					);
			}
			
			return mediaElement;
		}
		
		// Protected
		//
		
		/**
		 * Returns the most appropriate MediaFactoryItem for the specified resource
		 * out of the MediaFactoryItems in the specified list.
		 * 
		 * This method is invoked when <code>createMediaElement</code> is invoked
		 * with a resource that more than one MediaFactoryItem can handle.  Subclasses
		 * can override to select the most appropriate one.
		 * 
		 * The default behavior is to select the first item which is not "native" to
		 * the framework, under the theory that plugins ought to take precedence over
		 * core media types.  It makes this decision based on the presence or absence
	 	 * of an id value which starts with "org.osmf".
		 */
		protected function resolveItems(resource:MediaResourceBase, items:Vector.<MediaFactoryItem>):MediaFactoryItem
		{
			if (resource == null || items == null)
			{
				return null;
			}
			
			var firstNativeItem:MediaFactoryItem = null;
			
			for (var i:int = 0; i < items.length; i++)
			{
				var item:MediaFactoryItem = items[i] as MediaFactoryItem;
				if (item.id.indexOf("org.osmf") == -1)
				{
					// Non-native, we'll take it.
					return item;
				}
				else if (firstNativeItem == null)
				{
					firstNativeItem = item;
				}
			}
			
			return firstNativeItem;
		}
		
		// Internals
		//
		
		private function findOrCreateItems(type:String):Vector.<MediaFactoryItem>
		{
			if (allItems[type] == null)
			{
				allItems[type] = new Vector.<MediaFactoryItem>();
			}
			
			return allItems[type] as Vector.<MediaFactoryItem>;
		}
		
		private function createMediaElementByResource
			( resource:MediaResourceBase
			, itemType:String
			, wrappedElement:MediaElement=null
			):MediaElement
		{
			var mediaElement:MediaElement = null;
			
			var items:Vector.<MediaFactoryItem> = getItemsByResource(resource, allItems[itemType]);
			
			if (itemType == MediaFactoryItemType.STANDARD)
			{
				var item:MediaFactoryItem = resolveItems(resource, items) as MediaFactoryItem;
				if (item != null)
				{
					mediaElement = invokeMediaElementCreationFunction(item);
				}
			}
			else if (itemType == MediaFactoryItemType.PROXY)
			{
				var nextElementToWrap:MediaElement = wrappedElement;
				
				// Create our chain of proxies, starting from the bottom so
				// that we can assign the base wrapped element.  (Note that
				// we iterate from the end to the beginning simply to make
				// it easier to assign the wrappedElement in our for loop.
				// In the future, we may want to provide control for the
				// ordering to the client through some type of resolver method.
				for (var i:int = items.length; i > 0; i--)
				{
					var proxyItem:MediaFactoryItem = items[i-1] as MediaFactoryItem;
					var proxyElement:ProxyElement = invokeMediaElementCreationFunction(proxyItem) as ProxyElement;
					if (proxyElement != null)
					{
						proxyElement.proxiedElement = nextElementToWrap;
					
						nextElementToWrap = proxyElement;
					}
				}
				
				mediaElement = nextElementToWrap;
			}
			
			if (mediaElement != null)
			{
				mediaElement.resource = resource;
			}
			
			return mediaElement;
		}
		
		private static function getItemsByResource(resource:MediaResourceBase, items:Vector.<MediaFactoryItem>):Vector.<MediaFactoryItem>
		{
			var results:Vector.<MediaFactoryItem> = new Vector.<MediaFactoryItem>();
			
			for each (var item:MediaFactoryItem in items)
			{			
				if (item.canHandleResourceFunction(resource))
				{
					results.push(item);
				}
			}
			
			return results;
		}
		
		private static function getIndexOfItem(id:String, items:Vector.<MediaFactoryItem>):int
		{
			for (var i:int = 0; i < items.length; i++)
			{
				var item:MediaFactoryItem = items[i] as MediaFactoryItem;
				if (item.id == id)
				{
					return i;
				}
			}
			
			return -1;
		}
		
		private function onPluginLoad(event:PluginManagerEvent):void
		{
			dispatchEvent(new MediaFactoryEvent(MediaFactoryEvent.PLUGIN_LOAD, false, false, event.resource));
		}

		private function onPluginLoadError(event:PluginManagerEvent):void
		{
			dispatchEvent(new MediaFactoryEvent(MediaFactoryEvent.PLUGIN_LOAD_ERROR, false, false, event.resource));
		}

		private function invokeMediaElementCreationFunction(item:MediaFactoryItem):MediaElement
		{
			var mediaElement:MediaElement = null;
			try
			{
				mediaElement = item.mediaElementCreationFunction();
			}
			catch (error:Error)
			{
				// Swallow, the creation function is wrongly specified.
				// We'll just return a null MediaElement.
			}
			return mediaElement;
		}
		
		private function createPluginManager():void
		{
			if (pluginManager == null)
			{
				pluginManager = new PluginManager(this);
				pluginManager.addEventListener(PluginManagerEvent.PLUGIN_LOAD, onPluginLoad);
				pluginManager.addEventListener(PluginManagerEvent.PLUGIN_LOAD_ERROR, onPluginLoadError);
			}
		}
		
		private var pluginManager:PluginManager;

		private var allItems:Dictionary;
			// Keys are: String (MediaFactoryItemType)
			// Values are: Vector.<MediaFactoryItem>
	}
}