/* spuds.js - Trumba Spuds Client Library
 * Copyright (C) 2005-2006 Trumba Corporation, All Rights Reserved
 *
 * Portions of this file are derivatives of the prototype.js library
 * from http://prototype.conio.net.  Thank you Sam Stephenson.
 */
 
// Include Sentinel

if (typeof($Trumba) == "undefined") {

/**
 * $Trumba - Global Trumba Namespace
 */
window.$Trumba = {
	version:         3.0,
	emptyFunction:   function() { },
	baseUri:         "http://www.trumba.com/",
	loaderUri:       "http://www.trumba.com/s.aspx",
	debug:           false,
	showDebugWindow: false ||
					 (((typeof(trumba_showDebugWindow) != "undefined") && trumba_showDebugWindow) ? true : false),
	prologQueue:     []
}


/**
 * $Trumba.Class
 *
 * Helper for generating new class definitions.  Create your class as 
 * follows:
 *
 * $Trumba.Foo.Bar = $Trumba.Class.create();
 * $Trumba.Foo.Bar.prototype = {
 *     initialize: function(x) {  // required constructor!
 *         this.x = x;
 *     },
 *     method2: function() { ... }
 * }
 */
$Trumba.Class = {
	create: function() {
		return function() {
			this.initialize.apply(this, arguments);
		}
	},
	
	extend: function(destination, source) {
		for (property in source) {
			destination[property] = source[property];
		}
	return destination;
	},
	
	addNamespace: function(n) {
		var parts = n.split('.');
		var root = window;
		
		for (var i = 0 ; i < parts.length ; i++) {
			if (root[parts[i]] == null)
				eval("root." + parts[i] + " = { };");

			root = root[parts[i]];
		}
	}
}


/**
 * Useful helper for evaluating the first javascript function that succeeds.
 * For example:
 *     $Trumba.Try.these(function() { ... }, function() { ... });
 */
$Trumba.Try = {
  these: function() {
    var returnValue;

    for (var i = 0; i < arguments.length; i++) {
      var lambda = arguments[i];
      try {
        returnValue = lambda();
        break;
      } catch (e) {}
    }

    return returnValue;
  }
}


/**
 * $Trumba.$(id | [id,...]) Shortcut for document.getElementById()
 * 
 * Calls document.getElementById() for every argument passed returning the
 * resuls in an array.  If only one element is passed the first array member
 * is returned.
 *
 * Example : $Trumba.$('mydiv') returns a DIV element
 * Example : $Trumba.$('mydiv', 'myform') returns [DIV element, FORM element]
 */
$Trumba.$ = function(id) {
	var result = [];
	
	for (var i = 0 ; i < arguments.length ; i++) {
		var element = arguments[i];
		
		if (typeof(element) == "string")
			element = document.getElementById(element);
		
		result.push(element);
	}
	
	if (result.length == 1) return result[0];
	
	return result;
}

/**
 * Function.$trumba_bind()
 *
 * Create a method callback that binds an object to its
 * this pointer.  Useful for passing callbacks to people who expect functions
 * and not objects.
 */
Function.prototype.$trumba_bind = function() {
	var __method = this, args = $Trumba.$A(arguments), object = args.$trumba_shift();
	return function() {
		return __method.apply(object, args.concat($Trumba.$A(arguments)));
	}
}

/**
 * Array.$trumba_shift() - Shifts the array left one argument, returning the removed
 * argument.
 *
 * Example : ["Hello", "World!"].$trumba_shift() returns "Hello".
 */
Array.prototype.$trumba_shift = function() {
	var result = this[0];
	for (var i = 0; i < this.length - 1; i++)
		this[i] = this[i + 1];
	this.length--;
	return result;
}


/**
 * $Trumba.$A() - Converts an iterable object or an almost iterable object,
 * e.g. Arguments, into a real array.
 */
$Trumba.$A = Array.$trumba_from = function(iterable) {
	if (!iterable) 
		return [];
	if (iterable.toArray) {
		return iterable.toArray();
	} else {
		var results = [];
		for (var i = 0; i < iterable.length; i++)
			results.push(iterable[i]);
		return results;
	}
}

$Trumba.escape = function(sStr) {
	return escape(sStr).
		replace(new RegExp("\\+", "g"), '%2B').
		replace(new RegExp('\\\"', "g"), '%22').
		replace(new RegExp("\\'", "g"), '%27').
		replace(new RegExp("\\/", "g"), '%2F');
}

  
/*************************************************************************
 * $Trumba.String
 */
$Trumba.Class.addNamespace("$Trumba.String");

$Trumba.String = {
	/**
	 * $fill(c, count) - fills a string with count c's.
	 *
	 * Example : fill("hi!", 3) returns "hi!hi!hi!"
	 */
	fill: function(c, count) {
		var result = new Array(count);
		
		for (var i = 0 ; i < count ; i++)
			result.push(c);
		
		return result.join("");
	},

	/**
	 * Replaces {n} where n is a 0-based index with the nth argument.
	 *
	 * For example:
	 *		$Trumba.String.format("{0} is {1} years old.", "John", "25")
	 * returns:
	 *		"John is 25 years old."
	 */
	format: function() {
		if (arguments.length <= 1)
			return (arguments.length == 1) ? arguments[0] : "";
		
		var result = arguments[0];
			
		for (var i = 1 ; i < arguments.length ; i++)
			result = result.replace(new RegExp("\\{" + (i -1) + "}", "g"), arguments[i]);

		return result;
	}
}


/*************************************************************************
 * $Trumba.Event
 *
 * A class of static methods to make event handling easier cross browser.
 */
 
$Trumba.Event = { }

$Trumba.Class.extend($Trumba.Event, {
	KEY_BACKSPACE: 8,
	KEY_TAB:       9,
	KEY_RETURN:   13,
	KEY_ESC:      27,
	KEY_LEFT:     37,
	KEY_UP:       38,
	KEY_RIGHT:    39,
	KEY_DOWN:     40,
	KEY_DELETE:   46,

	/**
	 * Returns the target element for an event.
	 */
	element: function(event) {
		return event.target || event.srcElement;
	},

	keyCode: function(event) {
		if (event.charCode)
			return event.keyCode >0 ? event.keyCode : event.charCode;
			
		return event.keyCode;
	},
	
	isAltKey: function(event) {
		return (event.altKey && event.altKey) || false;
	},
	
	isShiftKey: function(event) {
		return (event.shiftKey && event.shiftKey) || false;  
	},
	
	isCtrlKey: function(event) {
		return (event.ctrlKey && event.ctrlKey) || false;  
	},
	
	/**
	 * True if the event was a left click.
	 */
	isLeftClick: function(event) {
		return (((event.which) && (event.which == 1)) ||
			((event.button) && (event.button == 1)));
	},

	/**
	 * Page relative x coordinate of mouse during event.
	 */
	pointerX: function(event) {
		return 
			event.pageX ||
			(event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft));
	},

	/**
	 * Page relative y coordinate of mouse during event.
	 */
	pointerY: function(event) {
		return 
			event.pageY ||
			(event.clientY + (document.documentElement.scrollTop || document.body.scrollTop));
	},

	/**
	 * Browser agnostic way in which to stop event processing.
	 * 
	 * @param event {Event}  Event to stop processing.
	 */
	stop: function(event) {
		if (event.preventDefault) {
			event.preventDefault();
			event.stopPropagation();
		} else {
			event.returnValue = false;
			event.cancelBubble = true;
		}
	},
	
	/**
	 * Find the first node with the given tagName, starting from the
	 * node the event was triggered on; traverses the DOM upwards
	 */
	findElement: function(event, tagName) {
		var element = $Trumba.Event.element(event);

		while (element.parentNode && (!element.tagName || (element.tagName.toUpperCase() != tagName.toUpperCase())))
			element = element.parentNode;

		return element;
	},

	observers: false,

	_observeAndCache: function(element, name, observer, useCapture) {
		if (!this.observers) this.observers = [];

		if (element.addEventListener) {
			this.observers.push([element, name, observer, useCapture]);
			element.addEventListener(name, observer, useCapture);
		} else if (element.attachEvent) {
			this.observers.push([element, name, observer, useCapture]);
			element.attachEvent('on' + name, observer);
		}
	},

	/**
	 * Clears the cached set of observers.
	 */
	unloadCache: function() {
		if (!$Trumba.Event.observers) return;

		for (var i = 0; i < $Trumba.Event.observers.length; i++) {
			$Trumba.Event.stopObserving.apply(this, $Trumba.Event.observers[i]);
			$Trumba.Event.observers[i][0] = null;
		}

		$Trumba.Event.observers = false;
	},

	/**
	 * Browser agnostic way to add an event listener.
	 * 
	 * @param element    {Element}  Element to attach to.
	 * @param name       {string}   Name of event not including 'on', e.g. load, not onload.
	 * @param observer   {Function} Method to call when event fires.
	 * @param useCapture {bool}     True to fire event during capture, false for bubble.
	 *                              Most should pass false for bubbling. DOM2 Only!
	 */
	observe: function(element, name, observer, useCapture) {
		var element = $Trumba.$(element);
		useCapture = useCapture || false;

		if (name == 'keypress' &&
			(navigator.appVersion.match(/Konqueror|Safari|KHTML/) || element.attachEvent))
			name = 'keydown';

		this._observeAndCache(element, name, observer, useCapture);
	},

	/**
	 * Browser agnostic way of removing an event listener.
	 *
	 * @param element    {Element}  Element observer was attached to.
	 * @param name       {string}   Name of event to detach from.
	 * @param observer   {Function} Method handling event.
	 * @param useCapture {bool}     True if observer is capturing event, false for bubbling. DOM2 Only!
	 */
	stopObserving: function(element, name, observer, useCapture) {
		var element = $Trumba.$(element);
		useCapture = useCapture || false;

		if (name == 'keypress' &&
			(navigator.appVersion.match(/Konqueror|Safari|KHTML/) || element.detachEvent))
			name = 'keydown';

		if (element.removeEventListener) {
			element.removeEventListener(name, observer, useCapture);
		} else if (element.detachEvent) {
			element.detachEvent('on' + name, observer);
		}
	}
});


/*************************************************************************
 * $Trumba.EventSource
 *
 * Extend your object with EventSource in order to source events using the 
 * Java/JavaScript event listener model.  Subscribers attach to your events
 * by passing the event name and an observer method to disptach to when the
 * event fires.  A protected method _fireEvent() is available for dispatching
 * your events.
 *
 * Event names are case-insensitive as all methods force them to lowercase.
 */

$Trumba.EventSource = {
	_eventSource_events: false,
	
	/**
	 * Adds a new listener for a specific event.
	 */
	addEventListener: function(name, observer) {
		if (name) name = name.toLowerCase();
		
		if (name && observer) {
			if (!this._eventSource_events) 
				this._eventSource_events = { };
			
			if (typeof(this._eventSource_events[name]) == "undefined")
				this._eventSource_events[name] = [];
				
			for (var i = 0 ; i < this._eventSource_events[name].length ; i++) {
				if (this._eventSource_events[name][i] == observer)
					return;
			}
			
			this._eventSource_events[name].push(observer);
		}
	},
	
	removeEventListener: function(name, observer) {
		if (name) name = name.toLowerCase();
		
		if (name && observer) {
			if (!this._eventSource_events) return;
			
			if (typeof(this._eventSource_events[name]) == "undefined")
				return;
				
			var copy = [];
			
			for (var i = 0 ; i < this._eventSource_events[name].length ; i++) {
				if (this._eventSource_events[name][i] != observer)
					copy.push(this._eventSource_events[name][i]);
			}
			
			if (copy.length > 0) {
				this._eventSource_events[name] = copy;
			}
			else {
				this._eventSource_events[name] = null;
				delete this._eventSource_events[name];
			}
		}
	},
	
	_fireEvent: function(name) {
		if (name) name = name.toLowerCase();
		
		if (this._eventSource_events[name]) {
			for (var i = 0 ; i < this._eventSource_events[name].length ; i++)
				this._eventSource_events[name][i]();
		}			
	}
}


/*************************************************************************
 * $Trumba.Logger
 *
 * Logging facility.
 */

$Trumba.Logger = $Trumba.Class.create();

$Trumba.Logger.LEVEL_ERROR   = 0x01;
$Trumba.Logger.LEVEL_WARNING = 0x02;
$Trumba.Logger.LEVEL_INFO    = 0x03;

$Trumba.Logger.prototype = {
	/**
	 * Constructor.
	 */
	initialize: function() {
		this.handlers = [];
	},
	
	/**
	 * Adds a new handler to the logger.  If the handler is already registered
	 * nothing happens.
	 *
	 * @param handler	Handler that processes log messages.
	 */
	addHandler: function(handler) {
		if (handler) {
			for (var i = 0 ; i < this.handlers.length ; i++) {
				if (this.handlers[i] == handler)
					return;
			}
			
			this.handlers.push(handler);
		}
	},
	
	/**
	 * Removes a handler from the logger. 
	 *
	 * @param handler	Handler to remove.
	 */
	removeHandler: function(handler) {
		if (handler && this.handlers.length > 0) {
			var handlers = [];
			
			for (var i = 0 ; i < this.handlers.length ; i++) {
				if (this.handlers[i] == handler)
					continue;
					
				handlers[handlers.length] == this.handlers[i];
			}
			
			this.handlers = handlers;
		}
	},

	/**
	 * Dispatches a log record to all the registered handlers.
	 *
	 * @param level {number} Type of message being logged.	
	 * @param message {string} Message to be logged.
	 * @param thrown {object} Exception thrown, if any.
	 * @param className {string} Name of class logging message.
	 * @param methodName {string} Name of method logging message.
	 */	
	_log: function(level, message, thrown, className, methodName) {
		// Skip it if there are no handlers.
		if (!this.handlers.length) return;
		
		// Create a log record for the consumers.
		var record = { };
		
		record.level = level;
		record.message = message;
		record.thrown = thrown;
		record.className = className;
		record.methodName = methodName;
		
		// Dispatch record to all consumers.
		for (var i = 0 ; i < this.handlers.length ; i++)
			this.handlers[i].processRecord(record);
	},
	
	/**
	 * Logs an error message.
	 *
	 * @param message {string} Message to be logged.
	 * @param thrown {object} Exception thrown, if any.
	 * @param className {string} Name of class logging message.
	 * @param methodName {string} Name of method logging message.
	 */
	error: function(message) {
		this._log.apply(this, [$Trumba.Logger.LEVEL_ERROR].concat($Trumba.$A(arguments)));
	},
	
	/**
	 * Logs a warning message.
	 *
	 * @param message {string} Message to be logged.
	 * @param thrown {object} Exception thrown, if any.
	 * @param className {string} Name of class logging message.
	 * @param methodName {string} Name of method logging message.
	 */
	warning: function(message) {
		this._log.apply(this, [$Trumba.Logger.LEVEL_WARNING].concat($Trumba.$A(arguments)));
	},
	
	/**
	 * Logs an informational message.
	 *
	 * @param message {string} Message to be logged.
	 * @param thrown {object} Exception thrown, if any.
	 * @param className {string} Name of class logging message.
	 * @param methodName {string} Name of method logging message.
	 */
	info: function(message) {
		this._log.apply(this, [$Trumba.Logger.LEVEL_INFO].concat($Trumba.$A(arguments)));
	}
}


/* Global instance of logger.
 */
$Trumba.logger = new $Trumba.Logger();


/*************************************************************************
 * $Trumba.SpudDebugWindow
 */

$Trumba.SpudDebugWindow = $Trumba.Class.create();

$Trumba.SpudDebugWindow.prototype = {
	initialize: function() {
		this.window = null;
		
		if ($Trumba.SpudDebugWindow.instance == null)
			$Trumba.SpudDebugWindow.instance = this;

		$Trumba.logger.addHandler(this);
		this._tryGetWindow();
		this.show();
	},
	
	/**
	 * Log Handler - Passes record to window if shown.
	 */	
	processRecord: function(record) {
		try {
			this.window.processRecord(record);
		} 
		catch (e) {
		}
	},

	_tryGetWindow: function() {
		if (this.window)
			return;
		
		var w = window.open("", "spudDebug", "scrollbars=1,status=1,resizable=yes,toolbar=no,menubar=yes");
		
		try {
			var doc;
			doc = w.document;
			if (typeof(w.processRecord) != "undefined") {
				this.window = w;
				$Trumba.logger.info("Attaching to existing debug window.");
			}
		}
		catch (e) {
		}
	},
		
	/** 
	 * Ensures that window is showing in case it was closed.
	 */	
	show: function() {
		if (this.window && !this.window.closed) {
			return;
		}
		
		this.window = null;
			
		var ie = /msie/i.test(navigator.userAgent);
		var width = (window.screen.availWidth * 0.5) - 10;
		var height = 425;
		var left = ie ? window.screen.availWidth * 0.5 + (window.screen.width - window.screen.availWidth) : 0;
		var top = window.screen.availHeight - height;
		var w = window.open("", "spudDebug", "scrollbars=1,status=1,resizable=yes,width=" + width + ",height=" + height + ",left=" + left + ",top=" + top + ",toolbar=no,menubar=no");
		
		try {
			var doc;
			doc = w.document;
			doc.write("<html><body>Loading...</body></html>");
			doc.close();
			w.moveTo(left,top);
			w.resizeTo(width, height);
		}
		catch (e) {
			if (w != null) {
				try { w.close(); } catch (e2) { }
				w = null;
			}
			//alert("Sorry, your pop-up blocker stopped me from showing the debug window!");
			return;
		}
		
		this.window = w;
		this.refresh();
	},
	
	refresh: function() {
		this.show();
		
		if (this.window) {
			var w = this.window;
			var callbacks = {
				onSuccess: function(result) { w.document.write(result.body);w.document.close(); },
				onTimeout: function() { w.document.write("Timeout");w.document.close(); },
				onFailure: function() { w.document.write("Fetch error!");w.document.close(); }
			}
			
			var url = $Trumba.loaderUri + "?spud.relpath=" + $Trumba.escape("spuddebug.html");
			if (typeof($Trumba.ScriptXmlHttpRequest) == "undefined")
				$Trumba.prologQueue.push(function() {
					var u = url;
					var c = callbacks;
					eval("new $Trumba.ScriptXmlHttpRequest(u, c).invoke();");
				 });
			else
				new $Trumba.ScriptXmlHttpRequest(url, callbacks).invoke();
		}
	}
}


// Singleton instance of debug window.
$Trumba.SpudDebugWindow.instance = null;


/**
 * Static method that handles key presses, looking for our magic incantation
 * to show the debug window.
 */
$Trumba.SpudDebugWindow.keyHandler = function(e) {
	if (!e) e = window.event;
	var c = String.fromCharCode($Trumba.Event.keyCode(e)).toLowerCase();

	if (c == "z" && $Trumba.Event.isAltKey(e) && $Trumba.Event.isCtrlKey(e)) {
		if ($Trumba.SpudDebugWindow.instance == null)
			new $Trumba.SpudDebugWindow();
			
		$Trumba.SpudDebugWindow.instance.show();
	}
}

// Install Spuds Debug Window Handler
$Trumba.Event.observe(document, "keypress", $Trumba.SpudDebugWindow.keyHandler, false);


// Show the debug window straight away if the server asked for it.
if ($Trumba.showDebugWindow) new $Trumba.SpudDebugWindow();


/*************************************************************************
 * $Trumba.Net.QueryString
 *
 * An object for manipulating query strings.  Stores the string as an 
 * array of name value pairs. The name is in the 0th position, value in the 
 * 1st.  Name and value are stored unescaped.
 */

$Trumba.Class.addNamespace("$Trumba.Net");
$Trumba.Net.QueryString = $Trumba.Class.create();

$Trumba.Net.QueryString.prototype = {
	initialize: function(search) {
		this.pairs = [];
		
		if (search != null) {
			if (typeof(search) == "string")
				this.from(search)
			else if (typeof(search) == "object")
				this.copyConstructor(search);
		}
	},
	
	copyConstructor: function(other) {
		var t = this;
		other.visit(function(n, v) { t.setAt(n, v); } );
	},
	
	from: function(search) {
		this.pairs = [];
		if (search == null || search == '') return;
		if (search.indexOf('?') == 0) search = search.substring(1);
		
		search = search.split('&');
		
		for (var i = 0 ; i < search.length ; i++) {
			if (search[i] != '') {
				var nv = search[i].split('=');
				
				if (nv.length == 1)
					this.pairs.push([unescape(nv[0]), null]);
				else
					this.pairs.push([unescape(nv[0]), unescape(nv[1])]);
			}
		}
	},
	
	isEmpty: function() {
		return this.pairs.length == 0;
	},
	
	setAt: function(name, value) {
		var i = this.findByName(name);
		if (i != -1)
			this.pairs[i][1] = value;
		else
			this.pairs.push([name, value]);
	},
	
	toString: function() {
		var r = new Array(this.pairs.length);
		for (var i = 0 ; i < r.length ; i++) {
			if (this.pairs[i][1] == null)
				r[i] = $Trumba.escape(this.pairs[i][0]);
			else
				r[i] = $Trumba.escape(this.pairs[i][0]) + '=' + $Trumba.escape(this.pairs[i][1]);
		}
		return r.join('&');
	},
	
	getAt: function(index) {
		if (typeof(index) == "string") {
			index = this.findByName(index);
			
			if (index == -1)
				return null;
		}
			
		return this.pairs[index];
	},
	
	remove: function(name) {
		var index = this.findByName(name);
		if (index != -1) 
			this.removeAt(index);
	},
	
	removeAt: function(index) {
		var temp = [];
		for (var i = 0 ; i < this.pairs.length ; i++) {
			if (i != index) temp.push(this.pairs[i]);
		}
		this.pairs = temp;
	},
	
	length: function() {
		return this.pairs.length;
	},

	visit: function(v) {
		for (var i = 0 ; i < this.pairs.length ; i++) {
			v(this.pairs[i][0], this.pairs[i].length > 1 ? this.pairs[i][1] : null);
		}
	},
		
	findByName: function(name) {
		for (var i = 0 ; i < this.pairs.length ; i++) {
			if (this.pairs[i][0] == name) return i;
		}
		
		return -1;
	},
	
	insert: function(other) {
		for (var i = 0 ; i < other.length() ; i++) {
			var op = other.getAt(i);
			this.setAt(op[0], op[1]);
		}
	},
	
	subtract: function(other) {
		for (var i = 0 ; i < other.length() ; i++) {
			var i = this.findByName(other.getAt(i)[0]);
			if (i != -1) this.removeAt(i);
		}
	},
	
	prefixWith: function(prefix) {
		for (var i = 0 ; i < this.pairs.length ; i++) {
			this.pairs[i][0] = prefix + this.pairs[i][0];
		}
	}
}


$Trumba.Net.Location = $Trumba.Class.create();

$Trumba.Net.Location.prototype = {
	initialize: function(url) {
		var i = url.indexOf('#');
		
		if (i >= 0) {
			this.hash = url.substring(i);
			url = url.substring(0, i);
		} else {
			this.hash = '';
		}

		i = url.indexOf('?');
		
		if (i >= 0) {
			this.search = new $Trumba.Net.QueryString(url.substring(i));
			url = url.substring(0, i);
		} else {
			this.search = new $Trumba.Net.QueryString();
		}
		
		this.path = url;
	},
	
	search: function() {
		return this.search;
	},
	
	toString: function() {
		return this.path + '?' + this.search.toString() + this.hash;
	}
}


/*************************************************************************
 * $Trumba.DOM
 *
 *	Methods and classes for manipulating the DOM.
 */
$Trumba.Class.addNamespace("$Trumba.DOM");

$Trumba.DOM.createElement = function(tag, attrs) {
	var e = document.createElement(tag);
	for (var i = 0 ; i < attrs.length ; i++)
		if (attrs[i][1] != '') e.setAttribute(attrs[i][0], attrs[i][1]);
	return e;
}


/*************************************************************************
 * $Trumba.Spuds - Simple Spud Static Methods
 */

$Trumba.Class.addNamespace("$Trumba.Spuds");

/* Keeps track of the next Spud ID to be generated.
 */
$Trumba.Spuds.nextSpudID = 0;


/** 
 * Generates a new Spud ID.  These are fairly unique so conflicts with user 
 * defined IDs shouldn't happen.
 */
$Trumba.Spuds.createSpudId = function() {
	return "trumba.spud." + $Trumba.Spuds.nextSpudID++;
}


/**
 * EZ-Spud API - A one-line call for creating and inserting spuds into a
 * document.  Handles inserting both the DIV wrapper and the IFRAME using
 * document.write(). You can only call this during document render, never
 * after else the document will be cleared.
 *
 * @param:webName		Calendar being viewed.
 * @param:spudType		Type of spud to be inserted.
 * @param:spudId		Id of DIV that will contain the spud.  If not passed
 *						id is auto-generated and a DIV container inserted
 *						inline to the document via document.write().
 * @param args			Associative array of extra url parameters.
 * @param properties    Associative array of per Spud properties.
 */
$Trumba.addSpud = function(webName, spudType, spudId, args, properties) {
	$Trumba.logger.info("Creating " + spudType + " for calendar " + webName, null, "$Trumba", "addSpud");

	// If a container ID was passed then that's our Spud id.  If not 
	// we generate both the container and the new Spud Id.
	
	var createContainer = spudId ? false : true;
		
	if (createContainer) {
		spudId = $Trumba.Spuds.createSpudId();
		var html = $Trumba.String.format($Trumba.Spuds.DIV_FORMAT, spudId);
		document.write(html);
	}
	
	var createAndRegisterSpud = function() {
		var spud = $Trumba.Spuds.SpudFactory.create(webName, spudType, spudId, args, properties);
		$Trumba.Spuds.controller.addSpud(spud);
	}
	
	// Sometimes the DOM is slow on the building of our Div.  If so wait
	// a bit.

	if ($Trumba.$(spudId) == null) {
		window.setTimeout(function() {
			// Still no div?  It probably never existed.
			if ($Trumba.$(spudId) == null) {
				$Trumba.logger.error('Cannot find our spud div with id ' + spudId, null, null, '$Trumba.addSpud');
				return;
			}
			createAndRegisterSpud();
			},
		100);
		return;
	}
	
	// Register Spud with controller.
	createAndRegisterSpud();
}

$Trumba.Opera8Fixup = function () {
	window["trumba_opera_8_fix_1"] = document.getElementById("TrumbaIframe");
}

/**
 * Called by k.aspx to render an IFRAME spud.
 */
$Trumba.addBackCompatSpud = function(webName, spudType, args, properties, frameID) {
	// Find IFRAME
	var iframe = $Trumba.$(frameID);
	if (typeof(iframe) != "object") return;
	iframe.style.display = "none";

	// Insert a new container for the spud as a sibling of the iframe.
	var container = $Trumba.DOM.createElement("div", [ ["id", "" + Math.random() ] ]);
	
	iframe.parentNode.insertBefore(container, iframe);
	$Trumba.addSpud(webName, spudType, container.id, args, properties);
}


// {0} == id, {1} == style, {2} == free-form
$Trumba.Spuds.IFRAME_FORMAT_SSL = '<iframe src="javascript:\'<html><body>orig</body></html>\'" id="{0}" name="{0}" style="{1}" frameborder="no" width="100%" scrolling="no" marginheight="0" marginwidth="0" allowtransparency="true" {2}><\/iframe>';
$Trumba.Spuds.IFRAME_FORMAT = '<iframe id="{0}" name="{0}" style="{1}" frameborder="no" width="100%" scrolling="no" marginheight="0" marginwidth="0" allowtransparency="true" {2}><\/iframe>';
$Trumba.Spuds.DIV_FORMAT = '<div id="{0}"></div>';


/**
 * Renders the HTML for an IFRAME that can contain a Trumba spud.
 *
 * @param:id		Spud ID
 */
$Trumba.Spuds.renderIFrameHTML = function(id, style, width, height) {
	// We always have a minimum height.
	height = height || "35";
	var ie = /msie/i.test(navigator.userAgent);
	var secure = window.location.protocol.toLowerCase() == ("https:");
	var format = (ie && secure) ? $Trumba.Spuds.IFRAME_FORMAT_SSL : $Trumba.Spuds.IFRAME_FORMAT;
	var freeForm = 'height="' + height + '" ';
	if (width) 
		freeForm += 'width="' + width + '" ';
	
	var html = $Trumba.String.format(
		format, 
		id, 
		style || "",
		freeForm);
	return html;
}


/**
 * Creates an iframe that can contain a Trumba spud.  NOTE - You must set the
 * contentWindow.name to the iframe.id after appending the returned node
 * to it's parent.  This is an IE weirdness.
 *
 * @param:id		Spud ID
 */
$Trumba.Spuds.createIFrame = function(id, style) {
	var iframe = document.createElement("iframe");
	
	iframe.setAttribute("id", id);
	iframe.setAttribute("name", id);
	iframe.frameBorder = "no";
	iframe.setAttribute("width", "100%");
	iframe.setAttribute("scrolling", "no");
	iframe.setAttribute("marginheight", "0");
	iframe.setAttribute("marginwidth", "0");
	iframe.setAttribute("allowtransparency", "true");
	if (style)
		iframe.setAttribute("style", style);
	
	return iframe;
}


/*************************************************************************
 * $Trumba.Spuds.SpudFactory
 *
 * Factory class for creating spuds by type.  Not really much to it yet.
 */

$Trumba.Class.addNamespace("$Trumba.Spuds.SpudFactory");

$Trumba.Spuds.SpudFactory.create = function(webName, spudType, id, args, properties) {
	return new $Trumba.Spuds.SimpleSpud(webName, spudType, id, args, properties);
}


/*************************************************************************
 * $Trumba.Spuds.ignoreList
 *
 * This object associates all known Spud names with the list of arguments that they
 * ignore during navigation.  This allows for optimized refreshes.
 */

$Trumba.Spuds.ignoreList = {
	/*
		Here are all the arguments as of 2006/07/10
	spud: {		
		date:{}, view:{}, index:{}, template:{}, pageby:{}, groupby:{}, subgroupby:{}, mix:{}, 
		filter1:{}, filter2:{}, search:{}, paging:{}, groupspan:{}, events:{}, eventid:{}, 
		zone:{}, fontscheme:{}, colorscheme:{}, mixcontrol:{}, updates:{}, select:{}, onemonth: {}
	}
	*/
	chooser: {
		date:{}, view:{}, index:{}, /*template:{},*/ pageby:{}, groupby:{}, subgroupby:{}, mix:{}, 
		filter1:{}, filter2:{}, search:{}, paging:{}, groupspan:{}, events:{}, eventid:{}, 
		zone:{}, /*fontscheme:{}, colorscheme:{},*/ mixcontrol:{}, updates:{}, select:{}, onemonth: {}
	},
	datefinder: {
		/*date:{},*/ view:{}, index:{}, template:{}, pageby:{}, groupby:{}, subgroupby:{}, mix:{}, 
		filter1:{}, filter2:{}, search:{}, paging:{}, groupspan:{}, events:{}, eventid:{}, 
		zone:{}, /*fontscheme:{}, colorscheme:{},*/ mixcontrol:{}, updates:{}, select:{}, onemonth: {}
	},
	
	teaser: { }, // Deprecated - See below.
	
	mix: {		
		date:{}, view:{}, index:{}, template:{}, pageby:{}, groupby:{}, subgroupby:{}, /*mix:{}, */
		filter1:{}, filter2:{}, search:{}, paging:{}, groupspan:{}, events:{}, eventid:{}, 
		zone:{}, /*fontscheme:{}, colorscheme:{},*/ mixcontrol:{}, updates:{}, select:{}, onemonth: {}
	},
	upcoming: {		
		date:{}, view:{}, index:{}, template:{}, pageby:{}, groupby:{}, subgroupby:{}, /*mix:{}, */
		/*filter1:{}, filter2:{},*/ search:{}, paging:{}, groupspan:{}, /*events:{},*/ eventid:{}, 
		/*zone:{}, fontscheme:{}, colorscheme:{},*/ mixcontrol:{}, updates:{}, select:{}, onemonth: {}
	},
	crawler: {		
		date:{}, view:{}, index:{}, template:{}, pageby:{}, groupby:{}, subgroupby:{}, /*mix:{}, */
		/*filter1:{}, filter2:{},*/ search:{}, paging:{}, groupspan:{}, /*events:{},*/ eventid:{}, 
		/*zone:{},*/ fontscheme:{}, colorscheme:{}, mixcontrol:{}, updates:{}, select:{}, onemonth: {}
	},
	search: {		
		date:{}, view:{}, index:{}, template:{}, pageby:{}, groupby:{}, subgroupby:{}, mix:{}, 
		filter1:{}, filter2:{}, /*search:{},*/ paging:{}, groupspan:{}, events:{}, eventid:{}, 
		zone:{}, /*fontscheme:{}, colorscheme:{},*/ mixcontrol:{}, updates:{}, select:{}, onemonth: {}
	},
	filter: {
		date:{}, view:{}, index:{}, template:{}, pageby:{}, groupby:{}, subgroupby:{}, mix:{}, 
		/*filter1:{}, filter2:{},*/ search:{}, paging:{}, groupspan:{}, events:{}, eventid:{}, 
		zone:{}, /*fontscheme:{}, colorscheme:{},*/ mixcontrol:{}, updates:{}, select:{}, onemonth: {}
	},
	monthlist: {
		date:{}, view:{}, index:{}, template:{}, pageby:{}, groupby:{}, subgroupby:{}, /*mix:{}, */
		/*filter1:{}, filter2:{}, search:{},*/ paging:{}, groupspan:{}, events:{}, eventid:{}, 
		zone:{}, fontscheme:{}, colorscheme:{}, mixcontrol:{}, updates:{}, select:{}/*, onemonth: {}*/
	},
	gotoDate: {		
		date:{}, view:{}, index:{}, template:{}, pageby:{}, groupby:{}, subgroupby:{}, mix:{}, 
		filter1:{}, filter2:{}, search:{}, paging:{}, groupspan:{}, events:{}, eventid:{}, 
		zone:{}, fontscheme:{}, colorscheme:{}, mixcontrol:{}, updates:{}, select:{}, onemonth: {}
	}
}

// Back compat for "teaser" Spud which is the deprecated name of the datefinder spud.

$Trumba.Spuds.ignoreList.teaser = $Trumba.Spuds.ignoreList.datefinder;


/*************************************************************************
 * $Trumba.Spuds.DivContainer
 *
 * This is a prototype container for DIV spuds.  This container will not work
 * for HTML that has SCRIPT tags mixed in as innerHTML does not evaluate
 * such tags.  We haven't spent the time to really fix this so this
 * container should not be used for the general case.
 */
 
$Trumba.Spuds.DivContainer = $Trumba.Class.create();

$Trumba.Spuds.DivContainer.prototype = {
	/**
	 * Constructor.
	 *
	 * @param parent {Element}  Parent node in DOM tree that we are to create
	 *   our container within.  Typically this is a DIV.
	 */
	initialize: function(parent) {
		this.parentDiv = parent;
	},
	
	/**
	 * Inserts HTML into the container.
	 *
	 * @param html {string}  HTML to write to container.
	 * @param wrapInHTML {boolean}  True if html is not wrapped in HTML and BODY tags.
	 *   Some containers will wrap it for you, namely IFrames.
	 */ 
	setHTML: function(html, wrapInHTML) {
		this.parentDiv.innerHTML = html;
	},
	
	/**
	 * Called when the window.onload event occurs inside the Spud.
	 */
	onSpudLoaded: function() {
		this.resize();
	},
	
	/**
	 * Causes the container to resize, if possible, to fit the contents.
	 */
	resize: function() {
		// Nothing to do
	},
	
	/**
	 * Returns the window context of the container.
	 */
	getWindow: function() {
		return window;
	},
	
	/**
	 * Fetches HTML from the Trumba server using a given queryString.
	 *
	 * @param querySting {$Trumba.Net.QueryString}  Querystring to fetch.
	 * @param callbacks  Optional object containing callbacks for onTimeout and
	 *   onFailure.  By default we will handle these and insert a simple error 
	 *   message.  Override these to do this yourself.
	 */
	fetch: function(queryString, callbacks) {
		this.queryString = new $Trumba.Net.QueryString(queryString);
		this.queryString.setAt("spud.con", "div");
		
		// Callbacks is optional.
		callbacks = callbacks || { };
		
		var myCallbacks = {
			onSuccess: this._onDataSuccess.$trumba_bind(this),
			onTimeout: callbacks.onTimeout || this._onDataTimeout.$trumba_bind(this),
			onFailure: callbacks.onFailure || this._onDataFailure.$trumba_bind(this)
		}
		
		$Trumba.Spuds.controller.loader.request(this.queryString.toString(), myCallbacks);
	},
	
	_setErrorHTML: function() {
		var html = "<div style=\"padding:10px;font-family:Arial;\">We're sorry, there was an error loading the Spud.</div>";
		this.setHTML(html);
	},
	
	_onDataSuccess: function(result) {
		/*
		if (result.body.scripts) {
			for (var i = 0 ; i < result.body.scripts.length;  i++) {
				var s = document.createElement("script");
				s.setAttribute("type", "text/javascript");
				s.innerHTML = result.body.scripts[i];
				this.parentDiv.appendChild(s);
			}
		}
		*/
		this.setHTML(result.body.body);
		this._fireEvent("onFetched");
	},
	
	_onDataTimeout: function(e) {
		$Trumba.logger.error(this.parentDiv.id +" <b>Timeout Error</b> loading(" + this.queryString.toString() + ")", null, "$Trumba.Spuds.DivContainer", "_onDataTimeout");
		this._setErrorHTML();
	},

	_onDataFailure: function(e) {
		$Trumba.logger.error(this.parentDiv.id +" <b>Data Failure </b> loading(" + this.queryString.toString() + ")", null, "$Trumba.Spuds.DivContainer", "_onDataFailure");
		this._setErrorHTML();
	}
} // $Trumba.Spuds.DivContainer

$Trumba.Class.extend($Trumba.Spuds.DivContainer.prototype, $Trumba.EventSource);


/*************************************************************************
 * $Trumba.Spuds.IFrameContainer
 */

$Trumba.Spuds.IFrameContainer = $Trumba.Class.create();

$Trumba.Spuds.IFrameContainer.prototype = {
	/**
	 * Constructor.
	 *
	 * @param parentDiv {Element}  Parent node in DOM tree that we are to create
	 *   our container within.  Typically this is a DIV.
	 */
	initialize: function(parentDiv) {
		this.parentDiv = parentDiv;
		this.id = parentDiv.id;
		this.iframeStyle = this._getDefaultStyle();
		this._initIFrame();
		
		// In FireFox we have to remove the iframes when we unload as navigating
		// back to the page causes all sorts of hellacious exceptions.  When the controller
		// unloads let's do it.
		
		if (navigator.userAgent.indexOf("Gecko") != -1)
			$Trumba.Spuds.controller.addEventListener("unload", this._removeIFrame.$trumba_bind(this));
	},
	
	/**
	 * Mainly here for extending classes to override.  Sets up the iframe in our 
	 * parent DIV.  We do something very different for IE.
	 */
	_initIFrame: function() {
		this.parentDiv.innerHTML = $Trumba.Spuds.renderIFrameHTML(this._getIFrameId(), this.iframeStyle);
		this.iframe = $Trumba.$(this._getIFrameId());
	},
	
	/**
	 * Computes the best default style for the iframe.
	 */
	_getDefaultStyle: function() {
		var bgcolor;
		
		try {
			// We try the parentDiv first ( in case we didn't create it ), then it's
			// parent and lastly resort to white.
			
			bgcolor = 
				   this.parentDiv.style.backgroundColor
				|| this.parentDiv.parentNode.style.backgroundColor
				|| "white";
		}
		catch (e) {
			bgcolor = "white";
		}
		
		bgcolor = $Trumba.String.format("background-color:{0};", bgcolor)
			
		return bgcolor;;
	},
	
	/**
	 * Inserts HTML into the container.
	 *
	 * @param html {string}  HTML to write to container.
	 * @param wrapInHTML {boolean}  True if html is not wrapped in HTML and BODY tags.
	 *   Some containers will wrap it for you, namely IFrames.
	 */ 
	setHTML: function(html, wrapInHTML) {
		if (wrapInHTML)
			html = $Trumba.String.format("<html><body>{0}</body></html>", html);
		var result = { body: html };
		this._onDataSuccess(result);
		this._resizeIFrameWithWait(400);
	},

	/**
	 * Called when the window.onload event occurs inside the Spud.
	 */
	onSpudLoaded: function() {
		$Trumba.logger.info(
			this._getIFrameId() + '<span style="color:red"> onSpudLoaded!</span>', 
			null, "$Trumba.Spuds.IFrameContainer", "onSpudLoaded");

		this._resizeIFrameWithWait();
		this._fireEvent("onFetched");
		this._spudLoaded();
	},
	
	_spudLoaded: function() {
	},
	
	/**
	 * Causes the container to resize, if possible, to fit the contents.
	 */
	resize: function() {
		this._resizeIFrame();
	},

	/**
	 * Returns the window context of the container.
	 */
	getWindow: function() {
		if (this.iframe) {
			if (this.iframe.contentWindow)
				return this.iframe.contentWindow;
			else
				return window.frames[this._getIFrameId()];
		}
			
		return null;
	},
	
	
	/**
	 * Fetches HTML from the Trumba server using a given queryString.
	 *
	 * @param querySting {$Trumba.Net.QueryString}  Querystring to fetch.
	 * @param callbacks  Optional object containing callbacks for onTimeout and
	 *   onFailure.  By default we will handle these and insert a simple error 
	 *   message.  Override these to do this yourself.
	 */
	fetch: function(queryString, callbacks) {
		this.queryString = queryString;
		
		// Callbacks is optional.
		callbacks = callbacks || { };
		
		var con = this;
		
		var myCallbacks = {
			onSuccess: this._onDataSuccess.$trumba_bind(this),
			onTimeout: callbacks.onTimeout || this._onDataTimeout.$trumba_bind(this),
			onFailure: callbacks.onFailure || this._onDataFailure.$trumba_bind(this)
		}

		// Show Busy Sign
		var win = this.getWindow()
		if (win.ShowBusy) win.ShowBusy();
		
		$Trumba.Spuds.controller.loader.request(this.queryString.toString(), myCallbacks);
	},
	
	/**
	 * Returns the id/name of the iframe associated with this Spud.
	 */	
	_getIFrameId: function() {
		return this.id + ".iframe";
	},

	/**
	 * Waits for a small amount of time then resizes the iframe.  Useful just after
	 * populating an iframe's html content.
	 */
	_resizeIFrameWithWait: function(delay) {
		var con = this;
		delay = delay || 200;
		window.setTimeout(function() { con._resizeIFrame(); }, delay);
	},

	/**
	 * Adjust the iframe's dimensions based on its contents.
	 */	
	_resizeIFrame: function() {
		var MaxIframeHeight = 12000;
		var fudge = 2;
		var iframe = this.iframe;
		var con = this;
		var firstAttempt = (arguments.length == 0);
		var b4 = iframe.height;
		
		if (navigator.userAgent.indexOf("Opera") != -1)
			fudge += 20;

		$Trumba.Try.these(
			// Mozilla/sometimes Opera
			function() { 
				iframe.height = Math.min(iframe.contentDocument.body.offsetHeight, MaxIframeHeight) + fudge;
			},
			
			// IE
			function() { 
				iframe.height = Math.min(iframe.Document.body.scrollHeight, MaxIframeHeight) + fudge;
			},
			
			// FireFox/Opera (sometimes)
			function() { 
				iframe.height = Math.min(frames[iframe.id].document.body.scrollHeight, MaxIframeHeight) + fudge;
			},
			
			// Unknown - retry if first attempt.
			function() {
				$Trumba.logger.warning(this._getIFrameId() + " Cannot figure out how to set IFRAME height!", null, "$Trumba.Spuds.IFrameContainer", "_resizeIFrame");
				if (firstAttempt) {
					window.setTimeout(function() { con._resizeIFrame(false); }, 200);
				}
				
			}
		);
		
		// Fire the sizeChanged method for those who need it.
		
		if (iframe.height != b4) {
			this._sizeChanged()
		}
		
		//$Trumba.logger.info($Trumba.String.format("{2} - before:={0}, after:={1}", b4, iframe.height, con._getIFrameId()));
	},
	
	_sizeChanged: function() {
	},
	
	/**
	 * Called by the controller when it wants us to remove our IFRAME from
	 * the DOM.  We'll do this for FireFox only.
	 */
	_removeIFrame: function() {
		$Trumba.logger.info("<b>_removeIFrame</b> " + this._getIFrameId());
		
		var doc;
		
		if (this.iframe.contentWindow)
			doc = this.iframe.contentWindow.document;
		else
			doc = window.frames[this._getIFrameId()].document;
			
		doc.write("<html><body></body></html>");
		doc.close();
	},

	/**
	 * Handles rebuilding our iframe when a document.write fails.  This is a FireFox
	 * issue only.
	 */	
	_rebuildIFrame: function(html) {
		// First yank the old iframe from the DOM.
		
		this.parentDiv.removeChild(this.iframe);
		this.iframe = null;
		
		// Generate a new IFRAME node to take it's place.  FYI - In IE if you use
		// createIFrame() the body of the iframe gets really huge margins and paddings.
		// Only the innerHTML = renderIFrame(...) version seem to not do this.  I'm
		// not sure what the deal is.
		
		this.iframe = $Trumba.Spuds.createIFrame(this._getIFrameId(), this.iframeStyle);
		this.parentDiv.appendChild(this.iframe);
		
		if (this.iframe.contentWindow) {
			// In IE and FireFox we can just slam in the new content.
			var doc = this.iframe.contentWindow.document;
			this._writeAndClose(doc, html);
			this._resizeIFrameWithWait();
		}
		else if (window.frames) {
			// Opera slowly adds the iframe to the DOM so wait a bit.
			var id = this._getIFrameId();
			var con = this;
			var f = function() { 
				if (!frames[id]) {
					window.setTimeout(arguments.callee, 1);
					return;
				}

				con._writeAndClose(frames[id].document, html);
				con._resizeIFrameWithWait();
			}
			window.setTimeout(f, 1);
		}
		else {
			$Trumba.logger.error("Ugh - no frames support, how did we get here?");
		}
	},
	
	_setErrorHTML: function() {
		this.setHTML(
			'<html><body><div style="font-style:italic;font-size:0.8em;font-family:Arial, Helvetica, Verdana;padding:10px;">Calendar is temporarily unavailable.</div></body></html>',
			false);
	},
	
	_onDataSuccess: function(result) {
		//$Trumba.logger.info(this._getIFrameId() +" <b>Data Arriveth!</b>", null, "$Trumba.Spuds.IFrameContainer", "_onDataSuccess");
		var doc;
		
		if (this.iframe.contentWindow)
			doc = this.iframe.contentWindow.document;
		else {
			doc = window.frames[this._getIFrameId()].document;
			
			if (typeof(doc) == "undefined") {
				$Trumba.logger.info("Waiting for browser (Opera??) to bind our frame's doc.");
				
				// Major potential for an infinite loop here.  How do we back down?
				
				var con = this;
				var f = function() {
					con._onDataSuccess(result);
				}
				window.setTimeout(f, 100);
				return;
			}
		}
		
		try {
			this._writeAndClose(doc, result.body);
		}
		catch (e) {
			$Trumba.logger.error("Exception thrown writing IFRAME contents!", e, "$Trumba.Spuds.IFrameContainer", "_onDataSuccess");
			this._rebuildIFrame(result.body);
			return;
		}
		
		// Resize us as well as everyone else.  In IE it seems that until the last 
		// doc.write has been done we really can't get all the iframe height's
		// correctly.  The call to controller.resize() fixes this.
		this._resizeIFrameWithWait();
		$Trumba.Spuds.controller.resize();
	},
	
	_onDataTimeout: function(e) {
		$Trumba.logger.error(this._getIFrameId() +" <b>Timeout Error</b> loading(" + this.queryString.toString() + ")");
		this._setErrorHTML();
	},

	_onDataFailure: function(e) {
		$Trumba.logger.error(this._getIFrameId() +" <b>Data Failure </b> loading(" + this.queryString.toString() + ")");
		this._setErrorHTML();
	},
	
	_writeAndClose: function(doc, html) {
		doc.write(html);
		doc.close();
	}
} // $Trumba.Spuds.IFrameContainer


$Trumba.Class.extend($Trumba.Spuds.IFrameContainer.prototype, $Trumba.EventSource);


// Overrides for IE - We defeat IE's per-iframe history by always re-creating the IFRAME.

if (/msie/i.test(navigator.userAgent)) {
	$Trumba.Class.extend($Trumba.Spuds.IFrameContainer.prototype, {
		/**
		 * Extended to force a size changed flag to _resizeIFrame that will
		 * guarantee that we clear the overflow et al css styles.
		 */
		_spudLoaded: function() {
			// Possible solution?
			// What about trying to put an resize listener on the div itself?
			/*
			var con = this;
			
			window.setTimeout(
				function() {
					if (con.parentDiv.style.overflow != "") {
						$Trumba.logger.warning("new way");
						con._sizeChanged();
					}
				}, 
				500);
			*/
		},
		
		/**
		 * Called by our base when the Spud window.onload event is fired.
		 */
		_sizeChanged: function() {
			this.parentDiv.style.overflow = "";
			this.parentDiv.style.width = "";
			this.parentDiv.style.height = "";
		},
		
		/**
		 * Rebuilds the IFrame in response to an RPC payload delivery.
		 */	
		_rebuildIFrame: function(html) {
			// Set the containing div's width and height to that of the iframe
			// until the iframe get's resized.
			/*
			$Trumba.logger.warning($Trumba.String.format(
				"({0}, {1}) ({2}, {3}) ({4}, {5})",
				this.parentDiv.offsetWidth,
				this.parentDiv.offsetHeight,
				this.parentDiv.style.width,
				this.parentDiv.style.height,
				this.iframe.width,
				this.iframe.height),
				null,
				"$Trumba.Spuds.IEIFrameContainer",
				"_rebuildIFrame");
			*/
			var width = this.iframe.offsetWidth;
			var height = this.iframe.offsetHeight;
			
			this.parentDiv.style.overflow = "hidden";
			this.parentDiv.style.width = width + "px";
			this.parentDiv.style.height = height + "px";
			
			// First yank the old iframe from the DOM.
			
			this.parentDiv.removeChild(this.iframe);
			this.iframe = null;
			
			// Generate a new IFRAME node to take it's place.
	
			this.parentDiv.innerHTML = $Trumba.Spuds.renderIFrameHTML(
				this._getIFrameId(), 
				this.iframeStyle, 
				width, 
				height);
			this.iframe = $Trumba.$(this._getIFrameId());
			
			if (this.iframe.contentWindow) {
				// In IE and FireFox we can just slam in the new content.
				var doc = this.iframe.contentWindow.document;
				this._writeAndClose(doc, html);
				this._resizeIFrameWithWait();
			}
			else if (window.frames) {
				// Opera slowly adds the iframe to the DOM so wait a bit.
				var id = this._getIFrameId();
				var con = this;
				var f = function() { 
					if (!frames[id]) {
						window.setTimeout(arguments.callee, 1);
						return;
					}

					con._writeAndClose(frames[id].document, html);
					con._resizeIFrameWithWait();
				}
				window.setTimeout(f, 1);
			}
			else {
				$Trumba.logger.error("Ugh - no frames support, how did we get here?");
			}
		},
		
		_onDataSuccess: function(result) {
			this._rebuildIFrame(result.body);
			return;
		}
	});
} // $Trumba.Spuds.IEIFrameContainer


/*************************************************************************
 * $Trumba.Spuds.SimpleSpud

 
	Spud Properties by Spud Type
	
	*
		styleRules   {[]}      Array of style rules to be added to Spud.
		divContainer {boolean} True to use a DIV instead of IFRAME.  Beta!
		noAsyncNav   {boolean} True to not asynchronously load spud when
		                       navigation happens.  This causes the entire
		                       page to refresh instead of doing it via AJAX.
		scrollTopOnNav {boolean} Causes the Spud to scroll the host page
							   back to 0,0 after a navigate.  This is useful
							   for the MonthList spuds which are typically
							   at the bottom of the screen and want to pop
							   up to the top.
		
	Crawler
		Width: "250px",
		FontSize: "26pt",
		FontFamily: "Courier",
		FontWeight: "100",
		FontColor: "green",
		HoverColor: "red",
		BackgroundColor: "magenta",
		Border: "2px solid yellow",
		Speed: "1"
		
	Upcoming
		BorderColor: "red"
		HeaderColor: "green"
 */

$Trumba.Spuds.SimpleSpud = $Trumba.Class.create();

$Trumba.Spuds.SimpleSpud.prototype = {
	/**
	 * Creates a new Spud API object.  To display this Spud you must also call
	 * $Trumba.Spuds.controller.addSpud(...);
	 *
	 * @param:webName		Calendar being viewed.
	 * @param:spudType		Type of Spud being created.
	 * @param:id			Spud Id as well as container Id.  Our IFrame 
	 *						will be inserted into this container.
	 * @param:args			Spud arguments, e.g. extra url arguments.
	 * @param:properties    Spud globas, e.g. non-url arguments.
	 */
	initialize: function(webName, spudType, id, args, properties) {
		// Stash our parameters.
		
		this.spudType = spudType;
		this.id = id;
		this.webName = webName;
		this.properties = properties || { };
		
		// Used to determine when we should show the 'loading' html page.
		
		this.firstFetch = true;

		// Store a reference to the wrapper DIV and generate a container for our HTML.
		
		this.parentDiv = $Trumba.$(id);
		
		try {
			this.parentDiv.innerHTML = "<div></div>";
		} catch (e) {
			this._fixDivInsideInline();
		}
		
		// Create a container for the Spud.
		
		if (true === this.getProperty("divContainer"))
			this.container = new $Trumba.Spuds.DivContainer(this.parentDiv);
		else
			this.container = new $Trumba.Spuds.IFrameContainer(this.parentDiv);
			
		this.container.addEventListener("onFetched", this.onFetched.$trumba_bind(this));

		// Create our query string from any arguments plus our widget and webname.
		
		this.setQueryString(this._getEmbedArguments(), args);
		
		$Trumba.logger.info(this.getUniqueID() + " Created for " + webName + "/" + spudType);
	},
	
	/**
	 * Handles fixing out container when it's contained inside of an inline element such as a
	 * P or SPAN tag.  IE tosses an exception when this happens as it's illegal whereas
	 * FireFox ignores and continues.
	 *
	 * The fix is to insert a new DIV container as a younger sibling (after) the inline parent
	 * element.  If that fails we keep going up until we hit the BODY.
	 */
	_fixDivInsideInline: function() {
		$Trumba.logger.warning(
			"Spud container embedded in a " + this.parentDiv.parentNode.tagName.toUpperCase(),
			null, "$Trumba.Spuds.SimpleSpud", "_fixDivInsideInline");

		var container = $Trumba.DOM.createElement("div", [ ["id", "" + Math.random() ] ]);
		var parent = this.parentDiv.parentNode;
		var done = false;
		
		// Walk up the tree until we find a suitable parent node.
		
		while (!done && parent) {
			var testCon = parent.parentNode.insertBefore(container, parent.nextSibling);
			
			try {
				testCon.innerHTML = "<div></div>";
				done = true;
			} catch (e2) {
				// Keep going up until we hit the body.
				
				if (/BODY/i.test(parent.tagName))
					parent == null
				else
					parent = parent.parentNode;
			}
		}
		
		// If we didn't find a good parent then throw, we can't do anything.  This
		// makes no sense really as the BODY is always a good parent.
		
		if (!done) {
			var msg = 
				"We're sorry but you have placed your Trumba Spud inside of a " + 
				parent.tagName.toUpperCase() +
				" element which is not supported.  You must place it inside of a DIV.";
			alert(msg);
			throw msg;
		}
		
		this.id = container.id;
		this.parentDiv = $Trumba.$(this.id);
	},
	
	onFetched: function() {
		var rules = this.getProperty("styleRules");
		
		if (rules && rules.length) {
			var win = this.container.getWindow();
			
			if (win && win.trumba_addStyleRules) {
				win.trumba_addStyleRules(rules);
			}
			
		this.resize();
		}
	},
	
	/**
	 * A callback method for the Spud content itself.  This is to be called only once
	 * window.onload is fired inside the Spud.  Hooking iframe.onload is unreliable.
	 */
	onSpudLoaded: function() {
		// Let our container do the work.
		this.container.onSpudLoaded();
	},
	
	/**
	 * Looks to the document's location string to see if there are any arguments
	 * encoded in the search string as 'trumbaEmbed'.  If so we'll start with those
	 * as they were most likely passed to us from another teaser page.
	 */
	_getEmbedArguments: function() {
		// See if there's anything on the Url.
		var qs = new $Trumba.Net.QueryString(location.search);
		
		qs = qs.getAt("trumbaEmbed");
		
		if (qs == null || qs.length == 1) 
			return null;
		
		return new $Trumba.Net.QueryString(qs[1]);
	},
	
	getProperty: function(name) {
		var result = this.properties[name];
		if (typeof(result) == "undefined") result = null;
		return result;
	},
	
	setProperty: function(name, value) {
		this.properties[name] = value;
	},
	
	getIFrameId: function() {
		if (this.container._getIFrameId)
			return this.container._getIFrameId();
		
		return window.name;
	},
	
	resize: function() {
		this.container.resize();
	},

	/**
	 * Returns the query string as a real, escaped Url querystring.
	 */	
	getQueryString: function() {
		return this.queryString.toString();
	},

	/**
	 * Sets the query string for the Spud.
	 */	
	setQueryString: function(queryString, args) {
		var qs = new $Trumba.Net.QueryString(queryString);
		
		if (args != null) {
			for (var name in args) {
				qs.setAt(name, args[name]);
			}
		}
		qs.setAt("calendar", this.webName);
		
		if (this.spudType != "calendar") 
			qs.setAt("widget", this.spudType);
		
		if ((this.spudType != "calendar") && (this.spudType != "main")) {
			qs.remove("eventid");
			qs.remove("view");
		}
		
		this.queryString = qs;
	},
	
	/**
	 * Called by controller when it wants us to load for the first time
	 * or refresh our contents.
	 */
	refresh: function() {
		this._fetchHTML();
	},
	
	/**
	 * Called by a hyperlink on our spud when it's clicked.
	 */
	navigate: function(url, absolute) {
		if (absolute) {
			$Trumba.logger.info(this.getUniqueID() +" abs navigate(" + url + ")");
			if (this.parent != null) 
				return this.parent.navigateAbs(url);
				
			window.top.location.href = url;
		}
		else {
			$Trumba.logger.info(this.getUniqueID() + " rel navigate(" + url + ")");
			
			// If we've got a parent then let them know what happened.
			
			this.ignoreNav = true;
			$Trumba.Spuds.controller.navigate(url);
			
			// Fetch the new HTML.
			
			this._fetchHTML(new $Trumba.Net.QueryString(url));
			
			// See if we should scroll the screen to the top
			
			if (this.getProperty("scrollTopOnNav"))
				window.scrollTo(0, 0);
		}
	},
	
	/**
	 * Returns a string that uniquely identifies this spud. 
	 */
	getUniqueID: function() {
		return "(" + this.id + "-" + this.spudType + ")";
	},
	
	/**
	 * Called by the controller whenever a spud causes a navigation.
	 *
	 * @param queryString {$Trumba.Net.QueryString} Destination of navigation.
	 */
	onNavigate: function(queryString) {
		$Trumba.logger.info(this.getUniqueID() + " onNavigate(" + queryString + ")");
		
		if (this.ignoreNav) {
			this.ignoreNav = false;
			return;
		}
		
		// Fetch the new HTML.
			
		this._fetchHTML(queryString);
	},

	/** 
	 * Merges a differential query string into ours and determines if, after
	 * the merge, the new string is any different.
	 * 
	 * @param queryString {string}  Differential querystring.
	 * @return true if no changes were found between query strings.
	 */
	_mergeQueryString: function(queryString) {
		var a = new $Trumba.Net.QueryString(this.queryString);
		var b = new $Trumba.Net.QueryString(queryString);
		
		$Trumba.logger.warning("Before : " + a.toString() + " == " + b.toString());
		
		// First we need apply the diff to our current qs to generate the 
		// intended destination.
		
		for (var i = 0 ; i < b.length() ; i++) {
			var item = b.getAt(i);
			
			if (item[0].charAt(0) == '-') {
				a.remove(item[0].substring(1));
			}
			else
				a.setAt(item[0], item[1])
		}
		
		// Special case for eventid.  If it's not present on the destination
		// then yank it from us, always.
		
		if (b.getAt("eventid") == null) {
			a.remove("eventid");
			a.remove("view");
		}
		
		// Now save the merged in query string as our own.
		var save = new $Trumba.Net.QueryString(this.queryString);
		this.setQueryString(a);
		a = save;
		b = new $Trumba.Net.QueryString(this.queryString);
		
		//$Trumba.logger.info("After : " + a.toString() + " == " + b.toString());
		
		// Now compute our diff.
		
		var diff = { };
		
		for (var i = 0 ; i < a.length() ; i++) {
			var itema = a.getAt(i);
			var itembIndex = b.findByName(itema[0]);
			var itemb = itembIndex != -1 ? b.getAt(itembIndex) : null;
			
			if (itemb != null) {
				if (itema[1] != itemb[1]) 
					diff[itema[0]] = { from: itema[1], to: itemb[1] };
			}
			else
				diff[itema[0]] = { from:itema[1], to: null };
				
			b.removeAt(itembIndex);
		}
		
		for (var i = 0 ; i < b.length() ; i++ ) {
			var itemb = b.getAt(i);
			diff[itemb[0]] = { from:null, to: itemb[0]};
		}
		
		var same = false;
		
		// Yank the widget param, it never counts since we override it.
		
		diff["widget"] = null
		delete diff["widget"];
		
		var myIgnore = $Trumba.Spuds.ignoreList[this.spudType];
		
		if (myIgnore) {
			var ignoredAll = true;
			
			for (var n in diff) {
				if (typeof(myIgnore[n]) != "undefined")
					continue;
					
				ignoredAll = false;
				break;
			}
			
			if (ignoredAll)
				same = true;
		}
		
		return same;
	},
		
	/**
	 * Ask the loader to fetch our data.
	 *
	 * @param queryString {$Trumba.Net.QueryString} Differential querystring
	 *   for spud or null to use existing.  Null forces a refresh.
	 */		
	_fetchHTML: function(queryString) {
		// Insert our loading HTML for first fetch.
		
		if (this.firstFetch) {
			this.firstFetch = false;
			this._setLoadingHTML();
		}
		else if (queryString) {
			// See if query string has changed.
			var same = this._mergeQueryString(queryString);

			//$Trumba.logger.info(this.getUniqueID() + " new querystring (from:" + this.queryString.toString() + ", to:" + queryString.toString(), null, "$Trumba.Spuds.SimpleSpud", "_fetchHTML");
			
			if (this.getProperty("noAsyncNav")) {
				var search = new $Trumba.Net.QueryString(window.location.search);
				search.setAt("trumbaEmbed", this.getQueryString());
				window.location.search = '?' + search.toString();
			}
			
			if (same) return;
		}

		// Tell our container to fetch the HTML.
				
		this.container.fetch(this.queryString);
	},
	
	_setLoadingHTML: function() {
		// Try and be really good here by grabbing our parent Div's computed background color.
		// That way the Loading icon looks really integrated to the page.

		var bgcolor = "";
		
		try {
			if (this.parentDiv.currentStyle) {
				// IE
				var div = this.parentDiv;
				
				while (div && div.currentStyle) {
					bgcolor = div.currentStyle.backgroundColor;
					if (bgcolor == "transparent")
						div = div.parentNode;
					else
						break;
				}

				// Fall back to window background color?				
				//if (bgcolor == "transparent" && div.bgColor) bgcolor = div.bgColor;
			}
			else if (window.getComputedStyle)
				bgcolor = window.getComputedStyle(this.parentDiv, null).backgroundColor;
		} catch (e) {
			$Trumba.logger.error("RIP getting background color!", e, "$Trumba.Spuds.SimpleSpud", "_setLoadingHTML");
		}
				
		if (bgcolor.length)
			bgcolor = $Trumba.String.format("background-color:{0};", bgcolor)
			
		var html = 
			$Trumba.String.format('<div style="{0}margin:0px;padding:0px;font-size:0.8em;font-family:Arial, Helvetica, Verdana;font-style:italic;"><img src="', bgcolor) +
			$Trumba.baseUri +
			'images/spinner.gif">&nbsp;&nbsp;Loading&nbsp;.&nbsp;.&nbsp;.</div>';
		
		this.container.setHTML(html, true);
		this.container.resize();
	}
	
} // $Trumba.Spuds.SimpleSpud


/*************************************************************************
 * $Trumba.Spuds.Loader
 *
 * Loads spuds asynchronously using SRPC via s.aspx.
 */

$Trumba.Spuds.Loader = $Trumba.Class.create();

$Trumba.Spuds.Loader.prototype = {
	initialize: function() {
		this.nextDomain = 0;
		this.maxDomains = 5;
		this.queue = [];
	},
	
	/**
	 * When www.trumba.com is the requested domain this method round-robins through 5
	 * domains starting at 0 ("") up to 4, e.g. www, www1, ... www4.  This cannot be
	 * done for SSL as we do not have www1 et al certs for those DNS entries.
	 *
	 * @param url {string}  Fully qualified request url.
	 *
	 * @return  Fully qualified request url, round-robin'ed if www.trumba.com.
	 */
	_setDomain: function(url) {
		if (url.indexOf("http://www.trumba.com") != 0)
			return url;
			
		var index = this.nextDomain++ % this.maxDomains;
		return url.replace(/www/, "www" + (index || ""));
	},
	
	/**
	 * Given a query string returns the fully qualified Url that should be used fro
	 * the request.
	 * 
	 * @param qeuryString {string}  Querystring being requested.
	 *
	 * @return Fully qualified url 
	 */
	_getSpudUrl: function(queryString) {
		return this._setDomain($Trumba.loaderUri) + "?" + queryString;
	},
	
	/**
	 * Places a spud request in the queue.  Of course this isn't true, there
	 * is now queue, we just blast the requests and let the browser handle
	 * queing them up.
	 * 
	 * @param qeuryString {string}  Querystring being requested.
	 * @param callbacks {object}    Object container ScriptXmlHttpRequest compliant
	 *   callback methods of onTimeout, onSuccess, onFailure.
	 */
	request: function(queryString, callbacks) {
		var url = this._getSpudUrl(queryString);
		$Trumba.logger.info("Loading " + url, null, "$Trumba.Spuds.Loader", "request");
		var request = new $Trumba.ScriptXmlHttpRequest(url, callbacks);
		request.invoke();
	}
}

/*************************************************************************
 * $Trumba.Spuds.PageController
 *
 * Lives in the parent document and acts as a container for all the spuds as well
 * as the top-level event handler for such things as navigation.
 */

$Trumba.Spuds.PageController = $Trumba.Class.create();

$Trumba.Spuds.PageController.prototype = {
	/**
	 * Constructor
	 */
	initialize: function() {
		this.spuds = { };
		this.loader = new $Trumba.Spuds.Loader();
		
		// We refire the unload event for our objects.  Hook the window here.
		$Trumba.Event.observe(window, 'unload', this.onPageUnload.$trumba_bind(this), false);
		
		// As pages resize we need to as well.
		var con = this;
		$Trumba.Event.observe(window, 'resize', function() { con.resize(); }, false);
		
		// IE Only - Sometimes, just sometimes there's a glitch in the Matrix and our
		// IFRAMEs aren't sized up in IE.  We fix this by running a 1 second interval
		// timer that resizes the IFRAMEs.  This only runs for the first 15 seconds,
		// not for every refresh.  By then we're ok.
		if (/msie/i.test(navigator.userAgent) && (typeof(window.opera) == "undefined")) {
			this.resizeTimeoutTicks = 0;
			this.resizeTimeoutId = window.setInterval(this.onResizeTimeout.$trumba_bind(this), 1000);
		}
	},
	
	/**
	 * Only for IE, tells all the Spuds to resize every second.
	 */
	onResizeTimeout: function() {
		$Trumba.logger.info("IE Only Spud Resize");
		
		this.resizeTimeoutTicks++;
		
		if (this.resizeTimeoutTicks > 15) {
			$Trumba.logger.info("Killing timer.");
			window.clearInterval(this.resizeTimeoutId);
			return;
		}
		
		this.resize();
	},
	
	/**
	 * Called when the page unloads.  We notify all the spuds so they can do whatever
	 * shutdown is appropriate.
	 */
	onPageUnload: function() {
		// Helps prevent some crashes in IE or so I hear.
		$Trumba.Event.unloadCache();
		
		// Let our objects know about the event.
		this._fireEvent("unload");
	},
	
	/**
	 * Called when a spud link was navigated.  In turn we let all of our
	 * spuds know about the navigation and let them request new content.
	 *
	 * @param:url		Url being navigated to.
	 */
	navigate: function(url) {
		//$Trumba.logger.info("navigating to <b>" + url + "</b>");
		if (typeof(url) == "string")
			url = new $Trumba.Net.QueryString(url);
		
		for (var s in this.spuds) {
			this.spuds[s].onNavigate(url);
		}
	},

	/**
	 * Adds a spud to our collection.
	 */	
	addSpud: function(spud) {
		if (this.spuds[spud.id] == null) {
			this.spuds[spud.id] = spud;
			spud.refresh();
		}
	},
	
	/**
	 * Searches for a Spud by iframe name.  This is the same as the iframe window
	 * name when coming from a call inside the iframe.
	 */
	getSpud: function(name) {
		for (var s in this.spuds) {
			if (this.spuds[s].getIFrameId() == name)
				return this.spuds[s];
		}
		
		return null;
	},

	/**
	 * Refreshes all of our spuds.  This should cause a simple reload.
	 */
	refresh: function() {
		for (var s in this.spuds) {
			this.spuds[s].refresh();
		}
	},
	
	/**
	 * Resizes all of our spuds.
	 */
	resize: function() {
		var i = 0;
		for (var s in this.spuds) {
			this.spuds[s].resize();
		}
	},
	
	/**
	 * Prompts the user for a password by opening up a new window with the
	 * given url.  When our window gets the focus back we refresh the entire page.
	 */
	promptForPassword: function(url) {
		$Trumba.logger.error("Prompting!");
		var con = this;
		$Trumba.Event.observe(
			window,
			"focus", 
			function() { 
				$Trumba.logger.error("Focus!");
				$Trumba.Event.stopObserving(window, "focus", arguments.callee, false);
				con.refresh()
			},
			false);
		
		window.open(url, "trumba_embedLogin", "width=750,height=325,scrollbars=1,status=1,resizable=yes,toolbar=no,menubar=no");
	}
} // $Trumba.Spuds.PageController


$Trumba.Class.extend($Trumba.Spuds.PageController.prototype, $Trumba.EventSource);


// Our global controller instance.

$Trumba.Spuds.controller = new $Trumba.Spuds.PageController();

/* No longer used but here for backwards compat only.
 */
function SizeTrumbaFrame(iframeName) { }


/*************************************************************************
 * $Trumba.ScriptXmlHttpRequest
 *
 * Our version of XmlHttpRequest that works cross-domain with Trumba 
 * servers.  It can perform async GET requests using the SCRIPT tag.
 * The service being called must return a callback to $Trumba.ScriptXmlHttpRequest
 * .requestComplete() passing a callback ID and the result, if any.  This is 
 * forwarded back to the original caller.
 * 
 * POST can also be done using PostXmlHttpRequest below.
 * 
 * Example:
 *     var request = new $Trumba.ScriptXmlHttpRequest(
 *         "http://www.trumba.com/s.aspx?calendar=kexp&widget=upcoming",
 *         { onSuccess: function(result) { ... } });
 *     request.invoke();
 */

$Trumba.ScriptXmlHttpRequest = $Trumba.Class.create();

$Trumba.ScriptXmlHttpRequest.prototype = {
	/**
	 Class constructor.  options can contain the following:
	 param:options - {
		timeout : # seconds for timeout (defaults to 60),
		onSuccess : Called on successful callback.
		onFailure : Called for general errors.  Exception might be passed.
		onTimeout : Called if server doesn't respond in time
	 */
	initialize: function(url, options) {
		this.url = url;
		this.cbid = $Trumba.ScriptXmlHttpRequest.CreateCBID();
		this.options = options || { };
		this.options.timeout = this.options.timeout || ($Trumba.debug ? 600 : 60);
	},
	
	invoke : function() {
		// Invoke the script tag creation on a 'background' thread using
		// the window timeout mechanism.
		
		window.setTimeout(this.onAsyncInvoke.$trumba_bind(this), 1);
	},
	
	onAsyncInvoke: function() {
		try {
			this.doGet();
		}
		catch (e) {
			(this.options.onFailure || $Trumba.emptyFunction)(e);
		}
	},
	
	doGet: function() {
		// Create a script reference but don't set the src just yet - in IE
		// it causes an immediate request.
		
		this.script = document.createElement("sc" + "ript");
		this.script.setAttribute("type", "text/javascript");
		
		// Setup a callback to this object to handle the result.
		
		$Trumba.ScriptXmlHttpRequest.addCallback(this.cbid, this.onComplete.$trumba_bind(this));

		// Set a timeout timer.
		this.timeoutID = window.setTimeout(this.onTimeout.$trumba_bind(this), this.options.timeout * 1000);

		// Set the src and add it to the DOM.  This fires off the request.
		
		this.script.setAttribute("src", this.url + "&srpc.cbid=" + this.cbid + "&srpc.get=true");
		
		// Note - In IE you get "operation aborted" if you appendChild() to an incomplete
		// element.  The body is incomplete during document load so make sure to use the head.
		// Q : What if HEAD isn't present?
		var scriptParent;
		scriptParent = document.getElementsByTagName("head")[0];
		scriptParent.appendChild(this.script);
	},

	/**
	 Cleans up the HTML elements we've generated and removes our callback
	 handler.
	 */
	cleanup: function() {
		// Yank the script and iframe nodes from the DOM.
		this.script.parentNode.removeChild(this.script);
		
		// Cleanup the callback property.
		
		$Trumba.ScriptXmlHttpRequest.removeCallback(this.cbid);
	},
			
	onTimeout: function() {
		this.cleanup();
		(this.options.onTimeout || $Trumba.emptyFunction)();
	},
	
	onComplete: function(result) {
		window.clearTimeout(this.timeoutID);
		
		var cu = this.cleanup.$trumba_bind(this);
		var r = result;
		var cb = (this.options.onSuccess || $Trumba.emptyFunction);
		
		// Cleanup must be executed outside this callstack or else IE dies.
		
		window.setTimeout(function() { cu(); cb(r); }, 1);
	}
}

// ID of most recent callback.  Monotonically increases.
$Trumba.ScriptXmlHttpRequest.rpcID = 0;

// A random number we prefix to all our callback IDs.  Decreases
// likelyhood of collisions for saved post data.
$Trumba.ScriptXmlHttpRequest.rpcGUID = "" + Math.random();

// Object that holds our callbacks by property name.
$Trumba.ScriptXmlHttpRequest.callbacks = { };

// Static method called by server's javascript response.
$Trumba.ScriptXmlHttpRequest.requestComplete = function(result) {
	result = eval(result);
	$Trumba.ScriptXmlHttpRequest.callbacks[result.cbid](result);
}

// Registers a callback object to a callback ID.
$Trumba.ScriptXmlHttpRequest.addCallback = function(cbid, cb) {
	$Trumba.ScriptXmlHttpRequest.callbacks[cbid] = cb;
}

// Registers a callback object to a callback ID.
$Trumba.ScriptXmlHttpRequest.removeCallback = function(cbid) {
	$Trumba.ScriptXmlHttpRequest.callbacks[this.cbid] = null;
	delete $Trumba.ScriptXmlHttpRequest.callbacks[this.cbid];
}

// Generates a new callback ID.
$Trumba.ScriptXmlHttpRequest.CreateCBID = function() {
	var result = $Trumba.ScriptXmlHttpRequest.rpcGUID;
	result += "-" + $Trumba.ScriptXmlHttpRequest.rpcID++;
	return result;
}


/*************************************************************************
 * $Trumba.PostXmlHttpRequest
 *
 * Similar to ScriptXmlHttpRequest, allows us to post cross-domain.  Uses
 * a hidden IFRAME to do the post.  Since we cannot retrieve the contents
 * of the post a subsequent GET must be done on the same Url to retrieve
 * the results.
 */

$Trumba.PostXmlHttpRequest = $Trumba.Class.create();

$Trumba.PostXmlHttpRequest.prototype = {
	/**
	 Class constructor.  options can contain the following:
	 
	 param:url - Url to post to.
	 param:options - {
		timeout : # seconds for timeout (defaults to 15),
		onSuccess : Called on successful callback.
		onFailure : Called for general errors.  Exception might be passed.
		onTimeout : Called if server doesn't respond in time
	 }
	 param:postdata - Data for populating post.
	 */
	initialize: function(url, options, postdata) {
		this.url = url;
		this.cbid = $Trumba.ScriptXmlHttpRequest.CreateCBID();
		this.options = options || { };
		this.postdata = postdata;
	},
	
	invoke : function() {
		// Invoke the script tag creation on a 'background' thread using
		// the window timeout mechanism.
		
		window.setTimeout(this.onAsyncInvoke.$trumba_bind(this), 1);
	},
	
	onAsyncInvoke: function() {
		try {
			this.doPost();
			this.doGet();
		}
		catch (e) {
			(this.options.onFailure || $Trumba.emptyFunction)(e);
		}
	},
	
	getIFrameDocument: function(iframe) {
		if (iframe.contentDocument)
			return iframe.contentDocument; 
		else if (iframe.contentWindow)
			return iframe.contentWindow.document;
		else if (iframe.document)
			return iframe.document; 
			
		return null;
	},
	
	doPost: function() {
		this.iframe = document.createElement("iframe");
		var id = 'cbirame' + this.cbid;
		this.iframe.setAttribute('id', id);
		this.iframe.style.border='0px';
		this.iframe.style.width='0px';
		this.iframe.style.height='0px';
		
		this.iframe = document.body.appendChild(this.iframe);
		var doc = this.getIFrameDocument(this.iframe);
		doc.write('\<html\>\<body\>\<form method="post"\>\<\/form\>\<\/body\>\<\/html\>');
		var form = doc.getElementsByTagName("form")[0];
		form.action = this.url + "&srpc.cbid=" + this.cbid + "&srpc.post=true";
		
		for (var data in this.postdata) {
			var input = doc.createElement("input");
			input.name = data;
			input.type = "hidden";
			input.value = this.postdata[data];
			form.appendChild(input);
		}
		
		// Set a timeout timer.
			
		this.timeoutID = window.setTimeout(this.onTimeout.$trumba_bind(this), (this.options.timeout || 15) * 1000);

		// Register the callback to us.  A subsequent get is going to 
		// run this for us.
		
		$Trumba.ScriptXmlHttpRequest.addCallback(this.cbid, this.onComplete.$trumba_bind(this));
		
		// Submit the post (seems sync on IE and async in FireFox).  In IE
		// the iframe.onload() event is documented as unreliable.  In FireFox
		// it always fires when the content is loaded so we could rely on it
		// in that case.
		
		form.submit();
	},
	
	
	curDelay: 0,
	delays: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144],
	
	doGet: function() {
		// use our own versions first then call back to the client
		var options = {
			onSuccess: this.getOnSuccess.$trumba_bind(this),
			onFailure: this.getOnFailure.$trumba_bind(this),
			onTimeout: this.onTimeout.$trumba_bind(this),
			timeout : (this.options.timeout || 15)
		}
		
		var url = this.url + "&srpc.origcbid=" + this.cbid;
		var srpc = new $Trumba.ScriptXmlHttpRequest(url, options, null);
		srpc.invoke();
	},
	
	getOnFailure: function() {
		(this.options.onFailure || $Trumba.emptyFunction())();
	},
	
	getOnSuccess: function(result) {
		// If null was returned the server still has no data.  Wait until
		// we either timeout or get to invoke it again.
		
		if (result == null) {
			// If we already timeout out then exit.
			if (this.timedOut) {
				return;
			}
			
			var again = this.doGet.$trumba_bind(this);
			window.setTimeout(again, this.delays[this.curDelay++] * 1000);
			return;
		}
		
		// The result should be the javascript we would have gotten if 
		// we could see the post results.  Eval it and our onComplete
		// will be called.
		
		eval(result);
	},

	/**
	 Cleans up the HTML elements we've generated and removes our callback
	 handler.
	 */
	cleanup: function() {
		if (typeof(this.iframe) != "undefined")
			this.iframe.parentNode.removeChild(this.iframe);
		
		$Trumba.ScriptXmlHttpRequest.removeCallback(this.cbid);
	},
			
	onTimeout: function() {
		window.clearTimeout(this.timeoutID);
		this.timedOut = true;
		this.cleanup();
		(this.options.onTimeout || $Trumba.emptyFunction)();
	},
	
	onComplete: function(result) {
		window.clearTimeout(this.timeoutID);
		
		var cu = this.cleanup.$trumba_bind(this);
		var r = result;
		var cb = (this.options.onSuccess || $Trumba.emptyFunction);
		
		window.setTimeout(function() { cu(); cb(r); }, 1);
	}
}


// Process any prolog queue items that were added.

for(var i = 0 ; i < $Trumba.prologQueue.length ; i++)
	$Trumba.prologQueue[i]();
	
$Trumba.prologQueue = [];


} // Include Sentinel
