/*****************************************************
*  
*  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.containers
{
	import flash.errors.IllegalOperationError;
	import flash.external.ExternalInterface;
	import flash.utils.Dictionary;
	
	import org.osmf.elements.HTMLElement;
	import org.osmf.elements.ProxyElement;
	import org.osmf.events.ContainerChangeEvent;
	import org.osmf.media.MediaElement;
	import org.osmf.traits.*;
	import org.osmf.utils.OSMFStrings;

	/**
	 * HTMLMediaContainer is an IMediaContainer-implementing class that uses ExternalInterface
	 * to expose the container's child media elements to JavaScript.
	 * 
	 * @includeExample HTMLMediaContainerExample.as -noswf
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */	
	public class HTMLMediaContainer implements IMediaContainer
	{
		// IMediaContainer
		//

		/**
		 * @private
		 */
		public function addMediaElement(child:MediaElement):MediaElement
		{
			requireExternalInterface;
			
			if (child == null)
			{
				throw new IllegalOperationError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}
			
			// Find out if the element at hand is an HTML element or not:
			var htmlElement:HTMLElement = elementAsHTMLElement(child);
			
			if (htmlElement == null)
			{
				throw new IllegalOperationError(OSMFStrings.getString(OSMFStrings.UNSUPPORTED_MEDIA_ELEMENT_TYPE))
			}
			
			var result:MediaElement;
			
			var elementId:String = "element_" + elementIdCounter++;
			var elementScriptPath:String = containerScriptPath + "elements." + elementId + "."; 
			
			elements[elementId] = htmlElement;
			
			htmlElement.scriptPath = elementScriptPath; 
			
			ExternalInterface.call(containerScriptPath + "__addElement__", elementId);
			
			// Media containers are under obligation to dispatch a gateway change event when
			// they add a media element:
			child.dispatchEvent
				( new ContainerChangeEvent
					( ContainerChangeEvent.CONTAINER_CHANGE
					, false, false
					, child.container, this
					)
				);
			
			return child;
		}
		
		/**
		 * @private
		 */
		public function removeMediaElement(child:MediaElement):MediaElement
		{
			requireExternalInterface;
			
			if (child == null)
			{
				throw new IllegalOperationError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}
			
			var elementId:String = getElementId(child);
			if (elementId == null)
			{
				throw new IllegalOperationError("Element is not a child element");
			}
			
			delete elements[elementId];
			
			ExternalInterface.call
				( containerScriptPath + "__removeElement__"
				, elementId
				);
				
			// Media containers are under obigation to dispatch a gateway change event when
			// they remove a media element:
			child.dispatchEvent
				( new ContainerChangeEvent
					( ContainerChangeEvent.CONTAINER_CHANGE
					, false, false
					, child.container, null
					)
				);
			
			return child;
		}
		
		/**
		 * @private
		 */
		public function containsMediaElement(child:MediaElement):Boolean
		{
			for each (var element:HTMLElement in elements)
			{
				if (element == child)
				{
					return true;
				}
			}
			return false;
		}
		
		// Public API
		//
		
		/**
		 * Constructor.
		 * 
		 * @param containerIdentifier The identifier that will be used for this container
		 * in JavaScript. If no identifier is specified, a random one will be generated.
		 * 
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function HTMLMediaContainer(containerIdentifier:String = null)
		{
			_containerIdentifier
				= 	containerIdentifier
				||	( "OSMF_HTMLMediaContainer_"
					+ instanceCounter
					+ "_"
					+ Math.round(0xffffffff * Math.random())
					);
					
			initialize();
			
			instanceCounter++;
		}
		
		// Internals
		//
		
		private function initialize():void
		{
			requireExternalInterface;
			
			containerScriptPath 
				= "document.osmf.mediaContainers."
				+ ExternalInterface.objectID
				+ "_"
				+ _containerIdentifier + ".";
			
			ExternalInterface.marshallExceptions = true;
			ExternalInterface.addCallback("osmf_getProperty", getPropertyCallback);
			ExternalInterface.addCallback("osmf_setProperty", setPropertyCallback);
			ExternalInterface.addCallback("osmf_invoke", invokeCallback);
			
			ExternalInterface.call
				( registerContainer_js
				, ExternalInterface.objectID
				, _containerIdentifier
				);
		}
		
		private var elements:Dictionary = new Dictionary();
		private var elementIdCounter:int = 0;
		
		private var _containerIdentifier:String;
		private var containerScriptPath:String;
		
		private function get requireExternalInterface():*
		{
			if (ExternalInterface.available == false)
			{
				throw new IllegalOperationError("No ExternalInterface available");
			}
			
			return undefined;
		}
		
		private function getElementId(element:MediaElement):String
		{
			var result:String;
			
			for (var index:String in elements)
			{
				if (elements[index] == element)
				{
					result = index;
					break;
				} 
			}
			
			return result;
		}
		
		private function getPropertyCallback(elementId:String, property:String):*
		{
			var result:*;
			
			var element:HTMLElement = elements[elementId];
			if (element)
			{
				result = element.getPropertyCallback(property);
			}
			
			return result;
		}
		
		private function setPropertyCallback(elementId:String, property:String, value:*):void
		{
			var element:HTMLElement = elements[elementId] as HTMLElement;
			if (element)
			{
				element.setPropertyCallback(property, value);
			}
		}
		
		private function invokeCallback(elementId:String, method:String, args:Array):*
		{
			var result:*;
			
			if (elementId == null)
			{
				switch (method)
				{
					// Container
					case "trace":
						if (args.length)
						{
							trace("JavaScript says:", args[0]);
						}
						break;
					default:
						throw new IllegalOperationError
							( "Method '"
							+ method
							+ "' invoked from JavaScript is not supported on a Container."
							);
						break;
				}
			}
			else
			{
				var element:MediaElement = elements[elementId];
				if (element)
				{
					switch (method)
					{
						// Currently no methods...
						default:
							throw new IllegalOperationError
								( "Method '"
								+ method 
								+ "' invoked from JavaScript is not supported on a MediaElement."
								);
							break;
					}
				}
				else
				{
					throw new IllegalOperationError
						( "Unable to resolve the element with identifier '"
						+ elementId
						+ "' on invoking the '"
						+ method
						+ "' method from JavaScript."
						);
				}
			}
			
			return result;
		}
		
		// Utils
		//
		
		private static function elementAsHTMLElement(element:MediaElement):HTMLElement
		{
			var result:HTMLElement;
			
			if (element != null)
			{
				if (element is ProxyElement)
				{
					return arguments.callee(ProxyElement(element).proxiedElement)
				}
				else
				{
					result = element as HTMLElement;	
				}
			}
			
			return result;
		}
				
        // JavaScript API
        //
        
        private static const utils_js:XML =
        	<![CDATA[
        	function addGetter(element, property)
        	{
        		element["get" + property]
        			= function()
        				{ 
        					return element
	        					.__container__
	        					.__flashObject__
	        					.osmf_getProperty(element.elementId, property);
        				}
        	}
        	
        	function addSetter(element, property)
        	{
        		element["set" + property]
        			= function(value)
        				{ 
        					return element
	        					.__container__
	        					.__flashObject__
	        					.osmf_setProperty(element.elementId, property, value);
        				}
        	}
        	
        	function addGetSet(element, property)
        	{
        		addGetter(element, property);
        		addSetter(element, property);
        	}
        	
        	// Adds an in between function that the Flash side can invoke
        	// on signaling an event:
        	function addCallback(element, method, numArguments)
        	{
        		element["__"+method+"__"] = function()
        		{
        			var result;
        			var callback = element[method];
        			if (callback && callback.length == numArguments)
        			{
        				result = callback.apply(element, arguments) || true;
        			}
        			return result;
        		}
        	}
        	
        	// Adds a method that the JavaScript side can invoke on the 
        	// Flash side:
        	function addMethod(element, method)
        	{
        		element[method] = function()
        		{
        			element.mediaContainer.__flashObject__.osmf_invoke
    					( element.elementId
    					, method
    					, arguments.length ? arguments : []
    					);
        		}
        	}
        	
        	]]>;
        	
        private static const constants_js:XML =
        	<![CDATA[
        	function Constants()
        	{
        		this.loadState =
        			{ UNINITIALIZED: "uninitialized"
        			, LOADING: "loading"
        			, READY: "ready"
        			, UNLOADING: "unloading"
        			, LOAD_ERROR: "loadError"
        			};
        			
        		this.playState =
        			{ PLAYING: "playing"
        			, PAUSED: "paused"
        			, STOPPED: "stopped"
        			};
        	}
        	]]>;
        
        // Defines the JS Container class:
		private static const container_js:XML =
			<![CDATA[
			function Container(objectId, containerId)
        	{
        		this.containerId = containerId;
        		
        		this.__flashObject__ = document.getElementById(objectId);
        		
        		this.__addElement__ = function(elementId)
        		{
        			this.elements = this.elements || new Object();
        			
        			var element = new MediaElement(this, elementId);
        			this.elements[elementId] = element;
        			
        			if	(	this["onElementAdd"] != null
        				&&	this.onElementAdd.length == 1
        				)
        			{
        				this.onElementAdd(element);
        			}
        		}
        		
        		this.__removeElement__ = function(elementId)
        		{
        			var element = this.elements[elementId];
        			if (element == null)
        			{
        				throw "Container doesn not contain the specified element ("
        					+ elementId
        					+ ")";
        			}
        			
        			delete this.elements[elementId];
        			
        			if	(	this["onElementRemove"] != null
        				&&	this.onElementRemove.length == 1
        				)
        			{
        				this.onElementRemove(element);
        			}
        		}
        	}
        	]]>;
        
        // Defines the JS MediaElement class:
        private static const mediaElement_js:XML =
        	<![CDATA[
        	function MediaElement(container, elementId)
        	{
        		this.elementId = elementId;
        		this.__container__ = container;
        		
        		// MediaElement core properties:
        		
        		addGetter(this, "resource");
        		
        		// LoadTrait bridge: (all HTML elements are loadable)
        		
        		addSetter	(this, 	"LoadState");
        		addCallback	(this, 	"load", 1);					// urlResource
        		addCallback	(this, 	"unload", 0); 
        		
				// PlayTrait bridge:
				addGetSet	(this,	"Playable");
				addGetSet	(this, 	"CanPause");
				addGetSet	(this,	"PlayState");
				addCallback (this,	"onPlayStateChange", 1);	// newPlayState string
				
        		// TimeTrait bridge:
        		addGetSet	(this,	"Temporal");
        		addGetSet	(this,	"Duration");
        		addGetSet	(this,	"CurrentTime");
        		
        		// AudioTrait bridge:
        		
        		addGetSet	(this,	"Audible");			
        		addGetSet	(this,	"Volume");
        		addCallback	(this,	"onVolumeChange", 1);		// volume;
        		addGetSet	(this,	"Muted");
        		addCallback	(this,	"onMutedChange", 1);		// muted;
        		addGetSet	(this,	"Pan");
        		addCallback	(this,	"onPanChange", 1);			// pan;
        	}
        	]]>;
        	
        // Defines the logic that sets up the document.osmf object, adding a container:
        private static const registrationLogic_js:XML =
        	<![CDATA[
        	// Get a reference to, or otherwise construct, the document.osmf.mediaContainers path:
            var osmf 
        		= document.osmf
        		= document.osmf || {};
        		
        	osmf.constants
        		= osmf.constants || new Constants();
        		
        	var containers
        		= osmf.mediaContainers
        		= osmf.mediaContainers || {};
        
        	// For debugging, provide a 'trace' function on: it will
        	// forward the message to Flash:
        	
        	if (osmf.trace == null)
        	{
        		osmf.trace = function(message)
        		{
        			document
        				.getElementById(objectId)
        				.osmf_invoke(null, "trace", [message]);
        		}
        	}
	        	
        	// See if the container with the specified name has been registered:
        	
        	var identifier = objectId + "_" + containerId;
        	
        	if (containers[identifier] != null)
        	{
        		throw "A container by the name of "+identifier+" has already been registered."
        	}
        	else
        	{
        		var container
        			= containers[identifier]
        			= new Container(objectId, containerId);
        	}
        	
        	// Invoke "onHTMLMediaContainerConstructed"
        	if 	(	this["onHTMLMediaContainerConstructed"] != null
        		&&	this.onHTMLMediaContainerConstructed.length == 1
        		)
        	{
        		this.onHTMLMediaContainerConstructed(container);
        	}
	        
        	]]>;
		
		private static const registerContainer_js:XML = new XML
			( "<![CDATA["
			+ "function(objectId, containerId)"
			+ "{"
			+ utils_js.toString()
			+ constants_js.toString()
            + container_js.toString()
            + mediaElement_js.toString()
            + registrationLogic_js.toString()
            + "}"
            + "]]>"
            );
     	
     	private static var instanceCounter:int;
	}
}