/*****************************************************
*  
*  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.f4mClasses
{
	import __AS3__.vec.Vector;
	
	import flash.utils.ByteArray;
	
	import org.osmf.media.MediaResourceBase;
	import org.osmf.media.MediaType;
	import org.osmf.media.URLResource;
	import org.osmf.metadata.Metadata;
	import org.osmf.metadata.MetadataNamespaces;
	import org.osmf.net.DynamicStreamingItem;
	import org.osmf.net.DynamicStreamingResource;
	import org.osmf.net.httpstreaming.dvr.DVRInfo;
	import org.osmf.net.NetStreamUtils;
	import org.osmf.net.StreamType;
	import org.osmf.net.StreamingURLResource;
	import org.osmf.utils.OSMFStrings;
	import org.osmf.utils.URL;
	
	[ExcludeClass]
	
	/**
	 * @private
	 **/
	public class ManifestParser
	{
		namespace xmlns = "http://ns.adobe.com/f4m/1.0";
		
		/**
		 * Parses a Manifest Object from a XML string.
		 * 
		 * @throws Error if the parse fails.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */ 
		public function parse(value:String, rootUrl:String=null):Manifest
		{
			var manifest:Manifest = new Manifest();
			
			var root:XML = new XML(value);
						
			if (root.xmlns::id.length() > 0)
			{
				manifest.id = root.xmlns::id.text();
			}
			
			if (root.xmlns::duration.length() > 0)
			{			
				manifest.duration = root.xmlns::duration.text();
			}	
			
			if (root.xmlns::startTime.length() > 0)
			{			
				manifest.startTime = DateUtil.parseW3CDTF(root.xmlns::startTime.text());
			}	
			
			if (root.xmlns::mimeType.length() > 0)
			{			
				manifest.mimeType = root.xmlns::mimeType.text();
			}	
			
			if (root.xmlns::streamType.length() > 0)
			{			
				manifest.streamType = root.xmlns::streamType.text();
			}
			
			if (root.xmlns::deliveryType.length() > 0)
			{			
				manifest.deliveryType = root.xmlns::deliveryType.text();
			}
			
			if (root.xmlns::baseURL.length() > 0)
			{			
				manifest.baseURL = root.xmlns::baseURL.text();
			}
			
			var baseUrl:String = (manifest.baseURL != null) ? manifest.baseURL :  rootUrl;
			
			// DVRInfo
			for each (var dvrInfo:XML in root.xmlns::dvrInfo)
			{
				parseDVRInfo(dvrInfo, baseUrl, manifest);	
				break;		
			}	
			
			// Media	
			
			var bitrateMissing:Boolean = false;
			
			for each (var media:XML in root.xmlns::media)
			{
				var newMedia:Media = parseMedia(media, baseUrl);
				manifest.media.push(newMedia);
				bitrateMissing ||= isNaN(newMedia.bitrate);
			}	
			
			if (manifest.media.length > 1 && bitrateMissing)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.F4M_PARSE_BITRATE_MISSING));
			}
								
			// DRM Metadata	
			
			for each (var data:XML in root.xmlns::drmAdditionalHeader)
			{
				parseDRMAdditionalHeader(data, manifest.media, baseUrl, manifest);
			}	
			
			// Bootstrap	
			
			for each (var info:XML in root.xmlns::bootstrapInfo)
			{
				parseBootstrapInfo(info, manifest.media, baseUrl, manifest);
			}	
			
			// Required if base URL is omitted from Manifest
			generateRTMPBaseURL(manifest);
									
			return manifest;
		}
		
		private function parseMedia(value:XML, baseUrl:String):Media
		{
			var decoder:Base64Decoder;
			var media:Media = new Media();
			
			if (value.attribute('url').length() > 0)
			{
				media.url = value.@url;
			}
			else  // Raise parse error
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.F4M_PARSE_MEDIA_URL_MISSING));
			}
			
			if (value.attribute('bitrate').length() > 0)
			{
				media.bitrate = value.@bitrate;
			}
				
			if (value.attribute('drmAdditionalHeaderId').length() > 0)
			{
				media.drmAdditionalHeader.id = value.@drmAdditionalHeaderId;
			}
			
			if (value.attribute('bootstrapInfoId').length() > 0)
			{
				media.bootstrapInfo = new BootstrapInfo();
				media.bootstrapInfo.id = value.@bootstrapInfoId;
			}
			
			if (value.attribute('height').length() > 0)
			{
				media.height = value.@height;
			}
			
			if (value.attribute('width').length() > 0)
			{
				media.width = value.@width;
			}
			
			if (value.xmlns::moov.length() > 0)
			{		
				decoder = new Base64Decoder();
				decoder.decode(value.xmlns::moov.text());
				media.moov = decoder.drain();	
			}
			
			if (value.xmlns::metadata.length() > 0)
			{
				decoder = new Base64Decoder();
				decoder.decode(value.xmlns::metadata.text());
				
				var data:ByteArray = decoder.drain();
				data.position = 0;
				data.objectEncoding = 0;
				
				try
				{
					var header:String = data.readObject() as String;
					var metaInfo:Object = data.readObject();
					media.metadata = metaInfo;			
				}
				catch (e:Error)
				{
				}			
			}
			
			if (value.xmlns::xmpMetadata.length() > 0)
			{
				decoder = new Base64Decoder();
				decoder.decode(value.xmlns::xmpMetadata.text());
				media.xmp = decoder.drain();	
			}

			return media;
		}
		
		private function parseDVRInfo(value:XML, baseUrl:String, manifest:Manifest):void
		{
			var dvrInfo:DVRInfo = new DVRInfo();
			if (value.attribute("id").length() > 0)
			{
				dvrInfo.id = value.@id;
			}
			if (value.attribute("url").length() > 0)
			{
				var url:String = value.@url;
				if (!isAbsoluteURL(url))
				{
					url = baseUrl + "/" + url;
				}
				dvrInfo.url = url;
			}
			if (value.attribute("beginOffset").length() > 0)
			{
				dvrInfo.beginOffset = Math.max(0, parseInt(value.@beginOffset));
			}
			if (value.attribute("endOffset").length() > 0)
			{
				dvrInfo.endOffset =  Math.max(0, parseInt(value.@endOffset));
			}
			if (value.attribute("offline").length() > 0)
			{
				var s:String = value.@offline;
				dvrInfo.offline = (s.toLowerCase() == "true");
			}
			
			manifest.dvrInfo = dvrInfo;
		}
		
		private function parseDRMAdditionalHeader(value:XML, allMedia:Vector.<Media>, baseUrl:String, manifest:Manifest):void
		{
			var url:String = null;
			var media:Media;
			
			var drmAdditionalHeader:DRMAdditionalHeader = new DRMAdditionalHeader();	
			
			if (value.attribute("id").length() > 0)
			{
				drmAdditionalHeader.id = value.@id; 
			}
			
			if (value.attribute("url").length() > 0)
			{
				url = value.@url;
				if (!isAbsoluteURL(url))
				{
					url = baseUrl + "/" + url;
				}
				drmAdditionalHeader.url = url;
			}
			else
			{			
				var metadata:String = value.text();
				var decoder:Base64Decoder = new Base64Decoder();
				decoder.decode(metadata);
				drmAdditionalHeader.data = decoder.drain();
			}
			
			manifest.drmAdditionalHeaders.push(drmAdditionalHeader);
								
			for each (media in allMedia)
			{
				if (media.drmAdditionalHeader.id == drmAdditionalHeader.id)
				{
					media.drmAdditionalHeader = drmAdditionalHeader;					
				}
			}
		}		
		
		private function parseBootstrapInfo(value:XML, allMedia:Vector.<Media>, baseUrl:String, manifest:Manifest):void
		{			
			var media:Media;	
			
			var url:String = null;
			var bootstrapInfo:BootstrapInfo = new BootstrapInfo();
			
			if (value.attribute('profile').length() > 0)
			{
				bootstrapInfo.profile = value.@profile;
			}
			else  // Raise parse error
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.F4M_PARSE_PROFILE_MISSING));
			}
			
			if (value.attribute("id").length() > 0)
			{
				bootstrapInfo.id = value.@id; 
			}
				
			if (value.attribute("url").length() > 0)
			{
				url = value.@url;
				if (!isAbsoluteURL(url) && baseUrl != null)
				{
					url = baseUrl + "/" + url;
				}
				bootstrapInfo.url = url;
			}
			else
			{			
				var metadata:String = value.text();
				var decoder:Base64Decoder = new Base64Decoder();
				decoder.decode(metadata);
				bootstrapInfo.data = decoder.drain();
			}
								
			for each (media in allMedia)
			{
				if (media.bootstrapInfo == null) //No per media bootstrap. Apply it to all items.
				{
					media.bootstrapInfo = bootstrapInfo;
				}
				else if (media.bootstrapInfo.id == bootstrapInfo.id)
				{
					media.bootstrapInfo = bootstrapInfo;
				}						
			}								
		}		
		
		/**
		 * @private
		 * Ensures that an RTMP based Manifest has the same server for all
		 * streaming items, and extracts the base URL from the streaming items
		 * if not specified. 
		 */ 
		private function generateRTMPBaseURL(manifest:Manifest):void
		{
			if (manifest.baseURL == null)
			{						
				for each(var media:Media in manifest.media)
				{
					 if (NetStreamUtils.isRTMPStream(media.url))
					 {					 	
					 	manifest.baseURL = media.url;
					 	break; 
					 }
				}
			}
		}
		
		/**
		 * Generates a MediaResourceBase for the given manifest. 
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */ 		
		public function createResource(value:Manifest, manifestResource:URLResource):MediaResourceBase
		{			
			var drmMetadata:Metadata = null;
			var httpMetadata:Metadata = null;
			var resource:StreamingURLResource;
			var media:Media;
			var serverBaseURLs:Vector.<String>;
			var url:String;
			var bootstrapInfoURLString:String;
			
			var manifestURL:URL = new URL(manifestResource.url); 
			var cleanedPath:String = "/" + manifestURL.path;
			cleanedPath = cleanedPath.substr(0, cleanedPath.lastIndexOf("/"));
			var manifestFolder:String = manifestURL.protocol + "://" +  manifestURL.host + (manifestURL.port != "" ? ":" + manifestURL.port : "") + cleanedPath;
			
			if (value.media.length == 1)  // Single Stream/Progressive Resource
			{
				media = value.media[0] as Media;
				url = media.url;
				
				var baseURLString:String = null;
				if (isAbsoluteURL(url))
				{
					// The server base URL needs to be extracted from the media's
					// URL.  Note that we assume it's the same for all media.
					baseURLString = media.url.substr(0, media.url.lastIndexOf("/"));
				}
				else if (value.baseURL != null)
				{
					baseURLString = value.baseURL;
				}
				else
				{
					baseURLString = manifestFolder;
				}
				
				if (isAbsoluteURL(url))
				{
					resource = new StreamingURLResource(url, value.streamType);
				}				
				else if (value.baseURL != null)	// Relative to Base URL					
				{
					resource = new StreamingURLResource(value.baseURL + "/" + url, streamType(value));
				}
				else // Relative to F4M file  (no absolute or base urls or rtmp urls).
				{
					resource = new StreamingURLResource(manifestFolder + "/" + url, streamType(value));
				}
				
				if (media.bootstrapInfo	!= null)
				{
					serverBaseURLs = new Vector.<String>();
					serverBaseURLs.push(baseURLString);
					
					bootstrapInfoURLString = media.bootstrapInfo.url;
					if (media.bootstrapInfo.url != null &&
						isAbsoluteURL(media.bootstrapInfo.url) == false)
					{
						bootstrapInfoURLString = manifestFolder + "/" + bootstrapInfoURLString;
						media.bootstrapInfo.url = bootstrapInfoURLString;
					}
					httpMetadata = new Metadata();
					httpMetadata.addValue(MetadataNamespaces.HTTP_STREAMING_BOOTSTRAP_KEY, media.bootstrapInfo);
					if (serverBaseURLs.length > 0)
					{
						httpMetadata.addValue(MetadataNamespaces.HTTP_STREAMING_SERVER_BASE_URLS_KEY, serverBaseURLs);
					}
				}
				
				if (media.metadata != null)
				{
					if (httpMetadata == null)
					{
						httpMetadata = new Metadata();
					}
					httpMetadata.addValue(MetadataNamespaces.HTTP_STREAMING_STREAM_METADATA_KEY, media.metadata);					
				}
				
				if (media.xmp != null)
				{
					if (httpMetadata == null)
					{
						httpMetadata = new Metadata();
					}
					httpMetadata.addValue(MetadataNamespaces.HTTP_STREAMING_XMP_METADATA_KEY, media.xmp);					
				}

				if (media.drmAdditionalHeader != null)
				{					
					drmMetadata = new Metadata();
					if (Media(value.media[0]).drmAdditionalHeader != null && Media(value.media[0]).drmAdditionalHeader.data != null)
					{
						drmMetadata.addValue(MetadataNamespaces.DRM_ADDITIONAL_HEADER_KEY, Media(value.media[0]).drmAdditionalHeader.data);
						
						resource.drmContentData = extractDRMMetadata(Media(value.media[0]).drmAdditionalHeader.data);
					}
				}
				
				if (httpMetadata != null)
				{
					resource.addMetadataValue(MetadataNamespaces.HTTP_STREAMING_METADATA, httpMetadata);
				}
				if (drmMetadata != null)
				{
					resource.addMetadataValue(MetadataNamespaces.DRM_METADATA, drmMetadata);
				}				
			}
			else if (value.media.length > 1) // Dynamic Streaming
			{
				var baseURL:String = value.baseURL != null ? value.baseURL : manifestFolder;
				serverBaseURLs = new Vector.<String>();
				serverBaseURLs.push(baseURL);
				
				// TODO: MBR streams can be absolute (with no baseURL) or relative (with a baseURL).
				// But we need to map them into the DynamicStreamingResource object model, which
				// assumes the latter.  For now, we only support the latter input, but we should
				// add support for the former (absolute URLs with no base URL).
				var dynResource:DynamicStreamingResource = new DynamicStreamingResource(baseURL, streamType(value));
				
				var streamItems:Vector.<DynamicStreamingItem> = new Vector.<DynamicStreamingItem>();
				
				// Only put this on HTTPStreaming, not RTMPStreaming resources.   RTMP resources always get a generated base url.
				if (NetStreamUtils.isRTMPStream(baseURL) == false)
				{
					httpMetadata = new Metadata();
					dynResource.addMetadataValue(MetadataNamespaces.HTTP_STREAMING_METADATA, httpMetadata);
					httpMetadata.addValue(MetadataNamespaces.HTTP_STREAMING_SERVER_BASE_URLS_KEY, serverBaseURLs);
				}
				
				for each (media in value.media)
				{	
					var stream:String;
					
					if (isAbsoluteURL(media.url))
					{
						stream = NetStreamUtils.getStreamNameFromURL(media.url);
					}
					else
					{
						stream = media.url;
					}					
					var item:DynamicStreamingItem = new DynamicStreamingItem(stream, media.bitrate, media.width, media.height);
					streamItems.push(item);
					if (media.drmAdditionalHeader != null)
					{						
						if (dynResource.getMetadataValue(MetadataNamespaces.DRM_METADATA) == null)
						{
							drmMetadata = new Metadata();
							dynResource.addMetadataValue(MetadataNamespaces.DRM_METADATA, drmMetadata);
						}						
						if (media.drmAdditionalHeader != null && media.drmAdditionalHeader.data != null)
						{
							drmMetadata.addValue(item.streamName, extractDRMMetadata(media.drmAdditionalHeader.data));	
							drmMetadata.addValue(MetadataNamespaces.DRM_ADDITIONAL_HEADER_KEY + item.streamName, media.drmAdditionalHeader.data);
						} 						
					}
					
					if (media.bootstrapInfo	!= null)
					{
						bootstrapInfoURLString = media.bootstrapInfo.url ? media.bootstrapInfo.url : null;
						if (media.bootstrapInfo.url != null &&
							isAbsoluteURL(media.bootstrapInfo.url) == false)
						{
							bootstrapInfoURLString = manifestFolder + "/" + bootstrapInfoURLString;
							media.bootstrapInfo.url = bootstrapInfoURLString; 
						}
						httpMetadata.addValue(MetadataNamespaces.HTTP_STREAMING_BOOTSTRAP_KEY + item.streamName, media.bootstrapInfo);
					}
			
					if (media.metadata != null)
					{
						httpMetadata.addValue(MetadataNamespaces.HTTP_STREAMING_STREAM_METADATA_KEY + item.streamName, media.metadata);					
					}
					
					if (media.xmp != null)
					{
						httpMetadata.addValue(MetadataNamespaces.HTTP_STREAMING_XMP_METADATA_KEY + item.streamName, media.xmp);					
					}
				}
				
				dynResource.streamItems = streamItems;
				
				resource = dynResource;
			}
			else if (value.baseURL == null)
			{	
				// This is a parse error, we need an rtmp url
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.F4M_PARSE_MEDIA_URL_MISSING));					
			}	
			
			if (value.mimeType != null)
			{
				resource.mediaType = MediaType.VIDEO;
				resource.mimeType = value.mimeType;			
			}
			
			// Add subclip info from original resource
			var streamingManifestResource:StreamingURLResource = manifestResource as StreamingURLResource;
			if (streamingManifestResource != null)
			{
				resource.clipStartTime = streamingManifestResource.clipStartTime;
				resource.clipEndTime = streamingManifestResource.clipEndTime;
			}
			
			// Add metadata to the created resource specifying the resource from
			// which it was derived.  This allows interested clients to determine
			// the origins of the resource.
			resource.addMetadataValue(MetadataNamespaces.DERIVED_RESOURCE_METADATA, manifestResource);
			
			addDVRInfo(value, resource);
			
			return resource;
		}
		
		private function isAbsoluteURL(url:String):Boolean
		{
			var theURL:URL = new URL(url);
			return theURL.absolute;
		}
		
		private function extractDRMMetadata(data:ByteArray):ByteArray
		{
			var metadata:ByteArray = null;
			
			data.position = 0;
			data.objectEncoding = 0;
			
			try
			{
				var header:Object = data.readObject();
				var encryption:Object = data.readObject();
				var enc:Object = encryption["Encryption"];
				var params:Object = enc["Params"];
				var keyInfo:Object = params["KeyInfo"];
				var fmrmsMetadata:Object = keyInfo["FMRMS_METADATA"];
				var drmMetadata:String = fmrmsMetadata["Metadata"] as String;

				var decoder:Base64Decoder = new Base64Decoder();
				decoder.decode(drmMetadata);
				metadata = decoder.drain();
			}
			catch (e:Error)
			{
				metadata = null;	
			}
			
			return metadata;
		}

		private function addDVRInfo(manifest:Manifest, resource:StreamingURLResource):void
		{
			if (manifest.dvrInfo == null)
			{
				return;
			}
			
			var metadata:Metadata = new Metadata();
			metadata.addValue(MetadataNamespaces.HTTP_STREAMING_DVR_BEGIN_OFFSET_KEY, manifest.dvrInfo.beginOffset);
			metadata.addValue(MetadataNamespaces.HTTP_STREAMING_DVR_END_OFFSET_KEY, manifest.dvrInfo.endOffset);
			metadata.addValue(MetadataNamespaces.HTTP_STREAMING_DVR_OFFLINE_KEY, manifest.dvrInfo.offline);
			metadata.addValue(MetadataNamespaces.HTTP_STREAMING_DVR_ID_KEY, manifest.dvrInfo.id);
			
			resource.addMetadataValue(MetadataNamespaces.DVR_METADATA, metadata);
		}
		
		private function streamType(value:Manifest):String
		{
			return (value.streamType == StreamType.LIVE && value.dvrInfo != null)? StreamType.DVR : value.streamType; 
		}
	}
}