/*
 * This is $URL: http://hermes.synatree.com/svn/repos/Spark/components/com_spark/js/spark.js $, part of the Spark Enterprise Content Management System.
 * 
 * (c) 2008 SynaTree Internet Growth Strategies, All Rights Reserved
 * --------------------------------------------------------------------
 * Portions of this software are licensed under the GNU General Public License,
 * version 2.  Other portions are licensed seperately.
 * --------------------------------------------------------------------
 * 
 * $Id: spark.js 447 2009-02-19 16:54:05Z synatree $
 */	
	$INSPECT = new Hash();
	VERSION = "$Rev: 447 $";
	var Dx = 1;
	$DEBUG = function()
	{
		if(! this.test)
		{
			var b = $('debug');
			this.test = b ? true : false;
			this.id = 1;
		}		
		else
		{
			var b = $('debug');
			var a = Array.prototype.slice.call(arguments);
			var u = new Element('ul').set('text', "Debug # " + this.id++);
			a.each( function(x){ new Element('li').set('text',x).injectInside(u) } );
			u.injectInside(b);	
		}
	}
	/**
	 * A utility function to help with $try blocks.
	 * pass a testable value to Assert and it will return a function that, when called
	 * will either return the value or throw an exception if the value evaluates
	 * to false.  The exception will contain the exact false-value.
	 * @param {Object} v
	 */
	function $assert(v) { var f = function () {if (!v) {throw v;}return v;}; return f; }
	/**
	 * The any function takes an undefined number of arguments, uses $assert and $try
	 * to test them.  If any of the arguments eval to true, the whole function
	 * returns true.  If all evaluate to false, the whole function is false.
	 */
	function $any(){
		var a = Array.prototype.slice.call(arguments);
		var p = [];
		a.each(function(v){
			p.push( $assert(v) );
		});
		return $try.pass(p).run();
	}
	/**
	 * makes a one-directional clone of Obj.  Changing the original
	 * will change the children, but not vise-versa.
	 * @param {Object} obj
	 */
	function uniClone(obj)
	{
		
		function Clone(){};
		Clone.prototype = obj;
		return new Clone;
	}
		/**
	 * We need to provide a consistent way to include files so that they will only
	 * be loaded when spark is ready.
	 */
	function loadModule()
	{
		// first, we need to make sure that mootools is loaded so that we have access
		// to basics like "window.addEvent"	
		var args = Array.prototype.slice.call(arguments);
		if(! window.MooTools || ! window.addEvent )
		{
			// wait a few moments, then try again.
			window.setTimeout(loadModule,100,args);
		}
		// add a sparkLoaded event...
		args.each(function(x)
		{
			var f = function(){
					new Asset.javascript( document.Spark.getDataURI('/components/com_spark/js/spark.'+x.toLowerCase()+'.js') );
				};
			if(!document.Spark)
			{
				window.addEvent('sparkLoaded', f);	
			}
			else
			{
				f();
			}
		});
		
	}


window.addEvent('domready', function(){
	Function.prototype.curry = function() 
	{ 
	  var method = this, args = Array.prototype.slice.call(arguments); 
	  return function() 
	  { 
	    return method.apply(this, args.concat(Array.prototype.slice.call(arguments))); 
	  }; 
	};
	// nQ ultra-simple performance-sensitive queueing mechanism.
	// ---------------------------------------------------------
	QT = null;
	Q = [];
	nQ = function(work){
		/*
		 * This first if is to roll in extra arguments that may arrive
		 * at nQ that were really meant for the work function.
		 */
		if(arguments.length-1)
		{
			var w = work;
			var args = Array.prototype.slice.call(arguments).slice(1);
			work = function(){ return w.apply(w, args) };
		}
		if(!Q)
			return work();
		Q.push(work);
		if(!QT)
			dQ.curry($time(), 100).delay(100);
		
	};
	
	dQ = function(start,spacer)
	{
		//if(window.console) window.console.log( "Spacer now " + spacer );
		var now = $time();
		if(Q.length)
		{
			var work = Q.shift(); work();
			if(!Q.length)	// if that was the last item in the queue
				return $clear(QT);
			var drift = now - start;
			var high = Math.floor(spacer*1.10);
			var low = Math.floor(spacer*.90);
			if(drift > high)
			{
				spacer = high;
			}
			if(drift <= low)
			{
				spacer = low;
			}
			dQ.curry(now, spacer).delay(spacer);
		}
		else
		{
			return $clear(QT);
		}	
	};
	// --------------------------------------------------
	// end nQ stuff
	
	
	Element.implement({
		stores:	function(obj)
		{
			$H(obj).each(function(v,k){
				this.store(k,v);
			}.bind(this));
		},
		retrieves:	function(arr)
		{
			var r = $H();
			$splat(arr).each( function(v) {
				r.set(v, this.retrieve(v));
			}.bind(this));
			return r;
		}
	});
	 
	var equal = Browser.Engine.trident ? window==window.top : window===window.top;
	if(equal)
	{
		document.Spark = new ( new Class({
			Extends: Events,
			Scope:	window,
			_eN:0,
			_ro:{},
			_siteroot: '',
			GoogleAPIKey: '',
			time:0,
			SymbolPrefix: 'onSymbol.',
			dataProvider: '',
			initialize: function(){
				try{
					if(SparkLiveURL)
						this._siteroot = SparkLiveURL;
					if(GoogleAPIKey)
						this.GoogleAPIKey = GoogleAPIKey;
				}
				catch(e){ /* ... */ };
				this.time = $time();
				this.modules = new Hash;
				window.fireEvent("onSparkLoad", this);
				document.Spark = this;
				this.Scope = window;
				this.dataProvider = this.getDataURI('index.php?option=com_spark&task=xmlhttpRouter&tmpl=component');
				this.addEvent('onError', this.error.bind(this));
				this.addEvent('onSymbol', function(a){
				/*
				 * Removed auto-error windows to allow regular widgets,etc to bind to these.
				 */
				/*	if (a().error) 
						this.fireEvent('onError', a) */
				});
				window.addEvent('sparkLoaded', function(){
					this.fireEvent('onLoad');
				}.bind(this));
				this.getRequestObject();
				window.fireEvent('sparkLoaded', this);
				
			},
			css:	function(rel,cb)
			{
				var opts = {};
				if(cb)
					opts.load = cb;
				new Asset.css( document.Spark.getDataURI(rel), opts);	
			},
			js:		function(rel,cb)
			{
				var opts = {};
				if(cb)
					opts.onload = cb;
				new Asset.javascript( document.Spark.getDataURI(rel), opts);	
			},
			getDataURI:	function(urifrag)
			{
				var y = this._siteroot;
				var x = urifrag;
				var a = '';
				if(y.charAt(y.length-1) == '/' && x.charAt(0) == '/'){ a = y.concat(x.slice(1,x.length)) }
				else { a = y + x; }
				var b = a.toLowerCase();
				// this bit is scary looking, but it just tests to see if the filename is js, html, or css.
				// it adds the ?time string to the end to prevent caching problems.
				a += $any(
					( b.indexOf('.html')!= -1 ),
					( b.indexOf('.css')!= -1 ),
					( b.indexOf('.js')!= -1 ),
					( b.indexOf('.swf')!= -1 ))
				?
					'?'+document.Spark.time
				: 	'';
				return a;
			},
			error: function(m)
			{
				if(this.Utility)
					return this.Utility.error(m);
				return true;
			},
			/**
			 * getRequestObject gets the current token from the server and saves it locally.
			 */
			getRequestObject:	function()
			{
				this._ro = this.getCookieObj();
				this.remoteProcedure('getToken',null,
				{
					'token': function(a){
						if(a().details)
						{
							var z = {};
							z[a().details] = 1;
							this._ro = $extend(this._ro, z);
							this.fireEvent('requestObjectLoaded');
						}	
					}.bind(this)
				});
			},
			_remoteRequest:  function(path, args, sync)
			{
				var SP = this.SymbolPrefix;
				async = sync ? false : true;
				var Connection = new Request({ async: async, method: 'post', url: this.dataProvider,
					onComplete: function(data)
					{
						var data = JSON.decode(data);
						if ($type(data.retval) == 'array') {
							data.retval.forEach(function(d){
								if (d.symbol) {
									var s = SP + d.symbol;
									/* The below is pretty clever.  It gives us read-access to this RUNNING INSTANCE of d.
									 * you can access the value and change it prior to any other events getting it.
					 				*/
									var o = function(){
										if (arguments.length == 1) 
											d = v;
										return d;
									};
									
									this.fireEvent('onSymbol', o); // give general symbol manipulation functions a chance
									this.fireEvent(s, o); // then run the specific trigger function.
								}
							}, this);
						}
					}.bind(this)
				}).send( 'json=' + escape( JSON.encode( this.SparkData('retval', {'path':path,'args':args})) ) );
				
			},
			remoteProcedure:	function(path,args,eventsObj)
			{
				var SP = this.SymbolPrefix;
				if (eventsObj) {
					$each(eventsObj, function(value, key){
						if ($type(value) == 'function') {
							this.addEvent(SP + key, value )
						}
					}.bind(this));
				}
				var sync = eventsObj.sync ? true : false;
				this._remoteRequest(path,args,sync);
			},
			clobberThisEvent:	function(event)
			{
				var SP = this.SymbolPrefix;
				var e = SP+event;
				if($type(this.clobberThisEvent.caller) == 'function')
					this.removeEvent(e, this.clobberThisEvent.caller);
			},
			getCookieObj: function()
			{
				var retObj = {};
				var c = document.cookie;
				var strings = c.split(';');
				var cookies = new Array();
				strings.each(function(item){
					item = item.trim();
					var parts = item.split('=');
					cookies.push(parts[0]);
				});
				cookies.each(function(item){
					retObj[item] = Cookie.read(item);
				});
				return retObj;
			},
			SparkData:	function()
			{
				var na = arguments.length;
				if (na > 0) {
					
					var r = {};
					$each(arguments, function(item, index){
						if(index==0)
							r.container = item;
						else
							$extend(r,item)
					});
					
					return r;
				}
				return null;
			},
			modules: null,
			/**
			 * notify all listeners that a module has become available.  obj can be provided optionally, and will be passed to listeners.
			 * @param {String} moduleName
			 * @param {Object} obj[optional] 
			 */
			moduleReady:	function(moduleName, obj)
			{
				var signature = 'SparkModule.' + moduleName + '.ready';
				window.fireEvent(signature, obj);
				this.modules.set(moduleName, 'complete');
				this.fireEvent(signature, obj);
			},
			getModuleReady:	function(moduleName, fn, doLoad)
			{
				doLoad = doLoad || false;
				if($type(moduleName) == 'array')
				{
					var collector = document.Spark.Aggregator({stages: moduleName}, fn);
					$A(moduleName).each( function(name){
						document.Spark.getModuleReady(name, collector.pass(name), doLoad);
					} );	
				}
				else
				{
					var m = this.modules.has(moduleName) && this.modules.get(moduleName) || false;
					if(m && m=='complete')
					{
						fn();
					}
					else
					{
						window.addEvent('SparkModule.' + moduleName + '.ready', fn);
						this.modules.set(moduleName, 'pending');
						if(doLoad)
						{
							loadModule( moduleName.toLowerCase() );		
						}
					}	
				}
			},
			getModuleStatus:	function()
			{
				return this.modules.getValues().associate( this.modules.getKeys() );
			},
			UI:	{
				navigate:	function(url){
					if(!url.contains('://'))
						url = document.Spark.getDataURI(url);
					document.Spark.fireEvent('onBeforeNavigate',url);
					window.top.location.href = url;
				}
			},
			
			/**
			 * The loader is an important part of Spark because it facilitates interactions
			 * between files not currently in the DOM-tree.  Right now these functions
			 * are all pretty seperate and specialized, but some changes are coming:
			 * TODO: Loader.load(
			 * {sync: false, scripts: true, bootstrap: false}, '/components/blah.html');
			 * This options-based approach will auto-select the appropriate loading method.
			 */
			Loader: new ( new Class({
						fragmentCache: null,
						workerNodeName: '_sparkLoaderWorkerNode',
						workerNode:		null,
						workerDoc: null,
						newWorkerDoc:	function()
						{
							if( $type(this.workerDoc) == 'element')
								this.workerDoc.destroy();
							this.workerDoc = new Element('iframe').setStyles(
								{width:1,height:1,'border':'none'}
							).injectInside(document.body);
						},
						initialize:	function()
						{
							this.fragmentCache = new Hash;
							if(!$(this.workerNode))
							{
								this.workerNode = new Element('div').set(
									{
										id: this.workerNodeName,
										styles: {display:'none'}
									}
								);
								this.workerNode.injectInside(document.body);
							}
							this.newWorkerDoc();
						},
						loadFragment: 	function(fragmentId, forceLoad)
						{
							if(!forceLoad)
								forceLoad=false;
							
							if (!this.fragmentCache.has(fragmentId) || forceLoad) {
								document.Spark.remoteProcedure('loadFragment', [fragmentId], {
									'fragment.ok': function(a){
										$try( function(){
											if(a().details && this.workerNode)
												return true;
											return false;
										}, document.Spark.Utility.errorLambda("loadFragment prerequisites not met"));
										var N = new Element('div').setHTML(a().details);
										var target = N.getElement('#scaffold').clone(true).injectInside(this.workerNode);
										N = null; // force garbage collection
										if (target.getTag() == 'iframe') {
											// load the IFRAME first
											target.addEvent('load', function(){
												// first, we have to extend the body with mootools:
												var b = $(target.contentDocument.body);
												// now we need to clone the inner nodes
												var Inner = b.clone(true);
												// now we can destroy the workerNode tree.
												this.workerNode.empty();
												// now reattach the cloned stuff.
												this.workerNode.adopt(Inner);
												// now proceed as normal
												this.fragmentCache.set(fragmentId, Inner);
												document.Spark.fireEvent('onFragmentLoad.' + fragmentId, Inner);
											}.bind(this));
										}
										else {
											var clone = target.clone(true).cloneEvents();
											target = null;
											this.fragmentCache.set(fragmentId, clone);
											document.Spark.fireEvent('onFragmentLoad.' + fragmentId, clone);
										}
										document.Spark.clobberThisEvent('fragment.ok');
									}.bind(this),
									'error.fragment': function(a){
										document.Spark.error('Fragment Error');
									}
								});
							}
							else
							{
								document.Spark.fireEvent('onFragmentLoad.'+fragmentId, this.fragmentCache.get(fragmentId));
							}
							
						},
						/**
						 * Find all "parseToObj" classes in the node tree and create links to those nodes
						 * inside an object, then return the object.
						 * @param {DOMNode} node
						 */
						parseNode:	function(node)
						{
							var n = $(node);
							if(!n)
								return false;
							var coll = n.getElements('.parseToObj');
							var ret = {};
							coll.each(function(node){
								var name = node.getProperty('name');
								if (name) {
									ret[name] = node;
									// remove the parseToObj class
									node.removeClass('parseToObj');
								}
							});
							return ret;
						},
						loadDocument:	function(reluri,cb)
						{
							var u = document.Spark.getDataURI(reluri);
							var d = this.workerDoc;
							var m = this;
							d.addEvent('load', function(){
								var d = m.workerDoc;	// pull into this scope.
								d.contentWindow.document.Spark = document.Spark;
								d.contentWindow.Browser = window.Browser;
								var tree = null;
								var nextstep = function(){
									var body = $(d.contentWindow.document.body);
									tree = body.getElement('*').clone(true,true);
									cb(tree);	
								};
								if(d.contentWindow.document.bootstrap)
								{
									d.contentWindow.document.bootstrap( nextstep );
								}
								d.removeEvents();
								m.newWorkerDoc.bind(m).delay(1000);
							});
							d.src = u;
						},
						/**
						 * This is the main point of entry for scripts that need
						 * to load a document fragment into memory and also run some
						 * javascript that might be inside that document.
						 * @return {DOMNode} tree, the root of a DOM-tree from the document.
						 * @param {Object} cb, a function that will accept the nodes returned
						 * @param {Object} frag, a relative filename to load.
						 */
						bootstrap:	function(cb, frag)
						{
							this._cb = cb;
							this.loadDocument( frag, this.continuation.bind(this));
						},
						continuation:	function(tree)
						{
							this._cb.attempt(tree);
							this._cb = null;
						},
						syncLoad: function(uri)
						{
							var node = new Element('div').injectInside(this.workerNode);
							var url = document.Spark.getDataURI(uri);
							node.set('load',{async: false, evalScripts: true});
							$SCAFFOLD = node;
							node.load(url);
							return node;
						},
						/**
						 * Get data from a server-side path store and create a grid out of it.
						 * @param {Object} path
						 * @return Grid
						 */
						load:	function(key, cb){
							var funcs = {};
							funcs['getdata.'+key] = function(a){
								if($type(cb)=='function')
									cb(a().details);
								document.Spark.clobberThisEvent('getdata.'+key);
							};
							funcs['error.getdata'] = function(a){
								if($type(cb)=='function')
									cb(false);
								document.Spark.clobberThisEvent('error.getdata');
							};
							document.Spark.remoteProcedure('getData', [ key ], funcs);
						},
						save:		function(key,value)
						{
							document.Spark.remoteProcedure('setData', [ key, value], {} );
						},
						clear:	function(key)
						{
							document.Spark.remoteProcedure('clearData', [key], {});					
						}
				}) ),
				Fetch:	new Class({
					Extends: Options,
					/**
					 * Next Generation Loader, aims to handle all your loading needs.
					 * 
					 * @param {Object} options
					 */
					request:	null,
					data:		null,
					tree:		null,
					store:		null,
					options:	{
						/* Request options*/
						returns:	'document',// || 'nodes' || 'json' || 'js' || 'string',
						uri:		'',		// the request uri (will be run through spark getDataURI
						expandUri:	true,	// run uri through the getdatauri filter.
						/* Request Options */
						async: 		true,	// sync or async mode
						method: 	'get', 	// request method
						link:		'chain',// chain means concurrent requests go one after another, ignore and cancel are other choices
						encoding: 	'utf-8',// encoding type
						emulation:	true,	// rails-mode for methods other than get/post
						urlencode:	false,	// urlencode data?
						data:		'',		// send nothing
						/*	Behavior options */
						notify:		false,	// run this function when the response is ready: fn(NodeRef,LoaderRef)
						errnotify:	false,	// run this function in event of error: fn(statuscode, RequestRef)
						/* document Only options */
						importspark:	true,	// set the inner document.Spark?
						importscope:	false,	// set the inner document.Spark.Scope to the parent window?
						bootstrap:		false	// run the inner bootstrap function?
						/*	GetData Options */
// not implemented		key:		false	// get this key from the SparkData server					
					},
					initialize:	function(options){
						this.setOptions(options);
						this.store = new Hash();
						var me = this;
						var opts = this.options;
						var styles = {
									width:1,
									height:1,
									border:'none',
									overflow:'hidden',
									bottom:0,
									right:0,
									position:'absolute'
								};
						var O = {
									async: this.options.async,
									method: this.options.method || 'get',
									link:	this.options.link || 'chain',
									encoding: this.options.encoding || 'utf-8',
									evalScripts: this.options.scripts || false,
									evalResponse: this.options.js || false,
									emulation:	this.options.emulation || true,
									urlEncoded:	this.options.urlencode || false,
									onFailure:	function(){ if(opts.errnotify) opts.errnotify.attempt(me); }
								};
						var treeparse = function(noderoot){
									me.tree = document.Spark.Scope.$(noderoot);
									if(opts.selectors)
									{
										opts.selectors.each(function(selector){
											me.store.set(selector, noderoot.getElements(selector) );
										});
									}
									if(opts.notify)
									{
										opts.notify.attempt(me);
									}
									if(opts.transplantCSS)
									{
										me.stylesheet = '';
										noderoot.getElements('style').each( function(sn){ me.stylesheet += "\n" + sn.get('text'); } );
									}
								};
						if(opts.expandUri)
							opts.uri = document.Spark.getDataURI(opts.uri);
						
						switch(opts.returns)
						{
							case 'document':
								var N = new Element('iframe').setStyles(styles).injectInside(document.body);
								N.addEvent('load', function(){
									var root = N.contentDocument;
									try{
										if(opts.importspark)
										{
											root.Spark = document.Spark;
										}
										if(!opts.importscope)
										{
											// copy $ function to subwindow.
											N.contentWindow.$ = document.Spark.Scope.$;
											N.contentWindow.$$ = document.Spark.Scope.$$;
											
											root.Spark.Scope = N.contentWindow;
										}
										if(opts.bootstrap)
										{
											if (N.contentWindow.bootstrap) {
												N.contentWindow.bootstrap();
											}	
										}	
									}catch(e){ alert(e); }
									
									treeparse(root.body);
								});	
								N.addEvent('error', function(){ if(opts.errnotify) opts.errnotify.attempt(me); } )
								N.src = opts.uri;
							break;
							case 'nodes':
								var N = new Element('div').setStyles(styles).injectInside(document.body);
								N.set('load', O);
								var R = N.get('load');
								if(opts.async)
								{
									R.addEvent('success', function(){
										treeparse(N);
									});
								}
								// actually do the load
								N.load( opts.uri );
								if(!opts.async)
								{
									treeparse(N);
								}
							break;
							case 'js':
								O.evalResponse = true;
								O.evalScripts = false;
								var R = new Request(O);
								me.request = R;
								if(opts.async)
								{
									R.addEvent('success', function(){
										if(opts.notify)
										{
											opts.notify.attempt(me);
										}
									});
								}
								R.options.url = opts.uri;
								R.send();
							break;
							case 'json':
								O.secure = false;
								delete O.evalResponse;
								delete O.evalScripts;
								var R = new Request.JSON(O);
								me.request = R;
								R.addEvent('complete', function(data, text){
									me.data = data;
									me.tree = text;
									if (opts.notify) 
										opts.notify.attempt(me);												
								});
								R.options.url = opts.uri;
								R.send();
							break;
							case 'string':
								O.evalResponse = false;
								var R = new Request(O);
								me.request = R;
								if(opts.async)
								{
									if(opts.notify)
									{
										R.addEvent('success', function(text){
											me.data = text;
											if(opts.notify)
												opts.notify.attempt(me);
										});
									}	
								}
								R.options.url = opts.uri;
								R.send();
							break;
						}
					}
				}),
				DataAdapter:	new Class({
					initialize:	function(modulekey, primaryvalue){
						this.key = modulekey;
						this.pv = primaryvalue;
						this.hash = document.Spark.Utility.Base64.encode( modulekey + '/' + primaryvalue );
						this.funcs = {sync: true};
						this.funcs[ 'fmcs.ok' ] = this.handle.bind(this);
						this.funcs[ 'error.fmcs' ] = this.error.bind(this);
					},
					get:		function(cb){
						this._listener = cb;
						document.Spark.remoteProcedure('fmcsGet', [this.key, this.pv], this.funcs);
					},
					set:		function(values, cb){
						this._listener = cb;
						document.Spark.remoteProcedure('fmcsSet', [this.key, this.pv, values], this.funcs);
					},
					handle:		function(a){
						if(this._listener)
							this._listener(a().details);
						document.Spark.clobberThisEvent('fmcs.'+this.hash);
					},
					error:		function(a){
						if(this._listener)
							this._listener( false );
						document.Spark.clobberThisEvent('error.fmcs');
					}
				}),
				/**
				 * The aggregator is an Ajax-friendly simple closure that allows multiple 'stages' to be
				 * defined that must be satisfied before the final function is called.  Stages are input
				 * like this:
				 * {
				 *   'stages': ['a','b','c','d'],
				 *   latch: function(state){ with (state){ return stage1 && stage2 || stage3; }}
				 * }
				 * 
				 * If the latch function is not defined, the default function requires all stages to be
				 * satisfied before releasing the latch.
				 * 
				 * Using the aggregator is simple:
				 * 
				 * var collector = document.Spark.Aggregator({stages: ['a','b','c']}, function(){ alert(arguments.length) } );
				 * 
				 * Once the collector has been set up, ajax methods use collector.curry('a'), while standard
				 * methods use collector.run(['a','data']); or simply collector('a','data');
				 * 
				 * An example:
				 * 
				 * Asset.image('image.png', {onload: collector.pass('a')});
				 * collector('b','hello');
				 * google.maps.GClientGeocoder('1 main street 10076', collector.curry('c') );
				 * 
				 * Once all the stages are satisifed, the function will fire.
				 * 
				 */
				Aggregator:	function(config, finalfn)
				{
					var latch = null; var priv = {}; var state = $H({});
					// first, assign the latching semantics.
					
					// either use the configured latch function or a simple one.
					// the simple latch below examines each member of 'state' to see if it
					// is defined.  If not, it quits. If the latch gets all the way through,
					// it succeeds.
					
					latch = config.latch ? config.latch : ( function(state,priv){
							// release when all values are defined.
							return ! (state.contains(false));
						});
					
					// next, set up the private space for stages and the latch state for each.
					$each(config.stages || config, function(v){
							state[v] = false;
							priv[v] = '';
					});
					
					// next, define the closure.
					
					var r = function(stage, data){
						if (stage && !state[stage]) {
							priv[stage] = data;
							state[stage] = true;
						}
						if(latch(state,priv))
							return finalfn(priv);
					}
					
					return r;
					
				},
				/**
				 * This is the major mechanism by which we capture general purpose
				 * User Interface events and pass them off to Spark to handle.
				 * @param {Object} destiny, should have 'type' and 'arg'
				 * @param {Array} tests, array of functions.  "this" is "environment". Must return t/f
				 * @param {Object} environment any object you'd like to have access to in the test functions.
				 */
				procedure:	function(destiny, tests, environment)
				{
					// default to 'action' type
					var type = destiny.type || 'action';
					var arg = destiny.arg || alert.pass('Bad destiny argument');
					// make destiny available in the environment
					var env = $merge(destiny, environment);
					var testfuncs = tests || [$lambda(true)];
					var destinyfn = function(){
						switch(type)
						{
							case 'link':
								return document.Spark.UI.navigate(arg);
							break;
							case 'window':
								return document.Spark.Chrome.subWindow(arg);
							break;
							case 'action':
								return arg.bind(env)();
							break;
						}
					};
					
					return {
						trigger: function(){
							// walk through each test and make sure it "passes"
							var s = true;
							$each(testfuncs, function(fn){
								// bind the fn to the environment and call the function.
								var x = fn.run(null, env);
								if(!x)
									s = false;
							});
							if(!s)
								return;
							// no function failed, so now trigger the destiny function.
							var r = destinyfn();
							destinyfn = null;
							testfuncs = null;	
							return r;
						}
					};
				},
				UsesTemplate:	new Class({
					Extends: Events,
					_scaffold: null,
					_state:	   false,
					_className: 'fromtemplate',
					parsed:		{},
					initialize:	function(file, selector)
					{
						if(!file)
							file = this._file;
						if(!selector)
							selector = this._selector;
						if(file)
						{
							new document.Spark.Fetch(
							{
								returns: 'document',
								uri: file,
								importspark: true,
								expandUri: true,
								bootstrap: true,
								async: true,
								notify: this._attach.curry(selector).bind(this),
								scripts: true
							});
							this._className = file.split('/').pop().replace(/\./g,'-').camelCase();
						}
					},
					_attach:	function(sel, nodes)
					{
						var s = sel || '#scaffold';
						this._scaffold = nodes.tree.getElement(s);
						this._state = true;
						this._scaffold.removeAttribute('id');
						this._scaffold.addClass(this._className);
						this.parsed = this.getParsed(this._scaffold);
						this.imgs = this.getImages(this._scaffold);
						this.fireEvent('template');
					},
					getParsed:	function(scaf,remove)
					{
						var mix = {};
						scaf.getElements('.parse').each(function(node){
							if(remove)
								node.removeClass('parse');
							mix[ node.get('id') ] = node;
							//alert('getParsed is adding node id ' + node.get('id') + ' final value ' + mix[ node.get('id') ] );
						});
						return mix;
					},
					/**
					 * Get all "image-relative" class images and resolve their correct addresses.
					 * @param {Object} scaf
					 */
					getImages:	function(scaf)
					{
						var col = scaf.getElements('.image-relative');
						if(col.length)
						{
							col.each(function(imgNode){
								imgNode.set('src', document.Spark.getDataURI( imgNode.get('src') ) );
							});
						}
						return col;
					},
					safe:		function(cb)
					{
                        try {
                            if (!this._state) {
                                this.addEvent('template', cb);
                            }
                            else {
                                cb.run();
                            }
                        } 
                        catch (e) {
                            alert(e);
                        }
						return this;	
					},
					getScaffold:	function()
					{
						return this._scaffold;
					},
					clone:			function()
					{
						var clone = this._scaffold.clone(true,true);
						return {parsed: this.getParsed(clone), scaffold: clone};
					}
				})
			}) );	
	}
	else{
		document.Spark = uniClone(window.top.document.Spark);
		document.Spark.Scope = window;
	}
 });
