// ADOBE SYSTEMS INCORPORATED
// Copyright 2003 Adobe Systems Incorporated
// All Rights Reserved

// NOTICE: Adobe permits you to use, modify, and distribute this file in
// accordance with the terms of the Adobe license agreement accompanying it.  
// If you have received this file from a source other than Adobe, then your use,
// modification, or distribution of it requires the prior written permission of Adobe.

// media.js - Adobe Acrobat multimedia support

// The app and app.media properties and methods in this file are part of the public Acrobat
// Multimedia API and may be used in your PDF JavaScript, EXCEPT where the name includes "priv"
// or is otherwise noted as private. DO NOT USE any of these private properties or methods in
// a PDF file, or YOUR PDF WILL BREAK in a future version of Acrobat.

// Greetings and thanks from the Acrobat Multimedia development team:
// Dylan Ashe - Multimedia Framework and QuickTime
// Michael Geary - JavaScript, Windows Media, Flash
// Scott Grant - User Interface, Authoring, Sound, PDF File Access
// Vivek Hebbar - Windows Built-in Player and Authoring
// Liz McQuarrie - RealOne Player and Browsers
// Ed Rowe - Project Lead and Mr. PDF
// Jason Beique, Paul Herrin, Renato Maschion, Jason Reuer, Xintai Chang - QA and Developer Tech


// Multimedia version number

console.println( 'Acrobat Multimedia Version 6.0' );

app.media.version = 6.0;


// Set app.media.trace = true to enable method and event tracing in this code.
// Note: app.media.trace is for testing only and will change in future versions.

app.media.trace = false;


// The app.media.* constants below are passed back and forth between C++ and JavaScript code.
// Always use these symbolic definitions instead of hard coded values, e.g.
//   settings.windowType = app.media.windowType.floating;  /* NOT settings.windowType = 2; */

// PDF files may be opened under both newer and older versions of Acrobat.
// Future versions of Acrobat may add new values to these lists. Your JavaScript code should
// gracefully handle any values it encounters beyond those listed here.
// Similarly, if you write JavaScript code for a future version of Acrobat, that code should
// check app.media.version before it depends on new app.media.* constant values added in
// that version. The lists below are marked to indicate which versions support which constants.


// Values for settings.layout

app.media.layout =
{
	meet:		1,	// scale to fit all content, preserve aspect, no clipping, background fill
	slice:		2,	// scale to fill window, preserve aspect, clip X or Y as needed
	fill:		3,	// scale X and Y separately to fill window
	scroll:		4,	// natural size with scrolling
	hidden:		5,	// natural size with clipping
	standard:	6	// use player's default settings
	// End 6.0 values
}


// Values for settings.windowType

app.media.windowType =
{
	docked:		1,
	floating:	2,
	fullScreen:	3
	// End 6.0 values
}


// Values for settings.monitorType

app.media.monitorType =
{
	document:		1,
	nonDocument:	2,
	primary:		3,
	bestColor:		4,
	largest:		5,
	tallest:		6,
	widest:			7
	// End 6.0 values
}


// Values for settings.floating.align

app.media.align =
{
	topLeft:		1,
	topCenter:		2,
	topRight:		3,
	centerLeft:		4,
	center:			5,
	centerRight:	6,
	bottomLeft:		7,
	bottomCenter:	8,
	bottomRight:	9
	// End 6.0 values
}


// Values for settings.floating.canResize

app.media.canResize =
{
	no:			1,
	keepRatio:	2,
	yes:		3
	// End 6.0 values
}


// Values for settings.floating.over

app.media.over =
{
	pageWindow:	1,
	appWindow:	2,
	desktop:	3,
	monitor:	4
	// End 6.0 values
}


// Values for settings.floating.ifOffScreen

app.media.ifOffScreen =
{
	allow:			1,
	forceOnScreen:	2,
	cancel:			3
	// End 6.0 values
}


// Default value for settings.visible

app.media.defaultVisible = true;


// Values for rendition.type

app.media.renditionType =
{
	unknown:	0,		// rendition type not recognized by Acrobat
	media:		1,
	selector:	2
	// End 6.0 values
}


// Values for event.media.code in Status event

app.media.status =
{						// event.media.text contains:
	clear:		 1,		// empty string - clears any message
	message:	 2,		// general message
	contacting:	 3,		// hostname being contacted
	buffering:	 4,		// percent complete
	init:		 5,		// name of the player being initialized
	seeking:	 6		// nothing
	// End 6.0 values
}


// Values for event.media.closeReason in Close event

app.media.closeReason =
{
	general:	 1,
	error:		 2,
	done:		 3,
	stop:		 4,
	play:		 5,
	uiGeneral:	 6,
	uiScreen:	 7,
	uiEdit:		 8,
	docClose:	 9,
	docSave:	10,
	docChange:	11
	// End 6.0 values
}


// Values for player.open() return value

app.media.openCode =
{
	success:					0,
	failGeneral:				1,
	failSecurityWindow:			2,
	failPlayerMixed:			3,
	failPlayerSecurityPrompt:	4,
	failPlayerNotFound:			5,
	failPlayerMimeType:			6,
	failPlayerSecurity:			7,
	failPlayerData:				8
	// End 6.0 values
}


// Values for Error.raiseSystem

app.media.raiseSystem =
{
	fileError:	10
}


// Values for Error.raiseCode

app.media.raiseCode =
{
	fileNotFound:	17,
	fileOpenFailed:	18
}


// In a PDF event, these event.name values indicate page-level actions.

app.media.pageEventNames =
{
	Open:		true,
	Close:		true,
	InView:		true,
	OutView:	true
}


// Create and return a MediaPlayer without opening it.  Explicit values can be provided for all
// the arguments listed below. If this function is called from a rendition action, it will get
// the annot, rendition, and doc values from the event object if they are not explicitly provided.	
// Returns player, or null on failure. Does not throw exceptions.
// Any failures are reported to user, and result in null being returned. 
// Events may be fired as a result of closing an existing player (see below).
// Unless noStockEvents is true, stock event handlers are added to the returned player,
// and will be added to the annot (if present) when player.open() is called later.

// player = app.media.createPlayer({
//     doc: Doc, /* Required if both annot and rendition are omitted, e.g. for URL playback */
//     annot: ScreenAnnot, /* Required for docked playback unless it is found in the Event object
//       or settings.page is provided. The new player is associated with the annot. If a player
//       was already associated with the annot, it is stopped and closed. */
//     rendition: MediaRendition or RenditionList, /* Required unless rendition found in Event,
//       or URL is present */
//     URL: String, /* Either URL or rendition is required, with URL taking precedence */
//     mimeType: string, /* Optional, ignored unless URL is present. If URL is present, either
//       mimeType or settings.players, as returned by app.media.getPlayers(), is required */
//     settings: MediaSettings, /* Optional, overrides the rendition settings */
//     events: EventListener, /* Optional (if stock events are used, added after stock events) */
//     noStockEvents: Boolean, /* Optional, default = false, use stock events or not */
//     fromUser: Boolean, /* Optional, default depends on Event object */
//     showAltText: Boolean, /* Optional, default = true */
//     showEmptyAltText: Boolean /* Optional, default= ! fromUser */
// });

app.media.createPlayer = function( args )
{
	try
	{
		return app.media.priv.createPlayer( app.media.argsDWIM( args ) );
	}
	catch( e )
	{
		app.media.alert( 'Exception', args, { error: e } );
		return null;
	}
}


// Create, open, and return a MediaPlayer. See app.media.createPlayer() for argument details
// and other information.
// This method fires several events which may include Open, Ready, Play and Focus. 
// Returns player, or null on failure. Does not throw exceptions.
// Any failures are reported to user.

app.media.openPlayer = function( args )
{
	var player = null;
	try
	{
		// Do our own DWIM here to make sure args.doc is set in case of error
		args = app.media.argsDWIM( args );	

		player = app.media.createPlayer( args );
		if( player )
		{
			var result = player.open();		
			if( result.code != app.media.openCode.success )	
			{	
				player = null;	
				app.media.alert( 'Open', args, { code: result.code } );
			}
			else if( player.visible )
				player.setFocus();	// fires Focus event
		}
	}
	catch( e )
	{
		player = null;
		app.media.alert( 'Exception', args, { error: e } );
	}

	return player;
}


// Open a new media player using the current event or explicit args as in app.media.createPlayer().
// If an annot is provided or found in event object, and there is already a player open for that
// annot, then start or resume playback on that player.
// See app.media.openPlayer for argument details and other information.
// Returns player, or null on failure. Does not throw exceptions.
// Any failures are reported to user.

app.media.startPlayer = function( args )
{
	try
	{
		args = app.media.argsDWIM( args );	

		var player = args.annot && args.annot.player;
		if( player && player.isOpen )
			player.play();  // already opened, resume play
		else
			player = app.media.openPlayer( args );  // open a new player

		return player;
	}
	catch( e )
	{
		app.media.alert( 'Exception', args, { error: e } );
		return null;
	}
},


// app.media.Events constructor and prototype

// The Events constructor, events.add, and events.remove methods each takes any number of
// arguments, where each argument can be either an event listener object or a previously
// constructed app.media.Events object. The constructor and add() method add each listener
// object to the Events object, and the remove() method removes listener objects.

// An event listener object is a collection of event methods and optional custom properties or
// methods. Any method whose name matches /^on[A-Z]/ or /^after[A-Z]/ is an event method. Custom
// methods and properties in an event listener should use names that do not match these case
// sensitive patterns.

// When an event listener method is called, 'this' is the event listener object. The event
// listener can have custom properties like any other object. If an event listener is nested
// inside another function (such as a constructor), then the event methods can also directly
// access any variables defined in the parent function, even when the event method is called
// after the parent function returns.

// The same event listener object may be added into more than one Events object. The object is
// not copied; each Events object has a reference to the original event listener object, so any
// properties of the listener object are shared by every Events object it is added to.

// Implementation note:
// An app.media.Events object has a listeners property which is a object containing
// onVariousEvent and afterVariousEvent properties. Each of these properties is an array of
// references to event listener objects which contain the event methods.
// So, for example, after this call:
//   var events = new app.media.Events({ onPlay: function(e){} });
// events.listeners.onPlay[0] is a reference to a new { onPlay: function(e){} } object, and
// events.listeners.onPlay[0].onPlay is a reference to the onPlay method.

app.media.Events = function()
{
	this.listeners = {};			// start with empty listeners object
	this.dispatching = 0;			// not currently dispatching any events
	this.removed = {};				// listener names that need delayed removal
	this.privAddRemove( arguments, this.privAdd );  // add any event listener object arguments
}


app.media.Events.prototype =
{


// Add any number of event listener objects or other app.media.Events objects.
// events.add() may be called inside an event listener method, and any new listeners that are
// added for the current event will be called for that same event.
// If the same listener object is added twice, the second add is ignored.
// Listeners for a given event (e.g. onClose) are called in the order in which they were added.

// events.add( event listener or app.media.Events object(s) )

add: function()
{
	this.privAddRemove( arguments, this.privAdd );
},


// Remove any number of event listener objects or other app.media.Events objects.
// events.remove() may be called inside an event listener method, to remove the current listener
// or any other.

// events.remove( event listener or app.media.Events object(s) )

remove: function()
{
	this.privAddRemove( arguments, this.privRemove );
},


// Private method for events.add() and events.remove().
// Loop through all the listener methods in each argument and call doAddRemove for every one.

privAddRemove: function( args, doAddRemove )
{
	for( var i = 0;  i < args.length;  i++ )  // for each event listener object argument in passed array
	{
		var events = args[i];
		if( events.listeners )
		{
			// It's an app.media.Events object, add or remove every listener in each array
			for( var name in events.listeners )
			{
				var array = events.listeners[name];
				for( var i = 0;  i < array.length;  i++ )
				{
					doAddRemove.call( this, array[i], name );
				}
			}
		}
		else
		{
			// It's an event listener object, add or remove each method
			for( var name in events )
			{
				// Only interested in onFoo and afterFoo methods, not custom properties
				if( name.search(/^on[A-Z]/) == 0  ||  name.search(/^after[A-Z]/) == 0 )
				{
					doAddRemove.call( this, events, name );
				}
			}
		}
	}

	this.privSetDispatch();  // Add or remove the dispatch() method as needed
},


// Private method for events.add().
// Adds a reference to a listener object into events.listeners[name]
// Does nothing if listener already added.

privAdd: function( listener, name )
{
	if( typeof(listener) != "object"  ||  typeof(listener[name]) != "function" )
		return;  // not a valid object and method

	var array = this.listeners[name];  // get our existing event listener array
	if( ! array )
	{
		this.listeners[name] = [ listener ];  // no array yet, add array with one listener object
	}
	else  // we have a listener array, append listener to it if it's not already present
	{
		for( var i = 0;  i < array.length;  i++ )
		{
			if( array[i] === listener )
				return;  // already present, don't add another
		}

			array[i] = listener;  // append listener to array
		}
},


// Private method for events.remove().
// Removes a listener object reference from events.listeners[name]

privRemove: function( listener, name )
{
	var array = this.listeners[name];  // existing event listener array
	if( ! array )
		return;  // no listeners with this name

	for( var i = 0;  i < array.length;  i++ )  // Look for the listener object in the array
	{
		if( array[i] === listener )
		{
			// Found the listener in the array, decide what to do with it
			if( this.dispatching )  // Can't remove while dispatching, mark for later removal
				array[i] = null,  this.removed[name] = this.needCleanup = true;
			else if( array.length > 1 )
				array.splice( i, 1 );  // Remove listener from array
			else
				delete this.listeners[name];  // Last one, remove array entirely

			return;  // Listener is already in the array
		}
	}
},


// Private function for events.add(), events.remove() and events.privCleanup().
// Sets or deletes the dispatch method depending on whether there are any event listeners.

privSetDispatch: function()
{
	for( var name in this.listeners )  // are there any listeners?
	{
		this.dispatch = this.privDispatch;  // found a listener, set the dispatch method
		return;
	}

	delete this.dispatch;  // no listeners, remove the dispatch method
},


// events.privDispatch() is a private method that contains the code for events.dispatch().
// To dispatch an event, C++ code calls events.dispatch(), only if that method exists.
// The rest of the event dispatching machinery is implemented in JavaScript.
// We turn event dispatching on and off dynamically by setting and removing the events.dispatch
// property, which is a reference to events.privDispatch().
// If you call events.dispatch() directly from JavaScript, event.target.doc or event.media.doc
// must match the current document.
// You can implement your own event dispatcher from scratch by providing an events object
// with a dispatch method that takes an event argument as this method does.
// This function is reentrant.

privDispatch: function( event )
{
	if( !event.media )
		event.media = {};

	// PDF events may have spaces in their names, so make a copy of event.name with spaces removed
	event.media.id = event.name.replace( / /, '' );

	// Use doc and events properties in either event.target or in the event object itself
	if( event.target )
	{
		event.media.doc = event.target.doc;
		event.media.events = event.target.events;
	}

	++this.dispatching;  // if this.dispatching > 0, events.remove() will use deferred removal

	try
	{
		// First call immediate (onFoo) listener methods
		this.privDispatchNow( 'on', event );  // may reenter this function

		// Turn stopDispatch off in case an immediate listener turned it on
		delete event.stopDispatch;

		// If there are any deferred (afterFoo) listeners, post event to queue,
		// but don't bother if an immediate listener stopped all dispatching
		if( ! event.stopAllDispatch )
			if( this.listeners[ 'afterEveryEvent' ]  ||  this.listeners[ 'after' + event.media.id ] )
				app.media.priv.postEvent( event );
	}
	catch( e )
	{
		app.media.priv.trace( 'di throw: ' + e.message );
	}

	--this.dispatching;

	// If any event listeners were marked for removal while we were dispatching events,
	// and we are done with any nested dispatch calls, then clean up the listener arrays.
	if( this.needCleanup  &&  ! this.dispatching )
		this.privCleanup();
},


// Private method for events.dispatch().
// Clean up any event listener arrays that have had entries marked for removal.
// Each event name that needs to be cleaned up has an entry in this.removed.
// Each listener that is to be removed has been set to null.

privCleanup: function()
{
	for( var name in this.removed )
	{
		var array = this.listeners[name];
		for( var i = 0;  i < array.length;  i++ )
		{
			if( ! array[i] )
				array.splice( i--, 1 );  // Remove listener from array and back up index
		}

		if( array.length == 0 )
			delete this.listeners[name];  // Remove listener array if it's now empty
	}

	this.removed = {};

	this.privSetDispatch();  // Remove the dispatch method if there are no more listeners

	delete this.needCleanup;
},


// Private method for events.dispatch().
// Immediately dispatch a single event to all listeners for that event.
// prefix is 'on' or 'after', and event is the event object.
// Calls both EveryEvent listener methods and any specific listener methods for the event.
// This function is reentrant.

privDispatchNow: function( prefix, event )
{
	this.privCallMethods( event, prefix + 'EveryEvent' );  // may reenter this function
	this.privCallMethods( event, prefix + event.media.id );  // may reenter this function
},


// Private method for events.dispatch().
// Loop through the events.listeners[name] array and call each event listener method found
// there, with 'this' as the event listener object that contains the method.
// If new listeners are added while dispatching, they will also be called.
// This function is reentrant.

privCallMethods: function( event, name )
{
	var array = this.listeners[name];
	if( array )
	{
		// Call each listener method in the array
		for( var i = 0;  i < array.length;  i++ )
		{
			if( event.stopDispatch || event.stopAllDispatch )
				break;

			var listener = array[i];
			if( listener )	// listener is null if removed while dispatching
			{
				listener[name]( event );  // may reenter this function
			}
		}
	}
},


}
// end app.media.Events.prototype


// A simple event queue.

// app.media.priv.postEvent(event) and app.media.priv.dispatchQueuedEvents() use
// doc.media.priv.queue to manage a per-doc event queue.
// This private method has the same restrictions on calling it as app.media.Events.privDispatch().

app.media.priv.postEvent = function( event )
{
	var q = event.media.doc.media.priv.queue;
	if( ! q )
		q = event.media.doc.media.priv.queue = {};

	if( ! q.list )
		q.list = [];

	q.list.push( event );

	if( ! q.timer )
	{
		q.timer = app.setTimeOut( 'app.media.priv.dispatchPostedEvents(this);', 1, false ); // no disp while modal dlg up
		q.timer.media = { doc: event.media.doc };  // allow access to doc from timer obj
	}
}


// Called from the short timer set by app.media.priv.postEvent() to dispatch all posted events.

app.media.priv.dispatchPostedEvents = function( doc )
{
	try
	{
		// If doc already closed, bail! Closing doc does NOT unregister timeouts!
		// They may or may not fire depending on whether they are GCed before getting fired.
		// Event.target is our timeout obj.
		if ( event.target.media.doc.closed )
			return;	
		
		// Grab and delete queue--any new event queued while dispatching will be dispatched later
		var q = doc.media.priv.queue;
		var list = q.list;
		delete q.list;
		delete q.timer;

		for( var i = 0;  i < list.length;  i++ )
		{
			// Stop dispatching "after" events if the doc is closed, checked here in case an
			// event method closes the doc. Do not check in privCallMethods--an event method
			// that closes the doc should set event.stopDispatchAll.
			if( doc.closed )
				return;  

			var e = list[i];
			if( e.media.events )
				e.media.events.privDispatchNow( "after", e );
		}
	}
	catch( e )
	{
		app.media.priv.trace( 'dpe throw: ' + e.message );
	}
}


// app.media.Markers constructor and prototype

app.media.Markers = function( player )
{
	this.player = player;
}


app.media.Markers.prototype =
{

// Finds a marker by name, index number, time, or frame.
// Index numbers are not in any guaranteed order.
// If a time or frame is given, returns the nearest marker at or before that location.
// Returns null if no matching marker is found.
//
// marker = markers.get( cName );
// marker = markers.get({ name: cName });
// marker = markers.get({ index: nIndex });
// marker = markers.get({ time: nSeconds });
// marker = markers.get({ frame: nFrame });

get: function( m )
{
	if( ! this.privByIndex )
		this.player.privLoadMarkers();

	var retMarker = null;

	if( this.privByIndex.length > 0 )
	{
		switch( typeof(m) )
		{
			case 'string':
				retMarker = this.privByName[m];
				break;
	
			case 'object':
				retMarker = (
					m.name  !== undefined ? this.privByName[ m.name ] :
					m.index !== undefined ? this.privByIndex[ m.index ] :
					m.time  !== undefined ? this.privFind( 'time',  m.time  ) :
					m.frame !== undefined ? this.privFind( 'frame', m.frame ) :
					undefined );
				break;
		}
	}

	if( retMarker === undefined )
		retMarker = null;

	return retMarker;
},


// Private method for markers.get() to find a marker by time or frame.

privFind: function( prop, value )
{
	if( value < 0 )
		return;  // negative time or frame not allowed

	var array = this.privByIndex;
	var length = array.length;

	// Search for nearest marker <= passed value; does not assume any sort order.
	var nearIdx;
	var nearDist = Infinity;
	for( var i = 0;  i < length;  i++ )
	{
		// Test for undefined in case some markers have time and some have frame
		var v = array[i][prop];
		if( v !== undefined )
		{
			var dist = ( value - v );
			if( dist >= 0  &&  dist < nearDist )
			{
				// have a new "nearest marker <= value"
				nearIdx = i;
				nearDist = dist;		
			}
		}
	}

	if( nearIdx !== undefined )
		return array[ nearIdx ];
},


}
// end app.media.Markers.prototype


// app.Monitors constructor and prototype

app.Monitors = function()
{
	this.length = 0;
}


app.Monitors.prototype =
{


// monitors.clear()

clear: function()
{
	while( this.length > 0 )
		delete this[ --this.length ];
},


// monitors.push( value )
// Appends a reference to a monitor object to the array.

push: function( value )
{
	this[ this.length++ ] = value;
},


// monitors = monitors.select( monitorType, doc )
// Filter a Monitors array based on an app.media.monitorType value as used in PDF.
// doc is required if monitorType is app.media.monitorType.document or
// app.media.monitorType.nonDocument, otherwise it is ignored.
// Returns new array of references to the selected monitor objects.

select: function( monitorType, doc )
{
	switch( monitorType )
	{
		default:
		case app.media.monitorType.document:	return this.document(doc).primary();
		case app.media.monitorType.nonDocument:	return this.nonDocument(doc).primary();
		case app.media.monitorType.primary:		return this.primary();
		case app.media.monitorType.bestColor:	return this.bestColor().primary();
		case app.media.monitorType.largest:		return this.largest().primary();
		case app.media.monitorType.tallest:		return this.tallest().primary();
		case app.media.monitorType.widest:		return this.widest().primary();
	}
},


// monitors.filter( ranker, minRank )
// Returns a Monitors array containing the monitors that score the highest rank according to
// the ranker function. The ranker function takes a Monitor parameter and returns a numeric or
// boolean rank for it (or any type that can be converted to a number).
// A numeric rank may be any finite value.
// If minRank is not specified, the array returned always contains at least one element (unless
// the original array was already empty).
// If minRank is specified but the final rank is less, the array returned is empty.
// If multiple monitors tie for the highest rank, the returned array contains those monitors in
// the same order as the original array.

filter: function( ranker, minRank )
{
	var r = new app.Monitors;
	var rank = ( minRank != undefined ? minRank : -Infinity );

	for( var i = 0;  i < this.length;  i++ )
	{
		// Rank the next Monitor object
		var m = this[i];
		var mRank = ranker( m );

		// If it outranks the best previous ranking, clear the result list.
		// If it's the same rank, add it to the result list.
		if( mRank >= rank )
		{
			if( mRank > rank )
				r.clear();  // new outranks old, clear result array

			r.push( m );  // append new result to any same-ranked results
			rank = mRank;  // save new rank
		}
	}

	return r;
},


// monitors.bestColor( minColor )
// Returns a Monitors array containing the monitor(s) that have the greatest color depth.
// Returns empty array if minColor is specified and no monitor in the array has a color depth of
// at least minColor bits.

bestColor: function( minColor )
{
	return this.filter(
		function( m ) { return m.colorDepth; },
		minColor );
},


// monitors.bestFit( width, height, bRequire )
// Returns a Monitors array containing the monitor(s) that have at least the specified width and
// height with the least amount of excess area. If all monitors are smaller than the specified
// width and height, then returns an empty array if bRequire is true, or an array of the largest
// available monitors if bRequire is false.

bestFit: function( width, height, bRequire )
{
	var tiny = -1000000000;
	var area = ( width * height );

	return this.filter(
		function( m )
		{
			var mWidth  = m.rect[2] - m.rect[0];
			var mHeight = m.rect[3] - m.rect[1];

			// Rank lowest if it doesn't fit at all, else rank by least excess area
			return(
				width > mWidth  ||  height > mHeight  ?  tiny  :
				area - ( mWidth * mHeight ) );
		},
		bRequire ? ( tiny + 1 ) : tiny );
},


// monitors.desktop()
// Returns a Monitors array with a single Monitor that represents the entire virtual desktop:
//    rect = the union of all Monitor.rect values
//    workRect = the union of all the workRect values (may include parts of monitors that are 
//               outside their workRects).
//    colorDepth = the least color depth of any monitor
//    isPrimary = (not present)

desktop: function()
{
	if( ! this.length )
		return [];

	var r = { rect: [0,0,0,0], workRect: [0,0,0,0], colorDepth: Number.MAX_VALUE };

	for( var i = 0;  i < this.length;  i++ )
	{
		var m = this[i];

		r.rect = app.media.priv.rectUnion( r.rect, m.rect );
		r.workRect = app.media.priv.rectUnion( r.workRect, m.workRect );
		r.colorDepth = Math.min( r.colorDepth, m.colorDepth );
	}

	var result = new app.Monitors;
	result.push( r );

	return result;
},


// monitors.document( doc, bRequire )
// Returns a Monitors array containing the monitor(s) that display the greatest amount of the
// specified document.
// If bRequire is true, returns empty array if the document does not appear on any monitor.
// If bRequire is false and document does not appear on any monitor, returns array containing
// all monitors.

document: function( doc, bRequire )
{
	return this.mostOverlap( doc.outerDocWindowRect, bRequire ? 1 : undefined );
},


// monitors.largest( minArea )
// Returns a Monitors array containing the monitor(s) with the greatest area.
// Returns empty array if minArea is specified and the greatest area is less than that.

largest: function( minArea )
{
	return this.filter(
		function( m ) { return app.media.priv.rectArea( m.rect ); },
		minArea );
},


// monitors.leastOverlap( rect, maxOverlapArea )
// Returns a Monitors array containing the monitor(s) which have the least area overlapping rect.
// Returns empty array if maxOverlapArea is specified and all monitors overlap rect by a greater
// amount.

leastOverlap: function( rect, maxOverlapArea )
{
	if( maxOverlapArea !== undefined )  // if undefined must stay undefined (-undefined is NAN)
		maxOverlapArea = -maxOverlapArea;

	return this.filter(
		function( m ) { return -app.media.priv.rectIntersectArea( m.rect, rect ); }, maxOverlapArea );
},


// monitors.mostOverlap( rect, minOverlapArea )
// Returns a Monitors array containing the monitor(s) which have the most area overlapping rect.
// Returns empty array if minOverlapArea is specified and there is no monitor with at least that
// much overlap.

mostOverlap: function( rect, minOverlapArea )
{
	return this.filter(
		function( m ) { return app.media.priv.rectIntersectArea( m.rect, rect ); },
		minOverlapArea );
},


// monitors.nonDocument( doc, bRequire )
// Returns a Monitors array containing the monitor(s) that display none of, or the least amount
// of the specified document.
// If parts of the document appear on every monitor, then returns empty array if bRequire is true
// or a copy of the original monitors array if bRequire is false.

nonDocument: function( doc, bRequire )
{
	return this.leastOverlap( doc.outerDocWindowRect, bRequire ? 0 : undefined );
},


// monitors.primary()
// Returns a Monitors array containing at most one entry, the primary monitor.

primary: function()
{
	return this.filter(
		function( m ) { return m.isPrimary; },
		1 );
},


// monitors.secondary()
// Returns a Monitors array containing all secondary (non-primary) monitors.

secondary: function()
{
	return this.filter(
		function( m ) { return ! m.isPrimary; },
		1 );
},


// monitors.tallest( minHeight )
// Returns a Monitors array containing the monitor(s) with the greatest height.

tallest: function( minHeight )
{
	return this.filter(
		function( m ) { return m.rect[3] - m.rect[1]; },
		minHeight );
},


// monitors.widest( minWidth )
// Returns a Monitors array containing the monitor(s) with the greatest width.

widest: function( minWidth )
{
	return this.filter(
		function( m ) { return m.rect[2] - m.rect[0]; },
		minWidth );
},


}
// end app.Monitors.prototype


// app.media.Players constructor and prototype

app.media.Players = function()
{
	this.length = 0;
}


app.media.Players.prototype =
{


// Players.clear()

clear: function()
{
	while( this.length > 0 )
		delete this[ --this.length ];
},


// Players.push( value )
// Appends a reference to a Player object to the array.

push: function( value )
{
	this[ this.length++ ] = value;
},


// players = Players.select( args )
// Filters a Players array based on any of the PlayerInfo properties.
// The array is not in any particular order.
// The object argument lists the properties to filter on.
// String properties (id, name, version) can use either strings or regular expressions.
// Example: get all players with 'QuickTime' in the id:
//	var p = app.media.getPlayers().select({ id: /QuickTime/ });
// All specified properties must be present and must match exactly (or must pass regex match).
// If no properties are specified, all Players in the array will be present in the returned array.
// If no players in the array match the search criteria, returns an empty Players array.

select: function( args )
{
	var r = new app.media.Players;

	for( var i = 0;  i < this.length;  i++ )
	{
		var info = this[i];  // Get the PlayerInfo object
		var ok = true;

		for( var prop in args )  // check each property that the caller passed in
		{
			if( !( prop in info ) )
				return [];  // unknown selection property, probably future PDF version, give up

			// Handle either a regular expression or a string, number, or boolean comparison
			if( args[prop].exec ? args[prop].exec(info[prop]) == null : args[prop] != info[prop] )
			{
				ok = false;
				break;
			}
		}

		if( ok )
			r.push( info );  // passed all tests, append reference to PlayerInfo
	}

	return r;
},


}
// end app.media.Players.prototype


// app.media.MediaPlayer constructor and prototype

app.media.MediaPlayer = function()
{
}


app.media.MediaPlayer.prototype =
{


// MediaPlayer.open()

open: function()
{
	var ret;

	try
	{
		// Add stock annot events and cross-references only if we open the movie
		if( this.annot ) 
		{
			app.media.priv.AddStockEventsHelper( this.annot, app.media.getAnnotStockEvents( this.settings.windowType ) );
			this.annot.player = this;
		}

		ret = this.privOpen.apply( this, arguments );
		if( ret.code != app.media.openCode.success )
			app.media.removeStockEvents( this );
	}
	catch( e ) 
	{
		app.media.removeStockEvents( this );
		throw e;
	}
	
	return ret;
},


}
// end app.media.MediaPlayer.prototype


// Determine whether any media playback is allowed and return true if it is.
// If playback is not allowed, then alert the user and return false.

// bCanPlay = app.media.canPlayOrAlert({ doc: Doc });

app.media.canPlayOrAlert = function( args )
{
	var canPlay = args.doc.media.canPlay;
	if( canPlay.yes )
		return true;  // Playback is allowed

	app.media.alert( 'CannotPlay', args, { canPlayResult: canPlay } );

	return false;
}


// Return a settings object to play a rendition, if all playback requirements are met.
// Otherwise return settings to "play" alt text, if showAltText and showEmptyAltText allow and
// alt text is available, or else return null.

// settings = app.media.getRenditionSettings({
//	   doc: Doc,
//     settings: MediaSettings, /* Optional, shallow-copied into returned settings */
//     rendition: MediaRendition or RenditionList,
//     showAltText: Boolean, /* Optional, default = false */
//     showEmptyAltText: boolean, /* Optional, default = false */
//     fromUser: boolean /* Optional, default = false */
// });

app.media.getRenditionSettings = function( args )
{
	var settings;

	var selection = args.rendition.select( true );
	if( selection.rendition )
	{
		try
		{
			// Get playback settings from rendition - throws on failure, never returns null
			settings = selection.rendition.getPlaySettings( true );
			settings.players = selection.players;
			app.media.priv.copyProps( args.settings, settings );  // copy the user's settings

			return settings;
		}
		catch( e )
		{
			// FNF or open failure? Rethrow the exception unless we can handle it here
			if( e.name != "RaiseError" )
				throw e;

			if( e.raiseSystem != app.media.raiseSystem.fileError )
				throw e;

			if( e.raiseCode != app.media.raiseCode.fileNotFound  &&
				e.raiseCode != app.media.raiseCode.fileOpenFailed )
				throw e;

			app.media.alert( 'FileNotFound', args, { fileName: selection.rendition.fileName } );
		}
	}
	else  // no rendition in selection
	{
		app.media.alert( 'SelectFailed', args, { selection: selection } );
	}

	// Did we fail after finding a rendition? If so, use its alt text if allowed
	return app.media.getAltTextSettings( args, selection );
}


// Return the first media rendition in a rendition list, or null if there is no match.

app.media.getFirstRendition = function( list )
{
	for( var i = 0;  i < list.length;  i++ )
	{
		if( list[i].rendition.type == app.media.renditionType.media )
			return list[i].rendition;
	}

	return null;
}


// Return a settings object with a data property to play a URL.
// Any properties in args.settings are shallow-copied into the returned settings object.

// settings = app.media.getURLSettings({
//     URL: String, /* required */
//     mimeType: String, /* optional */
//     settings: MediaSettings /* optional */
// });

app.media.getURLSettings = function( args )
{
	// Get a data object for the URL and MIME type
	var settings =
	{
		data: app.media.getURLData( args.URL, args.mimeType )
	}

	app.media.priv.copyProps( args.settings, settings );  // copy the user's settings

	return settings;
}


// Return an alt text settings object for a selection, or null if there's no alt text available,
// or if alt text should not be used in this situation.
// Arguments are the same as app.media.getRenditionSettings().
// Any properties in args.settings are shallow-copied into the returned settings object.

app.media.getAltTextSettings = function( args, selection )
{
	if( ! args.showAltText )
		return null;

	var rendition = selection.rendition || app.media.getFirstRendition( selection.rejects );
	if( ! rendition )
		return null;

	settings = rendition.getPlaySettings( false );
	app.media.priv.copyProps( args.settings, settings );  // copy the user's settings

	// Use alt text only when docked (compute default windowType first if needed)
	if( ! settings.windowType ) 
		settings.windowType = app.media.priv.computeDefaultWindowType( args, settings );
	if( settings.windowType != app.media.windowType.docked )
		return null;

	// Get the alt text, or default text if none specified and showEmptyAltText is true
	var text = rendition.altText;
	if( text.length == 0 )
	{
		if( ! args.showEmptyAltText )
			return null;

		text = app.media.priv.getString( "IDS_ERROR_NO_ALT_TEXT_SPECIFIED" );
	}

	settings.data = app.media.getAltTextData( text ); 

	settings.players = [ app.media.priv.altTextPlayerID ];

	return settings;
}


// Add the standard event listeners to a player.
// If annot is specified, set up so when the player is opened, the annot will have its standard
// event listeners attached. The player.annot and annot.player cross-references will be 
// installed at the same time.
// The player must have a settings property. In the settings property, windowType and visible
// are the only values used here. The visible property may be modified here and restored later
// in the afterReady listener.

app.media.addStockEvents = function( player, annot )
{
	if( player.stockEvents )
		return;  // already added stock events

	app.media.priv.AddStockEventsHelper( player, app.media.getPlayerStockEvents( player.settings ) );

	if( annot )
	{
		// remember that annot needs stock events attached when player is opened
		player.annot = annot;
	}
}


// Private function to add stock events to an object. Saves a reference to the original stock
// events in object.stockEvents for later removal. object.stockEvents must not be modified after
// it is saved here, or removal will not work correctly.

app.media.priv.AddStockEventsHelper = function( object, events )
{
	object.stockEvents = events;

	if( ! object.events )
		object.events = new app.media.Events;

	object.events.add( events )
}


// Remove the standard event listeners and cross-references from a player and its associated annot.
// Does nothing if no stock events (never added or already removed).

app.media.removeStockEvents = function( player )
{
	if( ! player  ||  ! player.stockEvents )
		return;

	function removeProps( object )
	{
		if( object.events )
		{
			object.events.remove( object.stockEvents );
			delete object.stockEvents;
		}
	}

	removeProps( player );

	if( player.annot )
	{
		if( player.annot.stockEvents )
			removeProps( player.annot );

		delete player.annot.player;
		delete player.annot;
	}
}


// Return floating window rect for a doc, floating params, monitor to play on, and
// optional array containing the dimensions [l,r,t,b] of any additional controller UI.

// NOTE: this method is called from both JS and C++ code, so do not change its signature 
// without great care!

app.media.computeFloatWinRect = function( doc, floating, whichMonitor, uiSize )
{
	// Figure out rect in virtual desktop space that we are positioning relative to
	var overRect;
	switch( floating.over )
	{
		default:
		case app.media.over.pageWindow:
			overRect = doc.pageWindowRect;
			break;

		case app.media.over.appWindow:
			// Inner more consistent placement because no borders etc.
			overRect = doc.innerAppWindowRect;
			break;

		case app.media.over.desktop:
			overRect = app.monitors.desktop()[0].rect;
			break;

		case app.media.over.monitor:
			overRect = app.monitors.select( whichMonitor, doc )[0].workRect;
			break;
	}

	// Get the border sizes for this window
	var border = app.media.getWindowBorderSize( floating );

	// Align floating window with overRect according to align, using the
	// floating window rect plus the border sizes
	rect = app.media.priv.rectAlign(
		overRect, floating.align, 
		floating.width  + border[0] + border[2],
		floating.height + border[1] + border[3] );

	// Grow the rect by the UI size (if any)
	if( uiSize )
		rect = app.media.priv.rectGrow( rect, uiSize );

	return rect;
}


// Return a new instance of the standard player events for the given settings.
// In the settings property, windowType and visible are the only values used here.
// The settings.visible property may be modified here, and restored later in an afterReady event.
// If you call this method directly and there is an annot associated with the player, you must
// set player.annot and annot.player as shown in addStockEvents().

app.media.getPlayerStockEvents = function( settings )
{
	var events = new app.media.Events;

	if( app.media.trace )
		events.add( app.media.getPlayerTraceEvents() );

	events.add(
	{
		onClose: function( e )
		{
			var annot = e.target.annot;

			app.media.removeStockEvents( e.target );	// must do this before setFocus call below

			if( annot )
			{
				annot.extFocusRect = null;

				// If docked screen had focus when closed, and further playback is allowed,
				// put focus back on annot
				if( e.media.hadFocus  &&
				    e.target.settings.windowType == app.media.windowType.docked  &&
				    e.media.doc.media.canPlay.yes )
				{
					// Allow async setFocus since we're in event method
					// Does not fire stock annot Focus event because stock events removed above
					annot.setFocus( true );	
				}
			}
		},

		afterDone: function( e )
		{
			e.target.close( app.media.closeReason.done );  // fires Close and may fire Blur
		},

		afterError: function( e )
		{
			app.media.alert( 'PlayerError', e.target.args, { errorText: e.media.text } );
			e.target.close( app.media.closeReason.error );  // fires Close and may fire Blur
		},

		afterEscape: function( e )
		{
			e.target.close( app.media.closeReason.uiScreen );  // fires Close and may fire Blur
		}
	});

	// Add player event listeners for specific window types
	switch( settings.windowType )
	{
		case app.media.windowType.docked:
		{
			events.add(
			{
				onGetRect: function( e )
				{
					if( e.target.annot )
					{
						// Get the annot's rectangle and expand it to include any
                        // visible media player user interface. Return this rectangle in
						// the event object, and also use it as the annot's focus rect.
						e.target.annot.extFocusRect = e.media.rect =
							app.media.priv.rectGrow(
								e.target.annot.innerDeviceRect, e.target.uiSize );
					}
				},

				onBlur: function( e )
				{
					if( e.target.annot )
						e.target.annot.alwaysShowFocus = false;	
				},

				onFocus: function( e )
				{
					if( e.target.annot )
						e.target.annot.alwaysShowFocus = true;
				}
			});
		}
		break;

		case app.media.windowType.floating:
		{
			// Need either a rect or a width and height
			if ( !settings.floating.rect && ( !settings.floating.width || !settings.floating.height ) )
				app.media.priv.throwBadArgs();	// throw exception

			if( settings.visible === undefined )
				settings.visible = app.media.defaultVisible;

			if( settings.visible )
			{
				// Hide floating window while it's being created, then show it after the
				// controller dimensions are available
				settings.visible = false;

				events.add(
				{
					afterReady: function( e )
					{
						var floating = e.target.settings.floating;
						var rect = floating.rect;  // take user-provided rect, or calculate one
						if( ! rect )
						{
							rect = app.media.computeFloatWinRect( e.media.doc, floating,
								e.target.settings.monitorType, e.target.uiSize );
						}
						else
						{
							// Grow passed rect by UI size
							rect = app.media.priv.rectGrow( rect, e.target.uiSize );
						}

						// Are we supposed to move the window onscreen if it is offscreen?
						if( floating.ifOffScreen == app.media.ifOffScreen.forceOnScreen )
						{
							// Make sure window rect is totally onscreen, NOP if onscreen already																				   
							rect = app.media.constrainRectToScreen( rect,
								app.media.priv.rectAnchorPt( rect, floating.align ) );
						}

						// Set the outer rect
						e.target.outerRect = rect;

						// Show the window and give it the focus
						e.target.visible = true;
						e.target.setFocus();	// fires Focus event
					}
				});
			}
		}
		break;
	}

	return events;
}


// Return a new instance of the debug trace event listeners for a player.

app.media.getPlayerTraceEvents = function()
{
	return new app.media.Events(
	{
		onEveryEvent: function( e )
		{
			if( e.media.id != 'GetRect' )  // cannot trace inside onGetRect, it can hang Acrobat
				app.media.priv.trace( 'player event: on' + e.media.id );
		},

		afterEveryEvent: function( e )
		{
			app.media.priv.trace( 'player event: after' + e.media.id );
		},

		onScript: function( e )
		{
			app.media.priv.trace( "player onScript('" + e.media.command + "','" + e.media.param + "')" );
		},

		afterScript: function( e )
		{
			app.media.priv.trace( "player afterScript('" + e.media.command + "','" + e.media.param + "')" );
		},

		onStatus: function( e )
		{
			app.media.priv.trace( "player onStatus: " +
				( e.media.progress >= 0 ? e.media.progress + "/" + e.media.total + ", " : "" ) +
				"  status code: " + e.media.code + ": '" + e.media.text + "'" );
		},

		afterStatus: function( e )
		{
			app.media.priv.trace( "player afterStatus: " +
				( e.media.progress >= 0 ? e.media.progress + "/" + e.media.total + ", " : "" ) +
				"  status code: " + e.media.code + ": '" + e.media.text + "'" );
		}
	});
}


// Return a new instance of the standard annot events:
// For a docked player, handle Focus and Blur to give the player the focus instead of the annot.
// For any type of player, close the player on Destroy.

app.media.getAnnotStockEvents = function( windowType )
{
	var events = new app.media.Events;

	if( app.media.trace )
		events.add( app.media.getAnnotTraceEvents() );

	events.add(
	{
		onDestroy: function( e )
		{
			if( e.target.player ) 
			{ 
				// NOP if not open
				// fires Close and possibly other events
				e.target.player.close( app.media.closeReason.docChange );	
			}
		},
	} );

	if( windowType == app.media.windowType.docked ) 
	{
		events.add(
		{
			onFocus: function( e )
			{
				// If player is open, give it the focus. This event could be fired while doing
				// UI inside player.open() or the like.
				if( e.target.player.isOpen )
				{
					e.target.player.setFocus();  // fires Focus for player and Blur for annot
				}

				// Prevent any action from being fired for the Focus event, since focus
				// has already been removed inside setFocus(). If setFocus() not called,
				// we're in the process of some sort of UI and we don't want random actions
				// firing either.
				e.stopDispatch = true;
			},

			onBlur: function( e )
			{
				// As with the Focus event, prevent any action from being fired for the Blur event.
				// This also prevents anybody after us from seeing onBlur before onFocus because
				// of our setFocus() call within onFocus.
				e.stopDispatch = true;
			}
		});
	}

	return events;
}


// Return a new instance of the debug trace event listeners for an annot.

app.media.getAnnotTraceEvents = function()
{
	return new app.media.Events(
	{
		onEveryEvent: function( e )
		{
			app.media.priv.trace( 'annot event: on' + e.media.id );
		},

		afterEveryEvent: function( e )
		{
			app.media.priv.trace( 'annot event: after' + e.media.id );
		}
	});
}


// Make a shallow copy of args and run our "Do What I Mean" logic on it, to fill in default values
// used in app.media.createPlayer(). The original args object is not modified, and changes made
// later to the copy do not affect the original. Objects inside args are shared between the
// original and the copy, and changes made inside these objects are visible from both args objects.

// If args.annot or args.rendition are not defined, gets them from current event object.
// If args.doc is not defined, gets it from args.annot or args.rendition.
// Throws exception on failure.

app.media.argsDWIM = function( args )
{
	if( args && args.privDWIM )
		return args;  // already did a DWIM copy

	args = app.media.priv.copyProps( args );
	args.privDWIM = true;

	// Use annot and rendition passed in parameters, or get them from event object
	if( event && event.action )
	{
		if( ! args.annot ) 
			args.annot = event.action.annot;	// TODO: it'd be nice to verify type of annot here...

		if( ! args.rendition )
			args.rendition = event.action.rendition;
	}

	// Get doc from rendition or annot if args.doc not provided
	if( ! args.doc )
	{
			if( args.rendition && args.annot )
				if( args.rendition.doc != args.annot.doc )
					app.media.priv.throwBadArgs();
	
			if( args.rendition )
				args.doc = args.rendition.doc;
			else if( args.annot )
				args.doc = args.annot.doc;
		}

	// If fromUser is not specified, use !! to set it to true or false based on event name
	if( args.fromUser === undefined )
		args.fromUser = !!( event && event.name && ! app.media.pageEventNames[event.name] );

	if( args.showAltText === undefined )
		args.showAltText = true;

	if( args.showEmptyAltText === undefined )
		args.showEmptyAltText = ! args.fromUser;

	return args;
}


// Private function for app.media.priv.createPlayer().

app.media.priv.createPlayer = function( args )
{
	app.media.priv.trace( "app.media.priv.createPlayer" );

	if( ! args.doc )
		app.media.priv.throwBadArgs();	// doc is required

	if( ! app.media.canPlayOrAlert( args ) )
		return null;  // playback is not allowed, user has been notified

	if( args.annot && args.annot.player )
	{
		args.annot.player.close( app.media.closeReason.play );	// fires events
		// args.annot.player presumably is null now, unless onClose didn't null it out.
		// Cannot create new player in onClose so shouldn't have any issues there
	}

	var player = args.doc.media.newPlayer({ args: args });

	// Get a settings object for either a URL or a rendition, whichever was provided
	// URL wins if both present.
	player.settings =
		args.URL ? app.media.getURLSettings( args ) :
		args.rendition ? app.media.getRenditionSettings( args ) :
		app.media.priv.throwBadArgs();  // need either rendition or URL

	if( ! player.settings )
		return null;  // user has been notified already

	// If no windowType, compute default value
	if( ! player.settings.windowType ) 
		player.settings.windowType = app.media.priv.computeDefaultWindowType( args, player.settings );
	
	// If windowType couldn't be computed, throw
	if( ! player.settings.windowType ) 
		app.media.priv.throwBadArgs(); 

	switch( player.settings.windowType )
	{
		case app.media.windowType.docked:
		{
			if( player.settings.page === undefined )
			{
				if( ! args.annot )
					app.media.priv.throwBadArgs();  // need either an annot or a page number

				player.settings.page = args.annot.page;
			}
		}
		break;

		case app.media.windowType.fullScreen:
		{
			player.settings.monitor = app.monitors.select( player.settings.monitorType, args.doc );
		}
		break;
	}

	// Add any stock events to the player (and set up to add them to the annot later if needed). 
	// Even if the player is never opened, no need to remove them since they won't get used.
	if( ! args.noStockEvents )
		app.media.addStockEvents( player, args.annot );

	if( args.events )
	{
		if( ! player.events )
			player.events = new app.media.Events;

		player.events.add( args.events );  // Add caller's custom events
	}
		
	return player;
}


// Private function to get default windowType:
// docked if there is an annot,
// floating if there is no annot and there is a floating settings obj,
// undefined otherwise

app.media.priv.computeDefaultWindowType = function( args, settings )
{
	var retWT;
	if( args.annot )
		retWT = app.media.windowType.docked;
	else if( settings.floating )
		retWT = app.media.windowType.floating;

	return retWT;
}

// Display an alert, given an alert name (e.g. FileNotFound), createArgs object (the type of object
// processed by app.media.argsDWIM()), and optional specificArgs object which contains information 
// specific to the alert. The only property that MUST be present in createArgs is doc.
// NOTE: This implementation of app.media.alert is for private use within media.js only.
// Do not use it in PDF files! It will be changed in a future release.

app.media.alert = function( name, createArgs, specificArgs )
{
	var alertArgs = { name: name, createArgs: createArgs, alerterData: {} }
	app.media.priv.copyProps( specificArgs, alertArgs );

	// Set the default doc alerts if they are not already set
	if( !( 'alerts' in createArgs.doc.media ) )
		createArgs.doc.media.alerts = new app.media.Alerts;

	// If there are no alerts, use the doc alerts
	var curAlerts = createArgs.alerts;
	if ( !curAlerts )
		curAlerts = createArgs.doc.media.alerts; 

	// Keep on dispatching up parent chain of alerters until finally handled or no more alerters
	var gpMN = 'getParent';
	var handled = false;
	var lastAlerts = null;
	while ( curAlerts && !handled )
	{
		lastAlerts = curAlerts;

		// dispatch
		handled = dispatchAlert( curAlerts );
		if ( !handled ) 
		{
			// not handled, try to move to parent alerters
			var newAlerts = null;
			if ( gpMN in curAlerts )
			{
				newAlerts = curAlerts[ gpMN ]( alertArgs ); 
				if ( newAlerts ) 
				{
					// setup so parent alter can see child's alertArgs if it wants to
					// and to give reference to child alerter
					var newCI = {};
					newCI.alerts = curAlerts;
					newCI.alerterData = alertArgs.alerterData;
					newCI.next = alertArgs.childInfo;
					alertArgs.childInfo = newCI;

					// Clear stuff out so new alerter gets fresh shot
					alertArgs.alerterData = {};
					delete alertArgs.stop;
					delete alertArgs.sendToParent;
				}
			}
					
			curAlerts = newAlerts;
		}
	}
	// In inverse order, call afterParent as needed (not for top of course)
	curAlerts = lastAlerts;
	while ( alertArgs.childInfo ) {

		var newPI = {};
		newPI.alerts = curAlerts;
		newPI.alerterData = alertArgs.alerterData;
		newPI.next = alertArgs.parentInfo;
		alertArgs.parentInfo = newPI;

		alertArgs.alerterData = alertArgs.childInfo.alerterData;
		curAlerts = alertArgs.childInfo.alerts;
		alertArgs.childInfo = alertArgs.childInfo.next;

		var method = curAlerts[ 'afterParent' ];
		if( typeof method == 'function' )
			method.call( curAlerts, alertArgs, handled );
	}



	// Local function to dispatch to an alerts object and return true if handled
	function dispatchAlert( alerts )
	{
		var other = 'alertOther';
		var alertMtdName = 'alert' + name;

		if( alertMtdName in alerts || other in alerts )
		{
			callMethod( 'before' );

			if ( ! callMethod( alertMtdName ) )
				callMethod( other );

			callMethod( 'after' );

			return !alertArgs.sendToParent;
		}

		// Local function to call a single alerts method and return true if it is present
		function callMethod( methodName )
		{
			if( methodName in alerts  &&  ! alertArgs.stop  &&  !alertArgs.sendToParent )
			{
				var method = alerts[methodName];
				if( typeof method == 'function' )
					method.call( alerts, alertArgs );

				return true;
			}
		}

		return false;
	}
}


// app.media.Alerts - constructs a stock alerts object
// If an alert is the result of a user-triggered event, always show alert with no checkbox.
// If not user triggered, we include a "don't show again" checkbox and save its state,
// but don't show the alert if the user has previously checked that box in this document.
// The "don't show again" state is shared by all alerts (not tracked for each different type).
// NOTE: This implementation of app.media.Alerts is for private use within media.js only.
// Do not use it in PDF files! It will be changed in a future release.

app.media.Alerts = function()
{
	return {

		before: function( alertArgs )
		{
			if( ! alertArgs.createArgs.fromUser  &&  this.skip )
				alertArgs.stop = true;
		},

		after: function( alertArgs )
		{
			if( ! alertArgs.createArgs.fromUser )
				this.skip = alertArgs.alerterData.skip;
		},

		alertCannotPlay: function( alertArgs )
		{
			var canPlay = alertArgs.canPlayResult;
			if( ! canPlay.canShowUI )
				alertArgs.stop = true;  // Playback not allowed and may not show UI -- suppress after method
			else 
			{
				if( canPlay.no.authoring )
				{
					// Playback is not allowed while authoring
					alertArgs.alerterData.skip = this.privOK( alertArgs.createArgs, "IDS_PLAYBACK_DISALLOWED_WHILE_AUTHORING" );
				}
				else if( canPlay.no.security )
				{
					// User prefs say "no multimedia"
					alertArgs.alerterData.skip = this.privOK( alertArgs.createArgs, "IDS_PLAYBACK_DISALLOWED_CONFIGURATION" );
				}
				else 
				{
					// as of now, will not get here -- should probably put up generic error though just in case...
				}
			}
		},

		alertException: function( alertArgs )
		{
			// No don't show again checkbox for exceptions 
			app.alert( alertArgs.error.message );
		},

		alertFileNotFound: function( alertArgs )
		{
			alertArgs.alerterData.skip = app.media.alertFileNotFound( alertArgs.createArgs.doc, alertArgs.fileName, ! alertArgs.createArgs.fromUser );
		},

		alertOpen: function( alertArgs )
		{
			// TODO - show UI here, except if already shown (e.g. for failPlayerSecurityPrompt)
			// may want more info from open (e.g. to tell us if UI was shown already)
		},

		alertPlayerError: function( alertArgs )
		{
			alertArgs.alerterData.skip = this.privOK( alertArgs.createArgs, "IDS_JS_PLAYBACK_ERROR", alertArgs.errorText );
		},

		alertSelectFailed: function( alertArgs )
		{
			alertArgs.alerterData.skip = app.media.alertSelectFailed(
				alertArgs.createArgs.doc, alertArgs.selection.rejects, ! alertArgs.createArgs.fromUser, alertArgs.createArgs.fromUser );
		},

		alertOther: null,  // ignore any other alerts

		// Display a simple "OK" alert with optional "don't show again" checkbox.
		// Return checkbox result value, or undefined if no checkbox.
		privOK: function( createArgs, idMsg, strAppend )
		{
			var a = { cMsg: app.media.priv.getString(idMsg) + ( strAppend || '' ),
					  nIcon: 0, nType: 0, oDoc: createArgs.doc };

			if( ! createArgs.fromUser )
				a.oCheckbox = { cMsg: app.media.priv.getString("IDS_DONOT_SHOW_AGAIN_DOC"),
								bInitialValue: false };

			app.alert( a );

			if( a.oCheckbox )
				return a.oCheckbox.bAfterValue;
		},
	}
}


// Debugging code

app.media.priv.dumpObject = function( obj, str, bValues )
{
	if( ! str )
		str = "";
	else
		str += " ";

	str += "(" + obj + ") [" + typeof(obj) + "]\n";

	for( var prop in obj )
		str += "   " + prop + ( bValues ? ": " + obj[prop] : "" ) + "\n";

	app.media.priv.trace( str );
}


app.media.priv.dumpNames = function( obj, str )
{
	app.media.priv.dumpObject( obj, str, false );
}


app.media.priv.dumpValues = function( obj, str )
{
	app.media.priv.dumpObject( obj, str, true );
}


app.media.priv.dumpArray = function( array, str )
{
	if( ! str )
		str = "";
	else
		str += " ";

	str += "(" + array + ") [" + typeof(array) + "]\n{ ";

/*
	if( array.length )
		app.alert( "has length" );
	else
		app.alert( "no length" );
*/

	for( var i = 0;  i < array.length;  i++ )
		str += array[i] + ( i < array.length - 1 ? ", " : " }" );

	app.media.priv.trace( str );
}


app.media.priv.trace = function( str )
{
	if( app.media.trace )
		console.println( str );
}


// Private function called in a rendition action. Puts up UI on failure.

app.media.priv.stopAnnotPlayer = function()
{
	try
	{
		annot = event.action.annot;
		if( annot.player )
			annot.player.close( app.media.closeReason.stop );	// fires Close event, may fire Blur
	}
	catch( e )
	{
		app.alert( e.message );
	}
}


// Private function called in a rendition action. NOP if already paused. Puts up UI on failure.

app.media.priv.pauseAnnotPlayer = function()
{
	try
	{
		annot = event.action.annot;
		if( annot.player )
			annot.player.pause();
	}
	catch( e )
	{
		app.alert( e.message );
	}
}


// Private function called in a rendition action. NOP if not paused. Puts up UI on failure.

app.media.priv.resumeAnnotPlayer = function()
{
	try
	{
		annot = event.action.annot;
		if( annot.player )
			annot.player.play();
	}
	catch( e )
	{
		app.alert( e.message );
	}
}


// Enumerate and copy properties from one object to another object or to a new object, and
// return the resulting object. This is a shallow copy: any objects referenced by the from
// object will now be referenced by both the from and to objects. 

app.media.priv.copyProps = function( from, to )
{
	if( ! to )
		to = {};

	if( from ) 
	{
		for( var name in from )
			to[name] = from[name];
	}

	return to;
}


// Rectangle utility functions


// Tables used  by rectAlign to map app.media.align values to window positioning multipliers.

// see app.media.align.*       un    tl    tc    tr    cl    c     cr    bl    bc    br
app.media.priv.xPosTable =  [ 0.5,  0.0,  0.5,  1.0,  0.0,  0.5,  1.0,  0.0,  0.5,  1.0 ];
app.media.priv.yPosTable =  [ 0.5,  0.0,  0.0,  0.0,  0.5,  0.5,  0.5,  1.0,  1.0,  1.0 ];


// Given an app.media.align value and a desired width and height, return a rectangle
// aligned with rect according to the align value.

app.media.priv.rectAlign = function( rect, align, width, height )
{
	if( ! align )
		align = app.media.align.center;

	var x = rect[0] + ( rect[2] - rect[0] - width  ) * app.media.priv.xPosTable[align];
	var y = rect[1] + ( rect[3] - rect[1] - height ) * app.media.priv.yPosTable[align];

	return [ x, y, x + width, y + height ];
}


// Given an app.media.align value and a rect, return an anchor point

app.media.priv.rectAnchorPt = function( rect, align )
{
	if( ! align )
		align = app.media.align.center;

	var x = rect[0] + ( ( rect[2] - rect[0] ) * app.media.priv.xPosTable[align] );
	var y = rect[1] + ( ( rect[3] - rect[1] ) * app.media.priv.yPosTable[align] );

	return [ x, y ];
}


// Returns the area of a rectangle.  If rect is empty, 0 is returned.

app.media.priv.rectArea = function( rect )
{
	if( app.media.priv.rectIsEmpty( rect ) )	// empty rect might be [10,10,0,0] so cannot just do the math
		return 0;
	else
		return ( rect[2] - rect[0] ) * ( rect[3] - rect[1] );
}


// Returns rect grown by size, an array of four values giving the amount to grow each edge.
// Returned rect is a new object, input rect is not modified.

app.media.priv.rectGrow = function( rect, size )
{
	return [
		rect[0] - size[0],
		rect[1] - size[1],
		rect[2] + size[2],
		rect[3] + size[3]
	];
}


// Returns the intersection of two rectangles.
// If either input rect is empty, or there is no intersection, [0,0,0,0] is returned.
// Returned rect is a new object, input rects are not modified.

app.media.priv.rectIntersect = function( rectA, rectB )
{
	var newRect;

	if( app.media.priv.rectIsEmpty(rectA) || app.media.priv.rectIsEmpty(rectB) )
	{
		newRect = [ 0, 0, 0, 0 ];
	}
	else
	{
		newRect =
		[
			Math.max( rectA[0], rectB[0] ),
			Math.max( rectA[1], rectB[1] ),
			Math.min( rectA[2], rectB[2] ),
			Math.min( rectA[3], rectB[3] )
		];

		if( app.media.priv.rectIsEmpty( newRect ) )
			newRect = [ 0, 0, 0, 0 ];
	}

	return newRect;
}


// Take the intersection of two rectangles and return its area.

app.media.priv.rectIntersectArea = function( rectA, rectB )
{
	return app.media.priv.rectArea( app.media.priv.rectIntersect( rectA, rectB ) );
}


// Is a rectangle empty? 

app.media.priv.rectIsEmpty = function( rect )
{
	return  ! rect  ||  rect[0] >= rect[2]  ||  rect[1] >= rect[3];
}


// Returns new object that contains same values (not custom values) as input rect.

app.media.priv.rectCopy = function( rect )
{
	return [ rect[0], rect[1], rect[2], rect[3] ];
}


// Returns the union of two rectangles.
// Returned rect is a new object, input rects are not modified.

app.media.priv.rectUnion = function( rectA, rectB )
{
	return(
		app.media.priv.rectIsEmpty(rectA) ? app.media.priv.rectCopy( rectB ) :
		app.media.priv.rectIsEmpty(rectB) ? app.media.priv.rectCopy( rectA ) :
		[
			Math.min( rectA[0], rectB[0] ),
			Math.min( rectA[1], rectB[1] ),
			Math.max( rectA[2], rectB[2] ),
			Math.max( rectA[3], rectB[3] )
		]
	);
}


// Get a resource string

app.media.priv.getString = function( idString )
{
	return app.getString( 'Multimedia', idString );
}


// Return a value or a default if the value is undefined

app.media.priv.valueOr = function( value, def )
{
	return value !== undefined ? value : def;
}


// Private constants

app.media.priv.altTextPlayerID = 'vnd.adobe.swname:ADBE_AltText';


// End of media.js


