/*****************************************************
*  
*  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.compositeClasses
{
	import __AS3__.vec.Vector;
	
	import flash.errors.IllegalOperationError;
	import flash.events.Event;
	import flash.utils.Dictionary;
	
	import org.osmf.events.MetadataEvent;
	import org.osmf.metadata.Metadata;
	import org.osmf.metadata.MetadataGroup;
	import org.osmf.metadata.MetadataSynthesizer;
	import org.osmf.metadata.NullMetadataSynthesizer;
	import org.osmf.utils.OSMFStrings;
	
	CONFIG::LOGGING
	{
	import org.osmf.logging.Logger;
	}

	[ExcludeClass]
	
	/**
	 * Event fired when a child was added to the composite. 
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */	
	[Event(name="childAdd", type="org.osmf.events.CompositeMetadataEvent")]
	
	/**
	 * Event fired when a child was removed from the composite. 
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event(name="childRemove", type="org.osmf.events.CompositeMetadataEvent")]
		
	/**
	 * Event fired when a new metadata group emerged. 
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event(name="metadataGroupAdd", type="org.osmf.event.CompositeMetadataEvent")]
	
	/**
	 * Event fired when an existing metadata group ceased to exist. 
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event(name="metadataGroupRemove", type="org.osmf.event.CompositeMetadataEvent")]
	
	/**
	 * Event fired when a metadata group changed. 
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */
	[Event(name="metadataGroupChange", type="org.osmf.event.CompositeMetadataEvent")]
	
	/**
	 * @private
	 * 
	 * Defines a collection of meta data that keeps track of a collection
	 * of child meta data references as a plain list.
	 * 
	 * By default, no synthesis takes place. External clients can inspect metadata
	 * groups at will, and monitor them for change. However, the class provides
	 * an infrastructure for synthesis like so:
	 * 
	 * By using 'addMetadataSynthesizer' and 'removeMetadataSynthesizer', clients can
	 * define how metadata groups of a given name space will be synthesized into
	 * a new metadata. If a metadata group changes that matches an added metadata
	 * synthesizer's namespace, then this synthesizer is used to synthesize the
	 * composite value. After synthesis, the value gets added as one of the
	 * composite's own metadatas.
	 * 
	 * If a CompositeMetadata instance is itself a child of another
	 * CompositeMetadata instance, then any metadata synthesizer that is set on
	 * the parent instance, will be used by the child instance automatically
	 * too. If the child has its own metadata synthesizer listed, than that
	 * synthesizer takes precedence over the inherited one.
	 * 
	 * Last, metadata synthesis occurs if the first metadata from a metadata group
	 * returns a metadata synthesizer for its synthesizer property. Metadata
	 * synthesizers set on this class directly, or indirectly via its parent,
	 * take precedence over the metadata's suggested synthesizer.
	 * 
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */	
	public class CompositeMetadata extends Metadata
	{
		// Public Interface
		//
		
		/**
		 * Constructor
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function CompositeMetadata()
		{
			super();
			
			children = new Vector.<Metadata>();
			childMetadataGroups = new Dictionary();
			
			metadataSynthesizers = new Dictionary();
		}
		
		/**
		 * Adds a metadata child.
		 * 
		 * @param child The child to add.
		 * @throws IllegalOperationError Thrown if the specified child is
		 * already a child.
		 * @throws ArgumentError if the specified child is null.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function addChild(child:Metadata):void
		{
			if (child == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}
			
			var childIndex:int = children.indexOf(child);
			if (childIndex != -1)
			{
				throw new IllegalOperationError();	
			}
			else
			{
				children.push(child);
				
				child.addEventListener
					( MetadataEvent.VALUE_ADD
					, onChildMetadataAdd
					);
					
				child.addEventListener
					( MetadataEvent.VALUE_REMOVE
					, onChildMetadataRemove
					);
					
				if (child is CompositeMetadata)
				{
					child.addEventListener
						( CompositeMetadataEvent.METADATA_GROUP_CHANGE
						, onChildMetadataGroupChange
						);
				}
				
				for each (var url:String in child.keys)
				{
					processChildMetadataAdd
						( child
						, child.getValue(url) as Metadata
						, url
						);
				}
				
				dispatchEvent
					( new CompositeMetadataEvent
						( CompositeMetadataEvent.CHILD_ADD
						, false, false
						, child
						)
					);
			}
		}
		
		/**
		 * Removes a metadata child.
		 * 
		 * @param child The child to remove.
		 * @throws IllegalOperationError Thrown if the specified child is
		 * not a child.
		 * @throws ArgumentError if the specified child is null.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function removeChild(child:Metadata):void
		{
			if (child == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}
			
			var childIndex:int = children.indexOf(child);
			if (childIndex == -1)
			{
				throw new IllegalOperationError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}	
			else
			{
				children.splice(childIndex,1);
				
				child.removeEventListener
					( MetadataEvent.VALUE_ADD
					, onChildMetadataAdd
					);
					
				child.removeEventListener
					( MetadataEvent.VALUE_REMOVE
					, onChildMetadataRemove
					);
				
				if (child is CompositeMetadata)
				{
					child.removeEventListener
						( CompositeMetadataEvent.METADATA_GROUP_CHANGE
						, onChildMetadataGroupChange
						);
				}
				
				for each (var url:String in child.keys)
				{
					processChildMetadataRemove
						( child
						, child.getValue(url) as Metadata
						, url
						);
				}
				
				dispatchEvent
					( new CompositeMetadataEvent
						( CompositeMetadataEvent.CHILD_REMOVE
						, false, false
						, child
						)
					);
			}
		}
		
		/**
		 * Defines the number of children.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function get numChildren():int
		{
			return children.length;
		}
		
		/**
		 * Fetches the child at the indicated index. 
		 * 
		 * @param index The index of the child metadata to fetch.
		 * @return The requested metadata.
		 * @throws RangeError Thrown when the specified index is out of
		 * bounds.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function getChildAt(index:int):Metadata
		{
			if (index >= children.length || index < 0)
			{
				throw new RangeError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));	
			}
			
			return children[index];
		}
		
		/**
		 * Defines the composition mode that will be forwarded to metadata
		 * synthesizers when synthesizing a merged metadata is required.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function set mode(value:String):void
		{
			if (_mode != value)
			{
				_mode = value;
				
				processSynthesisDependencyChanged();
			}
		}
		public function get mode():String
		{
			return _mode;
		}
		
		/**
		 * Defines the active metadata that will be forwarded
		 * to metadata synthesizers when synthesizing a merged metadata is
		 * required.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function set activeChild(value:Metadata):void
		{
			if (_activeChild != value)
			{
				_activeChild = value;
				
				processSynthesisDependencyChanged();
			}
		}
		public function get activeChild():Metadata
		{
			return _activeChild;
		}
		
		/**
		 * Adds a metadata synthesizer.
		 * 
		 * A metadata synthesizer can synthesize a metadata from a given MetadataGroup,
		 * composition mode, and active child (if any).
		 * 
		 * Only one synthesizer can be registered for a given namespace URL.
		 * 
		 * @param namespaceURL The namespace URL to synthesize values for.
		 * @param synthesizer The metadata synthesizer to add.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function addMetadataSynthesizer(namespaceURL:String, synthesizer:MetadataSynthesizer):void
		{
			if (synthesizer == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}
			
			if (getMetadataSynthesizer(namespaceURL) != null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.INVALID_PARAM));
			}
			
			metadataSynthesizers[namespaceURL] = synthesizer;
		}
		
		/**
		 * Removes a metadata synthesizer.
		 * 
		 * @param namespaceURL The namespace URL that corresponds to the synthesizer to remove.
		 * @throws ArgumentError If namespaceURL is null.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function removeMetadataSynthesizer(namespaceURL:String):void
		{
			if (namespaceURL == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}
			
			if (getMetadataSynthesizer(namespaceURL) != null)
			{
				delete metadataSynthesizers[namespaceURL];
			}
		}
		
		/**
		 * Fetches the metadata synthesizer (if any) for the given namespace URL.
		 * 
		 * @param namespaceURL The namespace to retrieve the set synthesizer for.
		 * @return The requested syntesizer, if it was set, null otherwise.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function getMetadataSynthesizer(namespaceURL:String):MetadataSynthesizer
		{
			var result:MetadataSynthesizer;
			
			if (namespaceURL != null)
			{
				for (var rawUrl:String in metadataSynthesizers)
				{
					if (rawUrl == namespaceURL)
					{
						result = metadataSynthesizers[rawUrl];
						break;
					}
				}
			}
			
			return result;
		}
		
		/**
		 * Collects the namespaces of the metadata groups that are currently in existence.
		 *  
		 * @return The collected namespaces.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function getMetadataGroupNamespaceURLs():Vector.<String>
		{
			var result:Vector.<String> = new Vector.<String>();
			
			for (var url:String in childMetadataGroups)
			{
				result.push(url);
			}
			
			return result;
		}
		
		/**
		 * Fetches the metadata group for the given namenspace.
		 *  
		 * @param namespaceURL The namespace to fetch the metadata group for.
		 * @return The requested metadata group, or null if there is no such group.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function getMetadataGroup(namespaceURL:String):MetadataGroup
		{
			if (namespaceURL == null)
			{
				throw new ArgumentError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}
			
			return childMetadataGroups[namespaceURL];
		}
		
		// Internals
		//
		
		private function processChildMetadataAdd(child:Metadata, metadata:Metadata, metadataNamespaceURL:String):void
		{
			var groupAddEvent:CompositeMetadataEvent;
			
			if (metadata != null)
			{
				var childrenNamespaceURL:String = metadataNamespaceURL;
				
				var metadataGroup:MetadataGroup = childMetadataGroups[childrenNamespaceURL];
				if (metadataGroup == null)
				{
					childMetadataGroups[childrenNamespaceURL]
						= metadataGroup
						= new MetadataGroup(childrenNamespaceURL);
						
					metadataGroup.addEventListener(Event.CHANGE, onMetadataGroupChange);
						
					groupAddEvent
						= new CompositeMetadataEvent
							( CompositeMetadataEvent.METADATA_GROUP_ADD
							, false, false
							, child
							, metadataNamespaceURL
							, metadata
							, metadataGroup
							);
				}
				
				metadataGroup.addMetadata(child, metadata);
			}
					
			if (groupAddEvent != null)
			{
				dispatchEvent(groupAddEvent);
			}
		}
		
		private function processChildMetadataRemove(child:Metadata, metadata:Metadata, metadataNamespaceURL:String):void
		{
			var groupRemoveEvent:CompositeMetadataEvent;
			
			if (metadata != null)
			{
				var childrenNamespaceURL:String = metadataNamespaceURL;
				var metadataGroup:MetadataGroup = childMetadataGroups[childrenNamespaceURL];
				metadataGroup.removeMetadata(child, metadata);
				
				if (metadataGroup.metadatas.length == 0)
				{
					metadataGroup.removeEventListener(Event.CHANGE, onMetadataGroupChange);
					
					groupRemoveEvent
						= new CompositeMetadataEvent
							( CompositeMetadataEvent.METADATA_GROUP_REMOVE
							, false, false
							, child
							, metadataNamespaceURL
							, metadata
							, metadataGroup
							);
							
					delete childMetadataGroups[childrenNamespaceURL];
					
				}
			}
			
			if (groupRemoveEvent != null)
			{
				dispatchEvent(groupRemoveEvent);
			}
		}
		
		private function processSynthesisDependencyChanged():void
		{
			for each (var metadataGroup:MetadataGroup in childMetadataGroups)
			{
				onMetadataGroupChange(null, metadataGroup);
			}
		}
		
		// Event Handlers
		//
		
		private function onChildMetadataAdd(event:MetadataEvent):void
		{
			processChildMetadataAdd(event.target as Metadata, event.value as Metadata, event.key);	
		}
		
		private function onChildMetadataRemove(event:MetadataEvent):void
		{
			processChildMetadataRemove(event.target as Metadata, event.value as Metadata, event.key);
		}
		
		private function onChildMetadataGroupChange(event:CompositeMetadataEvent):void
		{
			// If no one before us was able to deliver a metadata synthesizer, then perhaps we can:
			if (event.suggestedMetadataSynthesizer == null)
			{
				event.suggestedMetadataSynthesizer = metadataSynthesizers[event.metadataGroup.namespaceURL];
			}
						
			var clonedEvent:CompositeMetadataEvent 
				=	event.clone()
				as	CompositeMetadataEvent;
				
			// Re-dispatch the event:
			dispatchEvent(clonedEvent);
			
			// If we didn't assign a metadata synthesizer, then perhaps another handler did:
			if (event.suggestedMetadataSynthesizer == null)
			{
				event.suggestedMetadataSynthesizer = clonedEvent.suggestedMetadataSynthesizer;
			}
		}
		
		private function onMetadataGroupChange(event:Event, metadataGroup:MetadataGroup = null):void
		{
			// This method is invoked as both a regular event handler, as well as directly
			// from processSynthesisDependencyChanged. In the latter case, the event will be
			// null, and the metadataGroup parameter will be set instead. To be prudent, check
			// for a metadata group being present either way:
			metadataGroup ||= event ? event.target as MetadataGroup : null;
			if (metadataGroup == null)
			{
				throw new IllegalOperationError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}
			
			var synthesizedMetadata:Metadata;
			var metadataSynthesizer:MetadataSynthesizer = metadataSynthesizers[metadataGroup.namespaceURL];
			
			var localEvent:CompositeMetadataEvent
				= new CompositeMetadataEvent
					( CompositeMetadataEvent.METADATA_GROUP_CHANGE
					, false, false
					, null, null, null
					, metadataGroup
					, metadataSynthesizer
					)
			
			dispatchEvent(localEvent);
			
 			// If no metadata synthesizer is set yet, then first see if any of the
			// event handlers provided us with one. If not, then use the metadata's
			// default synthesizer (if it provides for one):
			metadataSynthesizer
				||= localEvent.suggestedMetadataSynthesizer
				// If no synthesizer has been suggested by our parents, then look
				// at the metadata group, and take the synthesizer set on the first
				// metadata that we encounter:
				||	(	(metadataGroup.metadatas.length > 0)
							? metadataGroup.metadatas[0].synthesizer
							: null
				 	)
				// Last, revert to the default synthesizer:
				|| new MetadataSynthesizer();
			
			// If the activeChild was just removed, don't let it influence the
			// synthesis decision.
			var serialElementActiveChild:Metadata = _activeChild;
			if (_activeChild != null && children.indexOf(_activeChild) == -1)
			{
				serialElementActiveChild = null;
			}
			
			// Run the metadata synthesizer:
			synthesizedMetadata
				= metadataSynthesizer.synthesize
					( metadataGroup.namespaceURL
					, this
					, metadataGroup.metadatas
					, _mode
					, serialElementActiveChild
					);
 		
			if (synthesizedMetadata == null)
			{
				// If the synthesized value is null, then we might need to clear
				// out a previously set value. Don't clear out the value if the
				// current synthesizer is a null synthesizer.
				var currentMetadata:Metadata = getValue(metadataGroup.namespaceURL) as Metadata;
				if	(	currentMetadata != null
					&&	currentMetadata.synthesizer is NullMetadataSynthesizer == false
					)
				{
					CONFIG::LOGGING { logger.debug("removing metadata {0}", metadataGroup.namespaceURL); }
					removeValue(metadataGroup.namespaceURL);
				}
			}
			else
			{
				// Add, or overwrite the last set metadata value:
				addValue(metadataGroup.namespaceURL, synthesizedMetadata);
			}
		}
		
		private var children:Vector.<Metadata>;
		private var childMetadataGroups:Dictionary; 
		private var metadataSynthesizers:Dictionary;
		
		private var _mode:String;
		private var _activeChild:Metadata;
		
		CONFIG::LOGGING private static const logger:org.osmf.logging.Logger = org.osmf.logging.Log.getLogger("org.osmf.elements.compositeClasses.CompositeMetadata");
	}
}