/*
Script: Bumble.js
	Embedded Javascript HTML processor

License:
	BSD license.
*/


// Extended Support Methods

Array.prototype.forEach = Array.prototype.forEach || function(func, bind)
{
	for(var i=0;i<this.length;i++) 
		func.call(bind, this[i], i, this);
};

Array.prototype.each = Array.prototype.each || Array.prototype.forEach;

String.prototype.trim = String.prototype.trim || function()
{
	return this.replace(/(^\s+|\s+$)/g, "");
};

String.prototype.shrink = function()
{
	return this.replace(/[ \t]+/g, " ").replace(/\s*\n+\s*/g, "\n");
};

String.prototype.doubleQuote = function()
{
	return "\"" + this.replace(/\\/g, "\\\\").
		replace(/"/g, "\\\"").
		replace(/\n/g, "\\n") + "\"";
};

String.prototype.singleQuote = function()
{
	return "'" + this.replace(/\\/g, "\\\\").
		replace(/'/g, "\\'").
		replace(/\n/g, "\\n") + "'";	
};

String.prototype.xmlEncode = function()
{
	return this.replace(/&/g, "&amp;")
		.replace(/>/g, "&gt;")
		.replace(/</g, "&lt;")
		.replace(/"/g, "&quot;")
		/*.replace(/'/g, "&apos;")*/;
};

/*
Class: Bumble
	The primary object for the Bumble toolkit. 
	All methods are static.
*/
var Bumble = 
{	
	Cache: { },
	UniqueId: 0,
	
	/** Resolve one URL relative to another
	 */
	ResolveURL: function(base, url)
	{   
        if(url.match(/^[a-z]+:\/\/.+/i))
            return Bumble.CanonicalURL(url);
        else if(base.match(/^[a-z]+:\/\/[^\/]*$/i))
            base += "/";
        else
            base = base.replace(/\/[^\/]*$/, "/");
        if(url.substr(0,1) == "/" && base.match(/^[a-z]+:\/\/.+/i))
			return Bumble.CanonicalURL(base.replace(/^([a-z]+:\/\/[^\/]*)\/.*/i, "$1" + url));
        else if(url.substr(0,1) == "/")
        	return Bumble.CanonicalURL(url);
        else
			return Bumble.CanonicalURL(base + url);
    },


	/** Transform URL into its canonical form.
	 */
	CanonicalURL: function(url)
	{
        var SERVER_INFO	= 0;	var ROOT 	= 1;
        var CURRENT 	= 2;	var PARENT	= 3;
        var FOLDER		= 4;	var FILE	= 5;

        var normal = [ ];
        var serverMatch = url.match(/^([a-z]+:\/\/[^\/]*)/i);
        if(serverMatch)
        {
            normal.push({ type: SERVER_INFO, value: serverMatch[1] });
            url = url.replace(/^([a-z]+:\/\/[^\/]*)/i, "");
        }
        
        url = url.replace(/\/+/g, "/");
        if(url.substr(0,1) == "/")
        {
            normal.push({ type: ROOT, value: "" });
            url = url.replace(/^\//, "");
        }
        
        var parts = url.split("/");
        for(var i=0;i<parts.length;i++)
        {
			var parentType = (normal.length > 0) ? 
				normal[normal.length - 1].type : null;
            if(parts[i] == "..")
            {
				if(normal.length > 0 && parentType != PARENT &&
					parentType != SERVER_INFO && parentType != ROOT)
					normal.pop();
				else
                	normal.push({ type: PARENT, value: ".." });
            }
            else if(i == (parts.length - 1))
            {
                normal.push({ type: FILE, value: parts[i] });
                break;
            }
            else if(parts[i] != "." && !(i == (parts.length - 1) && 
                parts[parts.length - 1] == ""))
            {
                normal.push({ type: FOLDER, value: parts[i] });
            }
        }
        
        for(var i=0,url="";i<normal.length;i++)
        {
            url += normal[i].value;
            if(normal[i].type != FILE && normal[i].type != SERVER_INFO)
                url += "/";
        }
        return url;
    },


	/** Basic XHR wrapper 
	 */
	Remote: function(url, options)
	{
		options 			= options || { };
		options.async     	= options.async || false;
		options.onSuccess 	= options.onSuccess || function() { };
		options.onFailure 	= options.onFailure || function() { };
		
		var transport = window.XMLHttpRequest ? 
			new XMLHttpRequest() : 
			(window.ActiveXObject ? new ActiveXObject('Microsoft.XMLHTTP') : false);
		if(!transport) { return false; }
		transport.open("get", url, options.async);
		transport.onreadystatechange = function()
		{
			if(transport.readyState != 4) { return; }
			var status = 0;
			try { status = transport.status; } catch (e) { }
			if(((status >= 200) && (status < 300)) || status == 0)
			{
				options.onSuccess(transport.responseText);
			}
			else
			{
				options.onFailure();
			}
		};
	
		try { transport.send(null); } catch(e) { }
		if(transport.readyState == 4 && ((status >= 200) && 
			(status < 300)) || status == 0)
		{
			return transport.responseText;
		}
	},
	
	
	/** Execute the given template and return the results.
	 *  The template must have been loaded already.
	 */
	Execute: function(url, scope, base)
	{
		base = base || window.location.pathname;
		scope = scope || { };
		url = this.ResolveURL(base, url);
		var Me = this;
		var template = this.Cache[url];
		var scopeEval = "";
		for(x in scope)
		{
			if(x.match(/^[a-z0-9_]+$/i))
				scopeEval += "var " + x + " = scope['" + x + "'];\n";
		}
		eval(scopeEval);
		var _o = "";
		var _p = function(s){ if(s){ _o += s.toString().xmlEncode(); }};
		var print = function(s){ _o += s; };
		var encode = function(s){ if(s){ return s.toString().xmlEncode(); }};
		var embed = function(u,p){ _o += Me.Execute(template.imports[u] || u, p, url); };
		var unique_id = function(){ return ("bumble_id_" + Bumble.UniqueId++); };
		if(template && template.code) { eval(template.code); }
		return _o;
	},
	
	
	/** Loads templates from relative path. Accepts callback for asynchronous
	 *  mode.
	 */
	Load: function(url, base, callback)
	{
		base = base || window.location.pathname;
		url = this.ResolveURL(base, url);		
		var template = this.Cache[url];
		if(template && callback) { callback(template); }
		if(template) { return template; }
		return callback ? this.LoadAsync(url, callback) : this.LoadNow(url);
	},
	
	
	/** Synchronous, direct-URL template loader
	 */
	LoadNow: function(url)
	{
		var source = this.Remote(url, { async: false });
		var template = eval(this.Compile(source));
		this.Cache[url] = template;
		for(i in template.imports)
			this.Load(template.imports[i], url);
		return template;
	},
	
	
	/** Asynchronous, direct-URL template loader
	 */
	LoadAsync: function(url, callback)
	{
		var Me = this;
		Me.Remote(url, { async: true, onSuccess: function(source)
		{
			var template = Me.Compile(source);
			Me.Cache[url] = template;
			if(template.imports.length == 0) { callback(template); }			
			var tally = 0;
			var total = 0;
			for(i in template.imports) { total++; }
			for(i in template.imports)
				requestImport(template.imports[i]);
			
			function requestImport(importURL)
			{
				Me.Load(importURL, url, function() 
				{
					if(++tally >= total)
						callback(template);
				});
			}
		}});
	},
	
	
	/** Executes a template and writes its contents directly to document.
	 *  Loads templates immediately if they have not been cached.
	 */
	Inline: function(url, scope, document)
	{
		document = document || window.document;
		this.Load(url);
		var output = this.Execute(url, scope);
		document.write(output);
		return output;
	},
	
	
	/** Executes a template and writes its contents to an element's innerHTML.
	 *  Loads templates asynchronously if they have not been cached.
	 */
	Update: function(element, url, scope)
	{
		var Me = this;
		element = (typeof(element) == "string") ?
			document.getElementById(element) : element;
		Me.Load(url, null, function() {
			element.innerHTML = Me.Execute(url, scope);
		});
	},
	
	
	/** Compile template source into eval'able Javascript code.
	 *  Ignores pre-compiled code.
	 */
	Compile: function(source)
	{
		if(source.match(/^\/\*\$:X-BUMBLE\*\//)) { return source; }
		var start = "<%";	var echo = "=";
		var end   = "%>";	var meta = "@";		

		var imports = [ ];
		var compiled = "";
		var scratch = "";
		var echoing = true;
		var i = 0;
		while(i < source.length)
		{
			if(source.substr(i, start.length) == start)
			{
				var escaped = false;
				var quoted = false;
				var delimiter = "";
				i += start.length;
				if(echoing && scratch.length > 0)
					compiled += "_o+=" + scratch.shrink().singleQuote() + ";";
				echoing = false;
				scratch = "";
				while(i < source.length)
				{
					if(!quoted && source.substr(i, end.length) == end)
					{
						if(scratch.substr(0, echo.length) == echo)
						{
							scratch = "_p(" + 
								scratch.substr(echo.length).trim() +  ");";
						}						
						else if(scratch.substr(0, meta.length) == meta)
						{
							var sMatch = scratch.match(/\s*import\s*.*\ssource="([^"]*)"/i);
							var nMatch = scratch.match(/\s*import\s*.*\sname="([^"]*)"/i);
							scratch = "";
							if(sMatch && sMatch)
							{
								imports.push(nMatch[1].singleQuote() +
									":" + sMatch[1].singleQuote());
							}
						}
						
						compiled += scratch + "\n";
						scratch = "";
						i+= end.length;
						break;
					}
					else
					{
						var current = source.charAt(i);
						if(!quoted && (current == '"' || current == "'"))
						{
							quoted = true;
							delimiter = current ;
						}
						else if(quoted && !escaped && current == delimiter)
						{
							quoted = false;
						}
						
						escaped = (!escaped && current == "\\");
						scratch += current;
						i++;
					}
				}
			}
			else
			{
				echoing = true;
				scratch += source.charAt(i);
				i++;
			}
		}
		
		if(echoing && scratch.length > 0)
			compiled += "_o+=" + scratch.shrink().singleQuote() + ";\n";
			
		return "/*$:X-BUMBLE*/({code:" + compiled.shrink().doubleQuote() +
			",imports:{" + imports.join(",") + "}});";
	}
}