/*****************************************************
*  
*  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.metadata
{
	import flash.errors.IllegalOperationError;
	
	import org.osmf.events.MetadataEvent;
	import org.osmf.utils.OSMFStrings;
	
	[ExcludeClass]
	
	/**
	 * @private
	 * 
	 * The MetadataWatcher class is a convenience class for monitoring nested Metadata
	 * for change.  It is capable of watching for value add, remove, or change
	 * events.
	 *  
	 *  @langversion 3.0
	 *  @playerversion Flash 10
	 *  @playerversion AIR 1.5
	 *  @productversion OSMF 1.0
	 */	
	public class MetadataWatcher
	{
		/**
		 * Constructor.
		 * 
		 * @param parentMetadata The parent Metadata to watch for change.
		 * @param namespaceURL The namespace that identifies the Metadata instance to watch
		 * for change.
		 * @param key The key pointing to the value of interest to watch
		 * for change. Note that this parameter is optional: not specifying a key
		 * will result in the Metadata as a whole being watched for change.
		 * @param callback The method to invoke on either the Metadata or Metadata value (see
		 * key parameter description) changing. The callback function is expected
		 * to take one argument, which will be set to the new value.
		 * 
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function MetadataWatcher(parentMetadata:Metadata, namespaceURL:String, key:String, callback:Function)
		{
			if (parentMetadata == null || namespaceURL == null || callback == null)
			{
				throw new IllegalOperationError(OSMFStrings.getString(OSMFStrings.NULL_PARAM));
			}

			this.parentMetadata = parentMetadata;
			this.namespaceURL = namespaceURL;
			this.key = key;
			this.callback = callback;
		}
		
		/**
		 * Starts watching the target metadata.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */		
		public function watch(dispatchInitialChangeEvent:Boolean=true):void
		{
			if (watching == false)
			{
				watching = true;
				
				// Make sure we are watching for metadatas being added. process-
				// WatchedMetadataChange that is invoked later, will remove the
				// listener should it already be present on our metadata:
				parentMetadata.addEventListener
					( MetadataEvent.VALUE_ADD, onMetadataAdd
					, false, 0, true
					);
				
				processWatchedMetadataChange(parentMetadata.getValue(namespaceURL) as Metadata);
				
				// For convenience, always trigger a first change callback when
				// start watching:
				if (dispatchInitialChangeEvent == true)
				{
					if (key != null)
					{
						callback(currentMetadata ? currentMetadata.getValue(key) : undefined);
					}
					else
					{
						callback(currentMetadata ? currentMetadata : undefined);
					}
				}
			}
		}
		
		/**
		 * Stops watching the target.
		 *  
		 *  @langversion 3.0
		 *  @playerversion Flash 10
		 *  @playerversion AIR 1.5
		 *  @productversion OSMF 1.0
		 */
		public function unwatch():void
		{
			if (watching == true)
			{
				processWatchedMetadataChange(null, false);
				
				// If we weren't watching our metadata yet, then processWatched-
				// MetadataChange will not have remove our addition listener:
				parentMetadata.removeEventListener(MetadataEvent.VALUE_ADD, onMetadataAdd);
				
				watching = false;
			}
		}	
		
		// Internals
		//
			
		private function processWatchedMetadataChange(metadata:Metadata, dispatchChange:Boolean = true):void
		{
			// Don't change anything if the new metadata matches the old one:
			if (currentMetadata != metadata)
			{
				var oldMetadata:Metadata = currentMetadata;
				
				if (currentMetadata)
				{
					// Remove the event listeners for the currently set metadata:
					currentMetadata.removeEventListener(MetadataEvent.VALUE_CHANGE, onValueChange);
					currentMetadata.removeEventListener(MetadataEvent.VALUE_ADD, onValueAdd);
					currentMetadata.removeEventListener(MetadataEvent.VALUE_REMOVE, onValueRemove);
					
					parentMetadata.removeEventListener(MetadataEvent.VALUE_REMOVE, onMetadataRemove);
				}
				else
				{
					// If there's currently no metadata set, then remove the listener
					// that's out to capture the addition of the metadata:
					parentMetadata.removeEventListener(MetadataEvent.VALUE_ADD, onMetadataAdd);
				}
				
				// Now assign the new metadata value:
				currentMetadata = metadata;
				
				if (metadata)
				{
					// Listen to the metadata informing us about value changes:
					metadata.addEventListener
						( MetadataEvent.VALUE_CHANGE, onValueChange
						, false, 0, true
						);
					metadata.addEventListener
						( MetadataEvent.VALUE_ADD, onValueAdd
						, false, 0, true
						);
					metadata.addEventListener
						( MetadataEvent.VALUE_REMOVE, onValueRemove
						, false, 0, true
						);
					
					// Listen to the parent metadata informing us about the metadata being removed:
					parentMetadata.addEventListener(MetadataEvent.VALUE_REMOVE, onMetadataRemove);
				}
				else
				{
					// If there's currently no metadata set, then listen to the parent metadata
					// instance informing us about new metadatas being added:
					parentMetadata.addEventListener(MetadataEvent.VALUE_ADD, onMetadataAdd);
				}
			}
		}
		
		// Metadata Handlers
		//
		
		private function onMetadataAdd(event:MetadataEvent):void
		{
			// See if this is the metadata that we're watching:
			var metadata:Metadata = event.value as Metadata;
			if (metadata && event.key == namespaceURL)
			{
				processWatchedMetadataChange(metadata);
				
				// In case we're watching at the metadata level only, then
				// trigger the callback:
				if (key == null)
				{
					callback(metadata);
				}
				else
				{
					callback(metadata.getValue(key));
				}
			}
		}
		
		private function onMetadataRemove(event:MetadataEvent):void
		{
			// See if this is the metadata that we're watching:
			var metadata:Metadata = event.value as Metadata;
			if (metadata && event.key == namespaceURL)
			{
				processWatchedMetadataChange(null);
				
				callback(undefined);
			}
		}
		
		// Value Handlers
		//
		
		private function onValueChange(event:MetadataEvent):void
		{
			if (key)
			{
				// We're watching a specific value: only invoke the callback
				// if this is 'our' value that is changing:
				if (key == event.key)
				{
					callback(event.value);
				}	
			}
			else
			{
				// We're watching the entire metadata: invoke callback:
				callback(event.target as Metadata);
			}
		}
		
		private function onValueAdd(event:MetadataEvent):void
		{
			if (key)
			{
				// We're watching a specific value: only invoke the callback
				// if this is 'our' value that is being added:
				if (key == event.key)
				{
					callback(event.value);
				}	
			}
			else
			{
				// We're watching the entire metadata: invoke callback:
				callback(event.target as Metadata);
			}
		}
		
		private function onValueRemove(event:MetadataEvent):void
		{
			if (key)
			{
				// We're watching a specific value: only invoke the callback
				// if this is 'our' value that is being removed:
				if (key == event.key)
				{
					callback(undefined);
				}	
			}
			else
			{
				// We're watching the entire metadata: invoke callback:
				callback(event.target as Metadata);
			}
		}
		
		private var parentMetadata:Metadata;
		private var namespaceURL:String;
		private var key:String;
		private var callback:Function;
		
		private var currentMetadata:Metadata;
		private var watching:Boolean;
	}
}