// bootstrap.js

/*
	Bootstrap should be linked into any document whose scripts
	(a) share code with those of other documents, or
	(b) wish to load other scripts only when actually needed.
	
	If a document's scripting needs are met with a single script file, then
	Bootstrap is unnecessary.  But often a Web author has a library of commonly
	used Javascript routines, the use of which requires either duplication in
	each client script, server-side or preprocessed script generation (which
	makes the duplication acceptable, because the source remains unique), or a
	means of importing scripts at runtime.  The first is an unacceptable and
	unmaintainable coding practice, and the second adds a compile phase to the
	test cycle in the case of preprocessing and rules out local testing entirely
	in the case of server-side generation.  Bootstrap implements the third.
*/

/*
	Script-loading terminology
	
	Scripts are *loaded* (acquired by the browser given a URL reference) if
	external, and then *run*, during which the script is executed by the
	Javascript interpreter.  (We are not considering inline scripts either in
	<script> elements or on/event/ handler attributes.)
	
	Scripts which are referenced by <script> elements supplied in the serialized
	HTML document are *intrinsic*, or *linked* scripts.  Scripts which are
	dynamically added to the document via the DOM at runtime (thereby causing
	them to be executed) are *extrinsic* or *called* scripts.
	
	Called scripts may be *invoked* by another script, causing it to be run
	(once for each time it's invoked), or *imported*, by which the same script
	will not be run more than once, and subsequent import requests will have no
	effect.  It is in error to both import and invoke the same script, in any
	order.  Scripts are considered the same iff they have the same fully schemed
	URL.  Invoked scripts may be passed parameters (which may vary from one
	invocation to the next), but imported scripts may not.
*/

/*
	Public API
	
	Bootstrap expects to be the first <script> element in the document head, and
	will complain if it isn't.  Loading bootstrap.js an additional time (via
	either a second <script> link or a dynamic call through Bootstrap) yields
	undefined behavior.  Don't Do That.
	
	Bootstrap exposes the following public interface:
	
	document.head;
	
		Bootstrap creates the head property of document if it doesn't exist,
		storing in it the HTML document's <head> element.
	
	Bootstrap.invokeScript( href );
	Bootstrap.importScript( href );
	
		The invokeScript() method, given a pathname for a script file, causes
		that file to be queued for later execution.  It's important to note that
		the script is *not* executed immediately, but sometime after the caller
		exits, possibly after the execution of other scripts unknown to the
		caller.  Therefore, any API defined by the imported script is not
		available for use in the same execution flow that called invokeScript().
		However, any scripts loaded subsequently will benefit.
		
		The importScript() method is like invokeScript(), but does nothing if the
		script has already been imported.  Calling both invokeScript() and
		importScript() on the same script yields undefined behavior.
		
		The href parameter is either a fully schemed URL or a url-path relative
		to the caller script -- not the document.  I went to a lot of trouble to
		get this to work; now I'm going to make you use it.  Someday you'll
		thank me.
*/

// Constants

var gDynamicallyAddedScriptClass       = 'dynamically-added';
var gDynamicallyAddedHelperScriptClass = 'dynamically-added helper';

var gTryToFixAsyncProblem = false;

var gTraceCalls = false;

//gTraceCalls = true;


function declare( condition, comment, lcPlural_declarations, prefix, action )
{
	if ( comment == null )
	{
		alert( "Please comment your " + lcPlural_declarations );
	}
	
	if ( condition )  return;
	
	var text = prefix;
	
	if ( comment != null )
	{
		text += ": " + comment;
	}
	
	action( text );
}

function expect( condition, comment )
{
	declare( condition, comment, "expectations", "Expectation unfulfilled", alert );
}

function assert( condition, comment )
{
	function fail( text )
	{
		alert( text );
		
		throw text;
	}
	
	declare( condition, comment, "assertions", "Assertion failed", fail );
}

function assertQ( code )
{
	assert( eval( code ), code );
}

function boundMethod( implementor, method )
{
	return function(){ return method.apply( implementor, arguments ) };
}

function delegateMethod( target, methodName, implementor )
{
	//target[methodName] = function(){ return implementor[methodName].apply( implementor, arguments ) };
	target[methodName] = boundMethod( implementor, implementor[methodName] );
}

function isHrefSchemed( href )
{
	return href.match( /:/ );
}

function testAssertions()
{
	assertQ( '2 + 2 == 4' );
	
	assertQ( ' isHrefSchemed( "http://foo" )' );
	assertQ( '!isHrefSchemed(        "foo" )' );
	
	assertQ( 'getRelativeBaseHref( "foo"         ) == ""        ' );
	assertQ( 'getRelativeBaseHref( "foo/"        ) == "foo/"    ' );
	assertQ( 'getRelativeBaseHref( "foo/bar"     ) == "foo/"    ' );
	assertQ( 'getRelativeBaseHref( "foo/bar/baz" ) == "foo/bar/"' );
	
	assertQ( 'this == window' );
	assertQ( 'this.assert == assert' );
	assertQ( 'this.fail == undefined' );
	//assertQ( 'assert.fail != undefined' );  // Fails in Firefox
	assertQ( 'assert.fail == undefined  ||  typeof( assert.fail ) == "function"' );
	
	assertQ( 'document.body == null' );
}

function getRelativeBaseHref( url )
{
	return url.replace( /[^\/]*$/, "" );
}

function addScriptWithClass( href, className )
{
	var script  = document.createElement( 'script' );
	
	script.className = className;
	
	script.setAttribute( 'type', 'text/javascript' );
	script.setAttribute( 'src', href );
	
	document.head.appendChild( script );
}

function addHelperScript( href )
{
	addScriptWithClass( href, gDynamicallyAddedHelperScriptClass );
}

function removeFirstHelperScript()
{
	var scripts = document.head.childNodes;
	
	for ( var i = 0;  i < scripts.length;  ++i )
	{
		var script = scripts[i];
		
		if ( script.nodeName != 'SCRIPT' )  continue;
		
		if ( script.className == gDynamicallyAddedHelperScriptClass )
		{
			document.head.removeChild( script );
			return;
		}
	}
	
	expect( false, "Ran out of helper scripts to purge" );
}

function scriptIsIntrinsicallyLinked( script )
{
	switch ( script.className )
	{
		case gDynamicallyAddedHelperScriptClass:
		case gDynamicallyAddedScriptClass:
			return false;
	}
	
	return true;
}

function getLastIntrinsicScript()
{
	//var scripts = document.head.getElementsByTagName( 'script' );
	var scripts = document.head.childNodes;
	
	for ( var i = scripts.length - 1;  i >= 0;  --i )
	{
		var script = scripts[i];
		
		if ( script.nodeName != 'SCRIPT' )  continue;
		
		if ( scriptIsIntrinsicallyLinked( script ) )
		{
			return script;
		}
	}
	
	assert( false, "Somehow you have no linked scripts, only dynamically added ones.  You're more clever than you look." );
}

var Bootstrap;

// Bootstrap object constructor.
function Bootstrap_Singleton()
{
	function Internals()
	{
		var scripts = document.head.getElementsByTagName( 'script' );
		
		assert( scripts.length == 1, "bootstrap.js must be loaded before other scripts." );
		
		// Cache the bootstrap <script>'s src attribute.
		// This, like all script src attributes, is a fully schemed URL (regardless of
		// what was actually provided in the HTML document).  Unless you're IE.
		this.url = scripts[0].src;
		
		// Are we testing self-invocation?
		this.testInProgress = false;
		
		// When we run as a scaffold, are we the header or trailer?
		this.scaffoldIsHeader = true;
		
		this.scaffoldCounter = 0;
		
		this.scriptData = {};
		
		this.callStack = null;
		this.pendingCallQueue = [];
		
		this.tracedCalls = [];
		
		// Called during first run (init mode).
		function runInit()
		{
			this.hasImmediateSelfInvocation = this.testImmediateSelfInvocation();
		}
		
		// Called during second run (test mode).
		function runTest()
		{
			this.testInProgress = false;
		}
		
		// Called during third and subsequent runs (scaffold mode).
		function runScaffold()
		{
			if ( this.scaffoldIsHeader )
			{
				var callFrame = this.pendingCallQueue.shift();
				
				assert( callFrame != null, "Null call frame in pending call queue." );
				
				this.callStack = callFrame;
			}
			else
			{
				assert( this.callStack != null, "Can't pop null call stack" );
				
				// pop stack
				this.callStack = this.callStack.callerFrame;
				
				if ( gTryToFixAsyncProblem  &&  this.pendingCallQueue.length > 0 )
				{
					// Insert our header
					Bootstrap.internals.addScaffoldingScript();
					
					// Insert the called script
					document.head.appendChild( Bootstrap.internals.pendingCallQueue[0].script );
					
					// Insert our trailer
					Bootstrap.internals.addScaffoldingScript();
				}
			}
			
			this.scaffoldIsHeader = !this.scaffoldIsHeader;
			
			removeFirstHelperScript();
		}
		
		function callDepth()
		{
			// A null call stack has depth one, because there's no stack frame for the
			// original caller (a linked script).
			var depth = this.callStack == null ? 1 : this.callStack.depth + 1;
			
			return depth;
		}
		
		// Called by runInit().
		function testImmediateSelfInvocation()
		{
			this.testInProgress = true;
			
			addHelperScript( this.url );
			
			var immediate = !this.testInProgress;
			
			//gTryToFixAsyncProblem = immediate;  // Safari has issues
			
			removeFirstHelperScript();
			
			return immediate;
		}
		
		function getCurrentlyLoadingScript()
		{
			return   this.callStack != null ? this.callStack.script     // top of call stack
			       : true                   ? getLastIntrinsicScript()  // last linked script
			       :                          null;                     // some handler at runtime
		}
		
		function getURLFromCallerRelativeHref( href )
		{
			if ( isHrefSchemed( href ) )  return href;
			
			assert( this.getCurrentlyLoadingScript() != null, "getURLFromCallerRelativeHref():  No script is currently loading" );
			
			return getRelativeBaseHref( this.getCurrentlyLoadingScript().src ) + href;
		}
		
		function getURLSuffixForScaffolding()
		{
			++this.scaffoldCounter;
			
			if ( this.hasImmediateSelfInvocation )
			{
				return '?foo=' + this.scaffoldCounter;
			}
			
			return "";
		}
		
		function addScaffoldingScript()
		{
			var url = this.url;
			
			url += this.getURLSuffixForScaffolding();
			
			addHelperScript( url );
		}
		
		function addCalledScript( calledScript, callType )
		{
			if ( this.scriptData[ calledScript.src ] == undefined )
			{
				this.scriptData[ calledScript.src ] =
				{
					script: calledScript,
					invoked:  0,
					imported: 0
				};
			}
			
			++this.scriptData[ calledScript.src ][ callType ];
			
			calledScript.className = gDynamicallyAddedScriptClass;
			
			calledScript.setAttribute( 'type', 'text/javascript' );
			
			var callFrame = { script: calledScript,
							  //depth: this.callDepth(),
							  callerFrame: this.callStack };
			
			this.pendingCallQueue.push( callFrame );
			
			if ( !gTryToFixAsyncProblem  ||  this.pendingCallQueue.length == 1 )
			{
				// Insert our header
				this.addScaffoldingScript();
				
				// Insert the called script
				document.head.appendChild( calledScript );
				
				// Insert our trailer
				this.addScaffoldingScript();
			}
		}
		
		function callScript( href, callType, filter )
		{
			this.traceCall( href );
			
			var script = document.createElement( 'script' );
			
			script.setAttribute( 'src', this.getURLFromCallerRelativeHref( href ) );
			
			if ( !filter.apply( this, [ script.src ] ) )  return;
			
			this.addCalledScript( script, callType );
		}
		
		function invokeScript( href )
		{
			return this.callScript( href, 'invoked', function(){ return true; } );
		}
		
		function importScript( href )
		{
			function filter( url )
			{
				if ( this.scriptData[ url ] == undefined )  return true;
				
				assert( !this.scriptData[ url ].invoked, "Invalid import of already-invoked script: " + url )
				
				return !this.scriptData[ url ].imported;
			}
			
			return this.callScript( href, 'imported', filter );
		}
		
		function scriptIsLoaded( href )
		{
			var script = document.createElement( 'script' );
			script.setAttribute( 'src', this.getURLFromCallerRelativeHref( href ) );
			
			var url = script.src;  // breaks in IE
			
			if ( this.scriptData[ url ] == undefined )  return false;
			
			var scripts = document.head.childNodes;
			
			for ( var i = 0;  i < scripts.length;  ++i )
			{
				var script = scripts[i];
				
				if ( script.nodeName != 'SCRIPT' )  continue;
				
				if ( script.src == url )
				{
					return false;  // Script gets removed after execution, so it hasn't run yet
				}
			}
			
			return true;
			
		}
		
		function ready( prerequisites )
		{
			var ready = true;
			
			for ( var i = 0;  i < prerequisites.length;  ++i )
			{
				if ( this.scriptIsLoaded( prerequisites[i] ) )  continue;
				
				ready = false;
				
				this.importScript( prerequisites[i] );
			}
			
			if ( !ready )
			{
				this.invokeScript( getCurrentlyLoadingScript().src );
			}
			
			return ready;
		}
		
		function traceCall( href )
		{
			if ( !gTraceCalls )  return;
			
			var script = this.getCurrentlyLoadingScript();
			
			var call = { caller: this.callStack,
						 callerURL: script.src,
						 href: href,
						 depth: this.callDepth() };
			
			this.tracedCalls.push( call );
		}
		
		this.runInit     = runInit;
		this.runTest     = runTest;
		this.runScaffold = runScaffold;
		this.callDepth   = callDepth;
		
		this.testImmediateSelfInvocation = testImmediateSelfInvocation;
		
		this.getCurrentlyLoadingScript = getCurrentlyLoadingScript;
		this.getURLFromCallerRelativeHref = getURLFromCallerRelativeHref;
		
		this.getURLSuffixForScaffolding = getURLSuffixForScaffolding;
		
		this.scriptIsLoaded = scriptIsLoaded;
		this.ready = ready;
		
		this.addScaffoldingScript = addScaffoldingScript;
		this.addCalledScript      = addCalledScript;
		
		this.callScript   = callScript;
		this.invokeScript = invokeScript;
		this.importScript = importScript;
		
		this.traceCall = traceCall;
	}
	
	this.internals = new Internals();
	
	//this.invokeScript = boundMethod( this.internals, this.internals.invokeScript );
	delegateMethod( this, 'invokeScript', this.internals );
	delegateMethod( this, 'importScript', this.internals );
	delegateMethod( this, 'ready', this.internals );
	
	delegateMethod( this, 'getCurrentlyLoadingScript',    this.internals );
	delegateMethod( this, 'getURLFromCallerRelativeHref', this.internals );
	
}


// The Bootstrap object.  This doubles as a sort of include guard, although
// unlike in C/C++ we don't immediately bail the second (or third, etc.) time
// around, since we still have work to do.
var bootstrap;
var Bootstrap;


if ( Bootstrap == undefined )
{
	// First run.
	
	testAssertions();
	
	// Cache the <head> element.
	if ( document.head == undefined )
	{
		document.head = document.getElementsByTagName( 'head' )[0];
	}
	
	// Create the bootstrap object.
	Bootstrap = new Bootstrap_Singleton();
	
	bootstrap = Bootstrap;
	
	// All this does is run the test for immediate self-invocation.
	// Since it inserts a new script that could run immediately (and will, in
	// Safari), we have to be concerned with reentrancy.  In particular,
	// Bootstrap must be fully constructed.
	Bootstrap.internals.runInit();
	
	// Bootstrap has no dependency on windowLoader, but for client scripts that
	// do, once they're running it's too late to import it.  So we do it now.
	Bootstrap.importScript( "window-load.js" );
}
else if ( Bootstrap.internals.testInProgress )
{
	// Second run.
	
	// If we're Safari, the first run has just inserted our <script> node into
	// the document head and is still in progress.  After we exit the first run
	// will resume and notice that the flag has been cleared.
	
	// If we're Firefox, the first run never paused to run us, so the flag
	// remained set after inserting our <script> node.  The first run has
	// completed, and nobody is checking the flag any more.
	
	// Turn off the test flag.
	Bootstrap.internals.runTest();
}
else
{
	// Third (or subsequent) run.
	
	// We're a scaffold script.  Odd ordinals are headers, even ones are trailers.
	Bootstrap.internals.runScaffold();
}


