/*****************************************************
*  
*  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. 
* 
*  Contributor(s): Akamai Technologies
*  
*****************************************************/

package org.osmf.net
{
	CONFIG::LOGGING 
	{	
		import org.osmf.logging.Logger;
	}
	
	import __AS3__.vec.Vector;
	
	import flash.net.NetConnection;
	import flash.utils.Dictionary;
	
	import org.osmf.events.NetConnectionFactoryEvent;
	import org.osmf.media.URLResource;
	import org.osmf.utils.URL;
	
	/**
	 * The NetConnectionFactory class is used to generate connected NetConnection
	 * instances and to manage sharing of these instances.  The NetConnectionFactory
	 * can also handle port/protocol negotiation.
	 * 
	 * <p>NetConnectionFactory is stateless. Multiple parallel create() requests
	 * may be made. A hash of the resource URL is used as a key to determine
	 * which NetConnections may be shared.</p>
	 * 
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	public class NetConnectionFactory extends NetConnectionFactoryBase
	{
		/**
		 * Constructor.
		 * 
		 * @param shareNetConnections Boolean specifying whether created NetConnections
		 * may be shared or not.  The default is true.
		 * 
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function NetConnectionFactory(shareNetConnections:Boolean=true)
		{
			super();
			
			this.shareNetConnections = shareNetConnections;
		}
		
		/**
		 * @private
		 * 
		 * Begins the process of creating a new NetConnection.  The method creates two dictionaries to help it 
		 * manage previously shared connections as well as pending connections. Only if a NetConnection is not shareable
		 * and not pending is a new connection sequence initiated via a new NetNegotiator instance.
		 * <p/>
		 * 
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		override public function create(resource:URLResource):void
		{
			var key:String = createNetConnectionKey(resource);
			
			// The first time this method is called, we create our dictionaries.
			if (connectionDictionary == null)
			{
				connectionDictionary = new Dictionary();
				keyDictionary = new Dictionary();
				pendingDictionary = new Dictionary();
			}
			var sharedConnection:SharedConnection = connectionDictionary[key] as SharedConnection;
			var connectionsUnderway:Vector.<URLResource> = pendingDictionary[key] as Vector.<URLResource>;
			
			// Check to see if we already have this connection ready to be shared.
			if (sharedConnection != null && shareNetConnections)
			{
				CONFIG::LOGGING
				{
					logger.info("Reusing shared NetConnection: " + sharedConnection.netConnection.uri);
				}

				sharedConnection.count++;
				dispatchEvent
					( new NetConnectionFactoryEvent
						( NetConnectionFactoryEvent.CREATION_COMPLETE
						, false
						, false
						, sharedConnection.netConnection
						, resource
						)
					);
			} 
			// Check to see if there is already a connection attempt pending on this resource.
			else if (connectionsUnderway != null)
			{
				// Add this resource to the vector of resources to be notified once the
				// connection has either succeeded or failed.
				connectionsUnderway.push(resource);
			}
			// If no connection is shareable or pending, then initiate a new connection attempt.
			else
			{
				// Add this connection to the list of pending connections
				var pendingConnections:Vector.<URLResource> = new Vector.<URLResource>();
				pendingConnections.push(resource);
				pendingDictionary[key] = pendingConnections;
				
				// Set up our URLs and NetConnections
				var urlIncludesFMSApplicationInstance:Boolean = resource is StreamingURLResource ? StreamingURLResource(resource).urlIncludesFMSApplicationInstance : false 
				var netConnectionURLs:Vector.<String> = createNetConnectionURLs(resource.url, urlIncludesFMSApplicationInstance);
				var netConnections:Vector.<NetConnection> = new Vector.<NetConnection>();
				for (var j:int = 0; j < netConnectionURLs.length; j++)
				{
					netConnections.push(createNetConnection());
				} 
				
				// Perform the connection attempt
				var negotiator:NetNegotiator = new NetNegotiator();
				negotiator.addEventListener(NetConnectionFactoryEvent.CREATION_COMPLETE, onConnected);
				negotiator.addEventListener(NetConnectionFactoryEvent.CREATION_ERROR, onConnectionFailed);
				negotiator.createNetConnection(resource, netConnectionURLs, netConnections);
				
				function onConnected(event:NetConnectionFactoryEvent):void
				{
					CONFIG::LOGGING 
					{	
						logger.info("NetConnection established with: " + event.netConnection.uri);
					}

					negotiator.removeEventListener(NetConnectionFactoryEvent.CREATION_COMPLETE, onConnected);
					negotiator.removeEventListener(NetConnectionFactoryEvent.CREATION_ERROR, onConnectionFailed);
					
					var pendingEvents:Vector.<NetConnectionFactoryEvent> = new Vector.<NetConnectionFactoryEvent>();
					
					// Dispatch an event for each pending LoadTrait.
					var pendingConnections:Vector.<URLResource> = pendingDictionary[key];
					for (var i:Number = 0; i < pendingConnections.length; i++)
					{
						var pendingResource:URLResource = pendingConnections[i] as URLResource;
						if (shareNetConnections)
						{
							var alreadyShared:SharedConnection = connectionDictionary[key] as SharedConnection;
							if (alreadyShared != null)
							{
								alreadyShared.count++;
							}
							else
							{
								var obj:SharedConnection = new SharedConnection();
								obj.count = 1;
								obj.netConnection = event.netConnection;
								connectionDictionary[key] = obj;
								keyDictionary[obj.netConnection] = key;
							}
						} 
						
						// We don't dispatch immediately, but add it to a queue.  It's important
						// that we delete the key first, since this event could trigger a subsequent
						// request.
						pendingEvents.push
							( new NetConnectionFactoryEvent
								( NetConnectionFactoryEvent.CREATION_COMPLETE
								, false
								, false
								, event.netConnection
								, pendingResource
								)
							);
					}
					
					delete pendingDictionary[key];
					
					// Now we're safe, dispatch the events.
					for each (var pendingEvent:NetConnectionFactoryEvent in pendingEvents)
					{
						dispatchEvent(pendingEvent);
					}
				}
				
				function onConnectionFailed(event:NetConnectionFactoryEvent):void
				{
					CONFIG::LOGGING 
					{
						var fmsURL:FMSURL = new FMSURL(resource.url);
						logger.info("NetConnection failed for: " + fmsURL.protocol + "://" + fmsURL.host + (fmsURL.port.length > 0 ? ":" + fmsURL.port : "" ) + "/" + fmsURL.appName + (fmsURL.useInstance ? "/" + fmsURL.instanceName:""));
					}

					negotiator.removeEventListener(NetConnectionFactoryEvent.CREATION_COMPLETE, onConnected);
					negotiator.removeEventListener(NetConnectionFactoryEvent.CREATION_ERROR, onConnectionFailed);
		
					// Dispatch an event for each pending resource.
					var pendingConnections:Vector.<URLResource> = pendingDictionary[key];
					for each (var pendingResource:URLResource in pendingConnections)
					{
						dispatchEvent
							( new NetConnectionFactoryEvent
								( NetConnectionFactoryEvent.CREATION_ERROR
								, false
								, false
								, null
								, pendingResource
								, event.mediaError
								)
							);
					}
					delete pendingDictionary[key];
				}
			}
		}
		
		/**
		 * @private
		 * 
		 * Manages the closing of a shared NetConnection. NetConnections
		 * are only physically closed after the last sharer has requested a close().
		 * 
		 * @param netConnection The NetConnection to close.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		override public function closeNetConnection(netConnection:NetConnection):void
		{
			if (shareNetConnections)
			{
				var key:String = keyDictionary[netConnection] as String;
				if (key != null)
				{
					var obj:SharedConnection = connectionDictionary[key] as SharedConnection;
					obj.count--;
					if (obj.count == 0)
					{
						netConnection.close();
						
						delete connectionDictionary[key];
						delete keyDictionary[netConnection];
					}
				}
			}
			else
			{
				super.closeNetConnection(netConnection);
			}
		}
		
		// Protected
		//
		
		/**
		 * Generates a key to uniquely identify each connection.  This key is used
		 * to determine whether a particularly URLResource can share an existing
		 * NetConnection.  If the keys for two URLResources are identical, then
		 * they can share the same NetConnection.
		 * 
		 * By default, this method returns a String consisting of the protocol,
		 * host, port, and FMS application name. 
		 * 
		 * @param resource a URLResource
		 * @return a String hash that uniquely identifies the NetConnection
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		protected function createNetConnectionKey(resource:URLResource):String
		{
			var fmsURL:FMSURL = new FMSURL(resource.url);
			return fmsURL.protocol + fmsURL.host + fmsURL.port + fmsURL.appName;
		}
		
		/**
		 *  The factory function for creating a NetConnection.
		 *
		 *  @return An unconnected NetConnection.
	     * 	@see flash.net.NetConnection
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		protected function createNetConnection():NetConnection
		{
			return new NetConnection();
		}

		/**
		 * Assembles a vector of URLs that should be used during the connection
		 * attempt.  The default protocols attempted when a "rtmp" connection is
		 * specified are "rtmp", "rtmps", and "rtmpt". When a "rtmpe" connection
		 * is requested, both "rtmpe" and "rtmpte" protocols are attempted. When
		 * "rtmps","rtmpt" or "rtmpte" are requested, only those protocols are
		 * attempted.  The default ports are 1935, 443 and 80. If a specific port
		 * is specified in the URL, then only that port is used.
		 * 
		 * Subclasses can override this method to change this default behavior.
		 * 
		 * @param url The URL to be loaded.
		 * @param urlIncludesFMSApplicationInstance Indicates whether the URL includes
		 * the FMS application instance name.  See StreamingURLResource for more info.
		 **/
		protected function createNetConnectionURLs(url:String, urlIncludesFMSApplicationInstance:Boolean=false):Vector.<String>
		{
			var urls:Vector.<String> = new Vector.<String>();
			
			var portProtocols:Vector.<PortProtocol> = buildPortProtocolSequence(url);
			for each (var portProtocol:PortProtocol in portProtocols)
			{
				urls.push(buildConnectionAddress(url, urlIncludesFMSApplicationInstance, portProtocol));
			}
			
			return urls;
		}
		
		// Internals
		//
				
		/** 
		 * Assembles a vector of PortProtocol Objects to be used during the connection attempt.
		 * 
		 * @param url the URL to be loaded
		 * @returns a Vector of PortProtocol objects. 
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		private function buildPortProtocolSequence(url:String):Vector.<PortProtocol>
		{
			var portProtocols:Vector.<PortProtocol> = new Vector.<PortProtocol>;
			
			var theURL:URL = new URL(url);
			
			var allowedPorts:String = (theURL.port == "") ? DEFAULT_PORTS: theURL.port;
			var allowedProtocols:String = "";
			switch (theURL.protocol)
			{
				case PROTOCOL_RTMP:
					allowedProtocols = DEFAULT_PROTOCOLS_FOR_RTMP;
					break;
				case PROTOCOL_RTMPE:
					allowedProtocols = DEFAULT_PROTOCOLS_FOR_RTMPE;
					break;
				case PROTOCOL_RTMPS:
				case PROTOCOL_RTMPT:
				case PROTOCOL_RTMPTE:
					allowedProtocols = theURL.protocol;
					break;
			}
			var portArray:Array = allowedPorts.split(",");
			var protocolArray:Array = allowedProtocols.split(",");
			for (var i:int = 0; i < protocolArray.length; i++)
			{
				for (var j:int = 0; j < portArray.length; j++)
				{
					var attempt:PortProtocol = new PortProtocol();
					attempt.protocol = protocolArray[i];
					attempt.port = portArray[j];
					portProtocols.push(attempt);
				}
			} 
			return portProtocols;
		}
		
		/**
		 * Assembles a connection address. 
		 * 
		 * @param url The URL to be loaded.
		 * @param urlIncludesFMSApplicationInstance Indicates whether the URL includes
		 * the FMS application instance name.  See StreamingURLResource for more info.
		 * @param portProtocol The port and protocol being used for the connection.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		private function buildConnectionAddress(url:String, urlIncludesFMSApplicationInstance:Boolean, portProtocol:PortProtocol):String
		{
			var fmsURL:FMSURL = new FMSURL(url, urlIncludesFMSApplicationInstance);
			var addr:String = portProtocol.protocol + "://" + fmsURL.host + ":" + portProtocol.port + "/" + fmsURL.appName + (fmsURL.useInstance ? "/" + fmsURL.instanceName:"");
			
			// Pass along any query string params
			if (fmsURL.query != null && fmsURL.query != "")
			{
				addr += "?" + fmsURL.query;
			}
			
			return addr;
		}
		
		private var shareNetConnections:Boolean;
		private var negotiator:NetNegotiator;
		private var connectionDictionary:Dictionary;
		private var keyDictionary:Dictionary;
		private var pendingDictionary:Dictionary;
		
		private static const DEFAULT_PORTS:String = "1935,443,80";
		private static const DEFAULT_PROTOCOLS_FOR_RTMP:String = "rtmp,rtmps,rtmpt"
		private static const DEFAULT_PROTOCOLS_FOR_RTMPE:String = "rtmpe,rtmpte";

		private static const PROTOCOL_RTMP:String = "rtmp";
		private static const PROTOCOL_RTMPS:String = "rtmps";
		private static const PROTOCOL_RTMPT:String = "rtmpt";
		private static const PROTOCOL_RTMPE:String = "rtmpe";
		private static const PROTOCOL_RTMPTE:String = "rtmpte";
		private static const PROTOCOL_HTTP:String = "http";
		private static const PROTOCOL_HTTPS:String = "https";
		private static const PROTOCOL_FILE:String = "file";
		private static const PROTOCOL_EMPTY:String = "";
		private static const MP3_EXTENSION:String = ".mp3";

		CONFIG::LOGGING
		private static const logger:org.osmf.logging.Logger = org.osmf.logging.Log.getLogger("org.osmf.net.NetConnectionFactory");
	}
}

import flash.net.NetConnection;

/**
 * Utility class for structuring shared connection data.
 *
 *  
 *  @langversion 3.0
 *  @playerversion Flash 10
 *  @playerversion AIR 1.5
 *  @productversion OSMF 1.0
 */
class SharedConnection
{
	public var count:Number;
	public var netConnection:NetConnection;
}

