/* spuds.js - Trumba Spuds Client Library
 * Copyright (C) 2005-2007 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,
	loadTime:        new Date(),
	emptyFunction:   function() { },
	baseUri:         "http://www.trumba.com/",
	loaderUri:       "http://www.trumba.com/s.aspx",
	busyImageUri:	 "http://www.trumba.com/images/spinner_trumba.gif",
	showDebugOutput: ((typeof(trumba_showDebugOutput) != "undefined") && trumba_showDebugOutput),
	prologQueue:     [],
	iframeBgColor:   "transparent"
};

// First Time Initialization Code
(function() {
	var backCompat = false;
	
	if (typeof(doV3BackCompat) != "undefined") {
		backCompat = true;
	}
	else {
		var scripts = document.getElementsByTagName("script");
		var spudsjs = null;
		
		for (var i = 0 ; i < scripts.length ; i++) {
			var src = scripts[i].attributes["src"];
			
			if (src && (src.value.indexOf("/spuds.js") != -1)) {
				spudsjs = true;
				break;
			}
		}
		
		backCompat = !spudsjs;
	}
	
	if (backCompat) {
		$Trumba.iframeBgColor = "white";
	}
})();

/**
 * $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 = $Trumba.Array.shift(args);
	return function() {
		return __method.apply(object, args.concat($Trumba.$A(arguments)));
	}
}

/**
 * Function.$trumba_bindStr()
 *
 * Similar to $trumba_bind except it registers a dynamic callback for the proc and returns
 * a string that when eval()ed will call the proc.  Used for setTimeout() calls where only
 * a string is accepted.
 *
 * THIS IS DEPRECATE - DON'T USE
 */
Function.prototype.$trumba_bindStr = function() {
	var __method = this, args = $Trumba.$A(arguments), object = $Trumba.Array.shift(args);
	var f = function() {
		return __method.apply(object, args.concat($Trumba.$A(arguments)));
	}
	var methodName = "dynCb_" + Math.round(Math.random() * 1000000000) + "_cb";
	window[methodName] = function() { f(); };
	return methodName + "()";
}

/**
 * $Trumba.Array.shift() - Shifts the array left one argument, returning the removed
 * argument.
 *
 * Example : ["Hello", "World!"].$trumba_shift() returns "Hello".
 */
$Trumba.Class.addNamespace("$Trumba.Array");

$Trumba.Array.shift = function(v) {
	var result = v[0];
	for (var i = 0; i < v.length - 1; i++)
		v[i] = v[i + 1];
	v.length--;
	return result;
}


/**
 * $Trumba.$A() - Converts an iterable object or an almost iterable object,
 * e.g. Arguments, into a real array.
 */
$Trumba.$A = 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) {
    // query components need en/decodeURIComponent, not escape/unescape
	return encodeURIComponent(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;

		// Handle permission denied error (handle HTMLElement and Document/Window)
		var permissionCheck = null;
		try { permissionCheck = element.tagName || element.location; } catch (ex) {}
		if (permissionCheck) {
			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();
		
		var args = $Trumba.$A(arguments);
		args.shift();
		
		if (this._eventSource_events[name]) {
			for (var i = 0 ; i < this._eventSource_events[name].length ; i++) {
				// Handle "permission denied" and "script freed" errors
				// that occur if event target has been unloaded.
				try { 
					this._eventSource_events[name][i](args);
				}
				catch (ex) {}
			}
		}			
	}
}


/*************************************************************************
 * $Trumba.Logger
 *
 * Logging facility.  Add your logger using addHandler() passing an object
 * that implements the method processRecord() that takes a single record param.
 */

$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.Debug.ConsoleLogger
 *
 * Support FireBug console and Safari too if ya want.
 */

$Trumba.Debug = { };
$Trumba.Debug.ConsoleLogger = $Trumba.Class.create();

$Trumba.Debug.ConsoleLogger.prototype = {
	initialize: function() {
		if ($Trumba.Debug.ConsoleLogger.init) return;
		if (typeof(console) == "undefined" || typeof(console.log) == "undefined") return;
		
		this._methods = {};
		this._methods[$Trumba.Logger.LEVEL_INFO]    = console.log;
		this._methods[$Trumba.Logger.LEVEL_WARNING] = console.warn  || console.log;
		this._methods[$Trumba.Logger.LEVEL_ERROR]   = console.error || console.log;
		
		$Trumba.Debug.ConsoleLogger.init = $Trumba.Debug.ConsoleLogger.init ||  true;
		$Trumba.logger.addHandler(this);
	},
	
	_getMethod: function(record) {
		return this._methods[record.level] || function() { };
	},
	
	/**
	 * Log Handler - Passes record to window if shown.
	 */	
	processRecord: function(record) {
		var prefix = '';//"[" + (record.className || "") + ":" + (record.methodName || "") + "] ";
		prefix = prefix.length > 4 ? prefix : "";
		this._getMethod(record).apply(console, [prefix + record.message]);
	}
}


/*************************************************************************
 * $Trumba.Debug.SpudDebugWindow
 */

$Trumba.Debug.SpudDebugWindow = $Trumba.Class.create();

$Trumba.Debug.SpudDebugWindow.prototype = {
	initialize: function() {
		this.window = null;
		
		if ($Trumba.Debug.SpudDebugWindow.instance == null)
			$Trumba.Debug.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;

		try {
			var w = window.open("", "spudDebug", "scrollbars=1,status=1,resizable=yes,toolbar=no,menubar=yes");

			var doc;
			doc = w.document;
			if (typeof(w.processRecord) != "undefined") {
				this.window = w;
				$Trumba.logger.info("Attaching to existing debug window.");
				// set focus back to our window
				window.focus();
			}
		}
		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 = null;
		
		try {
			w = window.open("", "spudDebug", "scrollbars=1,status=1,resizable=yes,width=" + width + ",height=" + height + ",left=" + left + ",top=" + top + ",toolbar=no,menubar=no");
			var doc;
			doc = w.document;
			doc.write("<html><body>Loading...</body></html>");
			doc.close();
			w.moveTo(left,top);
			w.resizeTo(width, height);
			// set focus back to our window
			window.focus();
		}
		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) {
					if (!w.closed) w.document.write(result.body);w.document.close();
				},
				onTimeout: function() { 
					if (!w.closed) w.document.write("Timeout");w.document.close();
				},
				onFailure: function() {
					if (!w.closed) 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.Debug.SpudDebugWindow.instance = null;

/**
 * Static method that handles key presses, looking for our magic incantation
 * to show the debug window.
 */
$Trumba.Debug.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.Debug.SpudDebugWindow.instance == null)
			new $Trumba.Debug.SpudDebugWindow();
			
		$Trumba.Debug.SpudDebugWindow.instance.show();
	}
}

// Install Spuds Debug Window Handler
$Trumba.Event.observe(document, "keypress", $Trumba.Debug.SpudDebugWindow.keyHandler, false);


// Log debug messages if desired.

if ($Trumba.showDebugOutput) {
	var o = null;
	
	if ($Trumba.showDebugOutput['console']) {
		o = new $Trumba.Debug.ConsoleLogger();
		
		// Safari's console is hidden by default so use a window as well.
		
		if (/Safari/i.test(navigator.userAgent)) 
			o = new $Trumba.Debug.SpudDebugWindow();
	}
	
	if ($Trumba.showDebugOutput['window']) {
		o = new $Trumba.Debug.SpudDebugWindow();
	}
	
	// If neither was asked for by name then choose the best one.
	
	if (!o) {
		if (typeof(console) == "undefined")
			new $Trumba.Debug.SpudDebugWindow();
		else
			new $Trumba.Debug.ConsoleLogger();
	}
}


/*************************************************************************
 * $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([decodeURIComponent(nv[0]), null]);
				else
					this.pairs.push([decodeURIComponent(nv[0]), decodeURIComponent(nv[1])]);
			}
		}
	},
	
	isEmpty: function() {
		return this.pairs.length == 0;
	},
	
	getCount: function() {
		return this.pairs.length;
	},
	
	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.QueryString


/*************************************************************************
 * $Trumba.Net.Url
 *
 * An object for manipulating Urls.
 */

$Trumba.Net.Url = $Trumba.Class.create();

$Trumba.Net.Url.prototype = {
	initialize: function(url) {
		this._path = '';
		this._queryString = new $Trumba.Net.QueryString();
		this._hash = '';
		
		if (url != null) {
			if (typeof(url) == "string")
				this.fromString(url);
			else if (url["href"])
				this.fromString(url.href);
			else if (typeof(url) == "object")
				this.copyConstructor(search);
		}
	},
	
	copyConstructor: function(other) {
		this._path = other._path;
		this._queryString = new $Trumba.Net.QueryString(other._queryString);
		this._hash = other._hash;
	},
	
	fromString: function(url) {
		this._path = '';
		this._queryString = new $Trumba.Net.QueryString();
		this._hash = '';
		
		var i = url.indexOf('#');
		
		if (i >= 0) {
			this._hash = url.substring(i + 1);
			url = url.substring(0, i);
		}

		i = url.indexOf('?');
		
		if (i >= 0) {
			this._queryString = new $Trumba.Net.QueryString(url.substring(i));
			url = url.substring(0, i);
		}
		
		this._path = url;
	},
	
	getPath: function() {
		return this._path;
	},
	
	getHash: function() {
		return this._hash;
	},
	
	getQueryString: function() {
		return this._queryString;
	},
	
	toString: function() {
		var result = this._path;
		
		var qs = this._queryString.toString();
		
		if (qs.length)
			result += '?' + qs;
		
		if (this._hash.length)
			result += '#' + this._hash;
			
		return result;
	}
} // $Trumba.Net.Url


/*************************************************************************
 * $Trumba.Net.Cookie
 *
 * An object for manipulating Cookies.  Use it as an associative array to add any 
 * cookie name/value pairs you wan't.
 */

$Trumba.Net.Cookie = $Trumba.Class.create();

$Trumba.Net.Cookie.prototype = {
	/**
	 *	Constructor for a cookie.  You are required to pass in a name and document.
	 *
	 *	@param doc		{document}	Document for persisting cookie.
	 *	@param name		{string}	Cookie name.
	 *	@param hours	{Integer}	# of hours cookie will persist.
	 *	@param path		{string}	Root relative path of cookie.
	 *	@param domain	{string}	DNS domain name for cookie.  Can only be a subset
	 *								of current domain.
	 *	@param secure	{boolean}	True for cookies that should only be sent over SSL.
	 */
	initialize: function(doc, name, hours, path, domain, secure) {
		this._document = doc;
		this._name = name;
		
		if (hours)
			this._expiration = new Date((new Date()).getTime() + hours * 3600000);
		else
			this._expiration = null;
		
		if (path) this._path = path; else this._path = null;
		if (domain) this._domain = domain; else this._domain = null;
		if (secure) this._secure = secure; else this._secure = null;
	},
	
	/**
	 *	Saves the cookie to the document cookie collection.
	 */
	save: function() {
		// The cookie value is an & delimited list of name:value pairs, e.g.
		//     mycookiename=foo:bar&name:value

		var cookieval = "";
		
		for (var prop in this) {
			if ((prop.charAt(0)=='_') || (typeof(this[prop])=='function'))
				continue;
			
			if (cookieval != "") 
				cookieval += '&';
			
			var propName = prop;
			
			if (typeof(this[prop]) == "number")
				propName = "[n]" + prop;
			else if (typeof(this[prop]) == "boolean")
				propName = "[b]" + prop;
			
			cookieval += propName + ':' + $Trumba.escape(this[prop]);
		}
		
		var cookie = this._name + '=' + cookieval;
		
		if (this._expiration)
			cookie += '; expires=' + this._expiration.toGMTString();
		
		if (this._path) cookie += '; path=' + this._path;
		if (this._domain) cookie += '; domain=' + this._domain;
		if (this._secure) cookie += '; secure=' + this._secure;
		
		this._document.cookie = cookie;
	},
	
	/**
	 *	Loads the cookie from the document cookie collection.
	 */
	load: function() {
		var allcookies = this._document.cookie;
		if (typeof(allcookies) != "string") return false;
		
		var start = allcookies.indexOf(this._name + '=');
		
		if (start == -1) 
			return false;
		
		start += this._name.length + 1;
		
		var end = allcookies.indexOf(';', start);
		
		if (end == -1)
			end = allcookies.length;
		
		var cookieval = allcookies.substring(start, end);
		
		var a = cookieval.split('&');
		
		for (var i = 0; i < a.length; i++)
			a[i] = a[i].split(':');
			
		for (var i = 0 ; i < a.length ; i++) {
			var name = a[i][0];
			var typeParam = null;
			
			if (name.charAt(0) == '[') {
				typeParam = name.charAt(1);
				name = name.substring(3);
			}
			
			var value = decodeURIComponent(a[i][1]);
			
			if (typeParam != null) {
				if ("n" == typeParam)
					value = Number(value);
				else if ("b" == typeParam)
					value = (value == "true" ? true : false);
			}
			
			this[name] = value;
		}
			
		return true;
	},
	
	/**
	 *	Expires the cookie from the document cookie collection.
	 */
	expire: function() {
		var cookie = this._name + '=';	
		
		if (this._path)
			cookie += '; path=' + this._path;
		
		if (this._domain)
			cookie += '; domain=' + this._domain;
		
		cookie += '; expires=' + new Date(0).toGMTString();
		
		this._document.cookie = cookie;
	}
} // $Trumba.Net.Cookie


/*************************************************************************
 * $Trumba.DOM
 *
 *	Methods and classes for manipulating the DOM.
 */
$Trumba.Class.addNamespace("$Trumba.DOM");

$Trumba.DOM = {
	/**
	 * Creates a new element from a tag and array of name/value pairs.
	 * e.g. createElement("div", [['id','foo'], ['width', 123]]);
	 */
	createElement: function(tag, attrs) {
		attrs = 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;
	},
	
	/**
	 * Shallow compares two DOM elements by tag and attributes.  Returns true 
	 * if tag and attributes match.
	 */
	compareElements: function (e1, e2) {
		if (e1 == e2) return true;
		if (!e1 || !e2) return false;
		if (e1.tagName != e2.tagName) return false;
		if (e1.attributes.length != e2.attributes.length) return false;
		
		for (var i = 0 ; i < e1.attributes.length ; i++) {
			// Order is not guaranteed to be the same.  Use the attribute name.
			var name = e1.attributes[i].name;
			
			if (e2.attributes[name] == null) return false;
			if (e1.attributes[name].value != e2.attributes[name].value) return false;
		}
		return true;
	},
	
	/**
	 * Similar to Node.appendChild except it won't insert duplicates.
	 */
	appendChild: function (element, parent) {
		var kids = parent.getElementsByTagName(element.tagName) || [];
		
		for (var i = 0 ; i < kids.length ; i++) {
			if ($Trumba.DOM.compareElements(element, kids[i]))
				return;
		}
				
		parent.appendChild(element);
	}
};


/*************************************************************************
 * $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:args			Associative of Spud parameters as shown below.
 *   webName {string}		(Required) Calendar being viewed.
 *   spudType {string}		(Required) Type of spud to be inserted.
 *   spudId	{string}		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().
 *   url {object}			Associative array of extra url parameters.
 *   * {*}	                Arbitrary Spud specific properties.
 */
$Trumba.addSpud = function(args) {
	var webName = args["webName"];
	
	if (!webName || webName == "") {
		var msg = "ERROR : You must provide a webName parameter for your Spud!";
		alert(msg);
		throw msg;
	}
	
	delete args["webName"];
	
	var spudType = args["spudType"];
	
	if (!spudType || spudType == "") {
		var msg = "ERROR : You must provide a spudType parameter for your Spud!";
		alert(msg);
		throw msg;
	}
	
	// Force the calendar spud to always be of type "main".
	
	if (spudType.toLowerCase() == "calendar")
		spudType = "main";
		
	delete args["spudType"];
		
	var spudId = args["spudId"];
	
	if (spudId) 
		delete args["spudId"];
		
	var urlArgs = args["url"];
	
	if (urlArgs) {
		delete args["url"];
	}
	
	// The Spud properties are just anything left over.
	
	var properties = args;
	
	$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();
		
		if (args.position) {
			args.position.removeOnClose = true;
			
			// Positioned DIVs we create automatically as last children of the body.
			// Don't create these during document creation/load or you'll RIP in IE.
			// Since most pop-ups overlap we need a background color.
			
			var div = document.createElement("div");
			
			div.id = spudId;
			div.style.position = "absolute";
			div.style.zIndex = 100000;
			div.style.backgroundColor = args['backgroundColor'] || 'white';
			
			document.body.appendChild(div);
		}
		else {
			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, urlArgs, 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 spudId;
	}
	
	// Register Spud with controller.
	createAndRegisterSpud();
	
	return spudId;
}

$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, urlArgs, 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($Trumba.Class.extend(
		{ webName: webName,  spudType: spudType,  spudId: container.id,  url: urlArgs },
		properties));
}


// {0} == id, {1} == style, {2} == free-form, {3} allowtransparency="true"
$Trumba.Spuds.IFRAME_FORMAT_SSL = '<iframe src="javascript:\'<html><body style={4}>&nbsp;</body></html>\'" id="{0}" name="{0}" style="{1}" frameborder="no" width="100%" scrolling="no" marginheight="0" marginwidth="0" {2} {3}><\/iframe>';
$Trumba.Spuds.IFRAME_FORMAT = '<iframe src="javascript:\'<html><body style={4}></body></html>\'" id="{0}" name="{0}" style="{1}" frameborder="no" width="100%" scrolling="no" marginheight="0" marginwidth="0" {2} {3}><\/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, transparent) {
	// We always have a minimum height.
	height = height || "35";
	
	if (typeof(transparent) == "undefined")
		transparent = true;
		
	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) {
		if (typeof(width) == "number")
			width = width + "px";
			
		freeForm += 'width="' + width + '" ';
	}
	
	var html = $Trumba.String.format(
		format, 
		id, 
		style || "",
		freeForm,
		(transparent ? 'allowtransparency="true"' : ''),
		"background-color:transparent");
	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, width, height, transparent) {
	var iframe = document.createElement("iframe");

	if (typeof(transparent) == "undefined")
		transparent = true;
		
	iframe.setAttribute("id", id);
	iframe.setAttribute("name", id);
	iframe.frameBorder = "no";
	iframe.setAttribute("width", width || "100%");
	if (height)
		iframe.setAttribute("height", height);
	iframe.setAttribute("scrolling", "no");
	iframe.setAttribute("marginheight", "0");
	iframe.setAttribute("marginwidth", "0");
	if (transparent)
		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, urlArgs, properties) {
	return new $Trumba.Spuds.SimpleSpud(webName, spudType, id, urlArgs, 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: {}
	},
	timezone: {
		date:{}, view:{}, index:{}, template:{}, pageby:{}, groupby:{}, subgroupby:{}, mix:{}, 
		filter1:{}, filter2:{}, search:{}, paging:{}, groupspan:{}, events:{}, eventid:{},
		/* zone:{}, */ fontscheme:{}, colorscheme:{}, mixcontrol:{}, updates:{}, select:{}, onemonth: {}
	}
}

$Trumba.Spuds.ignoreList.teaser = $Trumba.Spuds.ignoreList.datefinder;
$Trumba.Spuds.ignoreList.searchlabeled = $Trumba.Spuds.ignoreList.search;


/*************************************************************************
 * $Trumba.Spuds.removeList
 *
 * Associative array of all arguments that spuds don't want on their url.
 * These parameters are always removed during parameter merging unless
 * the parameter appears on a corresponding transient list below.
 */

$Trumba.Spuds.removeList = {
	eventid: {}, view: {}	
}


/*************************************************************************
 * $Trumba.Spuds.TransientParams
 *
 * Collection of transient url parameters per Spud.  These parameters are
 * always removed from the url during merging unless they are present
 */

$Trumba.Spuds.TransientParams = {
	_params: {
		"calendar":  { eventid: {}, view: {} },
		"main":      { eventid: {}, view: {} },
		"ryomain.*": { eventid: {}, view: {} }
	},
	
	getParams: function(spudType) {
		for (var t in $Trumba.Spuds.TransientParams._params) {
			var reg = new RegExp(t, "i");
			
			if (reg.test(spudType)) 
				return $Trumba.Spuds.TransientParams._params[t];
		}
		
		return { };
	}
}


/*************************************************************************
 * $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.
	 * @param spud {object}     Spud that created this container.
	 */
	initialize: function(parent, spud) {
		this.parentDiv = parent;
		this._spud = spud;
	},
	
	/**
	 * 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, 
			{ cache: this._spud.getProperty("cache") });
	},
	
	_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.
	 * @param spud {object}     Spud that created this container.
	 */
	initialize: function(parentDiv, spud) {
		this._spud = spud;
		this.parentDiv = parentDiv;
		this.id = parentDiv.id;
		this.iframeStyle = this._getDefaultStyle();
		this._position = this._spud.getProperty("position");
		this._initIFrame();
		this._loaded = false;       // Set to true after Spud window.onload is fired.
		
		// Check if we're a positioned Spud.
		
		if (this._position != null) {
			this.parentDiv.style.display = "";
			this.parentDiv.style.position = "absolute";
			this.parentDiv.style.left = this._position.left + "px";
			this.parentDiv.style.top = this._position.top	 + "px";
			this.parentDiv.style.width = this._position.width + "px";
			if (this._position.height)
				this.parentDiv.style.height = this._position.height	 + "px";
			this.parentDiv.style.overflow = "hidden";
			this.parentDiv.style.zIndex = 100000;
			this.parentDiv.style.backgroundColor = this._spud.getProperty('backgroundColor') || 'white';
			
			// Hide parentDiv until the iframe is loaded and sized.  Removes all flashing.
			// Firefox does wierd sizing things if you use visibility style.  0px height works well.
			// In IE, can use visibility style to hide.  0px doesn't work.
			
			if (/msie/i.test(navigator.userAgent))
				this.parentDiv.style.visibility = "hidden";
			else
				this.parentDiv.style.height = "0px";
		}
		
		// 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) {
			this._unloadCallback = this._removeIFrame.$trumba_bind(this)
			$Trumba.Spuds.controller.addEventListener("unload", this._unloadCallback);
		}
	},
	
	/**
	 * Create initial DIV that displays the busy image.
	 */
	_initIFrame: function() {
		var height = this._spud.getProperty("initialHeight") || 35;

		// Include busy image if this is an inline spud
		// Don't show busy image for popups here.  It is shown outside div because this div is hidden for popups.
		
		var imgBusy = "";
		if (! this._spud.getProperty("position"))
			imgBusy = $Trumba.String.format('<img id="{0}" src="{1}"></img>', this._spud._getBusyImageId(), $Trumba.busyImageUri);

		var html = $Trumba.String.format('<div id="{0}" style="background-color:transparent;height:{1}px;padding:0px;">{2}</div>', 
						this._getIFrameId(),
						height,
						imgBusy);
		this.parentDiv.innerHTML = html;
		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
				|| $Trumba.iframeBgColor;
		}
		catch (e) {
			bgcolor = "white";
		}
		
		bgcolor = $Trumba.String.format("background-color:{0};", bgcolor)
			
		return bgcolor;
	},
	
	/**
	 * Closes the container and releases resources.
	 */
	close: function() {
		this.parentDiv.innerHTML = "";
		this.parentDiv.style.display = "none";
		$Trumba.Spuds.controller.addEventListener("unload", this._unloadCallback);
		
		// If the div is positioned we yank the container.
		
		if (this._position != null && this._position.removeOnClose) {
			this.parentDiv.parentNode.removeChild(this.parentDiv);
			this.parentDiv = null;
		}
	},
	
	/**
	 * 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._loaded = true;
		this._resizeIFrameWithWait();
		this._fireEvent("onFetched");
		this._spudLoaded();
	},
	
	_spudLoaded: function() {
	},
	
	/**
	 * Causes the container to resize, if possible, to fit the contents.
	 */
	resize: function() {
		this._resizeIFrame();
	},
	
	/**
	 * Computes the cumulative offset of an element from the document body.
	 */
	cumulativeOffset: function (element) {
		var valueT = 0, valueL = 0;

		do {
			valueT += element.offsetTop  || 0;
			valueL += element.offsetLeft || 0;
			element = element.offsetParent;
		} while (element);

		return [valueL, valueT];
	},
	
	/**
	 * Returns the client location of the contianer.  We return both position as
	 * left,top and dimension as width, height.
	 */
	getLocation: function() {
		if (this.iframe == null)
			return { left: 0, top: 0, width: 0, height: 0 };

		var co = this.cumulativeOffset(this.iframe);

		return {		
			left: co[0], 
			top: co[1],
			width: this.iframe.offsetWidth,
			height: this.iframe.offsetHeight };
	},

	/**
	 * 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)
			// Uncomment out the following line to keep the s.aspx request Urls constant
			// in order to see if they will cache in the browser.
			,cbid: this.id
		}
		
		// Reset the loaded flag.
		this._loaded = false;
		
		$Trumba.Spuds.controller.loader.request(
			this.queryString.toString(), 
			myCallbacks,
			{ cache: this._spud.getProperty("cache") });
	},
	
	/**
	 * 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() {
		// Don't start resizing until after the Spud HTML is present.
		if (!this._loaded || this._spud.closed) return;

		var iframe = this.iframe;
		var con = this;
		var firstAttempt = (arguments.length == 0);
		var b4 = iframe.height;
		var path = "unk";
		
		$Trumba.Try.these(
			// Mozilla/Netscape/FireFox/Opera/Safari
			function() { 
				path = "Moz-" + iframe.contentDocument.body.offsetHeight;
				iframe.height = iframe.contentDocument.body.offsetHeight;
			},

			// Internet Explorer
			function() {
				// IE 6 exports the Document method which is really a stub to the IWebBrowser.Document
				// method.  It works fine and gives us more reliable sizing that using frames[]
				// for some reason.  Every IFRAME is an instance of a WebBrowser object.
				path = "IE-" + iframe.Document.body.scrollHeight;
				var newHeight = iframe.Document.body.scrollHeight;
				if (newHeight != iframe.height)
				{
					iframe.height = newHeight;
					//$Trumba.logger.info($Trumba.String.format("{2}(Setting Height) - old:={0}, new:={1}", b4, newHeight, con._getIFrameId()));
					
					// IE 6/7 has a bug where sometimes the spud is not visible.  Set and unset the display
					// property to get it to show up. 
					//if (/msie 7.0/i.test(navigator.userAgent)) {
					if (/msie/i.test(navigator.userAgent)) {
						window.setTimeout(function() {
							//$Trumba.logger.info(con._getIFrameId() + " - IE 7.0: Tickling iframe style to make sure we show.");
							iframe.style.display = "none";
							iframe.style.display = "";
							}, 1);
					}
				}
			},

			// Fallback to frames...
			function() { 
				path = "frames[]-" + frames[iframe.id].document.body.scrollHeight;
				iframe.height = frames[iframe.id].document.body.scrollHeight;
			},
			
			// 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);
				}
				
			}
		);

		// If popup, parentDiv height is absolute so reset it equal to the iframe height.
		if (con._position != null) {
			con.parentDiv.style.height = iframe.height + "px";
			// In IE div is hidden so restore visibility now that it is loaded. (other browsers use 0px height to hide)
			if (con.parentDiv.style.visibility == "hidden")
				con.parentDiv.style.visibility = "visible";
		}

		this._spud._hideBusyImage();

		// Fire the sizeChanged method for those who need it.
		
		if (iframe.height != b4) {
			this._sizeChanged()
		}

		// Let positioned spuds know about the resize.
				
		if (this._position) {
			this._fireEvent(
				"resize", 
				{ left: this._position.left, top: this._position.top, width:this._position.width, height: parseInt(iframe.height) });
		}		// Save the last known height as the new initial height.
	
		this._spud.setSpudCookieProperty("initialHeight", Number(iframe.height));
		
		//$Trumba.logger.info($Trumba.String.format("{2}({3}) - before:={0}, after:={1}", b4, iframe.height, con._getIFrameId(), path));
	},
	
	/**
	 * Called by our base when the Spud window.onload event is fired.
	 */
	_sizeChanged: function() {
		if (this._position != null) {
			this.parentDiv.style.overflow = "hidden";
			this.parentDiv.style.width = this._position.width + "px";
		}
		else {
			this.parentDiv.style.overflow = "";
			this.parentDiv.style.width = "";
			this.parentDiv.style.height = "";
		}
	},
		
	getPosition: function() {
		return this._position;
	},
	
	setPosition: function(position) {
		if (!this._position) return;
		
		this._position.left = position.left || this._position.left;
		this._position.top = position.top || this._position.top;
		this._position.width = position.width || this._position.width;
		
		this.parentDiv.style.left = this._position.left + "px";
		this.parentDiv.style.top = this._position.top	 + "px";
		this.parentDiv.style.width = this._position.width + "px";
	},

		
	/**
	 * Called when the controller unloads when it wants us to remove our IFRAME from
	 * the DOM.  This is typically only for FireFox (Gecko).
	 */
	_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();
	},

	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;
	},
	
	_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);
	},
	
	/**
	 * 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");
		*/
		
		// Exit if spud has already been closed.  Popups can get closed before fetch is complete.
		if (this._spud.closed) {
			$Trumba.logger.warning(this._getIFrameId() + "(" + this.queryString.toString() + ") Spud Closed - Not Rebuilding");
			return;
		}
		
		var height = this.iframe.offsetHeight;
		var divHeight = height;
		
		// If we've got no height and an initial was given use it but only
		// on the DIV, not the IFRAME, for if you do then IE will sometimes fail
		// to report anything but 0 for the scrollheight of the IFRAME
		// document.
		
		var initialHeight = this._spud.getProperty("initialHeight");
		
		if ((height == 0) && (initialHeight != null))
			divHeight = initialHeight;
		
		this.parentDiv.style.overflow = "hidden";
		this.parentDiv.style.height = divHeight + "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, 
			null,
			height,
			this._position == null);
		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);  // Isn't 1ms a bit too hasty?
					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;
	},
	
	_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) {
		this._spud._hideBusyImage();
		doc.write(html);
		doc.close();
	}
} // $Trumba.Spuds.IFrameContainer


$Trumba.Class.extend($Trumba.Spuds.IFrameContainer.prototype, $Trumba.EventSource);


/*************************************************************************
 * $Trumba.Spuds.SimpleSpud
 *
 *	This is the class that's instantiated for every Spud.
 */

$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:urlArgs		Spud arguments, e.g. extra url arguments.
	 * @param:properties    Spud globas, e.g. non-url arguments.
	 */
	initialize: function(webName, spudType, id, urlArgs, properties) {
		// Stash our parameters.
		urlArgs = urlArgs ||  {};
		this.spudType = spudType;
		this.id = id;
		this.webName = webName;
		this._initProperties(properties, urlArgs);
		
		// 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, this);
		else
			this.container = new $Trumba.Spuds.IFrameContainer(this.parentDiv, this);
			
		this.container.addEventListener("onFetched", this.onFetched.$trumba_bind(this));

		// Attach the popup behavior for popups.
		
		if (this.getProperty("position") != null) {
			new $Trumba.Spuds.PopUpBehavior(
				this,
				this.getProperty("closeOnFocus"),
				this.getProperty("closeOnFocus"));
		}
		
		// Create our query string from any arguments plus our widget and webname.
		
		this.setQueryString(this._getEmbedArguments(), urlArgs);
		
		$Trumba.logger.info(this.getUniqueID() + " Created for " + webName + "/" + spudType + " " + this.queryString.toString());
	},
	
	/**
	 * Stores any properties that were passed in to our constructor and loads any overrides
	 * from the calendar cookie, if present.
	 */
	_initProperties: function(properties, urlArgs) {
		this.properties = properties || { };
		
		// If there are any properties on the Spud cookie then they are overrides.
		
		var cookie = new $Trumba.Spuds.CalendarCookie(document, this.webName);
		cookie.load();
		var overrides = cookie.getAllSpudProperties(this.spudType);
		$Trumba.Class.extend(this.properties, overrides);
		
		// Anything prefixed with url. goes onto the url arguments and is
		// part of the global properties.
		
		overrides = cookie.getAllSpudProperties('*');
		$Trumba.Class.extend(this.properties, overrides);

		for (var n in overrides) {
			if (0 == n.indexOf("url.")) {
				urlArgs[n.substring(4)] = overrides[n];
				overrides[n] = null;
				delete overrides[n];
			}
		}
		
		// Apply the global properties
		$Trumba.Class.extend(this.properties, overrides);
	},

	/**
	 * Let's outsiders save a cookied property.
	 *
	 * @param name {string} Name of property.
	 * @param value {*}		Property value.
	 */	
	setSpudCookieProperty: function(name, value) {
		var cookie = new $Trumba.Spuds.CalendarCookie(document, this.webName);
		cookie.load();
		cookie.setSpudProperty(/^url\./i.test(name) ? '*' : this.spudType, name, value);
		cookie.save();
	},
	
	/**
	 * Closes the Spud and removes it from the controller.
	 */
	close: function() {
		if (this.closed) {
			$Trumba.logger.info("ALREADY CLOSED:" + this.queryString.toString());
			return;
		}
		$Trumba.logger.info("Closing Spud:" + this.queryString.toString());
		this._hideBusyImage();
		this.closed = true;
		this._fireEvent("close");
		this.container.close();
		$Trumba.Spuds.controller.removeSpud(this);
	},
	
	/**
	 * Handles fixing our 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);
	},

	/**
	 * Applies any style rules post fetch.
	 */	
	onFetched: function() {
		var resize = false;
		var win = this.container.getWindow();
		var doc = win ? win.document : null;

		if (win && win.trumba_addStyleRules) {
			var rules = this.getProperty("styleRules");
			
			if (rules && rules.length) {
				win.trumba_addStyleRules(rules);
				resize = true;
			}
		}
		
		// Insert any style sheet references.
		if (win && doc) {
			var sheets = this.getProperty("styleSheets");
			
			if (sheets && sheets.length) {
				var parent = doc.getElementsByTagName("head")[0];
				
				for (var i = 0 ; i < sheets.length ; i++) {
					var link = doc.createElement("link");
					link.setAttribute("href", sheets[i]);
					link.setAttribute("rel", "stylesheet");
					parent.appendChild(link);
				}
				
				// To avoid FoUC, spud should be hidden until new CSS has been applied.
				// It is up to the server spud to set visibility=hidden.
				doc.body.style.visibility = "visible";
			}
		}
		
		if (resize)
			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.
	 * @param urlArgs  JSON object of extra url arguments.
	 */	
	setQueryString: function(queryString, urlArgs) {
		var qs = new $Trumba.Net.QueryString(queryString);
		
		if (urlArgs != null) {
			for (var name in urlArgs) {
				qs.setAt(name, urlArgs[name]);
			}
		}
		
		qs.setAt("calendar", this.webName);
		qs.setAt("widget", this.spudType);
		
		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.
	 *
	 * @param url {string} Differential querystring or absolute Url.
	 * @param absolute {bool} true if url is absolute, false for differential querystring.
	 */
	navigate: function(url, absolute, baseUrl) {
		var savedBaseUrl = baseUrl;
		baseUrl = this.getProperty(baseUrl);
		
		if (absolute) {
			$Trumba.logger.info(this.getUniqueID() +" abs navigate(" + url + ")");

			window.top.location.href = url;
		}
		else if (baseUrl && baseUrl.length) {
			$Trumba.logger.info(this.getUniqueID() + " base navigate(" + baseUrl + " -- " + url + ")");
			
			// Destination Url with returnBase param - ignore remove list, we need to pass 
			// all our parameters forward.
			
			var savedQueryString = this.queryString;
			this._mergeQueryString(url, true);
			var gotoUrl = new $Trumba.Net.Url(baseUrl);

			// Return Url
			if ("detailBase" == savedBaseUrl) {
				// Handle permission denied in cases where we are nested in iframes (e.g. NYT Autos home page)
				var permissionCheck = null;
				try { permissionCheck = window.top.location.href; } 
				catch (ex) {}
				if (permissionCheck) {
					var returnUrl = new $Trumba.Net.Url(window.top.location);
					returnUrl.getQueryString().setAt("trumbaEmbed", savedQueryString.toString());
					this.queryString.setAt("returnUrl", returnUrl.toString());
				}
			}

			gotoUrl.getQueryString().setAt("trumbaEmbed", this.queryString.toString());

			window.top.location.href = gotoUrl.toString();
		}
		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);
			
			// Spuds, specifically popups, may have been closed by the navigate call.  If closed, then don't fetch.
			
			if (this.closed)
				return;
			
			// 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 + ")");

		// Spuds, specifically popups, like it when they close when a navigation occurs.  We've
		// already navigated the controller so we can exit now safely.
		
		if (this.getProperty("closeOnNav")) {
			this.close();
			return;
		}

		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.
	 * @param ignoreRemoveList {bool} Ignore's the Spud's remove list.
	 
	 * @return true if no changes were found between query strings.
	 */
	_mergeQueryString: function(queryString, ignoreRemoveList) {
		// Default to false.
		ignoreRemoveList = ignoreRemoveList || false;
		
		var a = new $Trumba.Net.QueryString(this.queryString);
		var b = new $Trumba.Net.QueryString(queryString);
		
		$Trumba.logger.warning("Before : " + a.toString() + " -=- " + b.toString());
		
		// Transient url parameters are params that we set once but don't want to persist
		// during subsequent navigations.  
		
		var transientParams = $Trumba.Spuds.TransientParams.getParams(this.spudType);
		var yankList = [];
		
		if (transientParams != null) {
			a.visit(function(name) {
				if (transientParams[name])
					yankList.push(name);
			});
		}
		
		for (var i = 0 ; i < yankList.length ; i++)
			a.remove(yankList[i]);
		
		// First we need apply the diff to our current qs to generate the 
		// intended destination.  Argument's prefixed with a '-' need to be removed.
		// Arguments on the removeList must also be removed unless they are
		// found on the transientParams.
		
		for (var i = 0 ; i < b.length() ; i++) {
			var item = b.getAt(i);
			var remove = (item[0].charAt(0) == '-') ||
				(($Trumba.Spuds.removeList[item[0]] != null && !ignoreRemoveList) && 
				(transientParams == null || transientParams[item[0]] == null));
				
			if (remove)
				a.remove(item[0].substring(1));
			else
				a.setAt(item[0], item[1])
		}
		
		// 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"];

        // Look for argList on the spud.  If found, this is a list of args that the spud cares about.		
        if (typeof(this.argList) != "undefined")
        {
		    same = true;
		    for (var n in diff) {
			    if (typeof(this.argList[n]) != "undefined") {
			        same = false;
			        break;
			    }
		    }
        }
        else
        {
            // Else look in global ignore list.
		    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;
		}
		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;
		}

		// Display busy image when content is being fetched.
		var spudPosition = this.getProperty("position");
		if (this.container._loaded || spudPosition != null) {
			// If popup, use popup position else ask container for position.
			if (spudPosition != null)
				this._showBusyImage(spudPosition);
			else
				this._showBusyImage(this.container.getLocation());
		}

		// Apply any non-persistent overrides.
		
		var qs = new $Trumba.Net.QueryString(this.queryString);
		
		// ssOverrides - Set to 1 if overrides are present, otherwise leave it out.
		
		var sheets = this.getProperty("styleSheets");

		if (sheets && sheets.length) {
			qs.setAt("ssOverrides", 1);
		}

		// Tell our container to fetch the HTML.
				
		this.container.fetch(qs);
	},
	
	_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();
	},

	/**
	 * Returns the id/name of the iframe associated with this Spud.
	 */	
	_getBusyImageId: function() {
		return this.id + ".busy";
	},
	
	_showBusyImage: function(pos) {
		var hideBusyImage = this.getProperty("hideBusyImage") || false;
		if (! hideBusyImage) {
			var imgLoading = $Trumba.DOM.createElement("img", [ ["id", this._getBusyImageId() ] ]);
			imgLoading.style.position = "absolute";
			imgLoading.style.left = pos.left + "px";
			imgLoading.style.top = pos.top + "px";
			imgLoading.style.zIndex = 100000;
			imgLoading.src = $Trumba.busyImageUri;
			document.body.appendChild(imgLoading);
			$Trumba.logger.info(this._getBusyImageId() + ", parent:" + imgLoading.parentNode.tagName, null, "$Trumba.Spuds.SimpleSpud", "_showBusyImage");
		}
	},
	
	_hideBusyImage: function() {
		var imgLoading = $Trumba.$(this._getBusyImageId());
		if (imgLoading) {
			$Trumba.logger.info(this._getBusyImageId() + ", parent:" + imgLoading.parentNode.tagName, null, "$Trumba.Spuds.SimpleSpud", "_hideBusyImage");
			imgLoading.parentNode.removeChild(imgLoading);
		}
	}
	
} // $Trumba.Spuds.SimpleSpud

$Trumba.Class.extend($Trumba.Spuds.SimpleSpud.prototype, $Trumba.EventSource);


/*************************************************************************
 * $Trumba.Spuds.PopUpBehavior
 *
 * Encapsulates the popup spud closing behavior.  We listen for the escape key
 * or a click outside of the Spud and close it when either event happens.
 */

$Trumba.Spuds.PopUpBehavior = $Trumba.Class.create();

$Trumba.Spuds.PopUpBehavior.prototype = {
	initialize: function(spud, closeOnEscape, closeOnLostFocus) {
		this._spud = spud;
		
		if (closeOnEscape) {
			// Main window.
			this._makeKeyHandler(window.document);
			
			// Each individual spud window
			
			var spuds = $Trumba.Spuds.controller.getSpuds();
			
			for (var i = 0 ; i < spuds.length ; i++) {
				this._makeKeyHandler(spuds[i].container.getWindow().document);
			}
		}
		
		if (closeOnLostFocus) {
			// Main window.
			this._makeClickHandler(window.document);
			
			// Each individual spud except the popup.
			
			var spuds = $Trumba.Spuds.controller.getSpuds();
			
			for (var i = 0 ; i < spuds.length ; i++) {
				if (spuds[i] == spud) continue;
				this._makeClickHandler(spuds[i].container.getWindow().document);
			}
		}
	},
	
	_makeKeyHandler: function(element) {
			this._makeHandler(
				element,
				"keypress",
				function(e) {
					if ($Trumba.Event.keyCode(e) == $Trumba.Event.KEY_ESC) {
						this.spud.close();
						this.stop(true);
					}
				},
				false);
	},
	
	_makeClickHandler: function(element) {
			this._makeHandler(
				element, 
				"click", 
				function(e) {
					this.spud.close();
					this.stopObserving();
				}, 
				false);
	},
	
	
	_makeHandler: function(element, name, observer, capture) {
		var args = $Trumba.$A(arguments);

		args.shift();args.shift();args.shift();args.shift();

		var o = {
			onEvent: function(e) {
				this._event = e || window.event;
				args = [];
				args.push(this._event);
				return this.observer.apply(this, args.concat(this.args));
			},

			stop: function(stopObserving) {
				$Trumba.Event.stop(this._event);
				
				if (stopObserving) 
					this.stopObserving();
			},
			
			stopObserving: function() {
				$Trumba.Event.stopObserving(this.element, this.name, this.boundObserver, this.capture);
			},
			
			element: element,
			name: name,
			observer: observer,
			capture: capture,
			boundObserver: null,
			args: args
		};

		this._spud.addEventListener("close", o.stopObserving.$trumba_bind(o));
		o.spud = this._spud;
		o.boundObserver = o.onEvent.$trumba_bind(o);
		$Trumba.Event.observe(o.element, o.name, o.boundObserver, o.capture);
	}	
}; // $Trumba.Spuds.PopupBehavior


/*************************************************************************
 * $Trumba.Spuds.CalendarCookie
 *
 * A cookie that is persisted for each Calendar.  You can add any properties
 * you want on a per Spud basis, e.g. 
 *
 *		setSpudProperty("main", "height", "750px");
 *
 * The cookie is stoed on the host site not www.trumba.com.
 */

$Trumba.Spuds.CalendarCookie = $Trumba.Class.create();

$Trumba.Spuds.CalendarCookie.prototype = {
	/**
	 * # of hours the cookie survives.
	 */
	_cookieLifeTimeHours: 48,
	
	/**
	 * Returns a spud specific property.
	 *
	 * @param spudType {string}	Type of Spud.  Note that if you have multiple instances
	 *								of a Spud this isn't enough to differentiate.
	 * @param name		{string}	Property name.
	 */
	getSpudProperty: function(spudType, name) {
		return this[spudType + "_" + name];
	},

	/**
	 * Returns an associated array of all the spud properties on an object.
	 *
	 * @param spudType {string}	Type of Spud.
	 */
	getAllSpudProperties: function(spudType) {
		var result = { };
		var prefix = spudType + '_';
		var len = prefix.length;
		
		for (var prop in this) {
			if (prop.indexOf(prefix) == 0)
				result[prop.substring(len)] = this[prop];
		}
		
		return result;
	},
	
	/**
	 * Retrieves a Spud specific property.
	 *
	 * @param spudType {string}	Type of Spud.  Note that if you have multiple instances
	 *								of a Spud this isn't enough to differentiate.
	 * @param name		{string}	Property name.
	 * @param value				Property value - don't use cookie reserved chars like ':&='.
	 */
	setSpudProperty: function(spudType, name, value) {
		this[spudType + "_" + name] = value;
	}
}

// We extend Cookie and hide it's initialize method.

$Trumba.Class.extend($Trumba.Spuds.CalendarCookie.prototype, $Trumba.Net.Cookie.prototype);

$Trumba.Spuds.CalendarCookie.prototype.base_initialize = $Trumba.Spuds.CalendarCookie.prototype.initialize;

$Trumba.Spuds.CalendarCookie.prototype.initialize = function(doc, calendar) {
	this.base_initialize(doc, "spudCookie_" + calendar, this._cookieLifeTimeHours);
}
	

/*************************************************************************
 * $Trumba.Spuds.Cache
 *
 * A simple cache that has a stale check built in.
 */

$Trumba.Spuds.Cache = $Trumba.Class.create();

$Trumba.Spuds.Cache.prototype = {
	initialize: function(lifetime) {
		this._cache = { };
		this._lifetime = lifetime >= 0 ? lifetime : (10 * 60 * 1000);
	},
	
	empty: function() {
		this._cache = { };
	},
	
	getItem: function(key) {
		var item = this._cache[key];
		
		// See if the item is stale.
		
		if (item) {
			if (item.expires > new Date())
				return item.value;

			this.removeItem(key);
		}
		
		return null;
	},
	
	addItem: function(key, value) {
		this._cache[key] = {
			expires: new Date() - 0 + this._lifetime,
			value: value
		};
	},
	
	removeItem: function(key) {
		if (this._cache[key]) {
			this._cache[key] = null;
			delete this._cache[key];
		}
	}
	
} // $Trumba.Spuds.Cache


/*************************************************************************
 * $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;
		// 4 domains is best with 2 connections per server and 8 max connections
		// open at once (IE 6).
		this._domains = ['', 'a.', 'b.', 'c.'];
		this._domainMap = { };		
		this._cache = new $Trumba.Spuds.Cache(10 * 60 * 1000);
	},
	
	/**
	 * When www.trumba.com is the requested domain this method round robins through
	 * optional domains of a.www.trumba.com, b, c and d.  This cannot be
	 * done for SSL as we do not have 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) {
		var m = /http:\/\/(qa|webstage|dev|www)\.trumba\.com/i.exec(url)
		if (!m) { return url; }

		var r = new RegExp('//' + m[1] + '.', 'i');
		url = url.replace(r, '//' + this._domains[this._nextDomain] + m[1] + '.');
		this._nextDomain++;
		this._nextDomain %= this._domains.length;
		return url;
	},
	
	/**
	 * 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) {
		// Preview's don't round-robin since the auth cookie isn't on each domain.
		
		var url;
		
		if (queryString.indexOf("preview=") != -1)
			url = $Trumba.loaderUri;
		else {
			// Fetch from the cache.
			url = this._domainMap[queryString];
			
			if (!url) {
				url = this._setDomain($Trumba.loaderUri)
				this._domainMap[queryString] = url;
			}
		}
		
		return url + "?" + queryString;
	},
	
	/**
	 * Used when caching successful results
	 */
	_onSuccess: function(queryString, cb, result) {
		$Trumba.logger.info("Caching " + queryString);
		this._cache.addItem(queryString, result);
		cb(result);
	},
	
	/**
	 * Places a spud request in the queue.  Of course this isn't true, there
	 * is no 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, options) {
		options = options || { };
		var url = this._getSpudUrl(queryString);
		$Trumba.logger.info("Loading " + url, null, "$Trumba.Spuds.Loader", "request");
		
		// Cached Requests
		if (options.cache) {
			var hit = this._domainMap[queryString];
			var value = this._cache.getItem(queryString);
			
			if (hit && value) {
				$Trumba.logger.info("CACHE HIT : " + queryString);
				callbacks.onSuccess(value);
				return;
			}
			
			// Override the success callback in order to cache a hit.  Only do this
			// if there's a cacheable entry.
			if (hit) {
				var cb = callbacks.onSuccess;
				callbacks.onSuccess = this._onSuccess.$trumba_bind(this, queryString, cb)
			}
		}
		
		var request = new $Trumba.ScriptXmlHttpRequest(url, callbacks);
		request.invoke();
	}
} // $Trumba.Spuds.Loader


/*************************************************************************
 * $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 few seconds,
		// not for every refresh.  By then we're ok.

/*
		if (/msie/i.test(navigator.userAgent) &&
			!(/msie 7./i.test(navigator.userAgent)) &&
			(typeof(window.opera) == "undefined")) {	
*/
		if (/msie/i.test(navigator.userAgent) && (typeof(window.opera) == "undefined")) {
			this.resizeTimeoutTicks = 0;
			this.resizeTimeoutId = window.setInterval(this.onResizeTimeout.$trumba_bind(this), 1000);
		}
	},
	
	/**
	 * Returns the hosting page window object.
	 */
	getHostWindow: function() {
		return window;
	},
	
	/**
	 * Returns a spud by Id.
	 */
	getSpudById: function(spudId) {
		return this.spuds[spudId];
	},
	
	/**
	 * A carryover from the old days - adds a new and unique child element to the HEAD
	 * of the hosting page.  Mainly for XML feeds.
	 */
	addHead: function(tag, attrs) {
		var heads = document.getElementsByTagName("head");
		
		if (heads.length) {
			var e = $Trumba.DOM.createElement(tag, attrs);
			$Trumba.DOM.appendChild(e, heads[0]);
		}
	},
	
	/**
	 * 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);
			this.resizeTimeoutId = null;
			return;
		}
		
		this.resize();
	},
	
	/**
	 * Called when the page unloads.  We notify all the spuds so they can do whatever
	 * shutdown is appropriate.
	 */
	onPageUnload: function() {
		// Kill the IE resize timer.
		if (this.resizeTimeoutId != null) {
			window.clearInterval(this.resizeTimeoutId);
			this.resizeTimeoutId = null;
		}
					
		// 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 {string}		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();
		}
	},
	
	/**
	 * Removes a spud from our collection.  This doesn't close the spud, you need
	 * to do that via it's close() method.
	 *
	 * @param spud {$Trumba.Spuds.SimpleSpud}	The spud to remove.
	 */
	removeSpud: function(spud) {
		if (this.spuds[spud.id] != null) {
			this.spuds[spud.id] = null;
			delete this.spuds[spud.id];
		}
	},
	
	/**
	 * 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;
	},
	
	/**
	 * Returns an array of all spuds on the page.
	 */
	getSpuds: function() {
		var result = [];
		
		for (var s in this.spuds)
			result.push(this.spuds[s]);

		return result;		
	},

	/**
	 * 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();
		}
	},
	
	findSpud: function(property, value) {
		var spuds = this.getSpuds();
		for (var i = 0 ; i < spuds.length ; i++) {
			var spudValue = spuds[i].getProperty(property);
			if (spudValue && spudValue == value)
				return spuds[i];
		}
		return null;
	},
	
	/**
	 * 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.options = options || { };
		this.options.timeout = this.options.timeout || 60;
		this.cbid = options.cbid || $Trumba.ScriptXmlHttpRequest.createCBID();
		this.url = url;
		var separator = (/\?/.test(this.url) ? "&" : "?");
		this.srcUrl = this.url + separator + "srpc.cbid=" + this.cbid + "&srpc.get=true";
	},
	
	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.srcUrl, 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.srcUrl);
		
		// 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.
		if (this.script.parentNode != null)	{
			this.script.parentNode.removeChild(this.script);
		}
	},
			
	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);
	}
} // $Trumba.ScriptXmlHttpRequest.prototype


// Safari/WebKit 1.3 (312 builds) don't suuport dynamic insertion of 
// SCRIPT tags into the HEAD.  We use a dynamic IFRAME instead.  Messy!
// However don't try and use this for IE because it tosses the legendary
// 'Operation Aborted" error if we happen to insert the IFRAME into the 
// BODY before the BODY tag is closed.  We never found a solid/reliable
// way to know when it was closed so we didn't use this for all browsers.

if (/(Safari\/)(\d+)/i.test(navigator.userAgent) && parseFloat(/(Safari\/)(\d+)/i.exec(navigator.userAgent)[2]) <= 312) {
	$Trumba.Class.extend($Trumba.ScriptXmlHttpRequest.prototype, {
		/**
		 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.innerIFRAME.parentNode.removeChild(this.innerIFRAME);
		},
				
		doGet: function() {
			// Set the src and add it to the DOM.  This fires off the request.
			var html =
				'<'+'html><'+'head><'+'script type="text/javascript">' +
				'\r\n' +
				'window["$Trumba"] = { };\r\n' +
				'$Trumba["ScriptXmlHttpRequest"] = { };\r\n' +
				'$Trumba.ScriptXmlHttpRequest.requestComplete = function(result) {' +
				'window.parent.$Trumba.ScriptXmlHttpRequest.requestComplete(result); }\r\n' +
				'document.write(\'<s\'+\'cript type="text/javascript" src="' + this.srcUrl + '"><\'+\'/script>\');\r\n' +
				'<'+'/script>\r\n<'+'/head>\r\n<'+'body><'+'/body>\r\n<'+'/html>\r\n';
			
			var pThis = this;

			var tryAppend = function() {
				// Wait for that body...
				if (document.body == null) {
					window.setTimeout(arguments.callee.$trumba_bind(this), 100);
					return;
				}
				// Setup a callback to this object to handle the result.
				$Trumba.ScriptXmlHttpRequest.addCallback(pThis.cbid, pThis.srcUrl, pThis.onComplete.$trumba_bind(pThis));

				// Set a timeout timer.
				pThis.timeoutID = window.setTimeout(pThis.onTimeout.$trumba_bind(pThis), pThis.options.timeout * 1000);

				// We create a new hidden IFRAME whose ID matches our callback ID.  It will 
				// make the request for us.  Once it's complete we'll yank it.
				
				var iframe = $Trumba.Spuds.createIFrame(pThis.cbid = ".iframe", "width:0px;height:0px;visibility:hidden;");
				pThis.innerIFRAME = document.body.appendChild(iframe);

				// Write the contents of the IFRAME
				var doc;
				
				if (pThis.innerIFRAME.contentWindow)
					doc = pThis.innerIFRAME.contentWindow.document;
				else
					doc = window.frames[pThis.innerIFRAME.name].document;

				doc.write(html);
				doc.close();
			}
			
			// The body is most likely going to be null on our first attempt so 
			// wait a bit.

			tryAppend();
		}
	}); // $Trumba.ScriptXmlHttpRequestSafari - An extension just for Safari
} 


// $Trumba.ScriptXmlHttpRequest Statics

$Trumba.Class.extend($Trumba.ScriptXmlHttpRequest, {
	// ID of most recent callback.  Monotonically increases.
	rpcID: 0,

	// A random number we prefix to all our callback IDs.  Decreases
	// likelyhood of collisions for saved post data.
	rpcGUID: "" + Math.random(),

	// Object that holds our callbacks by property name.
	callbacks: { },

	// Static method called by server's javascript response.
	requestComplete: function(result) {
		$Trumba.logger.info("Request completed : " + result.cbid + ":" + result.url);
		result = eval(result);
		var cb = $Trumba.ScriptXmlHttpRequest.getCallback(result.cbid, result.url);
		if (cb == null) {
			$Trumba.logger.error(
				"No callback found for result " + result.cbid + ":" + result.url,
				null,
				null,
				"$Trumba.ScriptXmlHttpRequest.requestComplete");
			return;
		}
		
		$Trumba.ScriptXmlHttpRequest._removeCallback(result.cbid, result.url);
		cb(result);
	},
	
	getCallback: function(cbid, url) {
		var lookupId = cbid + ":" + url;
		var cb = this.callbacks[lookupId];
		
		if (cb && typeof(cb.queue) != "undefined") {
			$Trumba.logger.info("Found " + cb.length + " callbacks.");
			return cb[0];
		}
		
		return cb;
	},

	// Registers a callback object to a callback Id.
	addCallback: function(cbid, url, cb) {
		var lookupId = cbid + ":" + url;
		$Trumba.logger.info("Adding Callback " + lookupId);
		// If duplicate then queue them up.
		if (this.callbacks[lookupId]) {
			if (typeof(this.callbacks[lookupId].queue) == "undefined") {
				this.callbacks[lookupId] = [this.callbacks[lookupId], cb];
				this.callbacks[lookupId].queue = true;
			}
			else {
				this.callbacks[lookupId].push(cb);
			}
			
			return;
		}
		this.callbacks[lookupId] = cb;
	},

	// Removes a callback object by Id.
	_removeCallback: function(cbid, url) {
		var lookupId = cbid + ":" + url;
		
		if (this.callbacks[lookupId]) {
			// If this is an array then there are multiple people waiting for the same
			// callback.  We treat it like a queue in that case.
			
			if (this.callbacks[lookupId].queue) {
				$Trumba.Array.shift(this.callbacks[lookupId]);
				
				if (this.callbacks[lookupId].length == 0)
					delete this.callbacks[lookupId];
					
				return;
			}
			
			this.callbacks[lookupId] = null;
			delete this.callbacks[lookupId];
		}
	},

	// Generates a new callback Id.
	createCBID: function() {
		var result = this.rpcGUID;
		result += "-" + this.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 60),
		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.options = options || { };
		this.options.timeout = this.options.timeout || 60;
		this.postdata = postdata;
		this.cbid = $Trumba.ScriptXmlHttpRequest.createCBID();
		this.url = url;
		var separator = (/\?/.test(this.url) ? "&" : "?");
		this.srcUrl = this.url + separator + "srpc.cbid=" + this.cbid + "&srpc.post=true";
	},
	
	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.srcUrl;
		
		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.srcUrl, 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
		}
		
		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;
			}
			
			window.setTimeout(this.doGet.$trumba_bind(this), 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);
	},
			
	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);
	}
}

// Timing
if (typeof(trumba_preSpudsJS) != "undefined") {
	$Trumba.logger.warning($Trumba.String.format(
		"spuds.js took {0} seconds to download.",
		($Trumba.loadTime - trumba_preSpudsJS) / 1000));
}

// Process any prolog queue items that were added.

for(var i = 0 ; i < $Trumba.prologQueue.length ; i++)
	$Trumba.prologQueue[i]();
	
$Trumba.prologQueue = [];


} // Include Sentinel
