/**
 * Returns the collected configuration properties.
 * @param config       Object An object containing configuration
 *                            parameters for the player.
 * @param flashVersion Number A required flash version.
 * @returns String
 */
function createExperience(config, flashVersion)
{
	config.bgcolor      = config.preloadBackColor ? config.preloadBackColor : config.bgcolor;
	config.flashVersion = flashVersion ? flashVersion : (config.requireFlash8 ? 8 : 7);
	
	var player = bcExperience.createPlayer(config);
	bcExperience._players.push(player);
	
	return player.config.flashId;
};

var bcExperience = {
	'cdnURL' : 'http://admin.brightcove.com',
	'servicesURL' : 'http://services.brightcove.com/services',
	'viewerSecureGatewayURL' : 'https://services.brightcove.com/services/amfgateway',
	
	/**
	 * Default player options. Can be overridden for all players by calling bcExperience.init
	 * with options as parameters, or normally for individual players.
	 */
	'options' : {
		'width' : '100%',
		'height' : '100%',
		'autoStart' : false,
		'preloadBackColor' : '#999999',
		'allowscriptaccess' : 'always',
		'allowfullscreen' : true,
		'seamlesstabbing' : false,
		'swliveconnect' : true, // Not necessary, but left in for backward compatibility
		'wmode' : 'window',
		'quality' : 'high',
		'bgcolor' : '#999999',
		'flashVersion' : 7,
		'objectPrefix' : 'bcPlayer',
		'externalAds' : false,
		'fromLink' : false
	},
	
	// _players is used by createExperience to store collected,
	// uninitialised players; players is for initialised players
	'_players' : [],
	'_evalFrame' : null,
	'_lastExperienceNumber' : 0,
	'_scriptText' : [],
	'_tdScripts' : [],
	'_runner' : null,
	'_ceFunction' : 'window.createExperience = function() { window.experienceNumber++; return "bcPlayer" + new Date().getTime().toString(); };',
	
	'players' : [],
	'playerIndex' : -1,
	'initialised' : false,
	'initialising' : false,
	'ID_DELIM' : '|||',
	'xmlns' : 'http://www.w3.org/1999/xhtml',
	'flashVersion' : 0,
	'flashCallback' : null,
	
	'version' : 14775,
	
	'_testThis' : function()
	{
		console.log(this);
	},
	
	/**
	 * Detects the version of Flash installed on the computer.
	 * @returns Flash version.
	 */
	'detectFlashVersion' : function()
	{
		if(this.flashVersion)
		{
			return this.flashVersion;
		}
	
		if(navigator.plugins && navigator.plugins.length > 0)
		{
			for(var i = navigator.plugins.length - 1; i >= 0; i--)
			{
				var plugin = /Flash ([0-9]+)\./.exec(navigator.plugins[i].description);
				if(plugin)
				{
					var flashVersion = parseInt(plugin[1]);
					continue;
				}
			}
		}
		else if(window.ActiveXObject)
		{
			try
			{
				var flash = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");
				var flashVersion = / ([0-9]+),/.exec(flash.GetVariable('$version'))[1];
			}
			catch(e) {}
		}

		return this.flashVersion = flashVersion || 0;
	},
	
	/**
	 * Extends a copy of an object with properties from another object.
	 * @param dst Object JavaScript object.
	 * @param src Object JavaScript object.
	 * @returns Object
	 **/
	'extend' : function(dst, src)
	{
		for(var property in src)
		{
			dst[property] = src[property];
		}
		
		return dst;
	},
	
	/**
	 * Creates an arbitrary HTML element and returns it, using a namespace if desired.
	 * @param el String Element tagName.
	 * @returns HTMLElement
	 */
	'createElement' : function(el, ns)
	{
		if(document.createElementNS)
		{
			return document.createElementNS(ns || this.xmlns, el);
		}
		else
		{
			return document.createElement(el);
		}
	},
	
	/**
	 * Creates an appropriate player object or an anchor with the
	 * upgrade message if Flash is too old or missing.
	 * @param config      Object Configuration variables.
	 * @param playerIndex Number The position of this player within
	 *                           the bcExperience.players array.
	 * @returns HTMLObjectElement|HTMLAnchorElement
	 */
	'createPlayer' : function(config, playerIndex)
	{
		if(!config)
		{
			throw bcExperience.i18n.MISSING_CONFIG;
			return false;
		}

		config = this.extend(this.extend({}, this.options), config);

		if(!playerIndex)
		{
			var playerIndex = ++this.playerIndex;
		}
		else if(this.playerIndex < parseInt(playerIndex))
		{
			this.playerIndex = parseInt(playerIndex);
		}
		else
		{
			throw bcExperience.i18n.INDEX_TOO_LOW;
			return false;
		}
	
		// Needs a newer version of Flash
		if(config.flashVersion > this.detectFlashVersion())
		{
			var img = this.createElement('img');
			img.src = bcExperience.i18n.FLASH_TOO_OLD_IMAGE;
			img.alt = bcExperience.i18n.FLASH_TOO_OLD;

			var player = this.createElement('a');
			player.href  = 'http://www.adobe.com/products/flashplayer';
			player.title = bcExperience.i18n.GO_TO_ADOBE;
			player.className += ' bcUpgradeNotice';
			
			player.appendChild(img);
		}
		else
		{
			var dataURI = this.getFederatedURL();
			
			var params = [];
			
			// Create "base" Flash parameter
			var param   = this.createElement('param');
			param.name  = 'base';
			param.value = this.cdnURL + '/viewer/';
			params.push(param);
			
			if(!config.flashId)
			{
				config.flashId = config.objectPrefix + playerIndex;
			}
			else
			{
				config.flashId = config.flashId + playerIndex;
			}
			
			// Copied from old code. Still have no idea what it does.
			config.externalAds = (typeof(playAd) != 'undefined');
			
			// Override player parameters from the query string
			// pid is the player ID, tid is a title (video) ID, lid is a lineup ID
			var pid = parseInt(this.getQueryValue('bcpid')) || null;
			if(!pid || pid.length == 0 || pid == config.playerId)
			{
				var titleParam = parseInt(this.getQueryValue('bctid')) || null;
				if(titleParam)
				{
					config.videoId   = titleParam;
					config.autoStart = true;
					config.fromLink  = true;
				}
				var lineupParam = parseInt(this.getQueryValue('bclid')) || null;
				if(lineupParam)
				{
					config.lineupId = lineupParam;
				}
			}
			
			for(var property in config)
			{
				switch(property)
				{
					// Flash player parameters
					case 'width':
					case 'height':
					case 'menu':
					case 'quality':
					case 'scale':
					case 'align':
					case 'salign':
					case 'wmode':
					case 'bgcolor':
					case 'flashvars':
					case 'allowscriptaccess':
					case 'allowfullscreen':
					case 'seamlesstabbing':
					case 'swliveconnect':
						var param   = this.createElement('param');
						param.name  = property;
						param.value = config[property];
						params.push(param);
					break;
					// Anything else is passed back to the SWF
					default:
						dataURI += '&' + encodeURIComponent(property) + '=' + encodeURIComponent(config[property]);
					break;
				}
			}

			if(window.ActiveXObject)
			{
				var fscommand     = this.createElement('script');
				fscommand.id      = '_fs_' + config.flashId;
				fscommand.text    = 'bcExperience.onFSCommand(command, args);';
				fscommand.event   = 'FSCommand(command, args)';
				fscommand.htmlFor = config.flashId;
				
				// The players stop loading when FSCommand scripts are
				// appended to the document unless we do this stupid trick
				var container = this.createElement('span');
				container.id            = '_' + config.flashId;
				container.style.display = 'inline-block';
				//container.innerHTML     = player.outerHTML;

				var player = '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'
				+ ' codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=' + config.flashVersion + ',0,0,0"'
				+ ' id="' + config.flashId + '"'
				+ ' width="' + config.width + '"'
				+ ' height="' + config.height + '"'
				+ ' class="bcPlayer">';
				
				player += '<param name="movie" value="' + this._htmlentities(dataURI) + '">';
				
				for(var i = 0; i < params.length; i++)
				{
					player += params[i].outerHTML;
				}
				
				player += '</object>';
				
				container.innerHTML = player;

				/*var player        = this.createElement('object');
				player.classid    = 'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000';
				player.codebase   = 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=' + config.flashVersion + ',0,0,0';
				player.id         = config.flashId;
				player.width      = config.width;
				player.height     = config.height;
				player.className += ' bcPlayer';

				var param = this.createElement('param');
				param.name  = 'movie';
				param.value = dataURI;
				params.push(param);
				
				for(var i = 0; i < params.length; i++)
				{
					player.appendChild(params[i]);
				}*/

				player              = container;
				player.config       = config;
				player.fsCommandFix = fscommand;
			}
			else
			{
				var player  = this.createElement('object');
				player.type = 'application/x-shockwave-flash';
				player.data = dataURI;
				player.id   = config.flashId;
				player.width      = config.width;
				player.height     = config.height;
				player.className += ' bcPlayer';

				for(var i = 0; i < params.length; i++)
				{
					player.appendChild(params[i]);
				}
				
				player.config = config;
			}
		}
		
		return player;
	},
	
	/**
	 * Gets the value of a name-value pair from a query string.
	 * If there are multiple matches only the last will be returned,
	 * unless the name ends with [] to indicate an array.
	 * If there are no matches, returns defaultReturn.
	 * @param name          String  Name to match with a value.
	 * @param defaultReturn Any     A default value to return if no matches are
	 *                              found.
	 * @param match         String  A query string to use instead of
	 *                              window.location.search.
	 * @returns defaultReturn
	 */
	'getQueryValue' : function(name, defaultReturn, match)
	{
		if(defaultReturn == undefined)
		{
			var defaultReturn = false;
		}
	
		var findMatch = new RegExp("[?;&]" + encodeURIComponent(name) + "=([^&;]*)", "g"), matches = [], result;
		
		while((result = findMatch.exec(match || window.location.search)) != undefined)
		{
			matches.push(decodeURIComponent(result[1]));
		}

		if(matches.length == 0)
		{
			return defaultReturn;
		}
		else if(name.substr(name.length - 2, 2) == '[]')
		{
			return matches;
		}
		else
		{
			return matches.pop();
		}
	},
	
	/**
	 * Gets the URL of the federated SWF.
	 * @param version Number If set, enforces a specific federated SWF version.
	 * @returns String
	 */
	'getFederatedURL' : function(version)
	{
		if(!version)
		{
			var version = this.detectFlashVersion();
		}
		
		var url = this.cdnURL;
		
		/*if(version >= 9)
		{
			url += '/viewer/federated_f9.swf?';
		}
		else*/ if(version >= 8)
		{
			url += '/viewer/federated_f8.swf?';
		}
		else if(version <= 7)
		{
			url += '/viewer/federated.swf?domain=' + document.domain + '&';
		}
		
		return url += 'servicesURL=' + encodeURIComponent(this.servicesURL) +
			'&viewerSecureGatewayURL=' + encodeURIComponent(this.viewerSecureGatewayURL) +
			'&cdnURL=' + encodeURIComponent(this.cdnURL) +
			'&buildNumber=' + this.version +
			'&ranNum=' + Math.floor(Math.random()*1000000);
	},
	
	/**
	 * Wrapper function for initialisation.
	 * @param options Object A list of parameters to override the default player options
	 *                       for all players.
	 */
	'init' : function(options)
	{
		if(bcExperience.initialised || bcExperience.initialising)
		{
			return;
		}
		
		bcExperience.initialising = true;
		window.__BCEVALWINDOW__ = true;
		
		// Workaround for IE JScript bug that causes external scripts
		// to be evaluated twice in the context of the parent window
		bcExperience._players.originalLength = bcExperience._players.length;

		// The Event object is sent as the first parameter if this is
		// called from an event listener; we don't want the config
		// to be extended with its parameters.
		if(typeof options != 'object' || options.eventPhase)
		{
			var options = {};
		}

		bcExperience._init.call(bcExperience, options);
	},

	/**
	 * Initialises bcPlayer objects.
	 * @param options Object A list of parameters to override the default player options
	 *                       for all players.
	 */
	'_init' : function(options)
	{
		// Allows default options to be overridden by the call to init
		// Of course, this will only affect players that are created programatically
		// maybe don't put it here??
		if(typeof options == 'object')
		{
			this.options = this.extend(this.options, options);
		}
		
		// Clone the current document so we can delete the scripts from it
		// before inserting it into a new frame
		if(window.ActiveXObject)
		{
			// Internet Explorer CRASHES if we try to clone the root documentElement
			var thisDocument = document.getElementsByTagName('body')[0].cloneNode(true);
			var tdScripts    = document.body.getElementsByTagName('script');
		}
		else
		{
			var thisDocument = document.documentElement.cloneNode(true);
			var tdScripts    = thisDocument.getElementsByTagName('body')[0].getElementsByTagName('script');
		}
		
		// Collect the script text and then delete the relevant nodes

		var scriptText = [];
		for(var i = 0; i < tdScripts.length; i++)
		{
			if(tdScripts[i].src)
			{
				scriptText.push({ 'type' : 'external', 'src' : tdScripts[i].src });
			}
			else
			{
				// innerHTML is required for Safari
				scriptText.push({ 'type' : 'inline', 'src' : (tdScripts[i].text || tdScripts[i].innerHTML) });
			}
		}
		
		if(window.ActiveXObject)
		{
			var tdScripts = thisDocument.getElementsByTagName('script');
		}
		
		// This is separated from the above for loop because IE sucks
		// and does not clone the content of <script> elements with
		// cloneNode
		for(var i = 0; i < tdScripts.length; i++)
		{
			tdScripts[i].parentNode.removeChild(tdScripts[i]);
			i--; // we've just reduced the length of the nodelist by one
		}
		
		// Create a separate context for re-evaluating the scripts
		var evalFrame = this.createElement('iframe');
		evalFrame.style.position = 'absolute';
		evalFrame.style.top = '0';
		evalFrame.style.left = '-32767em';
		evalFrame.style.width = '0';
		evalFrame.style.height = '0';
		evalFrame    = document.body.appendChild(evalFrame);
		this._domEvalFrame = evalFrame;
		evalFrame    = frames[frames.length - 1];
		
		this._evalFrame  = evalFrame;
		
		if(document.doctype)
		{
			var doctype = '<!DOCTYPE ' + document.doctype.name + ' PUBLIC "' + this._htmlentities(document.doctype.publicId) + '" "' + this._htmlentities(document.doctype.systemId) + '">' + "\n";
		}
		else
		{
			var doctype = '';
		}

		this._evalFrame.document.open();
		if(window.ActiveXObject)
		{
			// More work-arounds for IE's totally crap engine
			this._evalFrame.document.write(doctype + '<html><script type="text/javascript">parent.bcExperience._evalFrame.doEval = function(s) { return eval(s); }</script>'
			+ document.getElementsByTagName('head')[0].outerHTML
			+ thisDocument.outerHTML
			+ '</html>');
		}
		else
		{
			this._evalFrame.document.write(doctype + '<html><script type="text/javascript">parent.bcExperience._evalFrame.doEval = function(s) { return eval(s); }</script>' + thisDocument.innerHTML + '</html>');
		}
		this._evalFrame.document.close();

		this._scriptText = scriptText;
		this._scriptText.originalLength = this._scriptText.length;
		this._runner = setInterval(this._initCheckFrameLoad, 25);
	},
	
	/**
	 * Checks to see if the body of the new document has been parsed into the DOM yet
	 * and continues execution when it has.
	 */
	'_initCheckFrameLoad' : function()
	{
		if(bcExperience._evalFrame.document.getElementsByTagName('body') && bcExperience._evalFrame.document.getElementsByTagName('body').length)
		{
			clearInterval(bcExperience._runner);
			bcExperience._initDoNextScript();
		}
	},
	
	/**
	 * Checks if the current script has finished executing and loads the
	 * next one if it has.
	 */
	'_initCheckNextScript' : function()
	{
		if(bcExperience._players.length > bcExperience._players.originalLength)
		{
			bcExperience._players.splice(bcExperience._players.originalLength, bcExperience._players.length - bcExperience._players.originalLength);
		}
	
		if(bcExperience._evalFrame.ready)
		{
			clearInterval(bcExperience._runner);
			bcExperience._initInjectScript(bcExperience._scriptText.originalLength - bcExperience._scriptText.length - 1);
			bcExperience._initDoNextScript();
		}
	},
	
	/**
	 * Executes the next script block or goes to cleanup if there are no more
	 * scripts to execute.
	 */
	'_initDoNextScript' : function()
	{
		if(!this._scriptText.length)
		{
			this._initCleanup();
		}
		else
		{
			var script = this._scriptText.shift();
			this._evalFrame.doEval('window.ready = false; window.experienceNumber = 0; ' + this._ceFunction);
			
			if(script.type == 'external')
			{
				this._evalFrame.doEval('var tmp = bcExperience.createElement("script");'
					+ ' tmp.type = "text/javascript";'
					+ ' tmp.src = "' + script.src + '";'
					+ ' tmp.onreadystatechange = function() { if(this.readyState == "complete") { window.ready = true; }};'
					+ ' tmp.onload = function() { window.ready = true; };'
					+ ' document.body.appendChild(tmp);');
				
				// Safari before WebKit supoprts neither onReadyStateChange nor onLoad
				// on <script> tags. This seems to work OK.
				if(/AppleWebKit\/[0-9]+/.test(navigator.userAgent) && /AppleWebKit\/([0-9]+)/.exec(navigator.userAgent)[1] <= 419)
				{
					this._evalFrame.doEval('setTimeout(function() { window.ready = true; }, 1);');
				}
				this._runner = setInterval(this._initCheckNextScript, 25);
			}
			else
			{
				this._evalFrame.doEval(script.src);
				this._evalFrame.doEval('window.ready = true;');
				this._initCheckNextScript();
			}
		}
	},
	
	/**
	 * Inserts players as appropriate.
	 * @param i Number The location of the script tag that was just evaluated.
	 */
	'_initInjectScript' : function(i)
	{
		for(var j = 0; j < this._evalFrame.experienceNumber; j++)
		{
			this.insertPlayer(this._players[this._lastExperienceNumber], document.body.getElementsByTagName('script')[i]);
			this._lastExperienceNumber++;
		}
	},
	
	/**
	 * Completes initialisation.
	 */
	'_initCleanup' : function()
	{
		this._domEvalFrame.parentNode.removeChild(this._domEvalFrame);
		delete this._players;
		delete this._domEvalFrame;
		delete this._evalFrame;
		delete this._scriptText;
		delete this._tdScripts;
		
		this.initialising = false;
		this.initialised = true;
	},
	
	/**
	 * Inserts a player to the page.
	 * @param player        HTMLElement The player object.
	 * @param beforeElement HTMLElement The element before which the player
	 *                                  should be inserted, or if append is TRUE,
	 *                                  the parent that the player should be
	 *                                  appended to.
	 * @param append        Boolean     TRUE = use appendChild; FALSE = use
	 *                                  insertBefore
	 * @returns Object
	 */
	'insertPlayer' : function(player, beforeElement, append)
	{
		if(append)
		{
			beforeElement.appendChild(player);
		}
		else
		{
			beforeElement.parentNode.insertBefore(player, beforeElement);
		}

		if(window.ActiveXObject)
		{
			var fsCommandFix = document.getElementsByTagName('head')[0].appendChild(player.fsCommandFix);
			var config       = player.config;
			player           = player.getElementsByTagName('object')[0];
		}

		return this.players.push({
			'config' : (config ? config : player.config),
			'placeholder' : beforeElement,
			'player' : player,
			'fsCommandFix' : (fsCommandFix ? fsCommandFix : null) });
	},
	
	/**
	 * Removes a player from the page.
	 * @param player String|Object The ID of the player object to remove, or
	 *                             the player object itself.
	 * @returns Object|Boolean
	 */
	'removePlayer' : function(player)
	{
		for(var i = 0; i < this.players.length; i++)
		{
			if((typeof player == 'string' && this.players[i].config.flashId == player)
			  || (typeof player == 'object' && this.players[i].player == player))
			{
				if(this.players[i].player.parentNode.id && this.players[i].player.parentNode.id == '_' + this.players[i].config.flashId)
				{
					this.players[i].player.parentNode.parentNode.removeChild(this.players[i].player.parentNode);
				}
				else
				{
					this.players[i].player.parentNode.removeChild(this.players[i].player);
				}

				if(this.players[i].fsCommandFix)
				{
					this.players[i].fsCommandFix.parentNode.removeChild(this.players[i].fsCommandFix);
				}
				return this.players.splice(i, 1);
			}
		}
		
		return false;
	},
	
	/**
	 * Called by another JavaScript function to call one of the
	 * ActionScript API functions within the Flash object.
	 * @param method String The name of the function to call within the Flash player.
	 * @param argN   String Arguments passed to the Flash function.
	 */
	'callFlash' : function(method)
	{
		// Non-specific calls to callFlash should direct to the first player object.
		if(this == bcExperience)
		{
			this.callFlash.apply(bcExperience.players[0].player, arguments);
			return;
		}
	
		var params = [];
		for(var i = 1; i < arguments.length; i++)
		{
			params.push(arguments[i]);
		}
		
		this.callbackFlash(this.flashCallback, { 'method' : method, 'params' : params });
	},
	
	/**
	 * Directs a callFlash request to the appropriate Flash object.
	 * @param callback    String The callback function that should be called within Flash.
	 *                           Optionally, also includes an <object> ID.
	 *                           In the format: objId|||functionName
	 *                                 OR just: functionName
	 *                           I don't think that the second form is ever used, however.
	 * @param returnValue Object The Flash function function to call (method)
	 *                           and its arguments (params).
	 */
	'callbackFlash' : function(callback, returnValue)
	{
		// This default flashId will never be used.
		// var flashId = 'bcPlayer';
		
		var cb = callback.split(this.ID_DELIM);
		if(cb.length > 1)
		{
			var flashId = cb[0].length > 0 ? cb[0] : flashId;
			var callback = cb[1];
		}

		// Preliminary untested ExternalInterface support
		try
		{
			if(this.detectFlashVersion() < 8)
			{
				throw bcPlayer.i18n.NO_EXTERNALINTERFACE;
			}

			if(window.ActiveXObject)
			{
				window[flashId][returnValue.method].apply(window, returnValue.params);
			}
			else
			{
				document.getElementById(flashId)[returnValue.method].apply(window, returnValue.params);
			}

			/*var result = document.getElementById(flashId)[returnValue.method](returnValue.params);

			  Data returned from Flash when the ExternalInterface method is called could be used
			  to call the Result function straight from JavaScript, but since there is also an
			  ExternalInterface.call method within ActionScript, we'll let the ActionScript programmers
			  handle this instead.

			if(result)
			{
				try
				{
					window[returnValue.method + '_Result'].apply(window, result);
				} catch(e) { alert(bcExperience.i18n.FUNCTION_NOT_FOUND.replace('%FUNCTION%', returnValue.method + '_Result')); }
			}*/
		}
		catch(e)
		{
			try
			{
				document.getElementById(flashId).SetVariable(callback, this.convertToXML(returnValue, 'js2flash'));
			} catch (e) { alert(bcExperience.i18n.CALLBACK_ERROR); }
		}
	},
	
	/**
	 * Called by the Flash player using onFSCommand.
	 * Sets the name of the function that can be used
	 * to make calls to the player's ActionScript API.
	 * This call is followed by a call to onTemplateLoaded.
	 * @param flashId  String The player that has been intialised.
	 * @param callback String A function name.
	 */
	'setAPICallback' : function(flashId, callback)
	{
  	document.getElementById(flashId).flashCallback = callback;
  	document.getElementById(flashId).callbackFlash = function() { bcExperience.callbackFlash.apply(bcExperience, arguments); };
  	document.getElementById(flashId).callFlash     = function() { bcExperience.callFlash.apply(document.getElementById(flashId), arguments); };
	},
	
	/**
	 * Used by the player to call JavaScript functions, for versions of
	 * the player that do not use ExternalInterface.
	 * @param func String JavaScript function to call.
	 * @param args String A BrightCove XML String.
	 */
	'onFSCommand' : function(func, args)
	{
		if(!func)
		{
			return;
		}
	
		if(func == 'eval')
		{
			return eval(args);
		}

		var args = this.convertToObject(args);

		try
		{
			if(args)
			{
				var callback = args.callback;
				var flashId = args.flashId;
	
				if(flashId && callback)
				{
					callback = args.flashId + this.ID_DELIM + callback;
				}
				
				if(!args.args)
				{
					window[func](callback);
				}
				else
				{
					window[func](args.args, callback);
				}
			}
			else
			{
				window[func]();
			}
		} catch(e) { console.log(e); alert(bcExperience.i18n.FUNCTION_NOT_FOUND.replace('%FUNCTION%', func)); }
	},
	
	/**
	 * Converts a JavaScript object into BrightCove's Crazy XML Schema.
	 * BrightCove's Crazy XML schema seems to consist of a strflashId node, a strcallback
	 * node, and an objargs node.
	 * @param obj       Object  JavaScript object.
	 * @param rootNode  String  The XML tree's documentElement.tagName.
	 * @param recursion Boolean TRUE when function calls itself.
	 * @returns String
	 */
	'convertToXML' : function(obj, rootNode, recursion)
	{
		var str = '<' + (recursion ? rootNode : 'obj' + rootNode) + '>';
		
		for(var property in obj)
		{
			switch(typeof obj[property])
			{
				case 'string':
					str += '<str' + property + '>' + this._htmlentities(obj[property]) + '</str' + property + '>';
				break;
				case 'number':
					str += '<num' + property + '>' + obj[property] + '</num' + property + '>';
				break;
				case 'function':
					continue;
				break;
				default:
					if(obj[property] instanceof Boolean)
					{
						str += '<boo' + property + '>' + (obj[property] ? 1 : 0) + '</boo' + property + '>';
					}
					else if(obj[property] instanceof Array)
					{
						str += this.convertToXML(obj[property], 'arr' + property, true);
					}
					else if(obj[property] instanceof Date)
					{
						str += '<dat' + property + '>' + this._htmlentities(obj[property].toString()) + '</dat' + property + '>';
					}
					else
					{
						str += this.convertToXML(obj[property], 'obj' + property, true);
					}
				break;
			}
		}
		
		return str += '</' + (recursion ? rootNode : 'obj' + rootNode) + '>';
	},
	
	/**
	 * Convert reserved characters to XML entities.
	 * @param str String String data containing reserved characters.
	 * @returns String
	 */
	'_htmlentities' : function(str)
	{
		return str.replace('"', '&quot;').replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;');
	},
	
	/**
	 * Converts BrightCove's Crazy XML Schema into a JavaScript object.
	 * @param node      String|Node XML string or DOM node.
	 * @param recursion Boolean     TRUE when function calls itself.
	 * @returns Object
	 */
	'convertToObject' : function(node, recursion)
	{
		if(!node || node.length == 0 || (node.childNodes && node.childNodes.length == 0))
		{
			return null;
		}
		
		if(!recursion)
		{
			// BUGBUG: setAPICallback call is incorrectly URL encoded by ActionScript!
			node = unescape(node);

			// Parse the string into an XML tree
			if(typeof DOMParser != 'undefined')
			{
				node = new DOMParser().parseFromString(node, 'text/xml').documentElement;
				node.normalize();
			}
			else if(window.ActiveXObject)
			{
				try
				{
					var parser = new ActiveXObject('Microsoft.XMLDOM');
					parser.setProperty('SelectionLanguage', 'XPath');
					parser.async = false;
					parser.loadXML(node);
					node = parser.documentElement;
					node.normalize();
				} catch(e) {}
			}
			else
			{
				alert(bcExperience.i18n.BROWSER_TOO_OLD);
				return false;
			}
		}

		// Start converting the XML tree into a JavaScript object
		if(node.nodeName.substr(0, 3) == 'arr')
		{
			var obj = [];
		}
		else
		{
			var obj = {};
		}
		
		for(var i = 0; i < node.childNodes.length; i++)
		{
			var child = node.childNodes[i];
			
			if(child.nodeType != 1)
			{
				continue;
			}
			
			// textContent: "The Standards" (DOM 3): Firefox 1.5, Safari WebKit, Opera 9
			// innerText: Internet Explorer
			// nodeValue: Safari 1 & 2
			switch(child.nodeName.substr(0, 3))
			{
				case 'cda':
				case 'str':
					obj[child.nodeName.substr(3)] = child.textContent || child.innerText || child.childNodes[0].nodeValue || '';
				break;
				case 'boo':
					obj[child.nodeName.substr(3)] = 'true' == (child.textContent || child.innerText || child.childNodes[0].nodeValue || 0);
				break;
				case 'num':
					obj[child.nodeName.substr(3)] = parseFloat(child.textContent || child.innerText || child.childNodes[0].nodeValue || 0);
				break;
				case 'arr':
					obj[child.nodeName.substr(3)] = this.convertToObject(child, true);
				break;
				case 'dat':
					obj[child.nodeName.substr(3)] = new Date(child.textContent || child.innerText || child.childNodes[0].nodeValue || '');
				break;
				case 'obj':
					obj[child.nodeName.substr(3)] = this.convertToObject(child, true);
				break;
			}
		}

		return obj;
	}
};

bcExperience.i18n = {
	'BROWSER_TOO_OLD'      : 'The browser you are using is too old. Please upgrade to the latest version of your browser or switch to a modern browser such as Firefox or Opera.',
	'FLASH_TOO_OLD'        : 'The version of Flash Player you have is too old. Please upgrade to the latest version and reload this page.',
	'FLASH_TOO_OLD_IMAGE'  : bcExperience.cdnURL + '/viewer/upgrade_flash_player.gif',
	'GO_TO_ADOBE'          : 'Go to the Adobe Flash Player Web site.',
	'FUNCTION_NOT_FOUND'   : 'Error: %FUNCTION%: Function not found.',
	'CALLBACK_ERROR'       : 'An error occurred when trying to communicate with the Flash player.',
	'INDEX_TOO_LOW'        : 'Unique player index ID is too low.',
	'NO_EXTERNALINTERFACE' : 'ExternalInterface not implemented.',
	'MISSING_CONFIG'       : 'You must provide configuration options to the player.',
	'ALREADY_INITIALISED'  : 'This player experience has already been initialised. Please see the API documentation for information on how to programatically add new players to the page.'
};

/**
 * These functions have been moved into the bcExperience Object
 * to keep from polluting the global namespace. They are kept here
 * for backwards compatibility.
 */
var setAPICallback    = function() { bcExperience.setAPICallback.apply(bcExperience, arguments) };
var callFlash         = function() { bcExperience.callFlash.apply(bcExperience, arguments) };
var callFlashInstance = function(flashId) { var args = []; for(var i = 1; i < arguments.length; i++) { args.push(arguments[i]); }; document.getElementById(flashId).callFlash.apply(document.getElementById(flashId), args); };
var callbackFlash     = function() { bcExperience.callbackFlash.apply(bcExperience, arguments) };
var onFSCommand       = function() { bcExperience.onFSCommand.apply(bcExperience, arguments); };

if(typeof parent.__BCEVALWINDOW__ == 'undefined')
{
	if(/KHTML/i.test(navigator.userAgent))
	{
		var checkLoad = setInterval(function() { if(/loaded|complete/.test(document.readyState)) { clearInterval(checkLoad); bcExperience.init(); }}, 70);
		document.addEventListener('load', bcExperience.init, false);
	}
	if(typeof window.addEventListener != 'undefined')
	{
		document.addEventListener('DOMContentLoaded', bcExperience.init, false);
		document.addEventListener('load', bcExperience.init, false);
	}
	else if(typeof window.attachEvent != 'undefined')
	{
		//document.write('<script id=__ie_onload defer src=https:javascript:false></script>');
		//document.getElementById('__ie_onload').onreadystatechange = function(evt) { if(this.readyState == 'complete') bcExperience.init(); };
		
		window.attachEvent('onload', bcExperience.init);
		
		// Fix for Flash 9 player memory leaks in IE
		window.attachEvent('onbeforeunload', function()
		{
			__flash_unloadHandler = function() {};
			__flash_savedUnloadHandler = function() {};
		});
	}
	else
	{
		alert(bcExperience.i18n.BROWSER_TOO_OLD);
	}
}