//ai namespace
var ai = {
	globals: {ai:[]},
	helpers:{},
	libraries:{},
	ns:window,
	_log:[]
}

ai.ns.app = {
	controllers:{},
	models:{},
	views:{},
	data:{},
	config:{},
	helpers:{},
	libraries:{},
	hooks:{},
	loaded:false,
	globals:{}
};

ai.load_deferred = function(first, second, cb){
	ai.load_script(first, function(){
		ai.load_script(second, cb||function(){});
	});
};

ai.load_scripts = function(arr, cb){
	var scripts_out = 0;
	for(var i = 0; i < arr.length; i++){
		scripts_out++;
		ai.load_script(arr[i], function(){
			scripts_out--;			
			if(scripts_out === 0){
				if(cb){cb();}
			}
		});
	}
};
ai.load_script = function(src, cb){
		var head = document.getElementsByTagName("head")[0] || document.documentElement;
		var script = document.createElement("script");
		script.src = src
		var done = false;
		// Attach handlers for all browsers
		script.onload = script.onreadystatechange = function() {
			if ( !done && (!this.readyState || this.readyState === "loaded" || this.readyState === "complete") ) {		
				done = true;	
				if(cb){cb();}
				// Handle memory leak in IE
				script.onload = script.onreadystatechange = null;
				if ( head && script.parentNode ) {
					head.removeChild( script );
				}
			}
		};		
		// Use insertBefore instead of appendChild  to circumvent an IE6 bug.
		// This arises when a base node is used (#2709 and #4378).
		head.insertBefore( script, head.firstChild );
		// We handle everything using the script element injection
		return undefined;	
};



//Helper Functions
ai.bind = function(func, to, args){
	return function(){
		args = args || [];
		func.apply(to, args.concat(Array.prototype.slice.call(arguments)));
	}
};

ai.defined = function(obj){
	return (obj != undefined);
};
ai.each = function(iterable, fn, bind){
	if(iterable && typeof iterable.length == 'number' && ai.type(iterable) != 'object'){
		for(var i = 0; i < iterable.length; i++){
			fn.call(bind || iterable, iterable[i], i, iterable);
		}
	}else{
		 for(var name in iterable){
			fn.call(bind || iterable, iterable[name], name, iterable);
		}
	}
};
ai.extend = function(){
	var _args = arguments;
	for(var prop in _args[1]){
		_args[0][prop] = _args[1][prop];
	}
	return _args[0];
}
ai.filter = function(arr, func){
	var new_arr = [];
	ai.each(arr, function(el){
		if(func(el)){
			new_arr.push(el);
		}
	});
	return new_arr;
};

ai.merge = function(){
	var mix = {};
	for (var i = 0; i < arguments.length; i++){
		for (var property in arguments[i]){
			var ap = arguments[i][property];
			var mp = mix[property];
			if (mp && ai.type(ap) == 'object' && ai.type(mp) == 'object') mix[property] = ai.merge(mp, ap);
			else mix[property] = ap;
		}
	}
	return mix;
};
ai.run = function(action, post, add_to_history, return_value){
	if(add_to_history){
	//	dhtmlHistory.add(action, post||{});
		setTimeout(function(){ ignoreLocation = false; },600);
		ignoreLocation = true;
	}
	var val = app.fc.run(action,post||{});
	if(return_value) return val;
};
ai.type = function(obj){
	if (!ai.defined(obj)) return false;
	if (obj.htmlElement) return 'element';
	var type = typeof obj;
	if (type == 'object' && obj.nodeName){
		switch(obj.nodeType){
			case 1: return 'element';
			case 3: return (/\S/).test(obj.nodeValue) ? 'textnode' : 'whitespace';
		}
	}
	if (type == 'object' || type == 'function'){
		switch(obj.constructor){
			case Array: return 'array';
			case RegExp: return 'regexp';
			case ai.Class: return 'class';
		}
		if (typeof obj.length == 'number'){
			if (obj.item) return 'collection';
			if (obj.callee) return 'arguments';
		}
	}
	return type;
};

ai.form_run = function(action, post){
	return ai.run(action,post,false,true);
};
ai.go = function(action, post){
	return ai.run(action, post, true);
};

ai.get_action_from_url = function(default_action){
	return	(new String(window.location).split("#").length > 1) ? new String(window.location).split("#")[1] : default_action;
};

ai.run_from_url = function(default_action, data){
	ai.run(ai.get_action_from_url(default_action), data, false);
};
ai.go_from_url = function(default_action, data){
	ai.go(ai.get_action_from_url(default_action), data);
};

ai.get_instance = function(){
	//return loader class if controller isn't available, ie. one doesn't exist or one is being initialized
	if(ai.globals.return_loader || ai.globals.ai.length == 0){
		ai.log('return load');
		return ai.globals.obj.load;
	}else{
		ai.log('ai.get_instance()');
		return ai.globals.ai[ai.globals.ai.length-1];
	}
}

ai.log = function(message, level){
	var level = level || 'all';
	ai._log.push({message:message, level:level});
};
ai.show_log = function(which){
	var str = '';
	ai.each(ai._log, function(log){
		if(!which || log.level == which){
			str += '<font class="log log_'+log.level+'" >'+log.message+'</font><br />';	
		}
	});
	try{
		ai.html('log', str);
	}catch(e){}
};
ai.hide_log = function(){
	try{
		ai.html('log', ' ');
	}catch(e){}
};
ai.clear_log = function(){
	ai._log = [];
};

ai.byId = function(el){
	if(ai.type(el)=='element'){ 
		return el;
	}
	return document.getElementById(el);
}

//Classes
ai.Class = function(properties){
	var klass = function(){
		return (arguments[0] !== null && this.initialize && ai.type(this.initialize) == 'function') ? this.initialize.apply(this, arguments) : this;
	};
	ai.extend(klass, this);
	klass.prototype = properties;
	klass.constructor = ai.Class;
	if(!klass.prototype.initialize && klass.prototype.init){
		klass.prototype.initialize = klass.prototype.init;
	}
	return klass;
};
ai.Class.empty = function(){};

ai.Class.prototype = {
	extend: function(properties){
		var proto = new this(null);
		for (var property in properties){
			var pp = proto[property];
			proto[property] = ai.Class.Merge(pp, properties[property]);
		}
		return new ai.Class(proto);
	},

	implement: function(){
		for (var i = 0, l = arguments.length; i < l; i++) ai.extend(this.prototype, arguments[i]);
	}

};

ai.Class.Merge = function(previous, current){
	if (previous && previous != current){
		var type = ai.type(current);
		if (type != ai.type(previous)) return current;
		switch(type){
			case 'function':
				var merged = function(){
					this.parent = arguments.callee.parent;
					return current.apply(this, arguments);
				};
				merged.parent = previous;
				return merged;
			case 'object': return ai.merge(previous, current);
		}
	}
	return current;
};

ai.Config = new ai.Class({
	initialize: function(){
		if(!ai.defined(app.config)){
			app.config = {};
		}
		this.data = app.config;	
	},
	
	read: function(name){
		currentData = this.data;
		name = name.split('.');
		for(var i = 0; i < name.length; i++){
			if(i === name.length - 1){
				return currentData[name[i]];
			}else{
				currentData = currentData[name[i]];
			}
		}
	}
	
});

ai.Session = new ai.Class({
	initialize: function(){
		if(!ai.defined(app._session)){
			app._session = {};
		}
		this.data = app._session;
	},
	
	_write: function(name, value, type){
		if(ai.type(name) != 'array'){
			name = name.split('.');
		}
		currentData = this.data;
		ai.each(name, function(key, i){
			if(i === name.length - 1){
				if(type == 'write'){
					currentData[key] = value;
				}else if(type == 'push'){
					currentData[key].push(value);
				}else if(type == 'merge'){
					currentData[key] = ai.merge(currentData[key], value);
				}else if(type == 'update'){
					ai.extend(currentData[key], value);
				}
			}else{
				if(!ai.defined(currentData[key])){
					currentData[key] = {};
				}
				currentData = currentData[key];
			}
		});
		return this.data;
	},
	
	write: function(name, value){
		return this._write(name, value, 'write');
	},
	
	push: function(name, value){
		return this._write(name, value, 'push');
	},
	
	merge: function(name, object){
		return this._write(name, object, 'merge');
	},
	
	update: function(name, value){
		return this._write(name, value, 'update');
	},
	
	read: function(name){
		currentData = this.data;
		name = name.split('.');
		for(var i = 0; i < name.length; i++){
			 if(i === name.length - 1){
			 	 return currentData[name[i]];
			 }else{
				 currentData = currentData[name[i]];
			 }
		}
	}
});

ai.Loader = new ai.Class({
	initialize:function(){},

	library: function(library, args){
		ai.log('load.library('+library+');');
			if(app.libraries[library]){
				 this._loadClass(library, app.libraries[library], args);
			}else{
				this._loadClass(library, ai.libraries[library], args);
			}
		},

		helper:function(helpers, prefix){
			if(!this._helpers) this._helpers = {};
			if(ai.type(helpers) != 'array'){
				helpers = [helpers];
			}
			ai.each(helpers, function(helper){
				if(app.helpers[helper]){
					helper = app.helpers[helper];
				}else{
					helper = ai.helpers[helper];
				}
				this._loadHelper(helper, prefix|| app.config.helper_prefix);
			}, this);
		},

		view: function(view, data, outputInContainer){
			ai.log('load.view('+view+');');
			return this._loadView(app.views[view], data, outputInContainer);
		},

		model: function(model){
			ai.log('load.model('+model+');');
			var fwi = ai.get_instance();
			fwi[model] = new app.models[model]();
			fwi[model]._assignLibraries();
		},

		_loadClass: function(keyword, class_name, args){
			var fwi =  ai.get_instance();
			fwi[keyword] = new class_name(args); 
		},

		_loadView: function(view, data, outputInContainer){
			data = data || {};
	//		if(this._helpers) data._MODIFIERS = this._helpers;		
			return view.process(data, outputInContainer);
		},

		_loadHelper: function(helper, prefix){
			for(func in helper){
				var label = prefix ? (prefix+func) : func;
				ai.ns[label] = helper[func];
				this._helpers[label] = helper[func];
			}
		},

		//need these accessible for helpers when a controller isn't available
		Session: new ai.Session(),
		Config: new ai.Config()
});

ai.Base = ai.Loader.extend({
	initialize:function(){
		this.load = this;
		ai.globals.obj = {};
		for(var prop in this.load){
			ai.globals.obj[prop] = this.load[prop];
		} 
	}		
});

ai.Controller = ai.Base.extend({
	initialize:function(){
		this.parent();
		this.flash_message = '';
		this.return_value = false;
		this.Config = new ai.Config();
		this.Session = new ai.Session();
	},

	flash:function(message, clearTime){
		try{
			if(ai.byId(ai.Config.getItem('flashContainer'))){
				ai.html(ai.Config.getItem('flashContainer'), message);
			}
			if(clearTime){
				setTimeout(function(){ai.html(ai.configItem('flashContainer'), ' ');});
			}
		}catch(e){}
	},
	
	result:function(){
		return this.return_value;
	},

	run:function(action,post,addToHistory){
		var router = new ai.Router();
		router.setAction(action);
		var controller = new app.controllers[router.getClass()]();
		controller.post = post || {};
		controller[router.getMethod()].apply(controller, router.getParams());
	}
});

ai.DataStore = new ai.Class({
	initialize: function(obj,options){
		this.setOptions(options||{});
		this.length = 0;
		this.data = {};  
		this.ids = new Array();
		try{if(obj) this.add(obj)} catch(e){}
	},   
	count:function(){
		return this.length;
	},

	setOptions:function(options){
		this.key = options.key || 'id';
	},

	getIds:function(){
		return this.ids;
	},

	add:function(obj){
		if(ai.type(obj) == 'array'){
			for(var i=0;i<obj.length;i++){
				this.add(obj[i]);
			}
		}else{   
			this.ids.push(obj[this.key]);
			this.length++;    
			this.data[obj[this.key]] = obj;
		}
    },  

	exists: function(row, field){
		var found = false;
		var field = field || 'id';
		ai.each(this.data, function(data_row){
			if(data_row[field] == row[field]){
				found = true;
			}
		});
		return found;
	},

	update:function(data, id){
		if(!id){
			id = data['id'];
		}
		ai.extend(this.data[id], data);
	},

	byId: function(id){
	   return this.data[id];
	},

	remove: function(id_to_remove){
		var old_data = this.data;
		this.data = {};
		this.ids = [];
		this.length = 0;
		ai.each(old_data, function(data, id){
			if(id != id_to_remove){
				this.add(data);
			}
		}, this);
	},   

	removeIndex: function(index){
		var arr = new Array();
		for(i=0;i<this.ids.length;i++){
			if(i!=index) arr.push(this.data[this.ids[i]]);
		}   
		var dat = new ai.DataStore(arr);
		return dat;
	},

	toArray: function(sort_func)	{
		var arr = [];
		for(i=0; i<this.ids.length; i++){
			arr.push(this.data[this.ids[i]]);
		}
		return sort_func ? arr.sort(sort_func) : arr;
	},

	setOrder: function(order){       
		ord = [];  
		ai.each(order, function(id){ 
		    	if(id in this.ids) ord.push(id);
		}, this);
		this.ids = ord;
	},

	getAll:function(){
		return this.data;
	},

	reset: function(data){
		this.initialize();
		this.add(data);
	}
});


ai.FrontController = new ai.Class({
	initialize:function(){
		this.hooks = new ai.Hooks();
	},
	run:function(action, post){
		app.globals.current_action = action;
		this.hooks.runHook('preSystem');
		//load config
		this.router = new ai.Router();

		this.hooks.runHook('preController');

		this.router.setAction(action);
		this.class_name = this.router.getClass();
		this.method = this.router.getMethod();

		ai.globals.return_loader = true;
		
		var controller = new app.controllers[this.class_name]();
		controller.class_name = this.class_name;
		controller.method = this.method;
		ai.globals.ai.push(controller);
		ai.globals.return_loader = false;
		

		controller.post = controller.data = post;
		this.hooks.runHook('postControllerConstructor');

		_this = controller;
		
		controller[this.method].apply(controller, this.router.getParams());

		this.hooks.runHook('postController');

		var result = controller.result();

		controller = ai.globals.ai.pop();
		delete(controller);

		this.hooks.runHook('postSystem');

		return result;
	}
});

ai.Hooks = new ai.Class({
	initialize:function(){

	},

	runHook: function(which){
		try{
			app.hooks[which]();
		}catch(e){}
	}
});

ai.Model = new ai.Class({
	initialize:function(){	
		this.data = null;
		this.associations = {};	
		this.dont_attach = false;
		this.binds = [];
		this.auto_bind = false;
		this.sort_by = null;
	},
	
	_assignLibraries:function(){
		var aii = ai.get_instance();
		for(prop in aii){
			if(ai.type(this[prop]) == 'undefined'){
				this[prop] = aii[prop];
			}
		}
	},

	getRandom: function(){
		var ids = this.data.getIds();
		var data = this.byId(ids[Math.ceil(ids.length*Math.random())-1]);
		this.attachAssociations(data);
		return data;
	},

	getAll:function(){
		var data = this.data.toArray();	
		this.attachAssociations(data);
		return data;	
	},

	getSorted:function(func){
		var data = this.data.toArray();
		this.attachAssociations(data);
		data.sort(func);
		return data;
	},

	sortBy: function(func){
		this.sort_by = func;
	},

	cancelAssociations:function(){
		this.dont_attach = true;
		return this;
	},

	createAssociations:function(){
		this.dont_attach = false;
		return this;
	},	

	bind:function(assoc){
		//if bind() called without assoc, bind all associations
		if(!assoc){
			assoc = [];
			ai.each(this.associations, function(func, key){
				assoc.push(key);
			});
		}
		if(ai.type(assoc) != 'array') assoc = [assoc];
		ai.extend(this.binds, assoc);
		return this;
	},

	unbind:function(assoc){
		this.auto_bind = false;
		if(!assoc){
			this.binds = [];
			return this;
		}
		if(ai.type(assoc) != 'array') assoc = [assoc];
		var new_binds = [];
		ai.each(this.associations, function(func, key){
			ai.each(assoc, function(out){
				if(key != out){
					new_binds.push(key);
				}
			});
		}, this)
		this.binds = new_binds;
		return this;
	},

	attachAssociations:function(data){		
		//if auto_bind, bind all associations
		if(this.auto_bind) this.bind();

		var type = ai.type(data);
		if(type != 'array') var data = [data];


		ai.each(data, function(row){
			ai.each(this.binds, function(assoc){				
				row[assoc] = this.associations[assoc].call(this, row);
			}, this);
		}, this);
		return (type != 'array') ? data[0] : data;
	},


	findByCondition:function(func, return_single_object){
		var data = this.data.getAll();
		var ret = [];
		ai.each(data, function(row){
			if(func(row)) ret.push(row);
		});
		this.attachAssociations(ret);
		return (ret.length > 0) ? (return_single_object ? ret[0] : ret) : false;
	},

	findIn:function(in_array){
		var ret = [];
		ai.each(in_array, function(id){
			ret.push(this.data.byId(id));
		},this);
		this.attachAssociations(ret);
		return ret;
	},

	getArray:function(){
		return this.getAll();
	},

	byId:function(id){
		var data =  this.data.byId(id);
		return this.attachAssociations(data);
	},

	insert: function(vals){
		this.data.add(vals);
	},

	create:function(vals){
		this.data.add(vals);
	},

	update:function(vals, id){
		id = id ? id : vals['id'];
		this.data.update(vals, id);
	},

	resetData:function(data){
		this.data.reset(data);
	},

	remove: function(id){
		this.data.remove(id);
	}

});

ai.Router = new ai.Class({
	initialize: function(){
		this.controllersPath = app.controllers;
	},
	setAction: function(action){
		this.action = action;
		this.parseAction();
	},
	getClass:function(){
		return this.class_name;
	},
	getMethod: function(){
		return this.method;
	},
	getParams: function(){
		return this.params;
	},
	parseAction:function(){
		parts = this.action.split('/');
		this.class_name = parts[0];
		this.method = parts.length > 1 ? parts[1]: 'index';
		if(parts.length>2){
			this.params = parts.slice(2);
		}else{
			this.params = [];
		}
	}
});


ai.View = new ai.Class({
		initialize: function(options){			
			if(options['template_type'] == 'dom') this.parseDomTemplate(options['id']);
			else if(options['template_type'] == 'string') this.parseStringTemplate(options['string']);
			else if(options['template_type'] == 'url') this.handleUrlTemplate(options['url']);
			else if(options['template_type'] == 'builder') this.handleBuilderTemplate(options['id']);
			this.handleOptions(options);
		},

		handleOptions:function(options){
			if(options['container']) this.container = options['container']; 
		},

		parseDomTemplate:function(dom_id){
			ai.log('parseDomTemplate('+dom_id+');');
			this.template = TrimPath.parseDOMTemplate(dom_id);
			ai.log('end parseDomTEmp');
		},

		parseStringTemplate:function(string){
			this.template = TrimPath.parseTemplate(string);
		},

		handleUrlTemplate: function(url){
			//to do - request url, process template
		},
		
		handleBuilderObject: function(id){
			
		},

		process: function(data, outputInContainer){
			var result = this.template.process(data);
			if(!outputInContainer){
			 	return result;
			}else{
				var container = this.container ?  ai.byId(this.container) : ai.byId(outputInContainer);
				container.innerHTML = result;
			}
		}
});




//ai.helpers
ai.helpers = {
	html:{
		run: function(action, title, atts, post){
			return '<a href="javascript:ai.run('+ai._parse_action(action)+');" '+ai._parse_attributes(atts||null)+'>'+title+'</a>';
		},

		go: function(action, title, atts, post){
			return '<a href="#'+action+'" onclick="go('+ai._parse_action(action)+');" '+ai._parse_attributes(atts||null)+'>'+title+'</a>';
		},

		link: function(action, title, atts){
			return '<a href="#" onclick="function(){ '+action+';return false}" '+ai._parse_attributes(atts||null)+'>'+title+'</a>';
		}
	},

	array:{
		in_array: function(needle, haystack){
			var in_haystack = false;
			ai.each(haystack, function(el){
				if(needle === el){
					in_haystack = true;
				}
			});
			return in_haystack;
		}
	},

	string: {
		md5:function(str){
			return ai._md5.hex_md5(str);
		}
	}			
}



ai._parse_attributes = function(atts){
	if(!atts) return '';
	var str = '';
	ai.each(atts, function(value, attribute){
		str += attribute+'="'+value+'" ';
	});
	return str;
}
ai._parse_post = function(post){
	var str = '{';
	ai.each(post, function(value, key){
		str += "'"+key+"':'"+value+"',";
	});
	return str.substring(0, str.length-1).concat('}');
}


ai._parse_action =function(action){
	if(ai.type(action) == 'array'){
		return "'"+action[0]+"', "+ai._parse_post(action[1]);
	}else{
		return "'"+action+"'";
	}
}





var TrimPath = {
	evalEx: function(src){ return eval(src);},
	parseTemplate: function(tmplContent, optTmplName, optEtc) {
        if (optEtc == null)
            optEtc = this.parseTemplate_etc;
        var funcSrc = this.parse(tmplContent, optTmplName, optEtc);
        var func = this.evalEx(funcSrc, optTmplName, 1);
        if (func != null)
            return new optEtc.Template(optTmplName, tmplContent, funcSrc, func, optEtc);
        return null;
    },
	parseTemplate_etc: {
		statementTag: "forelse|for|if|elseif|else|var|macro",
		statementDef: { // Lookup table for statement tags.
	        "if"     : { delta:  1, prefix: "if (", suffix: ") {", paramMin: 1 },
	        "else"   : { delta:  0, prefix: "} else {" },
	        "elseif" : { delta:  0, prefix: "} else if (", suffix: ") {", paramDefault: "true" },
	        "/if"    : { delta: -1, prefix: "}" },
	        "for"    : { delta:  1, paramMin: 3, 
	                     prefixFunc : function(stmtParts, state, tmplName, etc) {
	                        if (stmtParts[2] != "in")
	                            throw new etc.ParseError(tmplName, state.line, "bad for loop statement: " + stmtParts.join(' '));
	                        var iterVar = stmtParts[1];
	                        var listVar = "__LIST__" + iterVar;
	                        return [ "var ", listVar, " = ", stmtParts[3], ";",
	                             // Fix from Ross Shaull for hash looping, make sure that we have an array of loop lengths to treat like a stack.
	                             "var __LENGTH_STACK__;",
	                             "if (typeof(__LENGTH_STACK__) == 'undefined' || !__LENGTH_STACK__.length) __LENGTH_STACK__ = new Array();", 
	                             "__LENGTH_STACK__[__LENGTH_STACK__.length] = 0;", // Push a new for-loop onto the stack of loop lengths.
	                             "if ((", listVar, ") != null) { ",
	                             "var ", iterVar, "_ct = 0;",       // iterVar_ct variable, added by B. Bittman     
	                             "for (var ", iterVar, "_index in ", listVar, ") { ",
	                             iterVar, "_ct++;",
	                             "if (typeof(", listVar, "[", iterVar, "_index]) == 'function') {continue;}", // IE 5.x fix from Igor Poteryaev.
	                             "__LENGTH_STACK__[__LENGTH_STACK__.length - 1]++;",
	                             "var ", iterVar, " = ", listVar, "[", iterVar, "_index];" ].join("");
	                     } },
	        "forelse" : { delta:  0, prefix: "} } if (__LENGTH_STACK__[__LENGTH_STACK__.length - 1] == 0) { if (", suffix: ") {", paramDefault: "true" },
	        "/for"    : { delta: -1, prefix: "} }; delete __LENGTH_STACK__[__LENGTH_STACK__.length - 1];" }, // Remove the just-finished for-loop from the stack of loop lengths.
	        "var"     : { delta:  0, prefix: "var ", suffix: ";" },
	        "macro"   : { delta:  1, 
	                      prefixFunc : function(stmtParts, state, tmplName, etc) {
	                          var macroName = stmtParts[1].split('(')[0];
	                          return [ "var ", macroName, " = function", 
	                                   stmtParts.slice(1).join(' ').substring(macroName.length),
	                                   "{ var _OUT_arr = []; var _OUT = { write: function(m) { if (m) _OUT_arr.push(m); } }; " ].join('');
	                     } }, 
	        "/macro"  : { delta: -1, prefix: " return _OUT_arr.join(''); };" }
	    },
		modifierDef: {
	        "eat"        : function(v)    { return ""; },
	        "escape"     : function(s)    { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); },
	        "capitalize" : function(s)    { return String(s).toUpperCase(); },
	        "default"    : function(s, d) { return s != null ? s : d; },
			'h': this.escape
	    },
		Template:function(tmplName, tmplContent, funcSrc, func, etc) {
	        this.process = function(context, flags) {
	            if (context == null)
	                context = {};
	            if (context._MODIFIERS == null)
	                context._MODIFIERS = {};
	            if (context.defined == null)
	                context.defined = function(str) { return (context[str] != undefined); };
	            for (var k in etc.modifierDef) {
	                if (context._MODIFIERS[k] == null)
	                    context._MODIFIERS[k] = etc.modifierDef[k];
	            }
	            if (flags == null)
	                flags = {};
	            var resultArr = [];
	            var resultOut = { write: function(m) { resultArr.push(m); } };
	            try {
	                func(resultOut, context, flags);
	            } catch (e) {
	                if (flags.throwExceptions == true)
	                    throw e;
	                var result = new String(resultArr.join("") + "[ERROR: " + e.toString() + (e.message ? '; ' + e.message : '') + "]");
	                result["exception"] = e;
	                return result;
	            }
	            return resultArr.join("");
	        }
	        this.name       = tmplName;
	        this.source     = tmplContent; 
	        this.sourceFunc = funcSrc;
	        this.toString   = function() { return "TrimPath.Template [" + tmplName + "]"; }
	    },
		ParseError: function(name, line, message) {
	        this.name    = name;
	        this.line    = line;
	        this.message = message;
			this.toString = function(){ return ("TrimPath template ParseError in " + this.name + ": line " + this.line + ", " + this.message);};
	    }
	},
	
	parse: function(body, tmplName, etc) {
        body = this.cleanWhiteSpace(body);
        var funcText = [ "var TrimPath_Template_TEMP = function(_OUT, _CONTEXT, _FLAGS) { with (_CONTEXT) {" ];
        var state    = { stack: [], line: 1 };                              // TODO: Fix line number counting.
        var endStmtPrev = -1;
        while (endStmtPrev + 1 < body.length) {
            var begStmt = endStmtPrev;
            // Scan until we find some statement markup.
            begStmt = body.indexOf("{", begStmt + 1);
            while (begStmt >= 0) {
                var endStmt = body.indexOf('}', begStmt + 1);
                var stmt = body.substring(begStmt, endStmt);
                var blockrx = stmt.match(/^\{(cdata|minify|eval)/); // From B. Bittman, minify/eval/cdata implementation.
                if (blockrx) {
                    var blockType = blockrx[1]; 
                    var blockMarkerBeg = begStmt + blockType.length + 1;
                    var blockMarkerEnd = body.indexOf('}', blockMarkerBeg);
                    if (blockMarkerEnd >= 0) {
                        var blockMarker;
                        if( blockMarkerEnd - blockMarkerBeg <= 0 ) {
                            blockMarker = "{/" + blockType + "}";
                        } else {
                            blockMarker = body.substring(blockMarkerBeg + 1, blockMarkerEnd);
                        }                        
                        
                        var blockEnd = body.indexOf(blockMarker, blockMarkerEnd + 1);
                        if (blockEnd >= 0) {                            
                            this.emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText);
                            
                            var blockText = body.substring(blockMarkerEnd + 1, blockEnd);
                            if (blockType == 'cdata') {
                                this.emitText(blockText, funcText);
                            } else if (blockType == 'minify') {
                                this.emitText(this.scrubWhiteSpace(blockText), funcText);
                            } else if (blockType == 'eval') {
                                if (blockText != null && blockText.length > 0) // From B. Bittman, eval should not execute until process().
                                    funcText.push('_OUT.write( (function() { ' + blockText + ' })() );');
                            }
                            begStmt = endStmtPrev = blockEnd + blockMarker.length - 1;
                        }
                    }                        
                } else if (body.charAt(begStmt - 1) != '$' &&               // Not an expression or backslashed,
                           body.charAt(begStmt - 1) != '\\') {              // so check if it is a statement tag.
                    var offset = (body.charAt(begStmt + 1) == '/' ? 2 : 1); // Close tags offset of 2 skips '/'.
                                                                            // 10 is larger than maximum statement tag length.
                    if (body.substring(begStmt + offset, begStmt + 10 + offset).search(TrimPath.parseTemplate_etc.statementTag) == 0) 
                        break;                                              // Found a match.
                }
                begStmt = body.indexOf("{", begStmt + 1);
            }
            if (begStmt < 0)                              // In "a{for}c", begStmt will be 1.
                break;
            var endStmt = body.indexOf("}", begStmt + 1); // In "a{for}c", endStmt will be 5.
            if (endStmt < 0)
                break;
            this.emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText);
            this.emitStatement(body.substring(begStmt, endStmt + 1), state, funcText, tmplName, etc);
            endStmtPrev = endStmt;
        }
        this.emitSectionText(body.substring(endStmtPrev + 1), funcText);
        if (state.stack.length != 0)
            throw new etc.ParseError(tmplName, state.line, "unclosed, unmatched statement(s): " + state.stack.join(","));
        funcText.push("}}; TrimPath_Template_TEMP");
   		return funcText.join("");
    },

	emitStatement: function(stmtStr, state, funcText, tmplName, etc) {
        var parts = stmtStr.slice(1, -1).split(' ');
        var stmt = etc.statementDef[parts[0]]; // Here, parts[0] == for/if/else/...
        if (stmt == null) {                    // Not a real statement.
            this.emitSectionText(stmtStr, funcText);
            return;
        }
        if (stmt.delta < 0) {
            if (state.stack.length <= 0)
                throw new etc.ParseError(tmplName, state.line, "close tag does not match any previous statement: " + stmtStr);
            state.stack.pop();
        } 
        if (stmt.delta > 0)
            state.stack.push(stmtStr);

        if (stmt.paramMin != null &&
            stmt.paramMin >= parts.length)
            throw new etc.ParseError(tmplName, state.line, "statement needs more parameters: " + stmtStr);
        if (stmt.prefixFunc != null)
            funcText.push(stmt.prefixFunc(parts, state, tmplName, etc));
        else 
            funcText.push(stmt.prefix);
        if (stmt.suffix != null) {
            if (parts.length <= 1) {
                if (stmt.paramDefault != null)
                    funcText.push(stmt.paramDefault);
            } else {
                for (var i = 1; i < parts.length; i++) {
                    if (i > 1)
                        funcText.push(' ');
                    funcText.push(parts[i]);
                }
            }
            funcText.push(stmt.suffix);
        }
    },

	emitSectionText: function(text, funcText) {
        if (text.length <= 0)
            return;
        var nlPrefix = 0;               // Index to first non-newline in prefix.
        var nlSuffix = text.length - 1; // Index to first non-space/tab in suffix.
        while (nlPrefix < text.length && (text.charAt(nlPrefix) == '\n'))
            nlPrefix++;
        while (nlSuffix >= 0 && (text.charAt(nlSuffix) == ' ' || text.charAt(nlSuffix) == '\t'))
            nlSuffix--;
        if (nlSuffix < nlPrefix)
            nlSuffix = nlPrefix;
        if (nlPrefix > 0) {
            funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("');
            var s = text.substring(0, nlPrefix).replace('\n', '\\n'); // A macro IE fix from BJessen.
            if (s.charAt(s.length - 1) == '\n')
            	s = s.substring(0, s.length - 1);
            funcText.push(s);
            funcText.push('");');
        }
        var lines = text.substring(nlPrefix, nlSuffix + 1).split('\n');
        for (var i = 0; i < lines.length; i++) {
            this.emitSectionTextLine(lines[i], funcText);
            if (i < lines.length - 1)
                funcText.push('_OUT.write("\\n");\n');
        }
        if (nlSuffix + 1 < text.length) {
            funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("');
            var s = text.substring(nlSuffix + 1).replace('\n', '\\n');
            if (s.charAt(s.length - 1) == '\n')
            	s = s.substring(0, s.length - 1);
            funcText.push(s);
            funcText.push('");');
        }
    },

	emitSectionTextLine: function(line, funcText) {
        var endMarkPrev = '}';
        var endExprPrev = -1;
        while (endExprPrev + endMarkPrev.length < line.length) {
            var begMark = "${", endMark = "}";
            var begExpr = line.indexOf(begMark, endExprPrev + endMarkPrev.length); // In "a${b}c", begExpr == 1
            if (begExpr < 0)
                break;
            if (line.charAt(begExpr + 2) == '%') {
                begMark = "${%";
                endMark = "%}";
            }
            var endExpr = line.indexOf(endMark, begExpr + begMark.length);         // In "a${b}c", endExpr == 4;
            if (endExpr < 0)
                break;
            this.emitText(line.substring(endExprPrev + endMarkPrev.length, begExpr), funcText);                
            // Example: exprs == 'firstName|default:"John Doe"|capitalize'.split('|')
            var exprArr = line.substring(begExpr + begMark.length, endExpr).replace(/\|\|/g, "#@@#").split('|');
            for (var k in exprArr) {
                if (exprArr[k].replace) // IE 5.x fix from Igor Poteryaev.
                    exprArr[k] = exprArr[k].replace(/#@@#/g, '||');
            }
            funcText.push('_OUT.write(');
            this.emitExpression(exprArr, exprArr.length - 1, funcText); 
            funcText.push(');');
            endExprPrev = endExpr;
            endMarkPrev = endMark;
        }
        this.emitText(line.substring(endExprPrev + endMarkPrev.length), funcText); 
    },

	emitText: function(text, funcText) {
        if (text == null ||
            text.length <= 0)
            return;
        text = text.replace(/\\/g, '\\\\');
        text = text.replace(/\n/g, '\\n');
        text = text.replace(/"/g,  '\\"');
        funcText.push('_OUT.write("');
        funcText.push(text);
        funcText.push('");');
    },

	emitExpression: function(exprArr, index, funcText) {
        // Ex: foo|a:x|b:y1,y2|c:z1,z2 is emitted as c(b(a(foo,x),y1,y2),z1,z2)
        var expr = exprArr[index]; // Ex: exprArr == [firstName,capitalize,default:"John Doe"]
        if (index <= 0) {          // Ex: expr    == 'default:"John Doe"'
            funcText.push(expr);
            return;
        }
        var parts = expr.split(':');
        funcText.push('_MODIFIERS["');
        funcText.push(parts[0]); // The parts[0] is a modifier function name, like capitalize.
        funcText.push('"](');
        this.emitExpression(exprArr, index - 1, funcText);
        if (parts.length > 1) {
            funcText.push(',');
            funcText.push(parts[1]);
        }
        funcText.push(')');
    },

	cleanWhiteSpace: function(result) {
        result = result.replace(/\t/g,   "    ");
        result = result.replace(/\r\n/g, "\n");
        result = result.replace(/\r/g,   "\n");
        result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev.
        return result;
    },

	scrubWhiteSpace: function(result) {
        result = result.replace(/^\s+/g,   "");
        result = result.replace(/\s+$/g,   "");
        result = result.replace(/\s+/g,   " ");
        result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev.
        return result;
    },

	parseDOMTemplate: function(elementId, optDocument, optEtc) {
        if (optDocument == null)
            optDocument = document;
        var element = optDocument.getElementById(elementId);
        var content = element.value;     // Like textarea.value.
        if (content == null)
            content = element.innerHTML; // Like textarea.innerHTML.
        content = content.replace(/&lt;/g, "<").replace(/&gt;/g, ">");
        return TrimPath.parseTemplate(content, elementId, optEtc);
    }, 

	processDOMTemplate: function(elementId, context, optFlags, optDocument, optEtc) {
        return TrimPath.parseDOMTemplate(elementId, optDocument, optEtc).process(context, optFlags);
    }	
};

app.fc = new ai.FrontController();
