//external jquery.js
/*
	Structure: eCarList

	The root namespace for eCarList.
*/

/*jslint
	browser: true,
	eqeqeq: true,
	laxbreak: true,
	nomen: false,
	onevar: false,
	plusplus: false,
	regexp: false,
	undef: false,
	white: false
*/
/*global
	eCarList: true,
	window: false 
*/

(function() {

	if (typeof eCarList !== 'object') {
		eCarList = { };
	}

	/*
		Function: use_jQuery

		Attaches given jQuery object to eCarList.jQuery_[version]. The first attached jQuery
		object will be aliased to eCarList.jQuery and eCarList.$, but they can be re-aliased
		by calling with a true override value.

		Parameters:
			jQuery   - jQuery object
			override - [optional] override default eCarList.$ & eCarList.jQuery aliases
	*/
	eCarList.use_jQuery = function(jQuery, override) {
		var version = 'jQuery_' + jQuery.fn.jquery.replace(/\./g, '_');
		eCarList[version] = jQuery;
		if (override || !eCarList.jQuery) {
			eCarList.jQuery = eCarList.$ = jQuery;
		}
	};

	/*
		Function: namespace
		
		Creates the given namespace if it doesn't exist already and invokes the given function with the namespace
		as the first argument and eCarList.jQuery as the second argument. The same namespace object is returned
		by this function.

		Parameters:
			namespace - namespace to grab/create
			fn        - [optional] function to invoke with namespace object

		Returns:
			namespace object
	*/
	eCarList.namespace = function(namespace, fn) {
		eCarList.assert(namespace && typeof namespace === 'string', 
						'invalid namespace: ' + namespace);
		
		var elements = namespace.split('.');
		var parent = window; // global object

		for (var i = 0; i < elements.length; i++) {
			var child = eCarList.$.trim(elements[i]);
			eCarList.assert(child.match(/^\w+$/), 'invalid namespace element: ' + child);

			if (typeof parent[child] !== 'object') {
				parent[child] = { };
			}
			parent = parent[child];
		}

		if (typeof fn === 'function') {
			fn(parent, eCarList.jQuery);
		}
		return parent;
	};

	/*
		Loggers
	*/
	var _noop = function() { };
	eCarList.debug = _noop;
	eCarList.info = _noop;
	eCarList.warn = _noop;
	eCarList.error = _noop;

	/*
		Method: attach_logger

		Aliases eCarlist.log_level to logger.log_level.
		(aliasing logger functions in order to preserve line numbers)
	*/
	eCarList.attach_logger = function(logger) {
		var levels = ['debug', 'info', 'warn', 'error'];
		for (var i = 0; i < levels.length; i++) {
			var level = levels[i];
			eCarList.assert(logger[level], 'invalid logger, missing level: ' + level);
			eCarList[level] = logger[level];
		}
	};

	/*
		Method: assert

		Verifies an expectation. If the expectation is false, then an error message is logged
		and an exception is thrown.
	*/
	eCarList.assert = function(expectation, message) {
		if (!expectation) {
			eCarList.error(message);
			throw message;
		}
	};

	var _init = function() {
		if (window.jQuery) {
			// use currently loaded jQuery by default
			//TODO: this is a weird hack to do deal with the 2 versions of jQuery on the websites
			eCarList.use_jQuery(window.$ec || window.jQuery);
		}

		eCarList.$(document).ready(function() {
			if (window.console && window.console.firebug) {
				// use firebug console as logger if available
				eCarList.attach_logger(window.console);
			}
		});
	};

	_init();
}());
//include eCarList
eCarList.namespace('eCarList', function(eCarList, $) {

	/*
		Structure: eCarList.Module

		Base module implementation. Just provides an easy way to extend module implementations.
	*/
	eCarList.Module = {

		/*
			Method: extend

			Makes a fresh copy of this object with the given modules copied over.

			Parameters:
				modules... - overriding modules
		*/
		extend: function() {
			var extend_args = [true, { }];
			for (var i = 0; i < arguments.length; i++) {
				extend_args.push(arguments[i]);
			}
			return $.extend.apply(null, extend_args);
		}
	};

});
//include eCarList.Module
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	/*
		Structure: eCarList.App.SmartChat.ChatService
	*/
	SmartChat.ChatService = eCarList.Module.extend({
	
		BASE: 'http://dev.ecarlist.com/devel/dohodges/smartchat',
		
		/*
			Method: register_teams

			HACK: the teams should ultimately be dynamically, but this temporary solution allows us
			to manually add teams for a site.
		*/
		register_teams: function() {
			SmartChat.ChatService._teams = [];
			for (var i = 0; i < arguments.length; i++) {
				SmartChat.ChatService._teams.push(arguments[i]);
			}
		},

		create_visitor: function(callback, scope) {
			$.getJSON(SmartChat.ChatService.BASE + '/create_visitor/?jsonp=?', _callback(callback, scope));
		},
	
		configure_page: function(path, callback, scope) {
			//$.getJSON(SmartChat.ChatService.BASE + '/visit/?jsonp=?', _callback(callback, scope));
			// HACK: config.teams & config.delay is static for now
			// eventually we'll retrieve this info from a web service so that delaers can customize their
			// configuration
			var config = {
				teams: SmartChat.ChatService._teams || []
			};

			if (path === '/' || path.match(/^\/web\/home/)) {
				config.offer_delay = 5000;
			} else if (path.match(/^\/web\/about/)) {
				config.offer_delay = 5000;
			} else if (path.match(/^\/web\/contact/)) {
				config.offer_delay = 3000;
			} else if (path.match(/^\/web\/financing/)) {
				config.offer_delay = 5000;
			} else if (path.match(/^\/web\/inventory/)) {
				config.offer_delay = 8000;
			} else if (path.match(/^\/web\/service/)) {
			} else if (path.match(/^\/web\/specials/)) {
				config.offer_delay = 8000;
			} else if (path.match(/^\/web\/\d+\/vehicle/)) {
				config.offer_delay = 5000;
			} else if (path.match(/^\/devel\/dohodges\/smartchat\/visitor\/page/)) {
				config.offer_delay = 3000;
			} else if (path.match(/^\/devel\/dohodges\/smartchat\/visitor/)) {
				config.offer_delay = 1000;
			}

			// throwing this in a setTimeout so it'll feel asynch like the real thing would
			setTimeout(function() {
				_callback(callback, scope)(config);
			}, 0);
		}
	});

	var _callback = function(callback, scope) {
		return function() {
			if (callback) {
				callback.apply(scope || window, arguments);
			}
		};
	};
	
});
/* JSJaC - The JavaScript Jabber Client Library
 * Copyright (C) 2004-2008 Stefan Strigler
 *
 * JSJaC is licensed under the terms of the Mozilla Public License
 * version 1.1 or, at your option, under the terms of the GNU General
 * Public License version 2 or subsequent, or the terms of the GNU Lesser
 * General Public License version 2.1 or subsequent. 
 *
 * Please visit http://zeank.in-berlin.de/jsjac/ for details about JSJaC.
 */

var JSJAC_HAVEKEYS = true;  // whether to use keys
var JSJAC_NKEYS    = 16;    // number of keys to generate
var JSJAC_INACTIVITY = 300; // qnd hack to make suspend/resume work more smoothly with polling
var JSJAC_ERR_COUNT = 10;   // number of retries in case of connection errors

var JSJAC_ALLOW_PLAIN = true; // whether to allow plaintext logins

var JSJAC_CHECKQUEUEINTERVAL = 1; // msecs to poll send queue
var JSJAC_CHECKINQUEUEINTERVAL = 1; // msecs to poll incoming queue

// Options specific to HTTP Binding (BOSH)
var JSJACHBC_BOSH_VERSION  = "1.6";
var JSJACHBC_USE_BOSH_VER  = true;

var JSJACHBC_MAX_HOLD = 1;
var JSJACHBC_MAX_WAIT = 300;

var JSJACHBC_MAXPAUSE = 0; // (default: 120)

/*** END CONFIG ***/

/**
 * @fileoverview Collection of functions to make live easier
 * @author Stefan Strigler
 * @version $Revision: 437 $
 */

/**
 * Convert special chars to HTML entities
 * @addon
 * @return The string with chars encoded for HTML
 * @type String
 */
String.prototype.htmlEnc = function() {
  var str = this.replace(/&/g,"&amp;");
  str = str.replace(/</g,"&lt;");
  str = str.replace(/>/g,"&gt;");
  str = str.replace(/\"/g,"&quot;");
  str = str.replace(/\n/g,"<br />");
  return str;
};

/**
 * Converts from jabber timestamps to JavaScript Date objects
 * @addon
 * @param {String} ts A string representing a jabber datetime timestamp as
 * defined by {@link http://www.xmpp.org/extensions/xep-0082.html XEP-0082}
 * @return A javascript Date object corresponding to the jabber DateTime given
 * @type Date
 */
Date.jab2date = function(ts) {
  var date = new Date(Date.UTC(ts.substr(0,4),ts.substr(5,2)-1,ts.substr(8,2),ts.substr(11,2),ts.substr(14,2),ts.substr(17,2)));
  if (ts.substr(ts.length-6,1) != 'Z') { // there's an offset
    var offset = new Date();
    offset.setTime(0);
    offset.setUTCHours(ts.substr(ts.length-5,2));
    offset.setUTCMinutes(ts.substr(ts.length-2,2));
    if (ts.substr(ts.length-6,1) == '+')
      date.setTime(date.getTime() - offset.getTime());
    else if (ts.substr(ts.length-6,1) == '-')
      date.setTime(date.getTime() + offset.getTime());
  }
  return date;
};

/**
 * Takes a timestamp in the form of 2004-08-13T12:07:04+02:00 as argument
 * and converts it to some sort of humane readable format
 * @addon
 */
Date.hrTime = function(ts) {
  return Date.jab2date(ts).toLocaleString();
};

/**
 * somewhat opposit to {@link #hrTime}
 * expects a javascript Date object as parameter and returns a jabber
 * date string conforming to
 * {@link http://www.xmpp.org/extensions/xep-0082.html XEP-0082}
 * @see #hrTime
 * @return The corresponding jabber DateTime string
 * @type String
 */
Date.prototype.jabberDate = function() {
  var padZero = function(i) {
    if (i < 10) return "0" + i;
    return i;
  };

  var jDate = this.getUTCFullYear() + "-";
  jDate += padZero(this.getUTCMonth()+1) + "-";
  jDate += padZero(this.getUTCDate()) + "T";
  jDate += padZero(this.getUTCHours()) + ":";
  jDate += padZero(this.getUTCMinutes()) + ":";
  jDate += padZero(this.getUTCSeconds()) + "Z";

  return jDate;
};
/* Copyright (c) 1998 - 2007, Paul Johnston & Contributors
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following
 * disclaimer. Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided
 * with the distribution.
 *
 * Neither the name of the author nor the names of its contributors
 * may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

/**
 * @fileoverview Collection of MD5 and SHA1 hashing and encoding
 * methods.
 * @author Stefan Strigler steve@zeank.in-berlin.de
 * @version $Revision: 482 $
 */

/*
 * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
 * in FIPS PUB 180-1
 * Version 2.1a Copyright Paul Johnston 2000 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for details.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = "="; /* base-64 pad character. "=" for strict RFC compliance   */
var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}
function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}
function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}
function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}
function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}

/*
 * Perform a simple self-test to see if the VM is working
 */
function sha1_vm_test()
{
  return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
}

/*
 * Calculate the SHA-1 of an array of big-endian words, and a bit length
 */
function core_sha1(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << (24 - len % 32);
  x[((len + 64 >> 9) << 4) + 15] = len;

  var w = Array(80);
  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;
  var e = -1009589776;

  for(var i = 0; i < x.length; i += 16)
    {
      var olda = a;
      var oldb = b;
      var oldc = c;
      var oldd = d;
      var olde = e;

      for(var j = 0; j < 80; j++)
        {
          if(j < 16) w[j] = x[i + j];
          else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
          var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
                           safe_add(safe_add(e, w[j]), sha1_kt(j)));
          e = d;
          d = c;
          c = rol(b, 30);
          b = a;
          a = t;
        }

      a = safe_add(a, olda);
      b = safe_add(b, oldb);
      c = safe_add(c, oldc);
      d = safe_add(d, oldd);
      e = safe_add(e, olde);
    }
  return Array(a, b, c, d, e);

}

/*
 * Perform the appropriate triplet combination function for the current
 * iteration
 */
function sha1_ft(t, b, c, d)
{
  if(t < 20) return (b & c) | ((~b) & d);
  if(t < 40) return b ^ c ^ d;
  if(t < 60) return (b & c) | (b & d) | (c & d);
  return b ^ c ^ d;
}

/*
 * Determine the appropriate additive constant for the current iteration
 */
function sha1_kt(t)
{
  return (t < 20) ?  1518500249 : (t < 40) ?  1859775393 :
    (t < 60) ? -1894007588 : -899497514;
}

/*
 * Calculate the HMAC-SHA1 of a key and some data
 */
function core_hmac_sha1(key, data)
{
  var bkey = str2binb(key);
  if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz);

  var ipad = Array(16), opad = Array(16);
  for(var i = 0; i < 16; i++)
    {
      ipad[i] = bkey[i] ^ 0x36363636;
      opad[i] = bkey[i] ^ 0x5C5C5C5C;
    }

  var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
  return core_sha1(opad.concat(hash), 512 + 160);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}

/*
 * Convert an 8-bit or 16-bit string to an array of big-endian words
 * In 8-bit function, characters >255 have their hi-byte silently ignored.
 */
function str2binb(str)
{
  var bin = Array();
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < str.length * chrsz; i += chrsz)
    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
  return bin;
}

/*
 * Convert an array of big-endian words to a string
 */
function binb2str(bin)
{
  var str = "";
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < bin.length * 32; i += chrsz)
    str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask);
  return str;
}

/*
 * Convert an array of big-endian words to a hex string.
 */
function binb2hex(binarray)
{
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i++)
    {
      str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
        hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8  )) & 0xF);
    }
  return str;
}

/*
 * Convert an array of big-endian words to a base-64 string
 */
function binb2b64(binarray)
{
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i += 3)
    {
      var triplet = (((binarray[i   >> 2] >> 8 * (3 -  i   %4)) & 0xFF) << 16)
        | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 )
        |  ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);
      for(var j = 0; j < 4; j++)
        {
          if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
          else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
        }
    }
  return str.replace(/AAA\=(\=*?)$/,'$1'); // cleans garbage chars at end of string
}

/*
 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
 * Digest Algorithm, as defined in RFC 1321.
 * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
// var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
// var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
// var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }

/*
 * Perform a simple self-test to see if the VM is working
 */
function md5_vm_test()
{
  return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}

/*
 * Calculate the MD5 of an array of little-endian words, and a bit length
 */
function core_md5(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << ((len) % 32);
  x[(((len + 64) >>> 9) << 4) + 14] = len;

  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;

  for(var i = 0; i < x.length; i += 16)
  {
    var olda = a;
    var oldb = b;
    var oldc = c;
    var oldd = d;

    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

    a = safe_add(a, olda);
    b = safe_add(b, oldb);
    c = safe_add(c, oldc);
    d = safe_add(d, oldd);
  }
  return Array(a, b, c, d);

}

/*
 * These functions implement the four basic operations the algorithm uses.
 */
function md5_cmn(q, a, b, x, s, t)
{
  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}

/*
 * Calculate the HMAC-MD5, of a key and some data
 */
function core_hmac_md5(key, data)
{
  var bkey = str2binl(key);
  if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);

  var ipad = Array(16), opad = Array(16);
  for(var i = 0; i < 16; i++)
  {
    ipad[i] = bkey[i] ^ 0x36363636;
    opad[i] = bkey[i] ^ 0x5C5C5C5C;
  }

  var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
  return core_md5(opad.concat(hash), 512 + 128);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function bit_rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}

/*
 * Convert a string to an array of little-endian words
 * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
 */
function str2binl(str)
{
  var bin = Array();
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < str.length * chrsz; i += chrsz)
    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
  return bin;
}

/*
 * Convert an array of little-endian words to a string
 */
function binl2str(bin)
{
  var str = "";
  var mask = (1 << chrsz) - 1;
  for(var i = 0; i < bin.length * 32; i += chrsz)
    str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
  return str;
}

/*
 * Convert an array of little-endian words to a hex string.
 */
function binl2hex(binarray)
{
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i++)
  {
    str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
           hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
  }
  return str;
}

/*
 * Convert an array of little-endian words to a base-64 string
 */
function binl2b64(binarray)
{
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var str = "";
  for(var i = 0; i < binarray.length * 4; i += 3)
  {
    var triplet = (((binarray[i   >> 2] >> 8 * ( i   %4)) & 0xFF) << 16)
                | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
                |  ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
    for(var j = 0; j < 4; j++)
    {
      if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
    }
  }
  return str;
}

/* #############################################################################
   UTF-8 Decoder and Encoder
   base64 Encoder and Decoder
   written by Tobias Kieslich, justdreams
   Contact: tobias@justdreams.de				http://www.justdreams.de/
   ############################################################################# */

// returns an array of byterepresenting dezimal numbers which represent the
// plaintext in an UTF-8 encoded version. Expects a string.
// This function includes an exception management for those nasty browsers like
// NN401, which returns negative decimal numbers for chars>128. I hate it!!
// This handling is unfortunately limited to the user's charset. Anyway, it works
// in most of the cases! Special signs with an unicode>256 return numbers, which
// can not be converted to the actual unicode and so not to the valid utf-8
// representation. Anyway, this function does always return values which can not
// misinterpretd by RC4 or base64 en- or decoding, because every value is >0 and
// <255!!
// Arrays are faster and easier to handle in b64 encoding or encrypting....
function utf8t2d(t)
{
  t = t.replace(/\r\n/g,"\n");
  var d=new Array; var test=String.fromCharCode(237);
  if (test.charCodeAt(0) < 0)
    for(var n=0; n<t.length; n++)
      {
        var c=t.charCodeAt(n);
        if (c>0)
          d[d.length]= c;
        else {
          d[d.length]= (((256+c)>>6)|192);
          d[d.length]= (((256+c)&63)|128);}
      }
  else
    for(var n=0; n<t.length; n++)
      {
        var c=t.charCodeAt(n);
        // all the signs of asci => 1byte
        if (c<128)
          d[d.length]= c;
        // all the signs between 127 and 2047 => 2byte
        else if((c>127) && (c<2048)) {
          d[d.length]= ((c>>6)|192);
          d[d.length]= ((c&63)|128);}
        // all the signs between 2048 and 66536 => 3byte
        else {
          d[d.length]= ((c>>12)|224);
          d[d.length]= (((c>>6)&63)|128);
          d[d.length]= ((c&63)|128);}
      }
  return d;
}
	
// returns plaintext from an array of bytesrepresenting dezimal numbers, which
// represent an UTF-8 encoded text; browser which does not understand unicode
// like NN401 will show "?"-signs instead
// expects an array of byterepresenting decimals; returns a string
function utf8d2t(d)
{
  var r=new Array; var i=0;
  while(i<d.length)
    {
      if (d[i]<128) {
        r[r.length]= String.fromCharCode(d[i]); i++;}
      else if((d[i]>191) && (d[i]<224)) {
        r[r.length]= String.fromCharCode(((d[i]&31)<<6) | (d[i+1]&63)); i+=2;}
      else {
        r[r.length]= String.fromCharCode(((d[i]&15)<<12) | ((d[i+1]&63)<<6) | (d[i+2]&63)); i+=3;}
    }
  return r.join("");
}

// included in <body onload="b64arrays"> it creates two arrays which makes base64
// en- and decoding faster
// this speed is noticeable especially when coding larger texts (>5k or so)
function b64arrays() {
  var b64s='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
  b64 = new Array();f64 =new Array();
  for (var i=0; i<b64s.length ;i++) {
    b64[i] = b64s.charAt(i);
    f64[b64s.charAt(i)] = i;
  }
}

// creates a base64 encoded text out of an array of byerepresenting dezimals
// it is really base64 :) this makes serversided handling easier
// expects an array; returns a string
function b64d2t(d) {
  var r=new Array; var i=0; var dl=d.length;
  // this is for the padding
  if ((dl%3) == 1) {
    d[d.length] = 0; d[d.length] = 0;}
  if ((dl%3) == 2)
    d[d.length] = 0;
  // from here conversion
  while (i<d.length)
    {
      r[r.length] = b64[d[i]>>2];
      r[r.length] = b64[((d[i]&3)<<4) | (d[i+1]>>4)];
      r[r.length] = b64[((d[i+1]&15)<<2) | (d[i+2]>>6)];
      r[r.length] = b64[d[i+2]&63];
      i+=3;
    }
  // this is again for the padding
  if ((dl%3) == 1)
    r[r.length-1] = r[r.length-2] = "=";
  if ((dl%3) == 2)
    r[r.length-1] = "=";
  // we join the array to return a textstring
  var t=r.join("");
  return t;
}

// returns array of byterepresenting numbers created of an base64 encoded text
// it is still the slowest function in this modul; I hope I can make it faster
// expects string; returns an array
function b64t2d(t) {
  var d=new Array; var i=0;
  // here we fix this CRLF sequenz created by MS-OS; arrrgh!!!
  t=t.replace(/\n|\r/g,""); t=t.replace(/=/g,"");
  while (i<t.length)
    {
      d[d.length] = (f64[t.charAt(i)]<<2) | (f64[t.charAt(i+1)]>>4);
      d[d.length] = (((f64[t.charAt(i+1)]&15)<<4) | (f64[t.charAt(i+2)]>>2));
      d[d.length] = (((f64[t.charAt(i+2)]&3)<<6) | (f64[t.charAt(i+3)]));
      i+=4;
    }
  if (t.length%4 == 2)
    d = d.slice(0, d.length-2);
  if (t.length%4 == 3)
    d = d.slice(0, d.length-1);
  return d;
}

if (typeof(atob) == 'undefined' || typeof(btoa) == 'undefined')
  b64arrays();

if (typeof(atob) == 'undefined') {
  atob = function(s) {
    return utf8d2t(b64t2d(s));
  }
}

if (typeof(btoa) == 'undefined') {
  btoa = function(s) {
    return b64d2t(utf8t2d(s));
  }
}

function cnonce(size) {
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  var cnonce = '';
  for (var i=0; i<size; i++) {
    cnonce += tab.charAt(Math.round(Math.random(new Date().getTime())*(tab.length-1)));
  }
  return cnonce;
}
/* Copyright (c) 2005-2007 Sam Stephenson
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/*
  json.js
  taken from prototype.js, made static
*/
function JSJaCJSON() {}
JSJaCJSON.toString = function (obj) {
  var m = {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '"' : '\\"',
    '\\': '\\\\'
  },
  s = {
    array: function (x) {
      var a = ['['], b, f, i, l = x.length, v;
      for (i = 0; i < l; i += 1) {
        v = x[i];
        f = s[typeof v];
        if (f) {
          v = f(v);
          if (typeof v == 'string') {
            if (b) {
              a[a.length] = ',';
            }
            a[a.length] = v;
            b = true;
          }
        }
      }
      a[a.length] = ']';
      return a.join('');
    },
    'boolean': function (x) {
      return String(x);
    },
    'null': function (x) {
      return "null";
    },
    number: function (x) {
      return isFinite(x) ? String(x) : 'null';
    },
    object: function (x) {
      if (x) {
        if (x instanceof Array) {
          return s.array(x);
        }
        var a = ['{'], b, f, i, v;
        for (i in x) {
          if (x.hasOwnProperty(i)) {
            v = x[i];
            f = s[typeof v];
            if (f) {
              v = f(v);
              if (typeof v == 'string') {
                if (b) {
                  a[a.length] = ',';
                }
                a.push(s.string(i), ':', v);
                b = true;
              }
            }
          }
        }
         
        a[a.length] = '}';
        return a.join('');
      }
      return 'null';
    },
    string: function (x) {
      if (/["\\\x00-\x1f]/.test(x)) {
                    x = x.replace(/([\x00-\x1f\\"])/g, function(a, b) {
          var c = m[b];
          if (c) {
            return c;
          }
          c = b.charCodeAt();
          return '\\u00' +
          Math.floor(c / 16).toString(16) +
          (c % 16).toString(16);
        });
  }
  return '"' + x + '"';
}
  };

switch (typeof(obj)) {
 case 'object':
   return s.object(obj);
 case 'array':
   return s.array(obj);
 case 'string':
   return s.string(obj);
 }
};

JSJaCJSON.parse = function (str) {
  try {
    return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
                                                       str.replace(/"(\\.|[^"\\])*"/g, ''))) &&
            eval('(' + str + ')');
    } catch (e) {
        return false;
    }
};

/* Copyright 2006 Erik Arvidsson
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You
 * may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */

/**
 * @fileoverview Wrapper to make working with XmlHttpRequest and the
 * DOM more convenient (cross browser compliance).
 * this code is taken from
 * http://webfx.eae.net/dhtml/xmlextras/xmlextras.html
 * @author Stefan Strigler steve@zeank.in-berlin.de
 * @version $Revision: 437 $
 */

/**
 * XmlHttp factory
 * @private
 */
function XmlHttp() {}

/**
 * creates a cross browser compliant XmlHttpRequest object
 */
XmlHttp.create = function () {
  try {
    if (window.XMLHttpRequest) {
      var req = new XMLHttpRequest();
     
      // some versions of Moz do not support the readyState property
      // and the onreadystate event so we patch it!
      if (req.readyState == null) {
	req.readyState = 1;
	req.addEventListener("load", function () {
			       req.readyState = 4;
			       if (typeof req.onreadystatechange == "function")
				 req.onreadystatechange();
			     }, false);
      }
     
      return req;
    }
    if (window.ActiveXObject) {
      return new ActiveXObject(XmlHttp.getPrefix() + ".XmlHttp");
    }
  }
  catch (ex) {}
  // fell through
  throw new Error("Your browser does not support XmlHttp objects");
};

/**
 * used to find the Automation server name
 * @private
 */
XmlHttp.getPrefix = function() {
  if (XmlHttp.prefix) // I know what you did last summer
    return XmlHttp.prefix;
 
  var prefixes = ["MSXML2", "Microsoft", "MSXML", "MSXML3"];
  var o;
  for (var i = 0; i < prefixes.length; i++) {
    try {
      // try to create the objects
      o = new ActiveXObject(prefixes[i] + ".XmlHttp");
      return XmlHttp.prefix = prefixes[i];
    }
    catch (ex) {};
  }
 
  throw new Error("Could not find an installed XML parser");
};


/**
 * XmlDocument factory
 * @private
 */
function XmlDocument() {}

XmlDocument.create = function (name,ns) {
  name = name || 'foo';
  ns = ns || '';
  try {
    var doc;
    // DOM2
    if (document.implementation && document.implementation.createDocument) {
      doc = document.implementation.createDocument(ns, name, null);
      // some versions of Moz do not support the readyState property
      // and the onreadystate event so we patch it!
      if (doc.readyState == null) {
	doc.readyState = 1;
	doc.addEventListener("load", function () {
			       doc.readyState = 4;
			       if (typeof doc.onreadystatechange == "function")
				 doc.onreadystatechange();
			     }, false);
      }
    } else if (window.ActiveXObject) {
      doc = new ActiveXObject(XmlDocument.getPrefix() + ".DomDocument");
    }
   
    if (!doc.documentElement || doc.documentElement.tagName != name ||
        (doc.documentElement.namespaceURI &&
         doc.documentElement.namespaceURI != ns)) {
          try {
            if (ns != '')
              doc.appendChild(doc.createElement(name)).
                setAttribute('xmlns',ns);
            else
              doc.appendChild(doc.createElement(name));
          } catch (dex) {
            doc = document.implementation.createDocument(ns,name,null);
           
            if (doc.documentElement == null)
              doc.appendChild(doc.createElement(name));

             // fix buggy opera 8.5x
            if (ns != '' &&
                doc.documentElement.getAttribute('xmlns') != ns) {
              doc.documentElement.setAttribute('xmlns',ns);
            }
          }
        }
   
    return doc;
  }
  catch (ex) { alert(ex.name+": "+ex.message); }
  throw new Error("Your browser does not support XmlDocument objects");
};

/**
 * used to find the Automation server name
 * @private
 */
XmlDocument.getPrefix = function() {
  if (XmlDocument.prefix)
    return XmlDocument.prefix;

  var prefixes = ["MSXML2", "Microsoft", "MSXML", "MSXML3"];
  var o;
  for (var i = 0; i < prefixes.length; i++) {
    try {
      // try to create the objects
      o = new ActiveXObject(prefixes[i] + ".DomDocument");
      return XmlDocument.prefix = prefixes[i];
    }
    catch (ex) {};
  }
 
  throw new Error("Could not find an installed XML parser");
};


// Create the loadXML method
if (typeof(Document) != 'undefined' && window.DOMParser) {

  /**
   * XMLDocument did not extend the Document interface in some
   * versions of Mozilla.
   * @private
   */
  Document.prototype.loadXML = function (s) {
	
    // parse the string to a new doc
    var doc2 = (new DOMParser()).parseFromString(s, "text/xml");
	
    // remove all initial children
    while (this.hasChildNodes())
      this.removeChild(this.lastChild);
		
    // insert and import nodes
    for (var i = 0; i < doc2.childNodes.length; i++) {
      this.appendChild(this.importNode(doc2.childNodes[i], true));
    }
  };
 }

// Create xml getter for Mozilla
if (window.XMLSerializer &&
    window.Node && Node.prototype && Node.prototype.__defineGetter__) {

  /**
   * xml getter
   *
   * This serializes the DOM tree to an XML String
   *
   * Usage: var sXml = oNode.xml
   * @deprecated
   * @private
   */
  // XMLDocument did not extend the Document interface in some versions
  // of Mozilla. Extend both!
  XMLDocument.prototype.__defineGetter__("xml", function () {
                                           return (new XMLSerializer()).serializeToString(this);
                                         });
  /**
   * xml getter
   *
   * This serializes the DOM tree to an XML String
   *
   * Usage: var sXml = oNode.xml
   * @deprecated
   * @private
   */
  Document.prototype.__defineGetter__("xml", function () {
                                        return (new XMLSerializer()).serializeToString(this);
                                      });

  /**
   * xml getter
   *
   * This serializes the DOM tree to an XML String
   *
   * Usage: var sXml = oNode.xml
   * @deprecated
   * @private
   */
  Node.prototype.__defineGetter__("xml", function () {
                                    return (new XMLSerializer()).serializeToString(this);
                                  });
 }
/* Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/**
 * @private
 * This code is taken from {@link
 * http://wiki.script.aculo.us/scriptaculous/show/Builder
 * script.aculo.us' Dom Builder} and has been modified to suit our
 * needs.<br/>
 * The original parts of the code do have the following
 * copyright and license notice:<br/>
 * Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us,
 * http://mir.acu lo.us) <br/>
 * script.aculo.us is freely distributable under the terms of an
 * MIT-style license.<br>
 * For details, see the script.aculo.us web site:
 * http://script.aculo.us/<br>
 */
var JSJaCBuilder = {
  /**
   * @private
   */
  buildNode: function(doc, elementName) {

    var element, ns = arguments[4];

    // attributes (or text)
    if(arguments[2])
      if(JSJaCBuilder._isStringOrNumber(arguments[2]) ||
         (arguments[2] instanceof Array)) {
        element = this._createElement(doc, elementName, ns);
        JSJaCBuilder._children(doc, element, arguments[2]);
      } else {
        ns = arguments[2]['xmlns'] || ns;
        element = this._createElement(doc, elementName, ns);
        for(attr in arguments[2]) {
          if (arguments[2].hasOwnProperty(attr) && attr != 'xmlns')
            element.setAttribute(attr, arguments[2][attr]);
        }
      }
    else
      element = this._createElement(doc, elementName, ns);
    // text, or array of children
    if(arguments[3])
      JSJaCBuilder._children(doc, element, arguments[3], ns);

    return element;
  },

  _createElement: function(doc, elementName, ns) {
    try {
      if (ns)
        return doc.createElementNS(ns, elementName);
    } catch (ex) { }

    var el = doc.createElement(elementName);

    if (ns)
      el.setAttribute("xmlns", ns);

    return el;
  },

  /**
   * @private
   */
  _text: function(doc, text) {
    return doc.createTextNode(text);
  },

  /**
   * @private
   */
  _children: function(doc, element, children, ns) {
    if(typeof children=='object') { // array can hold nodes and text
      for (var i in children) {
        if (children.hasOwnProperty(i)) {
          var e = children[i];
          if (typeof e=='object') {
            if (e instanceof Array) {
              var node = JSJaCBuilder.buildNode(doc, e[0], e[1], e[2], ns);
              element.appendChild(node);
            } else {
              element.appendChild(e);
            }
          } else {
            if(JSJaCBuilder._isStringOrNumber(e)) {
              element.appendChild(JSJaCBuilder._text(doc, e));
            }
          }
        }
      }
    } else {
      if(JSJaCBuilder._isStringOrNumber(children)) {
        element.appendChild(JSJaCBuilder._text(doc, children));
      }
    }
  },

  _attributes: function(attributes) {
    var attrs = [];
    for(attribute in attributes)
      if (attributes.hasOwnProperty(attribute))
        attrs.push(attribute +
          '="' + attributes[attribute].toString().htmlEnc() + '"');
    return attrs.join(" ");
  },

  _isStringOrNumber: function(param) {
    return(typeof param=='string' || typeof param=='number');
  }
};
var NS_DISCO_ITEMS =  "http://jabber.org/protocol/disco#items";
var NS_DISCO_INFO =   "http://jabber.org/protocol/disco#info";
var NS_VCARD =        "vcard-temp";
var NS_AUTH =         "jabber:iq:auth";
var NS_AUTH_ERROR =   "jabber:iq:auth:error";
var NS_REGISTER =     "jabber:iq:register";
var NS_SEARCH =       "jabber:iq:search";
var NS_ROSTER =       "jabber:iq:roster";
var NS_PRIVACY =      "jabber:iq:privacy";
var NS_PRIVATE =      "jabber:iq:private";
var NS_VERSION =      "jabber:iq:version";
var NS_TIME =         "jabber:iq:time";
var NS_LAST =         "jabber:iq:last";
var NS_XDATA =        "jabber:x:data";
var NS_IQDATA =       "jabber:iq:data";
var NS_DELAY =        "jabber:x:delay";
var NS_EXPIRE =       "jabber:x:expire";
var NS_EVENT =        "jabber:x:event";
var NS_XCONFERENCE =  "jabber:x:conference";
var NS_STATS =        "http://jabber.org/protocol/stats";
var NS_MUC =          "http://jabber.org/protocol/muc";
var NS_MUC_USER =     "http://jabber.org/protocol/muc#user";
var NS_MUC_ADMIN =    "http://jabber.org/protocol/muc#admin";
var NS_MUC_OWNER =    "http://jabber.org/protocol/muc#owner";
var NS_PUBSUB =       "http://jabber.org/protocol/pubsub";
var NS_PUBSUB_EVENT = "http://jabber.org/protocol/pubsub#event";
var NS_PUBSUB_OWNER = "http://jabber.org/protocol/pubsub#owner";
var NS_PUBSUB_NMI =   "http://jabber.org/protocol/pubsub#node-meta-info";
var NS_COMMANDS =     "http://jabber.org/protocol/commands";
var NS_STREAM =       "http://etherx.jabber.org/streams";

var NS_STANZAS =      "urn:ietf:params:xml:ns:xmpp-stanzas";
var NS_STREAMS =      "urn:ietf:params:xml:ns:xmpp-streams";

var NS_TLS =          "urn:ietf:params:xml:ns:xmpp-tls";
var NS_SASL =         "urn:ietf:params:xml:ns:xmpp-sasl";
var NS_SESSION =      "urn:ietf:params:xml:ns:xmpp-session";
var NS_BIND =         "urn:ietf:params:xml:ns:xmpp-bind";

var NS_FEATURE_IQAUTH = "http://jabber.org/features/iq-auth";
var NS_FEATURE_IQREGISTER = "http://jabber.org/features/iq-register";
var NS_FEATURE_COMPRESS = "http://jabber.org/features/compress";

var NS_COMPRESS =     "http://jabber.org/protocol/compress";

function STANZA_ERROR(code, type, cond) {
  if (window == this)
    return new STANZA_ERROR(code, type, cond);

  this.code = code;
  this.type = type;
  this.cond = cond;
}

var ERR_BAD_REQUEST =
        STANZA_ERROR("400", "modify", "bad-request");
var ERR_CONFLICT =
        STANZA_ERROR("409", "cancel", "conflict");
var ERR_FEATURE_NOT_IMPLEMENTED =
        STANZA_ERROR("501", "cancel", "feature-not-implemented");
var ERR_FORBIDDEN =
        STANZA_ERROR("403", "auth",   "forbidden");
var ERR_GONE =
        STANZA_ERROR("302", "modify", "gone");
var ERR_INTERNAL_SERVER_ERROR =
        STANZA_ERROR("500", "wait",   "internal-server-error");
var ERR_ITEM_NOT_FOUND =
        STANZA_ERROR("404", "cancel", "item-not-found");
var ERR_JID_MALFORMED =
        STANZA_ERROR("400", "modify", "jid-malformed");
var ERR_NOT_ACCEPTABLE =
        STANZA_ERROR("406", "modify", "not-acceptable");
var ERR_NOT_ALLOWED =
        STANZA_ERROR("405", "cancel", "not-allowed");
var ERR_NOT_AUTHORIZED =
        STANZA_ERROR("401", "auth",   "not-authorized");
var ERR_PAYMENT_REQUIRED =
        STANZA_ERROR("402", "auth",   "payment-required");
var ERR_RECIPIENT_UNAVAILABLE =
        STANZA_ERROR("404", "wait",   "recipient-unavailable");
var ERR_REDIRECT =
        STANZA_ERROR("302", "modify", "redirect");
var ERR_REGISTRATION_REQUIRED =
        STANZA_ERROR("407", "auth",   "registration-required");
var ERR_REMOTE_SERVER_NOT_FOUND =
        STANZA_ERROR("404", "cancel", "remote-server-not-found");
var ERR_REMOTE_SERVER_TIMEOUT =
        STANZA_ERROR("504", "wait",   "remote-server-timeout");
var ERR_RESOURCE_CONSTRAINT =
        STANZA_ERROR("500", "wait",   "resource-constraint");
var ERR_SERVICE_UNAVAILABLE =
        STANZA_ERROR("503", "cancel", "service-unavailable");
var ERR_SUBSCRIPTION_REQUIRED =
        STANZA_ERROR("407", "auth",   "subscription-required");
var ERR_UNEXPECTED_REQUEST =
        STANZA_ERROR("400", "wait",   "unexpected-request");

/**
 * @fileoverview Contains Debugger interface for Firebug and Safari
 * @class Implementation of the Debugger interface for {@link
 * http://www.getfirebug.com/ Firebug} and Safari
 * Creates a new debug logger to be passed to jsjac's connection
 * constructor. Of course you can use it for debugging in your code
 * too.
 * @constructor
 * @param {int} level The maximum level for debugging messages to be
 * displayed. Thus you can tweak the verbosity of the logger. A value
 * of 0 means very low traffic whilst a value of 4 makes logging very
 * verbose about what's going on.
 */
function JSJaCConsoleLogger(level) {
  /**
   * @private
   */
  this.level = level || 4;

  /**
   * Empty function for API compatibility
   */
  this.start = function() {};
  /**
   * Logs a message to firebug's/safari's console
   * @param {String} msg The message to be logged.
   * @param {int} level The message's verbosity level. Importance is
   * from 0 (very important) to 4 (not so important). A value of 1
   * denotes an error in the usual protocol flow.
   */
  this.log = function(msg, level) {
    level = level || 0;
    if (level > this.level)
      return;
    if (typeof(console) == 'undefined')
      return;
    try {
      switch (level) {
      case 0:
        console.warn(msg);
        break;
      case 1:
        console.error(msg);
        break;
      case 2:
        console.info(msg);
        break;
      case 4:
        console.debug(msg);
        break;
      default:
        console.log(msg);
        break;
      }
    } catch(e) { try { console.log(msg) } catch(e) {} }
  };

  /**
   * Sets verbosity level.
   * @param {int} level The maximum level for debugging messages to be
   * displayed. Thus you can tweak the verbosity of the logger. A
   * value of 0 means very low traffic whilst a value of 4 makes
   * logging very verbose about what's going on.
   * @return This debug logger
   * @type ConsoleLogger
   */
  this.setLevel = function(level) { this.level = level; return this; };
  /**
   * Gets verbosity level.
   * @return The level
   * @type int
   */
  this.getLevel = function() { return this.level; };
}
/* Copyright 2003-2006 Peter-Paul Koch
 */

/**
 * @fileoverview OO interface to handle cookies.
 * Taken from {@link http://www.quirksmode.org/js/cookies.html
 * http://www.quirksmode.org/js/cookies.html}
 * Regarding licensing of this code the author states:
 *
 * "You may copy, tweak, rewrite, sell or lease any code example on
 * this site, with one single exception."
 *
 * @author Stefan Strigler
 * @version $Revision: 481 $
 */

/**
 * Creates a new Cookie
 * @class Class representing browser cookies for storing small amounts of data
 * @constructor
 * @param {String} name  The name of the value to store
 * @param {String} value The value to store
 * @param {int}    secs  Number of seconds until cookie expires (may be empty)
 */
function JSJaCCookie(name,value,secs)
{
  if (window == this)
    return new JSJaCCookie(name, value, secs);

  /**
   * This cookie's name
   * @type String
   */
  this.name = name;
  /**
   * This cookie's value
   * @type String
   */
  this.value = value;
  /**
   * Time in seconds when cookie expires (thus being delete by
   * browser). A value of -1 denotes a session cookie which means that
   * stored data gets lost when browser is being closed. 
   * @type int
   */
  this.secs = secs;

  /**
   * Stores this cookie
   */
  this.write = function() {
    if (this.secs) {
      var date = new Date();
      date.setTime(date.getTime()+(this.secs*1000));
      var expires = "; expires="+date.toGMTString();
    } else
      var expires = "";
    document.cookie = this.getName()+"="+this.getValue()+expires+"; path=/";
  };
  /**
   * Deletes this cookie
   */
  this.erase = function() {
    var c = new JSJaCCookie(this.getName(),"",-1);
    c.write();
  };

  /**
   * Gets the name of this cookie
   * @return The name
   * @type String
   */
  this.getName = function() {
    return this.name;
  };
 
  /**
   * Sets the name of this cookie
   * @param {String} name The name for this cookie
   * @return This cookie
   * @type Cookie
   */
  this.setName = function(name) {
    this.name = name;
    return this;
  };

  /**
   * Gets the value of this cookie
   * @return The value
   * @type String
   */
  this.getValue = function() {
    return this.value;
  };
 
  /**
   * Sets the value of this cookie
   * @param {String} value The value for this cookie
   * @return This cookie
   * @type Cookie
   */
  this.setValue = function(value) {
    this.value = value;
    return this;
  };
}

/**
 * Reads the value for given <code>name</code> from cookies and return new
 * <code>Cookie</code> object
 * @param {String} name The name of the cookie to read
 * @return A cookie object of the given name
 * @type Cookie
 * @throws CookieException when cookie with given name could not be found
 */
JSJaCCookie.read = function(name) {
  var nameEQ = name + "=";
  var ca = document.cookie.split(';');
  for(var i=0;i < ca.length;i++) {
    var c = ca[i];
    while (c.charAt(0)==' ') c = c.substring(1,c.length);
    if (c.indexOf(nameEQ) == 0) return new JSJaCCookie(name, c.substring(nameEQ.length,c.length));
  }
  throw new JSJaCCookieException("Cookie not found");
};

/**
 * Reads the value for given <code>name</code> from cookies and returns
 * its valued new
 * @param {String} name The name of the cookie to read
 * @return The value of the cookie read
 * @type String
 * @throws CookieException when cookie with given name could not be found
 */
JSJaCCookie.get = function(name) {
  return JSJaCCookie.read(name).getValue();
};

/**
 * Deletes cookie with given <code>name</code>
 * @param {String} name The name of the cookie to delete
 * @throws CookieException when cookie with given name could not be found
 */
JSJaCCookie.remove = function(name) {
  JSJaCCookie.read(name).erase();
};

/**
 * Some exception denoted to dealing with cookies
 * @constructor
 * @param {String} msg The message to pass to the exception
 */
function JSJaCCookieException(msg) {
  this.message = msg;
  this.name = "CookieException";
}

/**
 * an error packet for internal use
 * @private
 * @constructor
 */
function JSJaCError(code,type,condition) {
  var xmldoc = XmlDocument.create("error","jsjac");

  xmldoc.documentElement.setAttribute('code',code);
  xmldoc.documentElement.setAttribute('type',type);
  xmldoc.documentElement.appendChild(xmldoc.createElement(condition)).
    setAttribute('xmlns','urn:ietf:params:xml:ns:xmpp-stanzas');
  return xmldoc.documentElement;
}
/**
 * @fileoverview This file contains all things that make life easier when
 * dealing with JIDs
 * @author Stefan Strigler
 * @version $Revision: 437 $
 */

/**
 * list of forbidden chars for nodenames
 * @private
 */
var JSJACJID_FORBIDDEN = ['"',' ','&','\'','/',':','<','>','@'];

/**
 * Creates a new JSJaCJID object
 * @class JSJaCJID models xmpp jid objects
 * @constructor
 * @param {Object} jid jid may be either of type String or a JID represented
 * by JSON with fields 'node', 'domain' and 'resource'
 * @throws JSJaCJIDInvalidException Thrown if jid is not valid
 * @return a new JSJaCJID object
 */
function JSJaCJID(jid) {
  /**
   *@private
   */
  this._node = '';
  /**
   *@private
   */
  this._domain = '';
  /**
   *@private
   */
  this._resource = '';

  if (typeof(jid) == 'string') {
    if (jid.indexOf('@') != -1) {
        this.setNode(jid.substring(0,jid.indexOf('@')));
        jid = jid.substring(jid.indexOf('@')+1);
    }
    if (jid.indexOf('/') != -1) {
      this.setResource(jid.substring(jid.indexOf('/')+1));
      jid = jid.substring(0,jid.indexOf('/'));
    }
    this.setDomain(jid);
  } else {
    this.setNode(jid.node);
    this.setDomain(jid.domain);
    this.setResource(jid.resource);
  }
}


/**
 * Gets the node part of the jid
 * @return A string representing the node name
 * @type String
 */
JSJaCJID.prototype.getNode = function() { return this._node; };

/**
 * Gets the domain part of the jid
 * @return A string representing the domain name
 * @type String
 */
JSJaCJID.prototype.getDomain = function() { return this._domain; };

/**
 * Gets the resource part of the jid
 * @return A string representing the resource
 * @type String
 */
JSJaCJID.prototype.getResource = function() { return this._resource; };


/**
 * Sets the node part of the jid
 * @param {String} node Name of the node
 * @throws JSJaCJIDInvalidException Thrown if node name contains invalid chars
 * @return This object
 * @type JSJaCJID
 */
JSJaCJID.prototype.setNode = function(node) {
  JSJaCJID._checkNodeName(node);
  this._node = node || '';
  return this;
};

/**
 * Sets the domain part of the jid
 * @param {String} domain Name of the domain
 * @throws JSJaCJIDInvalidException Thrown if domain name contains invalid
 * chars or is empty
 * @return This object
 * @type JSJaCJID
 */
JSJaCJID.prototype.setDomain = function(domain) {
  if (!domain || domain == '')
    throw new JSJaCJIDInvalidException("domain name missing");
  // chars forbidden for a node are not allowed in domain names
  // anyway, so let's check
  JSJaCJID._checkNodeName(domain);
  this._domain = domain;
  return this;
};

/**
 * Sets the resource part of the jid
 * @param {String} resource Name of the resource
 * @return This object
 * @type JSJaCJID
 */
JSJaCJID.prototype.setResource = function(resource) {
  this._resource = resource || '';
  return this;
};

/**
 * The string representation of the full jid
 * @return A string representing the jid
 * @type String
 */
JSJaCJID.prototype.toString = function() {
  var jid = '';
  if (this.getNode() && this.getNode() != '')
    jid = this.getNode() + '@';
  jid += this.getDomain(); // we always have a domain
  if (this.getResource() && this.getResource() != "")
    jid += '/' + this.getResource();
  return jid;
};

/**
 * Removes the resource part of the jid
 * @return This object
 * @type JSJaCJID
 */
JSJaCJID.prototype.removeResource = function() {
  return this.setResource();
};

/**
 * creates a copy of this JSJaCJID object
 * @return A copy of this
 * @type JSJaCJID
 */
JSJaCJID.prototype.clone = function() {
  return new JSJaCJID(this.toString());
};

/**
 * Compares two jids if they belong to the same entity (i.e. w/o resource)
 * @param {String} jid a jid as string or JSJaCJID object
 * @return 'true' if jid is same entity as this
 * @type Boolean
 */
JSJaCJID.prototype.isEntity = function(jid) {
  if (typeof jid == 'string')
	  jid = (new JSJaCJID(jid));
  jid.removeResource();
  return (this.clone().removeResource().toString() === jid.toString());
};

/**
 * Check if node name is valid
 * @private
 * @param {String} node A name for a node
 * @throws JSJaCJIDInvalidException Thrown if name for node is not allowed
 */
JSJaCJID._checkNodeName = function(nodeprep) {
    if (!nodeprep || nodeprep == '')
      return;
    for (var i=0; i< JSJACJID_FORBIDDEN.length; i++) {
      if (nodeprep.indexOf(JSJACJID_FORBIDDEN[i]) != -1) {
        throw new JSJaCJIDInvalidException("forbidden char in nodename: "+JSJACJID_FORBIDDEN[i]);
      }
    }
};

/**
 * Creates a new Exception of type JSJaCJIDInvalidException
 * @class Exception to indicate invalid values for a jid
 * @constructor
 * @param {String} message The message associated with this Exception
 */
function JSJaCJIDInvalidException(message) {
  /**
   * The exceptions associated message
   * @type String
   */
  this.message = message;
  /**
   * The name of the exception
   * @type String
   */
  this.name = "JSJaCJIDInvalidException";
}

/**
 * Creates a new set of hash keys
 * @class Reflects a set of sha1/md5 hash keys for securing sessions
 * @constructor
 * @param {Function} func The hash function to be used for creating the keys
 * @param {Debugger} oDbg Reference to debugger implementation [optional]
 */									 
function JSJaCKeys(func,oDbg) {
  var seed = Math.random();

  /**
   * @private
   */
  this._k = new Array();
  this._k[0] = seed.toString();
  if (oDbg)
    /**
     * Reference to Debugger
     * @type Debugger
     */
    this.oDbg = oDbg;
  else {
    this.oDbg = {};
    this.oDbg.log = function() {};
  }

  if (func) {
    for (var i=1; i<JSJAC_NKEYS; i++) {
      this._k[i] = func(this._k[i-1]);
      oDbg.log(i+": "+this._k[i],4);
    }
  }

  /**
   * @private
   */
  this._indexAt = JSJAC_NKEYS-1;
  /**
   * Gets next key from stack
   * @return New hash key
   * @type String
   */
  this.getKey = function() {
    return this._k[this._indexAt--];
  };
  /**
   * Indicates whether there's only one key left
   * @return <code>true</code> if there's only one key left, false otherwise
   * @type boolean
   */
  this.lastKey = function() { return (this._indexAt == 0); };
  /**
   * Returns number of overall/initial stack size
   * @return Number of keys created
   * @type int
   */
  this.size = function() { return this._k.length; };

  /**
   * @private
   */
  this._getSuspendVars = function() {
    return ('_k,_indexAt').split(',');
  }
}
/**
 * @fileoverview Contains all Jabber/XMPP packet related classes.
 * @author Stefan Strigler steve@zeank.in-berlin.de
 * @version $Revision: 480 $
 */

var JSJACPACKET_USE_XMLNS = true;

/**
 * Creates a new packet with given root tag name (for internal use)
 * @class Somewhat abstract base class for all kinds of specialised packets
 * @param {String} name The root tag name of the packet
 * (i.e. one of 'message', 'iq' or 'presence')
 */
function JSJaCPacket(name) {
  /**
   * @private
   */
  this.name = name;

  if (typeof(JSJACPACKET_USE_XMLNS) != 'undefined' && JSJACPACKET_USE_XMLNS)
    /**
     * @private
     */
    this.doc = XmlDocument.create(name,'jabber:client');
  else
    /**
     * @private
     */
    this.doc = XmlDocument.create(name,'');
}

/**
 * Gets the type (name of root element) of this packet, i.e. one of
 * 'presence', 'message' or 'iq'
 * @return the top level tag name
 * @type String
 */
JSJaCPacket.prototype.pType = function() { return this.name; };

/**
 * Gets the associated Document for this packet.
 * @type {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#i-Document Document}
 */
JSJaCPacket.prototype.getDoc = function() {
  return this.doc;
};
/**
 * Gets the root node of this packet
 * @type {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node}
 */
JSJaCPacket.prototype.getNode = function() {
  if (this.getDoc() && this.getDoc().documentElement)
    return this.getDoc().documentElement;
  else
    return null;
};

/**
 * Sets the 'to' attribute of the root node of this packet
 * @param {String} to
 * @type JSJaCPacket
 */
JSJaCPacket.prototype.setTo = function(to) {
  if (!to || to == '')
    this.getNode().removeAttribute('to');
  else if (typeof(to) == 'string')
    this.getNode().setAttribute('to',to);
  else
    this.getNode().setAttribute('to',to.toString());
  return this;
};
/**
 * Sets the 'from' attribute of the root node of this
 * packet. Usually this is not needed as the server will take care
 * of this automatically.
 * @type JSJaCPacket
 */
JSJaCPacket.prototype.setFrom = function(from) {
  if (!from || from == '')
    this.getNode().removeAttribute('from');
  else if (typeof(from) == 'string')
    this.getNode().setAttribute('from',from);
  else
    this.getNode().setAttribute('from',from.toString());
  return this;
};
/**
 * Sets 'id' attribute of the root node of this packet.
 * @param {String} id The id of the packet.
 * @type JSJaCPacket
 */
JSJaCPacket.prototype.setID = function(id) {
  if (!id || id == '')
    this.getNode().removeAttribute('id');
  else
    this.getNode().setAttribute('id',id);
  return this;
};
/**
 * Sets the 'type' attribute of the root node of this packet.
 * @param {String} type The type of the packet.
 * @type JSJaCPacket
 */
JSJaCPacket.prototype.setType = function(type) {
  if (!type || type == '')
    this.getNode().removeAttribute('type');
  else
    this.getNode().setAttribute('type',type);
  return this;
};
/**
 * Sets 'xml:lang' for this packet
 * @param {String} xmllang The xml:lang of the packet.
 * @type JSJaCPacket
 */
JSJaCPacket.prototype.setXMLLang = function(xmllang) {
  if (!xmllang || xmllang == '')
    this.getNode().removeAttribute('xml:lang');
  else
    this.getNode().setAttribute('xml:lang',xmllang);
  return this;
};

/**
 * Gets the 'to' attribute of this packet
 * @type String
 */
JSJaCPacket.prototype.getTo = function() {
  return this.getNode().getAttribute('to');
};
/**
 * Gets the 'from' attribute of this packet.
 * @type String
 */
JSJaCPacket.prototype.getFrom = function() {
  return this.getNode().getAttribute('from');
};
/**
 * Gets the 'to' attribute of this packet as a JSJaCJID object
 * @type JSJaCJID
 */
JSJaCPacket.prototype.getToJID = function() {
  return new JSJaCJID(this.getTo());
};
/**
 * Gets the 'from' attribute of this packet as a JSJaCJID object
 * @type JSJaCJID
 */
JSJaCPacket.prototype.getFromJID = function() {
  return new JSJaCJID(this.getFrom());
};
/**
 * Gets the 'id' of this packet
 * @type String
 */
JSJaCPacket.prototype.getID = function() {
  return this.getNode().getAttribute('id');
};
/**
 * Gets the 'type' of this packet
 * @type String
 */
JSJaCPacket.prototype.getType = function() {
  return this.getNode().getAttribute('type');
};
/**
 * Gets the 'xml:lang' of this packet
 * @type String
 */
JSJaCPacket.prototype.getXMLLang = function() {
  return this.getNode().getAttribute('xml:lang');
};
/**
 * Gets the 'xmlns' (xml namespace) of the root node of this packet
 * @type String
 */
JSJaCPacket.prototype.getXMLNS = function() {
  return this.getNode().namespaceURI;
};

/**
 * Gets a child element of this packet. If no params given returns first child.
 * @param {String} name Tagname of child to retrieve. Use '*' to match any tag. [optional]
 * @param {String} ns   Namespace of child. Use '*' to match any ns.[optional]
 * @return The child node, null if none found
 * @type {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node}
 */
JSJaCPacket.prototype.getChild = function(name, ns) {
  if (!this.getNode()) {
    return null;
  }
 
  name = name || '*';
  ns = ns || '*';

  if (this.getNode().getElementsByTagNameNS) {
    return this.getNode().getElementsByTagNameNS(ns, name).item(0);
  }

  // fallback
  var nodes = this.getNode().getElementsByTagName(name);
  if (ns != '*') {
    for (var i=0; i<nodes.length; i++) {
      if (nodes.item(i).namespaceURI == ns) {
        return nodes.item(i);
      }
    }
  } else {
    return nodes.item(0);
  }
  return null; // nothing found
}

/**
 * Gets the node value of a child element of this packet.
 * @param {String} name Tagname of child to retrieve.
 * @param {String} ns   Namespace of child
 * @return The value of the child node, empty string if none found
 * @type String
 */
JSJaCPacket.prototype.getChildVal = function(name, ns) {
  var node = this.getChild(name, ns);
  var ret = '';
  if (node && node.hasChildNodes()) {
    // concatenate all values from childNodes
    for (var i=0; i<node.childNodes.length; i++)
      if (node.childNodes.item(i).nodeValue)
        ret += node.childNodes.item(i).nodeValue;
  }
  return ret;
};

/**
 * Returns a copy of this node
 * @return a copy of this node
 * @type JSJaCPacket
 */
JSJaCPacket.prototype.clone = function() {
  return JSJaCPacket.wrapNode(this.getNode());
};

/**
 * Checks if packet is of type 'error'
 * @return 'true' if this packet is of type 'error', 'false' otherwise
 * @type boolean
 */
JSJaCPacket.prototype.isError = function() {
  return (this.getType() == 'error');
};

/**
 * Returns an error condition reply according to {@link http://www.xmpp.org/extensions/xep-0086.html XEP-0086}. Creates a clone of the calling packet with senders and recipient exchanged and error stanza appended.
 * @param {STANZA_ERROR} stanza_error an error stanza containing error cody, type and condition of the error to be indicated
 * @return an error reply packet
 * @type JSJaCPacket
 */
JSJaCPacket.prototype.errorReply = function(stanza_error) {
  var rPacket = this.clone();
  rPacket.setTo(this.getFrom());
  rPacket.setFrom();
  rPacket.setType('error');

  rPacket.appendNode('error',
                     {code: stanza_error.code, type: stanza_error.type},
                     [[stanza_error.cond]]);

  return rPacket;
};

/**
 * Returns a string representation of the raw xml content of this packet.
 * @type String
 */
JSJaCPacket.prototype.xml = typeof XMLSerializer != 'undefined' ?
function() {
  var r = (new XMLSerializer()).serializeToString(this.getNode());
  if (typeof(r) == 'undefined')
    r = (new XMLSerializer()).serializeToString(this.doc); // oldschool
  return r
} :
function() {// IE
  return this.getDoc().xml
};


// PRIVATE METHODS DOWN HERE

/**
 * Gets an attribute of the root element
 * @private
 */
JSJaCPacket.prototype._getAttribute = function(attr) {
  return this.getNode().getAttribute(attr);
};

/**
 * Replaces this node with given node
 * @private
 */
JSJaCPacket.prototype._replaceNode = function(aNode) {
  // copy attribs
  for (var i=0; i<aNode.attributes.length; i++)
    if (aNode.attributes.item(i).nodeName != 'xmlns')
      this.getNode().setAttribute(aNode.attributes.item(i).nodeName,
                                  aNode.attributes.item(i).nodeValue);

  // copy children
  for (var i=0; i<aNode.childNodes.length; i++)
    if (this.getDoc().importNode)
      this.getNode().appendChild(this.getDoc().importNode(aNode.
                                                          childNodes.item(i),
                                                          true));
    else
      this.getNode().appendChild(aNode.childNodes.item(i).cloneNode(true));
};
 
/**
 * Set node value of a child node
 * @private
 */
JSJaCPacket.prototype._setChildNode = function(nodeName, nodeValue) {
  var aNode = this.getChild(nodeName);
  var tNode = this.getDoc().createTextNode(nodeValue);
  if (aNode)
    try {
      aNode.replaceChild(tNode,aNode.firstChild);
    } catch (e) { }
  else {
    try {
      aNode = this.getDoc().createElementNS(this.getNode().namespaceURI,
                                            nodeName);
    } catch (ex) {
      aNode = this.getDoc().createElement(nodeName)
    }
    this.getNode().appendChild(aNode);
    aNode.appendChild(tNode);
  }
  return aNode;
};

/**
 * Builds a node using {@link
 * http://wiki.script.aculo.us/scriptaculous/show/Builder
 * script.aculo.us' Dom Builder} notation.
 * This code is taken from {@link
 * http://wiki.script.aculo.us/scriptaculous/show/Builder
 * script.aculo.us' Dom Builder} and has been modified to suit our
 * needs.<br/>
 * The original parts of the code do have the following copyright
 * and license notice:<br/>
 * Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us,
 * http://mir.acu lo.us) <br/>
 * script.aculo.us is freely distributable under the terms of an
 * MIT-style licen se.  // For details, see the script.aculo.us web
 * site: http://script.aculo.us/<br>
 * @author Thomas Fuchs
 * @author Stefan Strigler
 * @return The newly created node
 * @type {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node}
 */
JSJaCPacket.prototype.buildNode = function(elementName) {
  return JSJaCBuilder.buildNode(this.getDoc(),
                                elementName,
                                arguments[1],
                                arguments[2]);
};

/**
 * Appends node created by buildNode to this packets parent node.
 * @param {@link http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node} element The node to append or
 * @param {String} element A name plus an object hash with attributes (optional) plus an array of childnodes (optional)
 * @see #buildNode
 * @return This packet
 * @type JSJaCPacket
 */
JSJaCPacket.prototype.appendNode = function(element) {
  if (typeof element=='object') { // seems to be a prebuilt node
    return this.getNode().appendChild(element)
  } else { // build node
    return this.getNode().appendChild(this.buildNode(element,
                                                     arguments[1],
                                                     arguments[2],
                                                     null,
                                                     this.getNode().namespaceURI));
  }
};


/**
 * A jabber/XMPP presence packet
 * @class Models the XMPP notion of a 'presence' packet
 * @extends JSJaCPacket
 */
function JSJaCPresence() {
  /**
   * @ignore
   */
  this.base = JSJaCPacket;
  this.base('presence');
}
JSJaCPresence.prototype = new JSJaCPacket;

/**
 * Sets the status message for current status. Usually this is set
 * to some human readable string indicating what the user is
 * doing/feel like currently.
 * @param {String} status A status message
 * @return this
 * @type JSJaCPacket
 */
JSJaCPresence.prototype.setStatus = function(status) {
  this._setChildNode("status", status);
  return this;
};
/**
 * Sets the online status for this presence packet.
 * @param {String} show An XMPP complient status indicator. Must
 * be one of 'chat', 'away', 'xa', 'dnd'
 * @return this
 * @type JSJaCPacket
 */
JSJaCPresence.prototype.setShow = function(show) {
  if (show == 'chat' || show == 'away' || show == 'xa' || show == 'dnd')
    this._setChildNode("show",show);
  return this;
};
/**
 * Sets the priority of the resource bind to with this connection
 * @param {int} prio The priority to set this resource to
 * @return this
 * @type JSJaCPacket
 */
JSJaCPresence.prototype.setPriority = function(prio) {
  this._setChildNode("priority", prio);
  return this;
};
/**
 * Some combined method that allowes for setting show, status and
 * priority at once
 * @param {String} show A status message
 * @param {String} status A status indicator as defined by XMPP
 * @param {int} prio A priority for this resource
 * @return this
 * @type JSJaCPacket
 */
JSJaCPresence.prototype.setPresence = function(show,status,prio) {
  if (show)
    this.setShow(show);
  if (status)
    this.setStatus(status);
  if (prio)
    this.setPriority(prio);
  return this;
};

/**
 * Gets the status message of this presence
 * @return The (human readable) status message
 * @type String
 */
JSJaCPresence.prototype.getStatus = function() {
  return this.getChildVal('status');
};
/**
 * Gets the status of this presence.
 * Either one of 'chat', 'away', 'xa' or 'dnd' or null.
 * @return The status indicator as defined by XMPP
 * @type String
 */
JSJaCPresence.prototype.getShow = function() {
  return this.getChildVal('show');
};
/**
 * Gets the priority of this status message
 * @return A resource priority
 * @type int
 */
JSJaCPresence.prototype.getPriority = function() {
  return this.getChildVal('priority');
};


/**
 * A jabber/XMPP iq packet
 * @class Models the XMPP notion of an 'iq' packet
 * @extends JSJaCPacket
 */
function JSJaCIQ() {
  /**
   * @ignore
   */
  this.base = JSJaCPacket;
  this.base('iq');
}
JSJaCIQ.prototype = new JSJaCPacket;

/**
 * Some combined method to set 'to', 'type' and 'id' at once
 * @param {String} to the recepients JID
 * @param {String} type A XMPP compliant iq type (one of 'set', 'get', 'result' and 'error'
 * @param {String} id A packet ID
 * @return this
 * @type JSJaCIQ
 */
JSJaCIQ.prototype.setIQ = function(to,type,id) {
  if (to)
    this.setTo(to);
  if (type)
    this.setType(type);
  if (id)
    this.setID(id);
  return this;
};
/**
 * Creates a 'query' child node with given XMLNS
 * @param {String} xmlns The namespace for the 'query' node
 * @return The query node
 * @type {@link  http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node}
 */
JSJaCIQ.prototype.setQuery = function(xmlns) {
  var query;
  try {
    query = this.getDoc().createElementNS(xmlns,'query');
  } catch (e) {
    // fallback
    query = this.getDoc().createElement('query');
  }
  if (query && query.getAttribute('xmlns') != xmlns) // fix opera 8.5x
    query.setAttribute('xmlns',xmlns);
  this.getNode().appendChild(query);
  return query;
};

/**
 * Gets the 'query' node of this packet
 * @return The query node
 * @type {@link  http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 Node}
 */
JSJaCIQ.prototype.getQuery = function() {
  return this.getNode().getElementsByTagName('query').item(0);
};
/**
 * Gets the XMLNS of the query node contained within this packet
 * @return The namespace of the query node
 * @type String
 */
JSJaCIQ.prototype.getQueryXMLNS = function() {
  if (this.getQuery())
    return this.getQuery().namespaceURI;
  else
    return null;
};

/**
 * Creates an IQ reply with type set to 'result'. If given appends payload to first child if IQ. Payload maybe XML as string or a DOM element (or an array of such elements as well).
 * @param {Element} payload A payload to be appended [optional]
 * @return An IQ reply packet
 * @type JSJaCIQ
 */
JSJaCIQ.prototype.reply = function(payload) {
  var rIQ = this.clone();
  rIQ.setTo(this.getFrom());
  rIQ.setType('result');
  if (payload) {
    if (typeof payload == 'string')
      rIQ.getChild().appendChild(rIQ.getDoc().loadXML(payload));
    else if (payload.constructor == Array) {
      var node = rIQ.getChild();
      for (var i=0; i<payload.length; i++)
        if(typeof payload[i] == 'string')
          node.appendChild(rIQ.getDoc().loadXML(payload[i]));
        else if (typeof payload[i] == 'object')
          node.appendChild(payload[i]);
    }
    else if (typeof payload == 'object')
      rIQ.getChild().appendChild(payload);
  }
  return rIQ;
};

/**
 * A jabber/XMPP message packet
 * @class Models the XMPP notion of an 'message' packet
 * @extends JSJaCPacket
 */
function JSJaCMessage() {
  /**
   * @ignore
   */
  this.base = JSJaCPacket;
  this.base('message');
}
JSJaCMessage.prototype = new JSJaCPacket;

/**
 * Sets the body of the message
 * @param {String} body Your message to be sent along
 * @return this message
 * @type JSJaCMessage
 */
JSJaCMessage.prototype.setBody = function(body) {
  this._setChildNode("body",body);
  return this;
};
/**
 * Sets the subject of the message
 * @param {String} subject Your subject to be sent along
 * @return this message
 * @type JSJaCMessage
 */
JSJaCMessage.prototype.setSubject = function(subject) {
  this._setChildNode("subject",subject);
  return this;
};
/**
 * Sets the 'tread' attribute for this message. This is used to identify
 * threads in chat conversations
 * @param {String} thread Usually a somewhat random hash.
 * @return this message
 * @type JSJaCMessage
 */
JSJaCMessage.prototype.setThread = function(thread) {
  this._setChildNode("thread", thread);
  return this;
};
/**
 * Gets the 'thread' identifier for this message
 * @return A thread identifier
 * @type String
 */
JSJaCMessage.prototype.getThread = function() {
  return this.getChildVal('thread');
};
/**
 * Gets the body of this message
 * @return The body of this message
 * @type String
 */
JSJaCMessage.prototype.getBody = function() {
  return this.getChildVal('body');
};
/**
 * Gets the subject of this message
 * @return The subject of this message
 * @type String
 */
JSJaCMessage.prototype.getSubject = function() {
  return this.getChildVal('subject')
};


/**
 * Tries to transform a w3c DOM node to JSJaC's internal representation
 * (JSJaCPacket type, one of JSJaCPresence, JSJaCMessage, JSJaCIQ)
 * @param: {Node
 * http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247}
 * node The node to be transformed
 * @return A JSJaCPacket representing the given node. If node's root
 * elemenent is not one of 'message', 'presence' or 'iq',
 * <code>null</code> is being returned.
 * @type JSJaCPacket
 */
JSJaCPacket.wrapNode = function(node) {
  var aNode;
  switch (node.nodeName ? node.nodeName.toLowerCase() : '') {
  case 'presence':
    aNode = new JSJaCPresence();
    break;
  case 'message':
    aNode = new JSJaCMessage();
    break;
  case 'iq':
    aNode = new JSJaCIQ();
    break;
  default : // unknown
    return null;
  }

  aNode._replaceNode(node);

  return aNode;
};

/**
 * @fileoverview Contains all things in common for all subtypes of connections
 * supported.
 * @author Stefan Strigler steve@zeank.in-berlin.de
 * @version $Revision: 476 $
 */

/**
 * Creates a new Jabber connection (a connection to a jabber server)
 * @class Somewhat abstract base class for jabber connections. Contains all
 * of the code in common for all jabber connections
 * @constructor
 * @param {JSON http://www.json.org/index} oArg JSON with properties: <br>
 * * <code>httpbase</code> the http base address of the service to be used for
 * connecting to jabber<br>
 * * <code>oDbg</code> (optional) a reference to a debugger interface
 */
function JSJaCConnection(oArg) {

  if (oArg && oArg.oDbg && oArg.oDbg.log)
    /**
     * Reference to debugger interface
     *(needs to implement method <code>log</code>)
     * @type Debugger
     */
    this.oDbg = oArg.oDbg;
  else {
    this.oDbg = new Object(); // always initialise a debugger
    this.oDbg.log = function() { };
  }

  if (oArg && oArg.httpbase)
    /**
     * @private
     */
    this._httpbase = oArg.httpbase;
 
  if (oArg && oArg.allow_plain)
    /**
     * @private
     */
    this.allow_plain = oArg.allow_plain;
  else
    this.allow_plain = JSJAC_ALLOW_PLAIN;

  /**
   * @private
   */
  this._connected = false;
  /**
   * @private
   */
  this._events = new Array();
  /**
   * @private
   */
  this._keys = null;
  /**
   * @private
   */
  this._ID = 0;
  /**
   * @private
   */
  this._inQ = new Array();
  /**
   * @private
   */
  this._pQueue = new Array();
  /**
   * @private
   */
  this._regIDs = new Array();
  /**
   * @private
   */
  this._req = new Array();
  /**
   * @private
   */
  this._status = 'intialized';
  /**
   * @private
   */
  this._errcnt = 0;
  /**
   * @private
   */
  this._inactivity = JSJAC_INACTIVITY;
  /**
   * @private
   */
  this._sendRawCallbacks = new Array();

  if (oArg && oArg.timerval)
    this.setPollInterval(oArg.timerval);
}

JSJaCConnection.prototype.connect = function(oArg) {
  this._setStatus('connecting');

  this.domain = oArg.domain || 'localhost';
  this.username = oArg.username;
  this.resource = oArg.resource;
  this.pass = oArg.pass;
  this.register = oArg.register;

  this.authhost = oArg.authhost || this.domain;
  this.authtype = oArg.authtype || 'sasl';

  if (oArg.xmllang && oArg.xmllang != '')
    this._xmllang = oArg.xmllang;

  this.host = oArg.host || this.domain;
  this.port = oArg.port || 5222;
  if (oArg.secure)
    this.secure = 'true';
  else
    this.secure = 'false';

  if (oArg.wait)
    this._wait = oArg.wait;

  this.jid = this.username + '@' + this.domain;
  this.fulljid = this.jid + '/' + this.resource;

  this._rid  = Math.round( 100000.5 + ( ( (900000.49999) - (100000.5) ) * Math.random() ) );

  // setupRequest must be done after rid is created but before first use in reqstr
  var slot = this._getFreeSlot();
  this._req[slot] = this._setupRequest(true);

  var reqstr = this._getInitialRequestString();

  this.oDbg.log(reqstr,4);

  this._req[slot].r.onreadystatechange = 
  JSJaC.bind(function() {
               if (this._req[slot].r.readyState == 4) {
                 this.oDbg.log("async recv: "+this._req[slot].r.responseText,4);
                 this._handleInitialResponse(slot); // handle response
               }
             }, this);
  
  if (typeof(this._req[slot].r.onerror) != 'undefined') {
    this._req[slot].r.onerror = 
      JSJaC.bind(function(e) {
                   this.oDbg.log('XmlHttpRequest error',1);
                   return false;
                 }, this);
  }

  this._req[slot].r.send(reqstr);
};

/**
 * Tells whether this connection is connected
 * @return <code>true</code> if this connections is connected,
 * <code>false</code> otherwise
 * @type boolean
 */
JSJaCConnection.prototype.connected = function() { return this._connected; };

/**
 * Disconnects from jabber server and terminates session (if applicable)
 */
JSJaCConnection.prototype.disconnect = function() {
  this._setStatus('disconnecting');

  if (!this.connected())
    return;
  this._connected = false;

  clearInterval(this._interval);
  clearInterval(this._inQto);

  if (this._timeout)
    clearTimeout(this._timeout); // remove timer

  var slot = this._getFreeSlot();
  // Intentionally synchronous
  this._req[slot] = this._setupRequest(false);

  request = this._getRequestString(false, true);

  this.oDbg.log("Disconnecting: " + request,4);
  this._req[slot].r.send(request);

  try {
    //JSJaCCookie.read('JSJaC_State').erase();
	//eCarList.Util.Cookie.erase_validated('jsjac');
  } catch (e) {}

  this.oDbg.log("Disconnected: "+this._req[slot].r.responseText,2);
  this._handleEvent('ondisconnect');
};

/**
 * Gets current value of polling interval
 * @return Polling interval in milliseconds
 * @type int
 */
JSJaCConnection.prototype.getPollInterval = function() {
  return this._timerval;
};

/*
* Hodges - Returns timeout to use in fault recovery
* increasing amount of time based on number of faults up to the
* time the session dies on the server.
*/
JSJaCConnection.prototype.getFaultInterval = function() {
  var interval = Math.floor(1000.0 * Math.pow(((this._errcnt || 0) / 2.0), 2));
  this.oDbg.log('fault-interval(' + this._errcnt + '): ' + interval, 1);
  return interval;
};

/**
 * Registers an event handler (callback) for this connection.

 * <p>Note: All of the packet handlers for specific packets (like
 * message_in, presence_in and iq_in) fire only if there's no
 * callback associated with the id.<br>

 * <p>Example:<br/>
 * <code>con.registerHandler('iq', 'query', 'jabber:iq:version', handleIqVersion);</code>


 * @param {String} event One of

 * <ul>
 * <li>onConnect - connection has been established and authenticated</li>
 * <li>onDisconnect - connection has been disconnected</li>
 * <li>onResume - connection has been resumed</li>

 * <li>onStatusChanged - connection status has changed, current
 * status as being passed argument to handler. See {@link #status}.</li>

 * <li>onError - an error has occured, error node is supplied as
 * argument, like this:<br><code>&lt;error code='404' type='cancel'&gt;<br>
 * &lt;item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/&gt;<br>
 * &lt;/error&gt;</code></li>

 * <li>packet_in - a packet has been received (argument: the
 * packet)</li>

 * <li>packet_out - a packet is to be sent(argument: the
 * packet)</li>

 * <li>message_in | message - a message has been received (argument:
 * the packet)</li>

 * <li>message_out - a message packet is to be sent (argument: the
 * packet)</li>

 * <li>presence_in | presence - a presence has been received
 * (argument: the packet)</li>

 * <li>presence_out - a presence packet is to be sent (argument: the
 * packet)</li>

 * <li>iq_in | iq - an iq has been received (argument: the packet)</li>
 * <li>iq_out - an iq is to be sent (argument: the packet)</li>
 * </ul>

 * @param {String} childName A childnode's name that must occur within a
 * retrieved packet [optional]

 * @param {String} childNS A childnode's namespace that must occure within
 * a retrieved packet (works only if childName is given) [optional]

 * @param {String} type The type of the packet to handle (works only if childName and chidNS are given (both may be set to '*' in order to get skipped) [optional]

 * @param {Function} handler The handler to be called when event occurs. If your handler returns 'true' it cancels bubbling of the event. No other registered handlers for this event will be fired.
 */
JSJaCConnection.prototype.registerHandler = function(event) {
  event = event.toLowerCase(); // don't be case-sensitive here
  var eArg = {handler: arguments[arguments.length-1],
              childName: '*',
              childNS: '*',
              type: '*'};
  if (arguments.length > 2)
    eArg.childName = arguments[1];
  if (arguments.length > 3)
    eArg.childNS = arguments[2];
  if (arguments.length > 4)
    eArg.type = arguments[3];
  if (!this._events[event])
    this._events[event] = new Array(eArg);
  else
    this._events[event] = this._events[event].concat(eArg);

  // sort events in order how specific they match criterias thus using
  // wildcard patterns puts them back in queue when it comes to
  // bubbling the event
  this._events[event] =
  this._events[event].sort(function(a,b) {
    var aRank = 0;
    var bRank = 0;
    with (a) {
      if (type == '*')
        aRank++;
      if (childNS == '*')
        aRank++;
      if (childName == '*')
        aRank++;
    }
    with (b) {
      if (type == '*')
        bRank++;
      if (childNS == '*')
        bRank++;
      if (childName == '*')
        bRank++;
    }
    if (aRank > bRank)
      return 1;
    if (aRank < bRank)
      return -1;
    return 0;
  });
  this.oDbg.log("registered handler for event '"+event+"'",2);
};

JSJaCConnection.prototype.unregisterHandler = function(event,handler) {
  event = event.toLowerCase(); // don't be case-sensitive here

  if (!this._events[event])
    return;

  var arr = this._events[event], res = new Array();
  for (var i=0; i<arr.length; i++)
    if (arr[i].handler != handler)
      res.push(arr[i]);

  if (arr.length != res.length) {
    this._events[event] = res;
    this.oDbg.log("unregistered handler for event '"+event+"'",2);
  }
};

/**
 * Hodges
 * puts this connection in the grave
 */
JSJaCConnection.prototype.dispose = function() {
	// unregister all event handlers
	for (var event in this._events) {
		if (this._events.hasOwnProperty(event) && this._events[event].length) {
			this._events[event].length = 0;
		}
	}
};

/**
 * Register for iq packets of type 'get'.
 * @param {String} childName A childnode's name that must occur within a
 * retrieved packet

 * @param {String} childNS A childnode's namespace that must occure within
 * a retrieved packet (works only if childName is given)

 * @param {Function} handler The handler to be called when event occurs. If your handler returns 'true' it cancels bubbling of the event. No other registered handlers for this event will be fired.
 */
JSJaCConnection.prototype.registerIQGet =
  function(childName, childNS, handler) {
  this.registerHandler('iq', childName, childNS, 'get', handler);
};

/**
 * Register for iq packets of type 'set'.
 * @param {String} childName A childnode's name that must occur within a
 * retrieved packet

 * @param {String} childNS A childnode's namespace that must occure within
 * a retrieved packet (works only if childName is given)

 * @param {Function} handler The handler to be called when event occurs. If your handler returns 'true' it cancels bubbling of the event. No other registered handlers for this event will be fired.
 */
JSJaCConnection.prototype.registerIQSet =
  function(childName, childNS, handler) {
  this.registerHandler('iq', childName, childNS, 'set', handler);
};

/**
 * Resumes this connection from saved state (cookie)
 * @return Whether resume was successful
 * @type boolean
 */
JSJaCConnection.prototype.resume = function() {
  try {
    this._setStatus('resuming');
    //var s = unescape(JSJaCCookie.read('JSJaC_State').getValue());
    var s = eCarList.Util.Cookie.read('jsjac');
    //var s = eCarList.Util.Cookie.read_validated('jsjac');
     
    //this.oDbg.log('read cookie: '+s,2);

    var o = JSJaCJSON.parse(s);
     
    for (var i in o)
      if (o.hasOwnProperty(i))
        this[i] = o[i];
     
    // copy keys - not being very generic here :-/
    if (this._keys) {
      this._keys2 = new JSJaCKeys();
      var u = this._keys2._getSuspendVars();
      for (var i=0; i<u.length; i++)
        this._keys2[u[i]] = this._keys[u[i]];
      this._keys = this._keys2;
    }

    if (this._connected) {
      // don't poll too fast!
	  this._resume();
      //this._handleEvent('onresume'); // Hodges - premature resume event
      //setTimeout(JSJaC.bind(this._resume, this), this.getPollInterval());
      this._after_timeout(function() { this._handleEvent('onresume'); }, 500);
      this._interval = setInterval(JSJaC.bind(this._checkQueue, this),
				   JSJAC_CHECKQUEUEINTERVAL);
      this._inQto = setInterval(JSJaC.bind(this._checkInQ, this),
				JSJAC_CHECKINQUEUEINTERVAL);
    }

    try {
	  //eCarList.Util.Cookie.validate('jsjac', s, this._inactivity * 1000);
      //JSJaCCookie.read('JSJaC_State').erase();
    } catch (e) {}

    return (this._connected === true);
  } catch (e) {
    if (e.message)
      this.oDbg.log("Resume failed: "+e.message, 1);
    else
      this.oDbg.log("Resume failed: "+e, 1);
    return false;
  }
};

/**
 * Sends a JSJaCPacket
 * @param {JSJaCPacket} packet  The packet to send
 * @param {Function}    cb      The callback to be called if there's a reply
 * to this packet (identified by id) [optional]
 * @param {Object}      arg     Arguments passed to the callback
 * (additionally to the packet received) [optional]
 * @return 'true' if sending was successfull, 'false' otherwise
 * @type boolean
 */
JSJaCConnection.prototype.send = function(packet,cb,arg) {
  if (!packet || !packet.pType) {
    this.oDbg.log("no packet: "+packet, 1);
    return false;
  }

  if (!this.connected())
    return false;

  // remember id for response if callback present
  if (cb) {
    if (!packet.getID())
      packet.setID('JSJaCID_'+this._ID++); // generate an ID

    // register callback with id
    this._registerPID(packet.getID(),cb,arg);
  }

  try {
    this._handleEvent(packet.pType()+'_out', packet);
    this._handleEvent("packet_out", packet);
    this._pQueue = this._pQueue.concat(packet.xml());
  } catch (e) {
    this.oDbg.log(e.toString(),1);
    return false;
  }

  return true;
};

/**
 * Sends an IQ packet. Has default handlers for each reply type.
 * Those maybe overriden by passing an appropriate handler.
 * @param {JSJaCIQPacket} iq - the iq packet to send
 * @param {Object} handlers - object with properties 'error_handler',
 *                            'result_handler' and 'default_handler'
 *                            with appropriate functions
 * @param {Object} arg - argument to handlers
 * @return 'true' if sending was successfull, 'false' otherwise
 * @type boolean
 */
JSJaCConnection.prototype.sendIQ = function(iq, handlers, arg) {
  if (!iq || iq.pType() != 'iq') {
    return false;
  }

  handlers = handlers || {};
  var error_handler = handlers.error_handler || function(aIq) {
    this.oDbg.log(iq.xml(), 1);
  };
 
  var result_handler = handlers.result_handler ||  function(aIq) {
    this.oDbg.log(aIq.xml(), 2);
  };
  // unsure, what's the use of this?
  var default_handler = handlers.default_handler || function(aIq) {
    this.oDbg.log(aIq.xml(), 2);
  };

  var iqHandler = function(aIq, arg) {
    switch (aIq.getType()) {
      case 'error':
      error_handler(aIq);
      break;
      case 'result':
      result_handler(aIq, arg);
      break;
      default: // may it be?
      default_handler(aIq, arg);
    }
  };
  return this.send(iq, iqHandler, arg);
};

/**
 * Sets polling interval for this connection
 * @param {int} millisecs Milliseconds to set timer to
 * @return effective interval this connection has been set to
 * @type int
 */
JSJaCConnection.prototype.setPollInterval = function(timerval) {
  if (timerval && !isNaN(timerval))
    this._timerval = timerval;
  return this._timerval;
};

/**
 * Returns current status of this connection
 * @return String to denote current state. One of
 * <ul>
 * <li>'initializing' ... well
 * <li>'connecting' if connect() was called
 * <li>'resuming' if resume() was called
 * <li>'processing' if it's about to operate as normal
 * <li>'onerror_fallback' if there was an error with the request object
 * <li>'protoerror_fallback' if there was an error at the http binding protocol flow (most likely that's where you interested in)
 * <li>'internal_server_error' in case of an internal server error
 * <li>'suspending' if suspend() is being called
 * <li>'aborted' if abort() was called
 * <li>'disconnecting' if disconnect() has been called
 * </ul>
 * @type String
 */
JSJaCConnection.prototype.status = function() { return this._status; };

/**
 * Suspsends this connection (saving state for later resume)
 */
JSJaCConnection.prototype.suspend = function() {
	
    // remove timers
    clearTimeout(this._timeout);
    clearInterval(this._interval);
    clearInterval(this._inQto);

    this._suspend();

    var u = ('_connected,_keys,_ID,_inQ,_pQueue,_regIDs,_errcnt,_inactivity,domain,username,resource,jid,fulljid,_sid,_httpbase,_timerval,_is_polling').split(',');
    u = u.concat(this._getSuspendVars());
    var s = new Object();

    for (var i=0; i<u.length; i++) {
      if (!this[u[i]]) continue; // hu? skip these!
      if (this[u[i]]._getSuspendVars) {
        var uo = this[u[i]]._getSuspendVars();
        var o = new Object();
        for (var j=0; j<uo.length; j++)
          o[uo[j]] = this[u[i]][uo[j]];
      } else
        var o = this[u[i]];

      s[u[i]] = o;
    }
    //var c = new JSJaCCookie('JSJaC_State', escape(JSJaCJSON.toString(s)), this._inactivity);
    //this.oDbg.log("writing cookie: "+unescape(c.value)+"\n(length:"+ unescape(c.value).length+")",2);
    //c.write();
	var c = JSJaCJSON.toString(s);
	eCarList.Util.Cookie.write('jsjac', c, this._inactivity * 1000);
    //eCarList.Util.Cookie.write_validated('jsjac', c, this._inactivity * 1000);

    try {
      //var c2 = JSJaCCookie.read('JSJaC_State');
      //var c2 = eCarList.Util.Cookie.read_validated('jsjac');
      var c2 = eCarList.Util.Cookie.read('jsjac');
      //if (c.value != c2.value) {
      if (c != c2) {
        //this.oDbg.log("Suspend failed writing cookie.\nRead: "+ unescape(JSJaCCookie.read('JSJaC_State')), 1);
        //c.erase();
        this.oDbg.log('suspend failed writing cookie');
        //eCarList.Util.Cookie.erase_validated('jsjac');
      }

      this._connected = false;

      this._setStatus('suspending');
    } catch (e) {
      this.oDbg.log("Failed reading cookie 'JSJaC_State': "+e.message);
    }

  };

/**
 * @private
 */
JSJaCConnection.prototype._abort = function() {
  clearTimeout(this._timeout); // remove timer

  clearInterval(this._inQto);
  clearInterval(this._interval);

  this._connected = false;

  this._setStatus('aborted');

  this.oDbg.log("Disconnected.",1);
  this._handleEvent('ondisconnect');
  this._handleEvent('onerror',
                    JSJaCError('500','cancel','service-unavailable'));
};

/**
 * @private
 */
JSJaCConnection.prototype._checkInQ = function() {
  for (var i=0; i<this._inQ.length && i<10; i++) {
    var item = this._inQ[0];
    this._inQ = this._inQ.slice(1,this._inQ.length);
    var packet = JSJaCPacket.wrapNode(item);

    if (!packet)
      return;

    this._handleEvent("packet_in", packet);

    if (packet.pType && !this._handlePID(packet)) {
      this._handleEvent(packet.pType()+'_in',packet);
      this._handleEvent(packet.pType(),packet);
    }
  }
};

/**
 * @private
 */
JSJaCConnection.prototype._checkQueue = function() {
  if (this._pQueue.length != 0)
    this._process();
  return true;
};

/**
 * @private
 */
JSJaCConnection.prototype._doAuth = function() {
  if (this.has_sasl && this.authtype == 'nonsasl')
    this.oDbg.log("Warning: SASL present but not used", 1);

  if (!this._doSASLAuth() &&
      !this._doLegacyAuth()) {
    this.oDbg.log("Auth failed for authtype "+this.authtype,1);
    this.disconnect();
    return false;
  }
  return true;
};

/**
 * @private
 */
JSJaCConnection.prototype._doInBandReg = function() {
  if (this.authtype == 'saslanon' || this.authtype == 'anonymous')
    return; // bullshit - no need to register if anonymous

  /* ***
   * In-Band Registration see JEP-0077
   */

  var iq = new JSJaCIQ();
  iq.setType('set');
  iq.setID('reg1');
  iq.appendNode("query", {xmlns: "jabber:iq:register"},
                [["username", this.username],
                 ["password", this.pass]]);

  this.send(iq,this._doInBandRegDone);
};

/**
 * @private
 */
JSJaCConnection.prototype._doInBandRegDone = function(iq) {
  if (iq && iq.getType() == 'error') { // we failed to register
    this.oDbg.log("registration failed for "+this.username,0);
    this._handleEvent('onerror',iq.getChild('error'));
    return;
  }

  this.oDbg.log(this.username + " registered succesfully",0);

  this._doAuth();
};

/**
 * @private
 */
JSJaCConnection.prototype._doLegacyAuth = function() {
  if (this.authtype != 'nonsasl' && this.authtype != 'anonymous')
    return false;

  /* ***
   * Non-SASL Authentication as described in JEP-0078
   */
  var iq = new JSJaCIQ();
  iq.setIQ(this.server,'get','auth1');
  iq.appendNode('query', {xmlns: 'jabber:iq:auth'},
                [['username', this.username]]);

  this.send(iq,this._doLegacyAuth2);
  return true;
};

/**
 * @private
 */
JSJaCConnection.prototype._doLegacyAuth2 = function(iq) {
  if (!iq || iq.getType() != 'result') {
    if (iq && iq.getType() == 'error')
      this._handleEvent('onerror',iq.getChild('error'));
    this.disconnect();
    return;
  }

  var use_digest = (iq.getChild('digest') != null);

  /* ***
   * Send authentication
   */
  var iq = new JSJaCIQ();
  iq.setIQ(this.server,'set','auth2');

  query = iq.appendNode('query', {xmlns: 'jabber:iq:auth'},
                        [['username', this.username],
                         ['resource', this.resource]]);

  if (use_digest) { // digest login
    query.appendChild(iq.buildNode('digest', {xmlns: 'jabber:iq:auth'},
                                   hex_sha1(this.streamid + this.pass)));
  } else if (this.allow_plain) { // use plaintext auth
    query.appendChild(iq.buildNode('password', {xmlns: 'jabber:iq:auth'},
                                   this.pass));
  } else {
    this.oDbg.log("no valid login mechanism found",1);
    this.disconnect();
    return false;
  }

  this.send(iq,this._doLegacyAuthDone);
};

/**
 * @private
 */
JSJaCConnection.prototype._doLegacyAuthDone = function(iq) {
  if (iq.getType() != 'result') { // auth' failed
    if (iq.getType() == 'error')
      this._handleEvent('onerror',iq.getChild('error'));
    this.disconnect();
  } else
    this._handleEvent('onconnect');
};

/**
 * @private
 */
JSJaCConnection.prototype._doSASLAuth = function() {
  if (this.authtype == 'nonsasl' || this.authtype == 'anonymous')
    return false;

  if (this.authtype == 'saslanon') {
    if (this.mechs['ANONYMOUS']) {
      this.oDbg.log("SASL using mechanism 'ANONYMOUS'",2);
      return this._sendRaw("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>",
                           this._doSASLAuthDone);
    }
    this.oDbg.log("SASL ANONYMOUS requested but not supported",1);
  } else {
    if (this.mechs['DIGEST-MD5']) {
      this.oDbg.log("SASL using mechanism 'DIGEST-MD5'",2);
      return this._sendRaw("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>",
                           this._doSASLAuthDigestMd5S1);
    } else if (this.allow_plain && this.mechs['PLAIN']) {
      this.oDbg.log("SASL using mechanism 'PLAIN'",2);
      var authStr = this.username+'@'+
      this.domain+String.fromCharCode(0)+
      this.username+String.fromCharCode(0)+
      this.pass;
      this.oDbg.log("authenticating with '"+authStr+"'",2);
      authStr = btoa(authStr);
      return this._sendRaw("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>"+authStr+"</auth>",
                           this._doSASLAuthDone);
    }
    this.oDbg.log("No SASL mechanism applied",1);
    this.authtype = 'nonsasl'; // fallback
  }
  return false;
};

/**
 * @private
 */
JSJaCConnection.prototype._doSASLAuthDigestMd5S1 = function(el) {
  if (el.nodeName != "challenge") {
    this.oDbg.log("challenge missing",1);
    this._handleEvent('onerror',JSJaCError('401','auth','not-authorized'));
    this.disconnect();
  } else {
    var challenge = atob(el.firstChild.nodeValue);
    this.oDbg.log("got challenge: "+challenge,2);
    this._nonce = challenge.substring(challenge.indexOf("nonce=")+7);
    this._nonce = this._nonce.substring(0,this._nonce.indexOf("\""));
    this.oDbg.log("nonce: "+this._nonce,2);
    if (this._nonce == '' || this._nonce.indexOf('\"') != -1) {
      this.oDbg.log("nonce not valid, aborting",1);
      this.disconnect();
      return;
    }

    this._digest_uri = "xmpp/";
    //     if (typeof(this.host) != 'undefined' && this.host != '') {
    //       this._digest-uri += this.host;
    //       if (typeof(this.port) != 'undefined' && this.port)
    //         this._digest-uri += ":" + this.port;
    //       this._digest-uri += '/';
    //     }
    this._digest_uri += this.domain;

    this._cnonce = cnonce(14);

    this._nc = '00000001';

    var A1 = str_md5(this.username+':'+this.domain+':'+this.pass)+
    ':'+this._nonce+':'+this._cnonce;

    var A2 = 'AUTHENTICATE:'+this._digest_uri;

    var response = hex_md5(hex_md5(A1)+':'+this._nonce+':'+this._nc+':'+
                           this._cnonce+':auth:'+hex_md5(A2));

    var rPlain = 'username="'+this.username+'",realm="'+this.domain+
    '",nonce="'+this._nonce+'",cnonce="'+this._cnonce+'",nc="'+this._nc+
    '",qop=auth,digest-uri="'+this._digest_uri+'",response="'+response+
    '",charset=utf-8';
   
    this.oDbg.log("response: "+rPlain,2);

    this._sendRaw("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"+
                  binb2b64(str2binb(rPlain))+"</response>",
                  this._doSASLAuthDigestMd5S2);
  }
};

/**
 * @private
 */
JSJaCConnection.prototype._doSASLAuthDigestMd5S2 = function(el) {
  if (el.nodeName == 'failure') {
    if (el.xml)
      this.oDbg.log("auth error: "+el.xml,1);
    else
      this.oDbg.log("auth error",1);
    this._handleEvent('onerror',JSJaCError('401','auth','not-authorized'));
    this.disconnect();
    return;
  }

  var response = atob(el.firstChild.nodeValue);
  this.oDbg.log("response: "+response,2);

  var rspauth = response.substring(response.indexOf("rspauth=")+8);
  this.oDbg.log("rspauth: "+rspauth,2);

  var A1 = str_md5(this.username+':'+this.domain+':'+this.pass)+
  ':'+this._nonce+':'+this._cnonce;

  var A2 = ':'+this._digest_uri;

  var rsptest = hex_md5(hex_md5(A1)+':'+this._nonce+':'+this._nc+':'+
                        this._cnonce+':auth:'+hex_md5(A2));
  this.oDbg.log("rsptest: "+rsptest,2);

  if (rsptest != rspauth) {
    this.oDbg.log("SASL Digest-MD5: server repsonse with wrong rspauth",1);
    this.disconnect();
    return;
  }

  if (el.nodeName == 'success')
    this._reInitStream(this.domain, this._doStreamBind);
  else // some extra turn
    this._sendRaw("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>",
                  this._doSASLAuthDone);
};

/**
 * @private
 */
JSJaCConnection.prototype._doSASLAuthDone = function (el) {
  if (el.nodeName != 'success') {
    this.oDbg.log("auth failed",1);
    this._handleEvent('onerror',JSJaCError('401','auth','not-authorized'));
    this.disconnect();
  } else
    this._reInitStream(this.domain, this._doStreamBind);
};

/**
 * @private
 */
JSJaCConnection.prototype._doStreamBind = function() {
  var iq = new JSJaCIQ();
  iq.setIQ(this.domain,'set','bind_1');
  iq.appendNode("bind", {xmlns: "urn:ietf:params:xml:ns:xmpp-bind"},
                [["resource", this.resource]]);
  this.oDbg.log(iq.xml());
  this.send(iq,this._doXMPPSess);
};

/**
 * @private
 */
JSJaCConnection.prototype._doXMPPSess = function(iq) {
  if (iq.getType() != 'result' || iq.getType() == 'error') { // failed
    this.disconnect();
    if (iq.getType() == 'error')
      this._handleEvent('onerror',iq.getChild('error'));
    return;
  }
 
  this.fulljid = iq.getChildVal("jid");
  this.jid = this.fulljid.substring(0,this.fulljid.lastIndexOf('/'));
 
  iq = new JSJaCIQ();
  iq.setIQ(this.domain,'set','sess_1');
  iq.appendNode("session", {xmlns: "urn:ietf:params:xml:ns:xmpp-session"},
                []);
  this.oDbg.log(iq.xml());
  this.send(iq,this._doXMPPSessDone);
};

/**
 * @private
 */
JSJaCConnection.prototype._doXMPPSessDone = function(iq) {
  if (iq.getType() != 'result' || iq.getType() == 'error') { // failed
    this.disconnect();
    if (iq.getType() == 'error')
      this._handleEvent('onerror',iq.getChild('error'));
    return;
  } else
    this._handleEvent('onconnect');
};
 
/**
 * @private
 */
JSJaCConnection.prototype._handleEvent = function(event,arg) {
  event = event.toLowerCase(); // don't be case-sensitive here
  this.oDbg.log("incoming event '"+event+"'",3);
  if (!this._events[event])
    return;
  this.oDbg.log("handling event '"+event+"'",2);
  for (var i=0;i<this._events[event].length; i++) {
    var aEvent = this._events[event][i];
    if (aEvent.handler) {
      try {
        if (arg) {
          if (arg.pType) { // it's a packet
            if ((!arg.getNode().hasChildNodes() && aEvent.childName != '*') ||
				(arg.getNode().hasChildNodes() &&
				 !arg.getChild(aEvent.childName, aEvent.childNS)))
              continue;
            if (aEvent.type != '*' &&
                arg.getType() != aEvent.type)
              continue;
            this.oDbg.log(aEvent.childName+"/"+aEvent.childNS+"/"+aEvent.type+" => match for handler "+aEvent.handler,3);
          }
          if (aEvent.handler.call(this,arg)) // handled!
            break;
        }
        else
          if (aEvent.handler.call(this)) // handled!
            break;
      } catch (e) {
        if (e.name && e.message) {
          this.oDbg.log(aEvent.handler+"\n>>>"+e.name+": "+ e.message,1);
		} else {
          this.oDbg.log(aEvent.handler+"\n>>>"+e,1);
		}
      }
    }
  }
};

/**
 * @private
 */
JSJaCConnection.prototype._handlePID = function(aJSJaCPacket) {
  if (!aJSJaCPacket.getID())
    return false;
  for (var i in this._regIDs) {
    if (this._regIDs.hasOwnProperty(i) &&
        this._regIDs[i] && i == aJSJaCPacket.getID()) {
      var pID = aJSJaCPacket.getID();
      this.oDbg.log("handling "+pID,3);
      try {
        if (this._regIDs[i].cb.call(this, aJSJaCPacket,this._regIDs[i].arg) === false) {
          // don't unregister
          return false;
        } else {
          this._unregisterPID(pID);
          return true;
        }
      } catch (e) {
        // broken handler?
        this.oDbg.log(e.name+": "+ e.message);
        this._unregisterPID(pID);
        return true;
      }
    }
  }
  return false;
};

/**
 * @private
 */
JSJaCConnection.prototype._handleResponse = function(req) {
  var rootEl = this._parseResponse(req);

  if (!rootEl)
    return;

  for (var i=0; i<rootEl.childNodes.length; i++) {
    if (this._sendRawCallbacks.length) {
      var cb = this._sendRawCallbacks[0];
      this._sendRawCallbacks = this._sendRawCallbacks.slice(1, this._sendRawCallbacks.length);
      cb.fn.call(this, rootEl.childNodes.item(i), cb.arg);
      continue;
    }
    this._inQ = this._inQ.concat(rootEl.childNodes.item(i));
  }
};

/**
 * @private
 */
JSJaCConnection.prototype._parseStreamFeatures = function(doc) {
  if (!doc) {
    this.oDbg.log("nothing to parse ... aborting",1);
    return false;
  }

  var errorTag;
  if (doc.getElementsByTagNameNS)
    errorTag = doc.getElementsByTagNameNS("http://etherx.jabber.org/streams", "error").item(0);
  else {
    var errors = doc.getElementsByTagName("error");
    for (var i=0; i<errors.length; i++)
      if (errors.item(i).namespaceURI == "http://etherx.jabber.org/streams") {
        errorTag = errors.item(i);
        break;
      }
  }

  if (errorTag) {
    this._setStatus("internal_server_error");
    clearTimeout(this._timeout); // remove timer
    clearInterval(this._interval);
    clearInterval(this._inQto);
    this._handleEvent('onerror',JSJaCError('503','cancel','session-terminate'));
    this._connected = false;
    this.oDbg.log("Disconnected.",1);
    this._handleEvent('ondisconnect');
    return false;
  }

  this.mechs = new Object();
  var lMec1 = doc.getElementsByTagName("mechanisms");
  this.has_sasl = false;
  for (var i=0; i<lMec1.length; i++)
    if (lMec1.item(i).getAttribute("xmlns") ==
        "urn:ietf:params:xml:ns:xmpp-sasl") {
      this.has_sasl=true;
      var lMec2 = lMec1.item(i).getElementsByTagName("mechanism");
      for (var j=0; j<lMec2.length; j++)
        this.mechs[lMec2.item(j).firstChild.nodeValue] = true;
      break;
    }
  if (this.has_sasl)
    this.oDbg.log("SASL detected",2);
  else {
    this.authtype = 'nonsasl';
    this.oDbg.log("No support for SASL detected",2);
  }

  /* [TODO]
   * check if in-band registration available
   * check for session and bind features
   */

  return true;
};

/**
 * @private
 */
JSJaCConnection.prototype._process = function(timerval) {
  if (!this.connected()) {
    this.oDbg.log("Connection lost ...",1);
    if (this._interval)
      clearInterval(this._interval);
    return;
  }

  this.setPollInterval(timerval);

  if (this._timeout)
    clearTimeout(this._timeout);

  var slot = this._getFreeSlot();

  if (slot < 0)
    return;

  if (typeof(this._req[slot]) != 'undefined' &&
      typeof(this._req[slot].r) != 'undefined' &&
      this._req[slot].r.readyState != 4) {
    this.oDbg.log("Slot "+slot+" is not ready");
    return;
  }
	
  if (!this.isPolling() && this._pQueue.length == 0 &&
      this._req[(slot+1)%2] && this._req[(slot+1)%2].r.readyState != 4) {
    this.oDbg.log("all slots busy, standby ...", 2);
    return;
  }

  if (!this.isPolling())
    this.oDbg.log("Found working slot at "+slot,2);

  this._req[slot] = this._setupRequest(true);

	// Hodges - setup request timeout (abort connection after timeout 5 min 20 sec)
	this._req[slot]._timeout = setTimeout(JSJaC.bind(this._abort, this), 320000);

  /* setup onload handler for async send */
  this._req[slot].r.onreadystatechange = 
  JSJaC.bind(function() {
               if (!this.connected())
                 return;
               if (this._req[slot].r.readyState == 1 && this._status == 'resuming') {
                 // Hodges - at this point we can safely make requests after resume
                 this._setStatus('resumed');
                 this._after_timeout(function() {
                   //this._handleEvent('onresume');
                 }, 250);
               } else if (this._req[slot].r.readyState == 4) {
				// Hodges - cancel timeout
				if (this._req[slot]._timeout) { clearTimeout(this._req[slot]._timeout); }
                 this._setStatus('processing');
                 this.oDbg.log("async recv: "+this._req[slot].r.responseText,4);
                 this._handleResponse(this._req[slot]);
                 // schedule next tick
                 if (this._pQueue.length) {
                   this._timeout = setTimeout(JSJaC.bind(this._process, this),100);
                 } else {
                   this.oDbg.log("scheduling next poll in "+this.getPollInterval()+
                                 " msec", 4);
                   this._timeout = setTimeout(JSJaC.bind(this._process, this),this.getPollInterval());
                 }
               }
             }, this);

  try {
    this._req[slot].r.onerror = 
      JSJaC.bind(function() {
				if (this._req[slot]._timeout) { clearTimeout(this._req[slot]._timeout); }
                   if (!this.connected())
                     return;
                   this._errcnt++;
                   this.oDbg.log('XmlHttpRequest error ('+this._errcnt+')',1);
                   if (this._errcnt > JSJAC_ERR_COUNT) {
                     // abort
                     this._abort();
                     return false;
                   }
                   
                   this._setStatus('onerror_fallback');
			
                   // schedule next tick
                   setTimeout(JSJaC.bind(this._resume, this), this.getPollInterval());
                   return false;
                 }, this);
  } catch(e) { } // well ... no onerror property available, maybe we
  // can catch the error somewhere else ...

  var reqstr = this._getRequestString();

  if (typeof(this._rid) != 'undefined') // remember request id if any
    this._req[slot].rid = this._rid;

  this.oDbg.log("sending: " + reqstr,4);
  this._req[slot].r.send(reqstr);
};

/**
 * @private
 */
JSJaCConnection.prototype._registerPID = function(pID,cb,arg) {
  if (!pID || !cb)
    return false;
  this._regIDs[pID] = new Object();
  this._regIDs[pID].cb = cb;
  if (arg)
    this._regIDs[pID].arg = arg;
  this.oDbg.log("registered "+pID,3);
  return true;
};

/**
 * send empty request
 * waiting for stream id to be able to proceed with authentication
 * @private
 */
JSJaCConnection.prototype._sendEmpty = function JSJaCSendEmpty() {
  var slot = this._getFreeSlot();
  this._req[slot] = this._setupRequest(true);

  this._req[slot].r.onreadystatechange = 
  JSJaC.bind(function() {
               if (this._req[slot].r.readyState == 4) {
                 this.oDbg.log("async recv: "+this._req[slot].r.responseText,4);
                 this._getStreamID(slot); // handle response
               }
             },this);

  if (typeof(this._req[slot].r.onerror) != 'undefined') {
    this._req[slot].r.onerror = 
      JSJaC.bind(function(e) {
                   this.oDbg.log('XmlHttpRequest error',1);
                   return false;
                 }, this);
  }

  var reqstr = this._getRequestString();
  this.oDbg.log("sending: " + reqstr,4);
  this._req[slot].r.send(reqstr);
};

/**
 * @private
 */
JSJaCConnection.prototype._sendRaw = function(xml,cb,arg) {
  if (cb)
    this._sendRawCallbacks.push({fn: cb, arg: arg});
 
  this._pQueue.push(xml);
  this._process();

  return true;
};

/**
 * @private
 */
JSJaCConnection.prototype._setStatus = function(status) {
  if (!status || status == '')
    return;
  if (status != this._status) { // status changed!
    this._status = status;
    this._handleEvent('onstatuschanged', status);
    this._handleEvent('status_changed', status);
  }
};

/**
 * @private
 */
JSJaCConnection.prototype._unregisterPID = function(pID) {
  if (!this._regIDs[pID])
    return false;
  this._regIDs[pID] = null;
  this.oDbg.log("unregistered "+pID,3);
  return true;
};

JSJaCConnection.prototype._after_timeout = function(fn, timeout) {
  var _this = this;
  setTimeout(function() {
    fn.call(_this);
  }, timeout);
};
/**
 * @fileoverview All stuff related to HTTP Binding
 * @author Stefan Strigler steve@zeank.in-berlin.de
 * @version $Revision: 483 $
 */

/**
 * Instantiates an HTTP Binding session
 * @class Implementation of {@link
 * http://www.xmpp.org/extensions/xep-0206.html XMPP Over BOSH}
 * formerly known as HTTP Binding.
 * @extends JSJaCConnection
 * @constructor
 */
function JSJaCHttpBindingConnection(oArg) {
  /**
   * @ignore
   */
  this.base = JSJaCConnection;
  this.base(oArg);

  // member vars
  /**
   * @private
   */
  this._hold = JSJACHBC_MAX_HOLD;
  /**
   * @private
   */
  this._inactivity = 0;
  /**
   * @private
   */
  this._last_requests = new Object(); // 'hash' storing hold+1 last requests
  /**
   * @private
   */
  this._last_rid = 0;                 // I know what you did last summer
  /**
   * @private
   */
  this._min_polling = 0;

  /**
   * @private
   */
  this._pause = 0;
  /**
   * @private
   */
  this._wait = JSJACHBC_MAX_WAIT;
}
JSJaCHttpBindingConnection.prototype = new JSJaCConnection();

/**
 * Inherit an instantiated HTTP Binding session
 */
JSJaCHttpBindingConnection.prototype.inherit = function(oArg) {
  this.domain = oArg.domain || 'localhost';
  this.username = oArg.username;
  this.resource = oArg.resource;
  this._sid = oArg.sid;
  this._rid = oArg.rid;
  this._min_polling = oArg.polling;
  this._inactivity = oArg.inactivity;
  this._setHold(oArg.requests-1);
  this.setPollInterval(this._timerval);
  if (oArg.wait)
    this._wait = oArg.wait; // for whatever reason

  this._connected = true;

  this._handleEvent('onconnect');

  this._interval= setInterval(JSJaC.bind(this._checkQueue, this),
                              JSJAC_CHECKQUEUEINTERVAL);
  this._inQto = setInterval(JSJaC.bind(this._checkInQ, this),
                            JSJAC_CHECKINQUEUEINTERVAL);
  this._timeout = setTimeout(JSJaC.bind(this._process, this),
                             this.getPollInterval());
};

/**
 * Sets poll interval
 * @param {int} timerval the interval in seconds
 */
JSJaCHttpBindingConnection.prototype.setPollInterval = function(timerval) {
  if (timerval && !isNaN(timerval)) {
    if (!this.isPolling())
      this._timerval = 100;
    else if (this._min_polling && timerval < this._min_polling*1000)
      this._timerval = this._min_polling*1000;
    else if (this._inactivity && timerval > this._inactivity*1000)
      this._timerval = this._inactivity*1000;
    else
      this._timerval = timerval;
  }
  return this._timerval;
};

/**
 * whether this session is in polling mode
 * @type boolean
 */
JSJaCHttpBindingConnection.prototype.isPolling = function() { return (this._hold == 0) };

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._getFreeSlot = function() {
  for (var i=0; i<this._hold+1; i++)
    if (typeof(this._req[i]) == 'undefined' || typeof(this._req[i].r) == 'undefined' || this._req[i].r.readyState == 4)
      return i;
  return -1; // nothing found
};

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._getHold = function() { return this._hold; };

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._getRequestString = function(raw, last) {
  raw = raw || '';
  var reqstr = '';

  // check if we're repeating a request

  if (this._rid <= this._last_rid && typeof(this._last_requests[this._rid]) != 'undefined') // repeat!
    reqstr = this._last_requests[this._rid].xml;
  else { // grab from queue
    var xml = '';
    while (this._pQueue.length) {
      var curNode = this._pQueue[0];
      xml += curNode;
      this._pQueue = this._pQueue.slice(1,this._pQueue.length);
    }

    reqstr = "<body rid='"+this._rid+"' sid='"+this._sid+"' xmlns='http://jabber.org/protocol/httpbind' ";
    if (JSJAC_HAVEKEYS) {
      reqstr += "key='"+this._keys.getKey()+"' ";
      if (this._keys.lastKey()) {
        this._keys = new JSJaCKeys(hex_sha1,this.oDbg);
        reqstr += "newkey='"+this._keys.getKey()+"' ";
      }
    }
    if (last)
      reqstr += "type='terminate'";
    else if (this._reinit) {
      if (JSJACHBC_USE_BOSH_VER) 
        reqstr += "xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh'";
      this._reinit = false;
    }

    if (xml != '' || raw != '') {
      reqstr += ">" + raw + xml + "</body>";
    } else {
      reqstr += "/>";
    }

    this._last_requests[this._rid] = new Object();
    this._last_requests[this._rid].xml = reqstr;
    this._last_rid = this._rid;

    for (var i in this._last_requests)
      if (this._last_requests.hasOwnProperty(i) &&
          i < this._rid-this._hold)
        delete(this._last_requests[i]); // truncate
  }
	
  return reqstr;
};

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._getInitialRequestString = function() {
  var reqstr = "<body content='text/xml; charset=utf-8' hold='"+this._hold+"' xmlns='http://jabber.org/protocol/httpbind' to='"+this.authhost+"' wait='"+this._wait+"' rid='"+this._rid+"'";
  if (this.host || this.port)
    reqstr += " route='xmpp:"+this.host+":"+this.port+"'";
  if (this.secure)
    reqstr += " secure='"+this.secure+"'";
  if (JSJAC_HAVEKEYS) {
    this._keys = new JSJaCKeys(hex_sha1,this.oDbg); // generate first set of keys
    key = this._keys.getKey();
    reqstr += " newkey='"+key+"'";
  }
  if (this._xmllang)
    reqstr += " xml:lang='"+this._xmllang + "'";

  if (JSJACHBC_USE_BOSH_VER) {
    reqstr += " ver='" + JSJACHBC_BOSH_VERSION + "'";
    reqstr += " xmlns:xmpp='urn:xmpp:xbosh'";
    if (this.authtype == 'sasl' || this.authtype == 'saslanon')
      reqstr += " xmpp:version='1.0'";
  }
  reqstr += "/>";
  return reqstr;
};

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._getStreamID = function(slot) {

  this.oDbg.log(this._req[slot].r.responseText,4);

  if (!this._req[slot].r.responseXML || !this._req[slot].r.responseXML.documentElement) {
    this._handleEvent('onerror',JSJaCError('503','cancel','service-unavailable'));
    return;
  }
  var body = this._req[slot].r.responseXML.documentElement;

  // extract stream id used for non-SASL authentication
  if (body.getAttribute('authid')) {
    this.streamid = body.getAttribute('authid');
    this.oDbg.log("got streamid: "+this.streamid,2);
  } else {
    this._timeout = setTimeout(JSJaC.bind(this._sendEmpty, this),
                               this.getPollInterval());
    return;
  }

  this._timeout = setTimeout(JSJaC.bind(this._process, this),
                             this.getPollInterval());

  if (!this._parseStreamFeatures(body))
    return;

  if (this.register)
    this._doInBandReg();
  else
    this._doAuth();
};

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._getSuspendVars = function() {
  return ('host,port,secure,_rid,_last_rid,_wait,_min_polling,_inactivity,_hold,_pause').split(',');
};

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._handleInitialResponse = function(slot) {
  try {
    // This will throw an error on Mozilla when the connection was refused
    this.oDbg.log(this._req[slot].r.getAllResponseHeaders(),4);
    this.oDbg.log(this._req[slot].r.responseText,4);
  } catch(ex) {
    this.oDbg.log("No response",4);
  }

  if (this._req[slot].r.status != 200 || !this._req[slot].r.responseXML) {
    this.oDbg.log("initial response broken (status: "+this._req[slot].r.status+")",1);
    this._handleEvent('onerror',JSJaCError('503','cancel','service-unavailable'));
    return;
  }
  var body = this._req[slot].r.responseXML.documentElement;

  if (!body || body.tagName != 'body' || body.namespaceURI != 'http://jabber.org/protocol/httpbind') {
    this.oDbg.log("no body element or incorrect body in initial response",1);
    this._handleEvent("onerror",JSJaCError("500","wait","internal-service-error"));
    return;
  }

  // Check for errors from the server
  if (body.getAttribute("type") == "terminate") {
    this.oDbg.log("invalid response:\n" + this._req[slot].r.responseText,1);
    clearTimeout(this._timeout); // remove timer
    this._connected = false;
    this.oDbg.log("Disconnected.",1);
    this._handleEvent('ondisconnect');
    this._handleEvent('onerror',JSJaCError('503','cancel','service-unavailable'));
    return;
  }

  // get session ID
  this._sid = body.getAttribute('sid');
  this.oDbg.log("got sid: "+this._sid,2);

  // get attributes from response body
  if (body.getAttribute('polling'))
    this._min_polling = body.getAttribute('polling');

  if (body.getAttribute('inactivity'))
    this._inactivity = body.getAttribute('inactivity');

  if (body.getAttribute('requests'))
    this._setHold(body.getAttribute('requests')-1);
  this.oDbg.log("set hold to " + this._getHold(),2);

  if (body.getAttribute('ver'))
    this._bosh_version = body.getAttribute('ver');

  if (body.getAttribute('maxpause'))
    this._pause = Math.min(body.getAttribute('maxpause') || 0, JSJACHBC_MAXPAUSE);

  // must be done after response attributes have been collected
  this.setPollInterval(this._timerval);

  /* start sending from queue for not polling connections */
  this._connected = true;

  this._inQto = setInterval(JSJaC.bind(this._checkInQ, this),
                            JSJAC_CHECKINQUEUEINTERVAL);
  this._interval= setInterval(JSJaC.bind(this._checkQueue, this),
                              JSJAC_CHECKQUEUEINTERVAL);

  /* wait for initial stream response to extract streamid needed
   * for digest auth
   */
  this._getStreamID(slot);
};

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._parseResponse = function(req) {
  if (!this.connected() || !req)
    return null;

  var r = req.r; // the XmlHttpRequest

  try {
    if (r.status == 404 || r.status == 403) {
      // connection manager killed session
      this._abort();
      return null;
    }

    if (r.status != 200 || !r.responseXML) {
      this._errcnt++;
      var errmsg = "invalid response ("+r.status+"):\n" + r.getAllResponseHeaders()+"\n"+r.responseText;
      if (!r.responseXML)
        errmsg += "\nResponse failed to parse!";
      this.oDbg.log(errmsg,1);
      if (this._errcnt > JSJAC_ERR_COUNT) {
        // abort
        this._abort();
        return null;
      }
      this.oDbg.log("repeating ("+this._errcnt+")",1);
     
      this._setStatus('proto_error_fallback');
     
      // schedule next tick
      setTimeout(JSJaC.bind(this._resume, this), this.getPollInterval());
     
      return null;
    }
  } catch (e) {
    this.oDbg.log("XMLHttpRequest error: status not available", 1);
	this._errcnt++;
	if (this._errcnt > JSJAC_ERR_COUNT) {
	  // abort
	  this._abort();
	} else {
	  this.oDbg.log("repeating ("+this._errcnt+")",1);
     
	  this._setStatus('proto_error_fallback');
     
	  // schedule next tick
	  setTimeout(JSJaC.bind(this._resume, this), this.getPollInterval()); 
    }
    return null;
  }

  if (!r.responseText) {
    // Hodges - we get blank responses when the proxy resets, we can recover!
    this.oDbg.log("empty response", 1);
    this._errcnt++;
    if (this._errcnt > JSJAC_ERR_COUNT) {
      this._abort();
    } else {
      this.oDbg.log("retrying (" + this._errcnt + ")", 1);

	  this._setStatus('proto_error_fallback');
     
	  // schedule next tick
	  setTimeout(JSJaC.bind(this._resume, this), this.getPollInterval()); 
    }
    return null;
  }

  var body = r.responseXML.documentElement;
  if (!body || body.tagName != 'body' || body.namespaceURI != 'http://jabber.org/protocol/httpbind') {
    this.oDbg.log("invalid response:\n" + r.responseText,1);

    clearTimeout(this._timeout); // remove timer
    clearInterval(this._interval);
    clearInterval(this._inQto);

    this._connected = false;
    this.oDbg.log("Disconnected.",1);
    this._handleEvent('ondisconnect');

    this._setStatus('internal_server_error');
    this._handleEvent('onerror',
					  JSJaCError('500','wait','internal-server-error'));

    return null;
  }

  if (typeof(req.rid) != 'undefined' && this._last_requests[req.rid]) {
    if (this._last_requests[req.rid].handled) {
      this.oDbg.log("already handled "+req.rid,2);
      return null;
    } else
      this._last_requests[req.rid].handled = true;
  }


  // Check for errors from the server
  if (body.getAttribute("type") == "terminate") {
    this.oDbg.log("session terminated:\n" + r.responseText,1);

    clearTimeout(this._timeout); // remove timer
    clearInterval(this._interval);
    clearInterval(this._inQto);

    if (body.getAttribute("condition") == "remote-stream-error")
      if (body.getElementsByTagName("conflict").length > 0)
        this._setStatus("session-terminate-conflict");
    this._handleEvent('onerror',JSJaCError('503','cancel',body.getAttribute('condition')));
    this._connected = false;
    this.oDbg.log("Disconnected.",1);
    this._handleEvent('ondisconnect');
    return null;
  }

  // no error
  this._errcnt = 0;
  return r.responseXML.documentElement;
};

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._reInitStream = function(to,cb,arg) {
  /* [TODO] we can't handle 'to' here as this is not (yet) supported
   * by the protocol
   */

  // tell http binding to reinit stream with/before next request
  this._reinit = true;
  cb.call(this,arg); // proceed with next callback

  /* [TODO] make sure that we're checking for new stream features when
   * 'cb' finishes
   */
};

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._resume = function() {
  /* make sure to repeat last request as we can be sure that
   * it had failed (only if we're not using the 'pause' attribute
   */

	/*
		Hodges - 2/4/10
		Not sure why jsjac wants to repeat a request if the pause is zero, this just seems wrong.
		So i disabled it...
		if (this._pause == 0 && this._rid >= this._last_rid)
			this._rid = this._last_rid-1;
	*/

  this._process();
};

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._setHold = function(hold)  {
  if (!hold || isNaN(hold) || hold < 0)
    hold = 0;
  else if (hold > JSJACHBC_MAX_HOLD)
    hold = JSJACHBC_MAX_HOLD;
  this._hold = hold;
  return this._hold;
};

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._setupRequest = function(async) {
  var req = new Object();
  var r = XmlHttp.create();
  try {
    r.open("POST",this._httpbase,async);
    r.setRequestHeader('Content-Type','text/xml; charset=utf-8');
  } catch(e) { this.oDbg.log(e,1); }
  req.r = r;
  this._rid++;
  req.rid = this._rid;
  return req;
};

/**
 * @private
 */
JSJaCHttpBindingConnection.prototype._suspend = function() {
  if (this._pause == 0)
    return; // got nothing to do

  var slot = this._getFreeSlot();
  // Intentionally synchronous
  this._req[slot] = this._setupRequest(false);

  var reqstr = "<body pause='"+this._pause+"' xmlns='http://jabber.org/protocol/httpbind' sid='"+this._sid+"' rid='"+this._rid+"'";
  if (JSJAC_HAVEKEYS) {
    reqstr += " key='"+this._keys.getKey()+"'";
    if (this._keys.lastKey()) {
      this._keys = new JSJaCKeys(hex_sha1,this.oDbg);
      reqstr += " newkey='"+this._keys.getKey()+"'";
    }

  }
  reqstr += ">";

  while (this._pQueue.length) {
    var curNode = this._pQueue[0];
    reqstr += curNode;
    this._pQueue = this._pQueue.slice(1,this._pQueue.length);
  }

  //reqstr += "<presence type='unavailable' xmlns='jabber:client'/>";
  reqstr += "</body>";

  this.oDbg.log("Disconnecting: " + reqstr,4);
  this._req[slot].r.send(reqstr);
};
/**
 * @fileoverview All stuff related to HTTP Polling
 * @author Stefan Strigler steve@zeank.in-berlin.de
 * @version $Revision: 452 $
 */

/**
 * Instantiates an HTTP Polling session
 * @class Implementation of {@link
 * http://www.xmpp.org/extensions/xep-0025.html HTTP Polling}
 * @extends JSJaCConnection
 * @constructor
 */
function JSJaCHttpPollingConnection(oArg) {
  /**
   * @ignore
   */
  this.base = JSJaCConnection;
  this.base(oArg);

  // give hint to JSJaCPacket that we're using HTTP Polling ...
  JSJACPACKET_USE_XMLNS = false;
}
JSJaCHttpPollingConnection.prototype = new JSJaCConnection();

/**
 * Tells whether this implementation of JSJaCConnection is polling
 * Useful if it needs to be decided
 * whether it makes sense to allow for adjusting or adjust the
 * polling interval {@link JSJaCConnection#setPollInterval}
 * @return <code>true</code> if this is a polling connection,
 * <code>false</code> otherwise.
 * @type boolean
 */
JSJaCHttpPollingConnection.prototype.isPolling = function() { return true; };

/**
 * @private
 */
JSJaCHttpPollingConnection.prototype._getFreeSlot = function() {
  if (typeof(this._req[0]) == 'undefined' ||
      typeof(this._req[0].r) == 'undefined' ||
      this._req[0].r.readyState == 4)
    return 0;
  else
    return -1;
};

/**
 * @private
 */
JSJaCHttpPollingConnection.prototype._getInitialRequestString = function() {
  var reqstr = "0";
  if (JSJAC_HAVEKEYS) {
    this._keys = new JSJaCKeys(b64_sha1,this.oDbg); // generate first set of keys
    key = this._keys.getKey();
    reqstr += ";"+key;
  }
  var streamto = this.domain;
  if (this.authhost)
    streamto = this.authhost;

  reqstr += ",<stream:stream to='"+streamto+"' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'";
  if (this.authtype == 'sasl' || this.authtype == 'saslanon')
    reqstr += " version='1.0'";
  reqstr += ">";
  return reqstr;
};

/**
 * @private
 */
JSJaCHttpPollingConnection.prototype._getRequestString = function(raw, last) {
  var reqstr = this._sid;
  if (JSJAC_HAVEKEYS) {
    reqstr += ";"+this._keys.getKey();
    if (this._keys.lastKey()) {
      this._keys = new JSJaCKeys(b64_sha1,this.oDbg);
      reqstr += ';'+this._keys.getKey();
    }
  }
  reqstr += ',';
  if (raw)
    reqstr += raw;
  while (this._pQueue.length) {
    reqstr += this._pQueue[0];
    this._pQueue = this._pQueue.slice(1,this._pQueue.length);
  }
  if (last)
    reqstr += '</stream:stream>';
  return reqstr;
};

/**
 * @private
 */
JSJaCHttpPollingConnection.prototype._getStreamID = function() {
  if (this._req[0].r.responseText == '') {
    this.oDbg.log("waiting for stream id",2);
    this._timeout = setTimeout(JSJaC.bind(this._sendEmpty, this),1000);
    return;
  }

  this.oDbg.log(this._req[0].r.responseText,4);

  // extract stream id used for non-SASL authentication
  if (this._req[0].r.responseText.match(/id=[\'\"]([^\'\"]+)[\'\"]/))
    this.streamid = RegExp.$1;
  this.oDbg.log("got streamid: "+this.streamid,2);

  var doc;

  try {
    var response = this._req[0].r.responseText;
    if (!response.match(/<\/stream:stream>\s*$/))
      response += '</stream:stream>';

    doc = XmlDocument.create("doc");
    doc.loadXML(response);
    if (!this._parseStreamFeatures(doc))
      return;
  } catch(e) {
    this.oDbg.log("loadXML: "+e.toString(),1);
  }

  this._connected = true;

  if (this.register)
    this._doInBandReg();
  else
    this._doAuth();

  this._process(this._timerval); // start polling
};

/**
 * @private
 */
JSJaCHttpPollingConnection.prototype._getSuspendVars = function() {
  return new Array();
};

/**
 * @private
 */
JSJaCHttpPollingConnection.prototype._handleInitialResponse = function() {
  // extract session ID
  this.oDbg.log(this._req[0].r.getAllResponseHeaders(),4);
  var aPList = this._req[0].r.getResponseHeader('Set-Cookie');
  aPList = aPList.split(";");
  for (var i=0;i<aPList.length;i++) {
    aArg = aPList[i].split("=");
    if (aArg[0] == 'ID')
      this._sid = aArg[1];
  }
  this.oDbg.log("got sid: "+this._sid,2);

  /* start sending from queue for not polling connections */
  this._connected = true;

  this._interval= setInterval(JSJaC.bind(this._checkQueue, this),
                              JSJAC_CHECKQUEUEINTERVAL);
  this._inQto = setInterval(JSJaC.bind(this._checkInQ, this),
                            JSJAC_CHECKINQUEUEINTERVAL);

  /* wait for initial stream response to extract streamid needed
   * for digest auth
   */
  this._getStreamID();
};

/**
 * @private
 */
JSJaCHttpPollingConnection.prototype._parseResponse = function(r) {
  var req = r.r;
  if (!this.connected())
    return null;

  /* handle error */
  // proxy error (!)
  if (req.status != 200) {
    this.oDbg.log("invalid response ("+req.status+"):" + req.responseText+"\n"+req.getAllResponseHeaders(),1);

    this._setStatus('internal_server_error');

    clearTimeout(this._timeout); // remove timer
    clearInterval(this._interval);
    clearInterval(this._inQto);
    this._connected = false;
    this.oDbg.log("Disconnected.",1);
    this._handleEvent('ondisconnect');
    this._handleEvent('onerror',JSJaCError('503','cancel','service-unavailable'));
    return null;
  }

  this.oDbg.log(req.getAllResponseHeaders(),4);
  var sid, aPList = req.getResponseHeader('Set-Cookie');

  if (aPList == null)
    sid = "-1:0"; // Generate internal server error
  else {
    aPList = aPList.split(";");
    var sid;
    for (var i=0;i<aPList.length;i++) {
      var aArg = aPList[i].split("=");
      if (aArg[0] == 'ID')
        sid = aArg[1];
    }
  }

  // http polling component error
  if (typeof(sid) != 'undefined' && sid.indexOf(':0') != -1) {
    switch (sid.substring(0,sid.indexOf(':0'))) {
    case '0':
      this.oDbg.log("invalid response:" + req.responseText,1);
      break;
    case '-1':
      this.oDbg.log("Internal Server Error",1);
      break;
    case '-2':
      this.oDbg.log("Bad Request",1);
      break;
    case '-3':
      this.oDbg.log("Key Sequence Error",1);
      break;
    }

    this._setStatus('internal_server_error');

    clearTimeout(this._timeout); // remove timer
    clearInterval(this._interval);
    clearInterval(this._inQto);
    this._handleEvent('onerror',JSJaCError('500','wait','internal-server-error'));
    this._connected = false;
    this.oDbg.log("Disconnected.",1);
    this._handleEvent('ondisconnect');
    return null;
  }

  if (!req.responseText || req.responseText == '')
    return null;

  try {
    var response = req.responseText.replace(/\<\?xml.+\?\>/,"");
    if (response.match(/<stream:stream/))
        response += "</stream:stream>";
    var doc = JSJaCHttpPollingConnection._parseTree("<body>"+response+"</body>");

    if (!doc || doc.tagName == 'parsererror') {
      this.oDbg.log("parsererror",1);

      doc = JSJaCHttpPollingConnection._parseTree("<stream:stream xmlns:stream='http://etherx.jabber.org/streams'>"+req.responseText);
      if (doc && doc.tagName != 'parsererror') {
        this.oDbg.log("stream closed",1);

        if (doc.getElementsByTagName('conflict').length > 0)
          this._setStatus("session-terminate-conflict");
			
        clearTimeout(this._timeout); // remove timer
        clearInterval(this._interval);
        clearInterval(this._inQto);
        this._handleEvent('onerror',JSJaCError('503','cancel','session-terminate'));
        this._connected = false;
        this.oDbg.log("Disconnected.",1);
        this._handleEvent('ondisconnect');
      } else
        this.oDbg.log("parsererror:"+doc,1);
		
      return doc;
    }

    return doc;
  } catch (e) {
    this.oDbg.log("parse error:"+e.message,1);
  }
  return null;;
};

/**
 * @private
 */
JSJaCHttpPollingConnection.prototype._reInitStream = function(to,cb,arg) {
  this._sendRaw("<stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client' to='"+to+"' version='1.0'>",cb,arg);
};

/**
 * @private
 */
JSJaCHttpPollingConnection.prototype._resume = function() {
  this._process(this._timerval);
};

/**
 * @private
 */
JSJaCHttpPollingConnection.prototype._setupRequest = function(async) {
  var r = XmlHttp.create();
  try {
    r.open("POST",this._httpbase,async);
    if (r.overrideMimeType)
      r.overrideMimeType('text/plain; charset=utf-8');
    r.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
  } catch(e) { this.oDbg.log(e,1); }

  var req = new Object();
  req.r = r;
  return req;
};

/**
 * @private
 */
JSJaCHttpPollingConnection.prototype._suspend = function() {};

/*** [static] ***/

/**
 * @private
 */
JSJaCHttpPollingConnection._parseTree = function(s) {
  try {
    var r = XmlDocument.create("body","foo");
    if (typeof(r.loadXML) != 'undefined') {
      r.loadXML(s);
      return r.documentElement;
    } else if (window.DOMParser)
      return (new DOMParser()).parseFromString(s, "text/xml").documentElement;
  } catch (e) { }
  return null;
};
/**
 * @fileoverview Magic dependency loading. Taken from script.aculo.us
 * and modified to break it.
 * @author Stefan Strigler steve@zeank.in-berlin.de 
 * @version $Revision: 456 $
 */

var JSJaC = {
  Version: '$Rev: 456 $',
  require: function(libraryName) {
    // inserting via DOM fails in Safari 2.0, so brute force approach
    document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
  },
  load: function() {
    var includes =
    ['xmlextras',
     'jsextras',
     'crypt',
     'JSJaCConfig',
     'JSJaCConstants',
     'JSJaCCookie',
     'JSJaCJSON',
     'JSJaCJID',
     'JSJaCBuilder',
     'JSJaCPacket',
     'JSJaCError',
     'JSJaCKeys',
     'JSJaCConnection',
     'JSJaCHttpPollingConnection',
     'JSJaCHttpBindingConnection',
     'JSJaCConsoleLogger'
     ];
    var scripts = document.getElementsByTagName("script");
    var path = './';
    for (var i=0; i<scripts.length; i++) {
      if (scripts.item(i).src && scripts.item(i).src.match(/JSJaC\.js$/)) {
        path = scripts.item(i).src.replace(/JSJaC.js$/,'');
        break;
      }
    }
    for (var i=0; i<includes.length; i++)
      this.require(path+includes[i]+'.js');
  },
  bind: function(fn, obj, arg) {
    return function() {
      if (arg)
        fn.apply(obj, arg);
      else
        fn.apply(obj);
    };
  }
};

if (typeof JSJaCConnection == 'undefined')
  JSJaC.load();
//external jsjac.js
//include eCarList
var SmartChat = eCarList.namespace('eCarList.App.SmartChat');
SmartChat.jQuery = eCarList.jQuery;

SmartChat.DEBUG_ENABLED = false || (document.cookie.search(/lol_debug/i) >= 0);

SmartChat.ASSET_BASE = ''; 
SmartChat.CSS_BASE = SmartChat.ASSET_BASE + '/css/smartchat';
SmartChat.IMAGE_BASE = SmartChat.ASSET_BASE + '/images/smartchat';
SmartChat.JS_BASE = SmartChat.ASSET_BASE + '/js/smartchat';

SmartChat.BIND_PATH = '/jabber/';
SmartChat.ADMIN_DOMAIN = 'ecarlist.com';
SmartChat.VISITOR_DOMAIN = 'visitor.ecarlist.com';
SmartChat.SYSTEM_JID = new JSJaCJID('system@system.ecarlist.com');

/*
SmartChat.ASSET_BASE = 'http://assets.ecarlist.me/devel'; 
SmartChat.CSS_BASE = SmartChat.ASSET_BASE + '/css/dohodges/smartchat';
SmartChat.IMAGE_BASE = SmartChat.ASSET_BASE + '/images/dohodges/smartchat';
SmartChat.JS_BASE = SmartChat.ASSET_BASE + '/js/dohodges/smartchat';
*/

/*
	Enumeration: SmartChat.Show
	
	Enumeration of XMPP 'show' types.
*/
SmartChat.Show = {
	NONE: '',
	AWAY: 'away',
	CHAT: 'chat',
	DO_NOT_DISTURB: 'dnd',
	EXTENDED_AWAY: 'xa'
};

/*
	Enumeration: SmartChat.MessageType

	Enumeration of SmartChat message types.
*/
SmartChat.MessageType = {
	DECLINE: 'decline',
	INVITE: 'invite',
	MESSAGE: 'message',
	STATUS: 'status'
};

/*
	Enumeration: SmartChat.MessageType

	Enumeration of SmartChat room types.
*/
SmartChat.RoomType = {
	TEAM: 'team',
	PRIVATE: 'private'
};

/*
	Class: jQuery

	SmartChat jQuery extensions are housed in $.ecl and $.fn.ecl in order to avoid corrupting
	the client's jQuery lib.
*/
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	$.fn.ecl = function() {
		var _$ = this;
		return {

			/*
				Method: $.fn.ecl().horizontal_center

				Horizontally centers all matched elements.

				Parameters:
					offset - [optional] offset from horizontal center, defaults to 0
			*/
			horizontal_center: function(offset) {
				offset = offset || 0;
				return _$.each(function(index, el) {
					var el_width = $(el).outerWidth();
					var x = ($(window).width() - el_width) / 2 + offset;
					$(el).css({ right: x });
				});
			},


			/*
				Method: $.fn.ecl().vertical_center

				Vertically centers all matched elements.

				Parameters:
					offset - [optional] offset from vertical center, defaults to 0
			*/
			vertical_center: function(offset) {
				offset = offset || 0;
				return _$.each(function(index, el) {
					var el_height = $(el).outerHeight();
					var y = ($(window).height() - el_height) / 2 + offset;
					$(el).css({ top: y });
				});
			},


			/*
				Method: $.fn.ecl().center

				Centers all matched elements.

				Parameters:
					horizontal_offset - [optional] offset from horizontal center, defaults to 0
					vertical_offset   - [optional] offset from vertical center, defaults to 0
			*/
			center: function(horizontal_offset, vertical_offset) {
				return _$.each(function(index, el) {
					$(el).ecl().horizontal_center(horizontal_offset);
					$(el).ecl().vertical_center(vertical_offset);
				});
			},

			/*
				Method: $.fn.ecl().attr_set

				Creates an Object whose properties contain the values of the attributes named in the argument list.

				Parameters:
					attributes... - names of the attributes to retrieve

				Returns:
					Object containing defined attribute values.
			*/
			attr_set: function() {
				var attr_set = { };
				var el = _$.get(0);
				if (el) {
					for (var i = 0; i < arguments.length; i++) {
						var name = arguments[i].toString();
						var value = el.getAttribute(name);
						if (value) {
							attr_set[name] = value;
						}
					}
				}
				return attr_set;
			},

			/*
				Method: $.fn.ecl().disabled

				Disables an element by setting the disabled attribute and adding a disabled class. If no
				'disabled' argument is given it simply determines whether or not the first matched element
				is disabled.
			*/
			disabled: function(disabled) {
				if (typeof disabled === 'undefined') {
					return _$.attr('disabled');
				} else {
					_$.each(function() {
						if (disabled) {
							$(this).addClass('disabled');
							if (this.tagName.match(/button|input|select|textarea/i)) {
								$(this).attr('disabled', 'disabled');
							}
						} else {
							$(this).removeAttr('disabled').removeClass('disabled');
						}
					});
				}
			},

			/*
				Method: $.fn.ecl().select_class
				
				Adds 'selected_class' to queried elements and removes all classes from 'class_set'.
			*/
			select_class: function(class_set, selected_class) {
				_$.each(function() {
					for (var key in class_set) {
						if (class_set[key]) {
							$(this).removeClass(class_set[key]);
						}
					}
					if (selected_class) {
						$(this).addClass(selected_class);
					}
				});
			}
			
		};

	};

});

eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	$.ecl = { };

	$.ecl.string = {
		/*
			Function: $.ecl.escape_class
		
			Escapes some value so it can be used as a css class. Replaces funky chars with _.

			Parameters:
				value - string to be escaped
			
			Returns:
				escaped string
		*/
		escape_class: function(value) {
			return (value) ? value.replace(/\W/g, '_') : value;
		},

		/*
			Function: $.ecl.strip_html

			Strips HTML tags from the given text.
		*/
		strip_html: function(text) {
			return text.replace(/<\/?[A-Z][^>]*>/ig, '');
		},

		/*
			Function: $.ecl.first_name
			
			Retrieves the first name from the given string.

			Parameters:
				name - full/partial name

			Returns:
				guestimated first name
		*/
		first_name: function(name) {
			if (name) {
				// naive as a mofo
				var name_tokens = name.split(/\s+/);
				return name_tokens[0];
			}
			return name;
		}
	};
});

/*
	Class: SmartChat.Form

	Form utilities.
*/
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	SmartChat.Form = {

		/*
			Method: sanitize
		*/
		sanitize: function(value, fn) {
			if (value && typeof value === 'string') {
				value = $.trim(value);
				return (fn) ? fn(value) : value;
			} else if (value && typeof value === 'object') {
				value.each(function() {
					var val = $.trim($(this).val());
					val = (fn) ? fn(val) : val;
					$(this).val(val);
				});
			}
		},

		/*
			Method: sanitize_name

			Cleans up strings that are supposed to represent names.

			Parameters:
				name - string or form field to sanitize

			Returns:
				cleaned up string
		*/
		sanitize_name: function(name) {
			return SmartChat.Form.sanitize(name, function(value) {
				return value.replace(/[^\w\. \-]+/g, ' ').replace(/^[^A-Z]+/i, '');
			});
		},

		/*
			Method: validate

			Validates that the given input matches the given pattern. If the input is a form element which has
			an invalid value, the 'invalid' class is added to the form element, or removed if the form element
			value is valid.

			Parameters:
				value  - string or jquery consisting of elements that need to be validated
				regexp - pattern to use for validation

			Returns:
				True if the input element value or string matches the given regexp, false otherwise.
		*/
		validate: function(value, regexp) {
			if (value && typeof value === 'string') {
				return $.trim(value).match(regexp);
			} else if (value && typeof value === 'object') {
				var valid = true;
				value.each(function() {
					var val = $.trim($(this).val());
					if (!val.match(regexp)) {
						$(this).addClass('invalid');
						valid = false;
					} else {
						$(this).removeClass('invalid');
					}
				});
				return valid;
			}
			return false;
		},

		validate_email: function(value) {
			return SmartChat.Form.validate(value, /^[A-Z0-9\._%+\-]+@[A-Z0-9\.\-]+\.[A-Z]{2,4}$/i);
		},

		validate_name: function(value) {
			return SmartChat.Form.validate(value, /^[A-Z][\w\. \-]*/i);
		},

		validate_phone: function(value) {
			return SmartChat.Form.validate(value, /^(1\s*[\-\/\.]?)?(\((\d{3})\)|(\d{3}))\s*[\-\/\.]?\s*(\d{3})\s*[\-\/\.]?\s*(\d{4})\s*(([xX]|[eE][xX][tT])\.?\s*(\d+))*$/);
		},
	
		validate_text: function(value) {
			return SmartChat.Form.validate(value, /^[A-Z]/i);
		}

	};

});
//include eCarList
/*jslint newcap: false*/
eCarList.namespace('eCarList', function(eCarList, $) {

	/*
		Class: eCarList.Class

		Utility for simplifying inheritance in javascript.

		Based on John Resig's simple inheritance model:
			http://ejohn.org/blog/simple-javascript-inheritance/

		Synopsis:
			Inheriting from Class & Class children:
				var Person = eCarList.Class.extend({
					init: function(name) {
						this._super();
					}
				});

				var Ninja = Person.extend({
					init: function(name, specialty) {
						this._super(name);
					}
				});

	*/

	var _initializing = false;
	
	var _static = {
		next_id: 1
	};

	/*
		Constructor: Class
		
		Class base class constructor.
	*/
	eCarList.Class = function() {
		this.klass = eCarList.Class;
	};
	
	eCarList.Class._mixins = [];

	/*
		Method: mix

		Copies attributes from mixin modules to this klass. Once an attribute has been defined
		it will never be overridden. Object attributes (including prototype) are copied deeply.

		Parameters:
			mixins... - mixin modules

		Returns:
			klass constructor
	*/
	eCarList.Class.mix = function() {
		this._mixins = this._mixins || [];
		for (var i = 0; i < arguments.length; i++) {
			var mixin = arguments[i];
			eCarList.assert(mixin && typeof mixin === 'object', 'invalid mixin: ' + mixin);
			this._mixins.push(mixin);
			_deep_copy(this, mixin);
		}
		return this;
	};

	/*
		Method: extend

		Creates a new class that inherits from the eCarList.Class base class.

		Parameters:
			subclass_impl - subclass implementation

		Returns:
			subclass constructor.
	*/
	eCarList.Class.extend = function(subclass_impl) {
		
		var base = this.prototype;

		// The dummy class constructor
		var Subclass = function() {
			
			if (!_initializing) {
				this._id = _static.next_id++;

				// all construction is actually done in the init method
				if (this.init) {
					this.init.apply(this, arguments);
				}
			}

		};

		// inherit base class implementation & override with subclass impl
		// (must deep copy in order to clone object & array instances)
		$.extend(true, Subclass, this, subclass_impl);

		/// rebuild prototype to get instanceof to work properly		
		// Instantiate a base class (but only create the instance don't run the init constructor)
		_initializing = true;
		var prototype = new this();
		_initializing = false;

		// inherit subclass prototype impl (if it exists)
		if (subclass_impl.prototype && typeof subclass_impl.prototype === 'object') {
			var sub_prototype = subclass_impl.prototype;
			$.extend(true, prototype, sub_prototype);

			// enable _super() for overridden methods
			for (var attr in sub_prototype) {
				if ($.isFunction(base[attr]) && $.isFunction(sub_prototype[attr])) {
					// subclass override
					prototype[attr] = (function(method) {
						return function() {
							// point _super at superclass impl
							var prev_super = this._super;
							this._super = base[method];
							try {
								// invoke subclass overridden impl
								return sub_prototype[method].apply(this, arguments);
							} finally {
								// restore _super before returning
								this._super = prev_super;
							}
						};
					}(attr));
				}
			}
		}
	
		// alias klass so we can easily find out what this is
		prototype.klass = Subclass;

		// associate prototype with Subclass
		Subclass.prototype = prototype;

		// Enforce the constructor to be what we expect
		Subclass.constructor = Subclass;

		// point base at this class's prototype
		Subclass.base = base;

		// _mixins has to be manually copied since the deep copy will clone the mixins
		// and we need the originals to get mix_of to work
		Subclass._mixins = this._mixins.concat();
		
		return Subclass;
	};

	eCarList.Class.prototype = {

		/*
			Method: mix_of

			Determines if klass has been mixed with the given mixin.

			Parameters:
				mixin - mixin to check

			Returns:
				True if klass has been mixed with mixin, false otherwise.
		*/
		mix_of: function(mixin) {
			return ($.inArray(mixin, this.klass._mixins) >= 0);
		}

	};

	/*
		Method: _deep_copy

		Deeply copies attributes from source object to destination object. Unlike $.extend,
		_deep_copy will not override existing attributes.

		Parameters:
			dest - destination object
			src  - source object

		Returns:
			destination object
	*/
	var _deep_copy = function(dest, src) {
		for (var attr in src) {
			if (typeof dest[attr] === 'undefined') {
				if (typeof src[attr] === 'object') {
					dest[attr] = _deep_copy($.isArray(src[attr]) ? [] : { }, src[attr]);
				} else {
					dest[attr] = src[attr];
				}
			} else if (dest[attr] && (typeof dest[attr]).match(/(object|function)/)
					   && src[attr] && (typeof src[attr]).match(/object|function/)
					   && src[attr] !== Function.prototype[attr]) {
				_deep_copy(dest[attr], src[attr]);
			}
		}
		return dest;
	};

});
/*jslint newcap: true*/
//include eCarList.Module
eCarList.namespace('eCarList.Util', function(Util, $) {

	/*
		Structure: eCarList.Util.Bindable

		Bindable is a wrapper around jQuery's event dispatching functionality that makes it easier to
		create observable classes.
	*/
	Util.Bindable = eCarList.Module.extend({
		
		/*
			Property: Event
				The Event object exists on all Bindable classes and provides a place to define the events that
				are triggered by this class.
		*/
		Event: { },

		prototype: {

			/*
				Method: bind
			
				Binds listeners to events generated by this instance's elements witih jQuery.

				Parameters:
					selector - [optional] jQuery selector to use, binds to _element by default
					type     - event type
					fn       - callback function
					scope    - [optional] defines value of "this" in callback, defaults to this instance

				Returns:
					bound handler
			*/
			bind: function() {
				return _bind.call(this, 'bind', arguments);
			},
			
			/*
				Method: bind_once
			
				Binds listeners for a single occurrence of an event generated by this instance's
				elements with jQuery.

				Parameters:
					selector - [optional] jQuery selector to use, binds to _element by default
					type     - event type
					fn       - callback function
					scope    - [optional] defines value of "this" in callback, defaults to this instance

				Returns:
					bound handler
			*/
			bind_once: function() {
				return _bind.call(this, 'one', arguments);
			},

			/*
				Method: bind_to
				
				Binds this Bindable instance to the specified event type on another Bindable instance. All bindings
				created with bind_to or bind_to_once are unbound when this instance is disposed.
				
				Parameters:
					bindable - bindable instance to bind to
					type     - event type
					fn       - callback function
					scope    - [optional] defines value of "this" in callback, defaults to this instance
			*/
			bind_to: function(bindable, type, fn, scope) {
				var binding = {
					bindable: bindable,
					type: type,
					handler: (bindable.bind)
						? bindable.bind(type, fn, scope || this)
						: _bind_handler.call(this, $(bindable), 'bind', type, fn, scope || this)
				};

				this._bound_to = this._bound_to || [];
				this._bound_to.push(binding);
			},

			/*
				Method: bind_once_to
				
				Binds this Bindable instance to the specified event type on another Bindable instance. All bindings
				created with bind_to or bind_once_to are unbound when this instance is disposed.

				Parameters:
					bindable - bindable instance to bind to
					type     - event type
					fn       - callback function
					scope    - [optional] defines value of "this" in callback, defaults to this instance
			*/
			bind_once_to: function(bindable, type, fn, scope) {
				var binding = {
					bindable: bindable,
					type: type,
					handler: (bindable.bind_once) 
						? bindable.bind_once(type, fn, scope || this)
						: _bind_handler.call(this, $(bindable), 'one', type, fn, scope || this)
				};
				this._bound_to = this._bound_to || [];
				this._bound_to.push(binding);
			},

			/*
				Method: unbind
			
				Parameters:
					selector - [optional] jQuery selector to use, binds to _element by default
					type     - event type
					fn       - [optional] callback function
			*/
			unbind: function() {
				// parse args
				var selector, type, fn;
				if (typeof arguments[1]  === 'string') {
					selector = arguments[0];
					type = arguments[1];
					fn = arguments[2];
				} else {
					type = arguments[0];
					fn = arguments[1];
				}
				
				// unbind callback(s)
				var query = (selector) ? $(selector, this._element) : $(this._element);
				query.unbind(_event_type.call(this, type), fn);
			},

			/*
				Method: unbind_from
				
				Parameters:
					bindable - bindable instance to unbind from
					type     - [optional] event type
					fn       - [optional] callback function
			*/
			unbind_from: function(bindable, type, fn) {
				if (this._bound_to) {
					for (var i = 0; i < this._bound_to.length;) {
						var binding = this._bound_to[i];
						if (binding.bindable === bindable && (!type || binding.type === type)
							&& (!fn || binding.handler === fn)) {
							_unbind.call(this, binding);
							this._bound_to.splice(i, 1);
						} else {
							i++;
						}
					}
				}
			},

			/*
				Method: unbind_all
				
				Unbinds all of this instance's event listeners.
			*/
			unbind_all: function() {
				this.unbind('');
				$(this._element).unbind(_event_type.call(this, ''));
				
				if (this._bound_to) {
					for (var i = 0; i < this._bound_to.length; i++) {
						var binding = this._bound_to[i];
						_unbind.call(this, binding);
					}
					this._bound_to.length = 0;
				}
			},
			
			/*
				Method: trigger
				
				Triggers custom events from this instance's _element with jQuery.
				
				Parameters:
					type - event type
					data - custom event data
			*/
			trigger: function(type, data) {
				$(this._element).trigger(_event_type.call(this, type), data);
			}

		}

	});

	var _bind = function(bind_type, args)  {
		// parse args
		var selector, type, fn, scope;
		if (typeof args[1]  === 'string') {
			selector = args[0];
			type = args[1];
			fn = args[2];
			scope = args[3];
		} else {
			type = args[0];
			fn = args[1];
			scope = args[2];
		}
		
		// bind handler
		var query = (selector) ? $(selector, this._element) : $(this._element);
		return _bind_handler.call(this, query, bind_type, type, fn, scope);
	};

	var _bind_handler = function(query, bind_type, event_type, fn, scope) {
		var self = this;
		var handler = function(event, data) {
			fn.call(scope || self, event, data);
		};
		query[bind_type](_event_type.call(this, event_type), this, handler);
		
		return handler;
	};

	var _unbind = function(binding) {
		if (binding.bindable.unbind) {
			binding.bindable.unbind(binding.type, binding.handler);
		} else {
			$(binding.bindable).unbind(binding.type, binding.handler);
		}
	};

	var _event_type = function(type) {
		return (type + '.klass_' + this._id);
	};

});
//include eCarList.Module
eCarList.namespace('eCarList.Util', function(Util, $) {

	/*
		Structure: eCarList.Util.Configurable

		The Configurable mixin provides attribute management functionality to classes that have complex
		configurations. It allows a class to define default values for attributes and automatically
		create getter/setter functions for each attribute. If the class is Bindable, it will trigger events
		whenver a configuration attribute is changed.
	*/
	Util.Configurable = eCarList.Module.extend({

		/*
			Property: Attribute
				The Attribute object hosts a Configurable class's configuration attribute definitions. Each
				attribute definition is a key/value pair with a key that defines the attribute name and an
				object value that defines the attributes options.

			Options:
				init      - the attribute's initial value
				init_fn   - an initialization function that returns the initial value for the attribute
							(takes presedence over init)
				required  - if true, an initial value must be defined for the attribute
				read_only - if true, only a getter method is created
		*/
		Attribute: { },
		
		/*
			Events:
				CONFIG_CHANGE - Fired when one of this classes's attributes change (if this class is Bindable)
		*/
		Event: {
			CONFIG_CHANGE: 'CONFIG_CHANGE'
		},

		prototype: {

			/*
				Constructor: init

				Default constructor for Configurable classes.

				Parameters:
					config - [optional] initial values for this instance's configuartion attributes.
			*/
			init: function(config) {
				this.init_config(config);
			},
			
			/*
				Method: init_config

				Initializes the configuration of this instance. Must be invoked before reading/writing any
				of the classe's attributes. 

				Parameters:
					config - [optional] initial values for this instance's configuration attributes.
			*/
			init_config: function(config) {
				if (this._config) {
					return; // already initialized
				}
				this._config = { };
				config = config || { };

				for (var attr in this.klass.Attribute) {
					if (this.klass.Attribute.hasOwnProperty(attr)) {
						var attr_def = this.klass.Attribute[attr];
						
						// determine initial value
						if (typeof config[attr] !== 'undefined') {
							this._config[attr] = config[attr];
						} else if (attr_def.init_fn) {
							this._config[attr] = attr_def.init_fn.call(this, config);
						} else if (typeof attr_def.init !== 'undefined') {
							this._config[attr] = attr_def.init;
						} else if (attr_def.required) {
							eCarList.assert(false, 'required attribute [' + attr + '] is undefined');
						}

						// attach getter & setter
						var get_method = 'get_' + attr;
						var set_method = 'set_' + attr;

						if (!this[get_method]) {
							this[get_method] = _create_getter(attr);
						}
						if (!attr_def.read_only && !this[set_method]) {
							this[set_method] = _create_setter(attr);
						}
					}
				}
			},

			/*
				Method: _get

				Retrieves value for configuration attribute. Classes that define their own getter method should
				use this method to retrieve configuation values.

				Parameters:
					attr - configuration attribute name.

				Returns:
					Configuration value.
			*/
			_get: function(attr) {
				eCarList.assert(this._config, 'This eCarList.Util.Configurable has not been initialized');
				return this._config[attr];
			},

			/*
				Method: _set

				Sets value for a configuration attribute. Classes that define their own setter method should
				use this method to modify configuation values.

				Parameters:
					attr  - configuration attribute name
					value - configuration attribute value
			*/
			_set: function(attr, value) {
				eCarList.assert(this._config, 'This eCarList.Util.Configurable has not been initialized');
				var prev_value = this._config[attr];
				if (prev_value !== value) {
					this._config[attr] = value;
					if (this.mix_of(Util.Bindable)) {
						var change = {
							attr: attr,
							prev_value: prev_value,
							new_value: value
						};
						this.trigger(Util.Configurable.Event.CONFIG_CHANGE, change);
					}
				}
			}
		}
	});

	var _create_getter = function(attr) {
		return function() {
			return this._get(attr);
		};
	};

	var _create_setter = function(attr) {
		return function(value) {
			this._set(attr, value);
		};
	};
});
//include eCarList.Module
eCarList.namespace('eCarList.Util', function(Util, $) {

	/*
		Structure: eCarList.Util.Embeddable

		The Embeddable mixin provides multiple ways to embed and remove HTML that's associated 
		with an eCarList.Class. Once the HTML has been embedded, the $ method can be used to create
		jQuery objects within the context of the embedded HTML.
	*/
	Util.Embeddable = eCarList.Module.extend({

		/*
			HTML - The HTML content to embed.
		*/
		HTML: '<div></div>',

		prototype: {

			/*
				Method: $
			
				Provides a simple way to create a jQuery object for _element. If the optional selector argument is
				given, the jQuery object will use the selector to query within the context of  _element.

				Parameters:
					selector - [optional] jQuery selector to use

				Returns:
					jQuery object
			*/
			$: function(selector) {
				eCarList.assert(this._element, 'instance must be embedded before querying');
				return (selector) ? $(selector, this._element) : $(this._element);
			},

			/*
				Method: in_dom

				Determines whether this instance has been embedded into the DOM.

				Returns:
					True if instance has been embedded, false otherwise.
			*/
			in_dom: function() {
				return (this._element) ? true : false;
			},

			/*
				Method: append_to

				Appends HTML to container.

				Parameters:
					container - DOM element to append HTML to

				Returns:
					Embedded DOM element.
			*/
			append_to: function(container) {
				eCarList.assert(container && container.nodeType === 1, 'invalid container: ' + container);
				eCarList.assert(!this._element, 'instance already embedded');
				this._element = $(this.klass.HTML).appendTo(container).get(0);
				return this._element;
			},

			/*
				Method: prepend_to

				Prepends HTML to container.

				Parameters:
					container - DOM element to prepend HTML to

				Returns:
					Embedded DOM element.
			*/
			prepend_to: function(container) {
				eCarList.assert(container && container.nodeType === 1, 'invalid container: ' + container);
				eCarList.assert(!this._element, 'instance already embedded');
				this._element = $(this.klass.HTML).prependTo(container).get(0);
				return this._element;
			},

			/*
				Method: insert_after

				Inserts HTML after element.

				Parameters:
					element - DOM element to insert HTML after

				Returns:
					Embedded DOM element.
			*/
			insert_after: function(element) {
				eCarList.assert(container && container.nodeType === 1, 'invalid element: ' + element);
				eCarList.assert(!this._element, 'instance already embedded');
				this._element = $(this.klass.HTML).insertAfter(element).get(0);
				return this._element;
			},

			/*
				Method: insert_before

				Inserts HTML before element.

				Parameters:
					element - DOM element to insert HTML before

				Returns:
					Embedded DOM element.
			*/
			insert_before: function(element) {
				eCarList.assert(container && container.nodeType === 1, 'invalid element: ' + element);
				eCarList.assert(!this._element, 'instance already embedded');
				this._element = $(this.klass.HTML).insertBefore(element).get(0);
				return this._element;
			},
			
			/*
				Method: wrap

				Wrap element with HTML.

				Parameters:
					element - DOM element to insert HTML before

				Returns:
					Embedded DOM element.
			*/
			wrap: function(element) {
				eCarList.assert(container && container.nodeType === 1, 'invalid element: ' + element);
				eCarList.assert(!this._element, 'instance already embedded');
				this._element = $(this.klass.HTML).wrap(element).get(0);
				return this._element;
			},

			/*
				Method: wrap_inner

				Wrap element content with HTML.

				Parameters:
					element - DOM element to insert HTML before

				Returns:
					Embedded DOM element.
			*/
			wrap_inner: function(element) {
				eCarList.assert(container && container.nodeType === 1, 'invalid element: ' + element);
				eCarList.assert(!this._element, 'instance already embedded');
				this._element = $(this.klass.HTML).wrapInner(element).get(0);
				return this._element;
			},

			/*
				Method: remove

				Removes this instance's embedded content from the DOM.

				Returns:
					True if content is removed, false otherwise.
			*/
			remove: function() {
				if (this._element) {
					this.$().remove();
					delete this._element;
					return true;
				}
				return false;
			}

		}

	});

});
//include eCarList.Class
//include eCarList.Util.Bindable
//include eCarList.Util.Configurable
//include eCarList.Util.Embeddable
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {
	
	/*
		Class: SmartChat.UI
		
		Base class for all SmartChat UI components. UI also hosts static ui utility methods.
	*/
	SmartChat.UI = eCarList.Class.extend({

		/*
			Attributes:
				enabled        - enabled/disabled state of the component
				style_class    - css class for this component
				visible        - visibility of this component
				hide_animation - component's hide animation definition
				show_animation - component's show animation definition
		*/
		Attribute: {
			enabled: { init: true },
			style_class: { init: '' },
			visible: { init: true },
			hide_animation: { },
			show_animation: { }
		},

		escape_class: function(value) {
			return (value) ? value.replace(/\W/g, '_') : value;
		},

		prototype: {

			/*
				Constructor: init
				
				Parameters:
					config - [optional] initial values for this instance's configuration attributes
			*/
			init: function(config) {
				this.init_config(config);
				this._ui_children = [];
			},

			/*
				Method: dispose
			
				Removes this component's elements from the DOM.
			*/
			dispose: function() {
				// dispose the children
				this._dispose_child.apply(this, this._ui_children);
				// remove from dom & unbind all event listeners
				this.unbind_all();
				this.remove();
			},

			/*
				Method: set_visible
				
				Displays or hides component.
				
				Parameters:
					visible - show/hide component.
					animate - controls whether show/hide is animated.
			*/
			set_visible: function(visible, animate) {
				if (visible) {
					this.show(animate);
				} else {
					this.hide(animate);
				}
			},

			/*
				Method: toggle_visibility
				
				Toggles component visibility.
			*/
			toggle_visibility: function(animate) {
				this.set_visible(!this._get('visible'), animate);
			},

			/*
				Method: show
				
				Displays component.
			*/
			show: function(animate) {
				if (this.in_dom() && (!this._rendered || !this._get('visible'))) {
					this._set('visible', true);
					var animation = this._get('show_animation');
					if (animate && animation && $.fn[animation.method]) {
						$.fn[animation.method].call(this.$(), animation.speed || 'slow');
					} else {
						this.$().show();
					}
					this.layout();
				}
			},

			/*
				Method: hide
				
				Hides component.
			*/
			hide: function(animate) {
				if (this.in_dom() && (!this._rendered || this._get('visible'))) {
					this._set('visible', false);
					var animation = this._get('hide_animation');
					if (animate && animation && $.fn[animation.method]) {
						$.fn[animation.method].call(this.$(), animation.speed || 'slow');
					} else {
						this.$().hide();
					}
				}
			},

			/*
				Method: set_enabled
				
				Enables or disables component and children.
				
				Parameters:
					enabled - enable/disable component.
			*/
			set_enabled: function(enabled) {
				if (!this._rendered || enabled !== this._get('enabled')) {
					this._set('enabled', enabled);
				}
			},

			/*
				Method: focus
				
				Sets the focus to this UI object. By default focus does nothing.
			*/
			focus: function() {
			},

			/*
				Method: render
				
				Renders this instance into the given container. Subclasses should override the _render
				method which this method invokes.
				
				Parameters:
					container - element to append this instance to
			*/
			render: function(container) {
				this.append_to(container);
				this._render();
				this.layout();

				// call layout on window resize
				this.bind_to(window, 'resize', function() {
					this.layout();
				});

				this._rendered = true;
			},

			/*
				Method: _render
			
				Performs any custom rendering for this class.
			*/
			_render: function() {
				if (this._get('style_class')) {
					this.$().addClass(this._get('style_class'));
				}
				this.set_visible(this._get('visible'));
			},

			/*
				Method: _render_child
				
				Renders ui component and adds it to the ui children list.
				
				Parameters:
					container - container element
					ui_child  - ui object to render <SmartChat.UI>
			*/
			_render_child: function(container, ui_child) {
				this._ui_children.push(ui_child);
				ui_child.render(container);
				this.layout();
				return ui_child;
			},

			/*
				Method: _dispose_child
				
				Disposes ui component and removes it from the ui children list.
				
				Parameters:
					ui_child... - ui child objects to dispose <SmartChat.UI>
			*/
			_dispose_child: function() {
				var disposed = 0;
				for (var i = 0; i < arguments.length; i++) {
					var ui_child = arguments[i];
					for (var j = 0; j < this._ui_children.length; j++) {
						if (this._ui_children[j] === ui_child) {
							this._ui_children.splice(j, 1);
							this.unbind_from(ui_child);
							ui_child.dispose();
							disposed++;
							break;
						}
					}
				}
				if (disposed) {
					this.layout();
				}
			},

			/*
				Method: layout
				
				Performs any dynamic layout work that can't be achieved via CSS. Subclasses should override the
				_layout method which this method invokes.
			*/
			layout: function() {
				if (this._get('visible')) {
					// layout this UI component
					this._layout();

					// layout UI children
					for (var i = 0; i < this._ui_children.length; i++) {
						this._ui_children[i]._layout();
					}
				}
			},

			/*
				Method: _layout
			*/
			_layout: function() { }

		}
	})
	.mix(eCarList.Util.Bindable, eCarList.Util.Configurable, eCarList.Util.Embeddable);

});
//include eCarList.Class
//include eCarList.App.SmartChat.UI
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	/*
		Class: eCarList.App.SmartChat.Dialog
		
		Properties:
			closeable - closeability of dialog
			message   - dialog message
			options   - dialog option buttons
	*/
	SmartChat.Dialog = SmartChat.UI.extend({
		
		/*
			Attributes:
				closeable - if true, close button included
				message   - dialog message text
				options   - dialog button labels
		*/
		Attribute: {
			closeable: { init: false },
			message: { init: '' },
			options: { init: [] }
		},

		/*
			Events:
				CLOSE  - fired when the dialog is closed <SmartChat.Dialog>
				SELECT - fired when a button is clicked
		*/
		Event: {
			CLOSE: 'DIALOG_CLOSE',
			SELECT: 'DIALOG_SELECT'
		},

		HTML: '			<div class="dialog">				<div class="message"></div>				<div class="body"></div>				<div class="options"></div>			</div>		',

		BODY_HTML: '',
		
		prototype: {

			_render: function() {
				this._super();
				this._body = this.$('.body').get(0);
				
				this.set_closeable(this._get('closeable'));
				this.bind('.close_btn', 'click', _on_close_btn_click);

				this.set_message(this._get('message'));

				if (this._body) {
					if (this.klass.BODY_HTML) {
						$(this._body).append(this.klass.BODY_HTML);
					} else {
						$(this._body).hide();
					}
				}

				var options = this._get('options');
				if (options.length > 0) {
					for (var i = 0; i < options.length; i++) {
						_render_option.call(this, options[i], i === (options.length-1));
					}
					this.bind('.options > button', 'click', _on_option_click);
				} else {
					this.$('.options').hide();
				}
			},

			/*
				Method: close
				
				Hides window and triggers close event.
			*/
			close: function() {
				this.hide();
				this.trigger(this.klass.Event.CLOSE, this);
			},

			/*
				Method: focus
				
				Sets the focus for this dialog.
			*/
			focus: function() {
				if (!$.browser.msie) {
					this.$('.options > button.default').each(function() {
						this.focus();
					});
				}
			},


			/*
				Method: set_closeable
				
				Sets closability of this window.
			*/
			set_closeable: function(closeable) {
				this._set('closeable', closeable);
				if (closeable) {
					this.$('.close_btn').show();
				} else {
					this.$('.close_btn').hide();
				}
			},

			/*
				Method: set_option_enabled
				
				Enables/disables option buttons. If no option is specified, all options are enabled/disabled.
				
				Parameters:
					option  - [optional] option name
					enabled - enable/disable option
			*/
			set_option_enabled: function() {
				var option, enabled;
				if (arguments.length > 1) {
					option = arguments[0];
					enabled = arguments[1];
				} else {
					enabled = arguments[0];
				}
				var selector = '.options > button';
				if (option) {
					selector += ':contains(\'' + option + '\')';
				}
				this.$(selector).ecl().disabled(!enabled);
			},

			/*
				Method: set_message
				
				Sets the message displayed in this dialog.
				
				Parameters:
					message - message to display
			*/
			set_message: function(message) {
				this._set('message', message || '');
				if (message) {
					this.$('.message').text(message).show();
				} else {
					this.$('.message').hide();
				}
			}
			
		}

	});

	var _render_option = function(option, is_default) {
		var classes = (is_default) ? ['default'] : [];
		var option_html = '<button class=' + classes.join(' ') + '>' + option + '</button>';
		this.$('.options').append(option_html);
	};

	var _on_close_btn_click = function(event) {
		event.stopPropagation();
		this.close();
	};

	var _on_option_click = function(event) {
		var option = $(event.target).text();
		this.trigger(this.klass.Event.SELECT, option);
	};
	
});
//include eCarList.Module
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {
	
	SmartChat.FrameController = eCarList.Module.extend({

		/*
			Method: load

			Invoked by controlling window to load a SmartChat.BaseConsole into a frame.

			Parameters:
				frame   - iframe window to load console into
				console - console to embed <SmartChat.ConsoleBase>
		*/
		load: function(frame, console) {
			this._frame = frame;
			this._console = console;
		},

		/*
			Method: ready

			Invoked by controlled frame to inform controller that the frame DOM is ready for manipualtion.
			It's safe to invoke this method as soon as the FrameController javascript is loaded, the callback
			to the controller will not occur until DOM Ready.
		*/
		ready: function() {
			$(document).ready(function() {
				parent.eCarList.App.SmartChat.FrameController._on_frame_load();
			});
			$(window).bind('beforeunload', function() {
				parent.eCarList.App.SmartChat.FrameController._on_frame_unload();
			});
		},

		_on_frame_load: function() {
			this._console.load(this._frame, this._memento);
		},
	
		_on_frame_unload: function() {
			eCarList.debug('unloading console...');
			this._memento = this._console.unload();
		}

	});

});
//include eCarList.Module
eCarList.namespace('eCarList.Util', function(Util, $) {

	/*
		Structure: eCarList.Util.Browser
	*/
	Util.Browser = eCarList.Module.extend({
		
		/*
			The site unload event fires when we think the user is navigating away from the domain.
			It behaves in an optimistic manner, we assume the user is staying within the site unless we can
			prove that they're leaving.

			Events:
				PAGE_UNLOAD - occurs when a user leaves a page, but stays on the same domain <unload obj>
				SITE_UNLOAD - occurs when a user leaves a domain <unload obj>
		*/
		Event: {
			IDLE: 'BROWSER_IDLE',
			PAGE_UNLOAD: 'BROWSER_PAGE_UNLOAD',
			SITE_UNLOAD: 'BROWSER_SITE_UNLOAD'
		},

		/*
			Method: init_idle_timer

			Initializes idle timer. Idle events are fired every "interval" while the user remains idle.
			The event data is the number of millis since the user was last active.

			Parameters:
				window   - window to control
				interval - interval of time to fire events updating user's idle time.
		*/
		init_idle_timer: function(window, interval) {
			if (this._idle) {
				return; // already initialized, exit
			}
			this.idle = 0; // default to 0 idle time
			this._idle = {
				interval: interval || 30000 // default to 30 secs
			};
			
			var _this = this;

			var _on_idle_interval = function() {
				_this.idle = (new Date()).getTime() - _this._idle.date;
				$(window.document).trigger(_this.Event.IDLE, _this.idle);
			};
			
			var _reset_interval = function() {
				// clear interval if one exists
				if (_this._idle.interval_id) {
					clearInterval(_this._idle.interval_id);
					delete _this._idle.interval_id;
				}

				// reset interval
				if (_this._idle.interval) {
					_this._idle.date = (new Date()).getTime();
					_this._idle.interval_id = setInterval(_on_idle_interval, _this._idle.interval);
				}
			};

			var _on_user_event = function(event) {
				if (_this.idle > 0) {
					_this.idle = 0;
					$(window.document).trigger(_this.Event.IDLE, _this.idle);
				}
				_reset_interval();
			};
			
			$(window.document).bind('mousemove keydown DOMMouseScroll mousewheel mousedown', _on_user_event);
		},
		
		/*
			Method: init_unload_events

			Takes over window.onbeforeunload event in order to attempt to distinguish
			between page unloads and site unloads. Observers can 
		*/
		init_unload_events: function(window) {
			if (this._unload) {
				return; // already initialized, exit
			}
			this._unload = { };

			var _this = this;

			window.onbeforeunload = function() {
				var site_unloaded = false;
				var now = (new Date()).getTime();

				// determine if unload event is result of _this._unload event
				// (did it occur within 1 sec of _this._unload event)
				if (_this._unload.location && _this._unload.timeStamp
					&& ((now - _this._unload.timeStamp) < 1000)) {
					// see if _this._unload location has host/protocol info that's diff from current host
					var match = _this._unload.location.match(/^([a-z]+:)\/\/([^\/]+)/i);
					if (match && (match[1].toLowerCase() !== window.location.protocol.toLowerCase()
								  || match[2].toLowerCase() !== window.location.hostname.toLowerCase())) {
						// uh oh, they're definitely leaving the site
						site_unloaded = true;
					}
				}

				var unload = { };

				if (site_unloaded) {
					$(window).trigger(_this.Event.SITE_UNLOAD, unload);
				} else {
					$(window).trigger(_this.Event.PAGE_UNLOAD, unload);
				}

				return unload.message;
			};

			$(document).ready(function(event) {
				// watch link click events
				$('a').live('click', function(event) {
					_this._unload.location = this.getAttribute('href');
					_this._unload.timeStamp = event.timeStamp;
				});
				// watch form submissions
				$('form').submit(function(event) {
					_this._unload.location = this.getAttribute('action');
					_this._unload.timeStamp = event.timeStamp;
				});
			});
		},

		/*
			Method: blink_title

			Cause browser titlebar to blink with the given text. If multiple calls are made to
			blink_title with different id's, the title will rotate through all of the given messages
			until they're all cleared.

			Parameters:
				id   - id for future reference of this blink message
				text - text of message to blink
		*/
		blink_title: function(id, text) {
			this._blink = this._blink || {
				messages: []
			};

			var _this = this;
			var _on_blink_interval = function() {
				_this._blink.index = (++_this._blink.index) % (_this._blink.messages.length + 1);
				if (_this._blink.index === _this._blink.messages.length) {
					document.title = _this._blink.title;
				} else {
					document.title = _this._blink.messages[_this._blink.index].text;
				}
			};

			if (!this._blink.interval) {
				this._blink.title = document.title;
				this._blink.index = 0;
				this._blink.interval = setInterval(_on_blink_interval, 1000);
			}

			var message = $.grep(this._blink.messages, function(message) {
				return (message.id === id);
			})[0];

			if (message) {
				message.text = text;
			} else {
				this._blink.messages.push({ id: id, text: text });
			}
		},

		/*
			Method: parse_url

			Creates an object similar to window.location that maps out each elemenet of the given url string.

			Returns:
				location object
		*/
		parse_url: function(url) {
			eCarList.assert(typeof url === 'string', 'invalid url: ' + url);
			var match = url.match(/^([a-z]+:)\/\/([\-\.\w+]+)(:\d+)?(\/[^\s\?\#]*)?(\?[^\s#]*)?(#[^\s]*)?/);
			if (match) {
				var location = {
					href: match[0],
					protocol: match[1],
					hostname: match[2],
					port: match[3] || '',
					pathname: match[4] || '',
					search: match[5] || '',
					hash: match[6] || ''
				};
				location.host = location.hostname;
				if (location.port) {
					location.host += ':' + location.port;
				}
				return location;
			} else {
				throw 'invalid url: ' + url;
			}
		},

		/*
			Method: get_request_parameters

			Creates an object that maps the key/value pairs contained in the request parameters

			Returns:
				request parameter key/value paris
		*/
		get_request_parameters: function(search) {
			eCarList.assert(typeof search === 'string', 'invalid search: ' + search);
			var params = { };
			search = $.trim(search.replace(/^[\?#]/, ''));
			if (search) {
				var pairs = search.split('&');
				for (var i = 0; i < pairs.length; i++) {
					var pair = unescape(pairs[i]).split('=', 2);
					params[pair[0]] = pair[1];
				}
			}
			return params;
		},

		/*
			Method: unblink_title

			Remove a message from the list of titlebar blinking messages.

			Parameters:
				id - id of message used in blink_title call
		*/
		unblink_title: function(id) {
			if (this._blink) {
				for (var i = 0; i < this._blink.messages.length; i++) {
					if (this._blink.messages[i].id === id) {
						this._blink.messages.splice(i, 1);
						break;
					}
				}

				if (this._blink.messages.length === 0 && this._blink.interval) {
					clearInterval(this._blink.interval);
					document.title = this._blink.title || document.title;
					delete this._blink.interval;
				}
			}
		}

	});

});
//include eCarList.App.SmartChat.Dialog
//include eCarList.Util.Browser
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	var Browser = eCarList.Util.Browser,
		Dialog = SmartChat.Dialog;

	/*
		Class: eCarList.App.SmartChat.ModalDialog
	*/
	SmartChat.ModalDialog = SmartChat.Dialog.extend({
		
		/*
			Attributes:
				blink_title - [optional] if true, blink browser title
				style_class - dialog style
				title       - dialog title
		*/
		Attribute: {
			blink_title: { init: false },
			style_class: { init: 'modal_dialog' },
			title: { init: 'Dialog' }
		},

		HTML: '			<div class="modal_dialog dialog">				<div class="titlebar">					<div class="title"></div>					<div class="button_area">						<button class="close_btn" title="Close Dialog"></button>					</div>				</div>				<div class="message"></div>				<div class="body"></div>				<div class="options"></div>			</div>		',

		prototype: {

			/*
				Method: dispose

				Cancel browser title blinking.
			*/
			dispose: function() {
				Browser.unblink_title(this._id);
				this._super();
			},

			_render: function() {
				this._super();

				this.$('.titlebar > .title').html(this._get('title'));
				this.bind('.titlebar', 'mouseover', _on_titlebar_mouseover);
				this.bind('.titlebar', 'mouseout', _on_titlebar_mouseout);

				if (this._get('blink_title')) {
					Browser.blink_title(this._id, this._get('title'));
				}
			},
			
			_layout: function() {
				this.$().ecl().center();
			}

		}
	});

	var _on_titlebar_mouseover = function(event) {
		if (event.target.tagName.match(/button/i)) {
			$(event.target).addClass('hover');
		} else {
			this.$('.titlebar').addClass('hover');
		}
	};

	var _on_titlebar_mouseout = function(event) {
		if (event.target.tagName.match(/button/i)) {
			$(event.target).removeClass('hover');
		} else {
			this.$('.titlebar').removeClass('hover');
		}
	};
	
});
//include eCarList.App.SmartChat.Dialog
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	/*
		Class: eCarList.App.SmartChat.WindowDialog

		A WindowDialog is a dialog that occurs within the context of a window. They're displayed
		below the title bar and toolbar and appear over the window content.
	*/
	SmartChat.WindowDialog = SmartChat.Dialog.extend({
		
		Attribute: {
			style_class: { init: 'window_dialog' }
		}

	});

});
//include eCarList.Class
//include eCarList.App.SmartChat.Dialog
//include eCarList.App.SmartChat.UI
//include eCarList.App.SmartChat.WindowDialog
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	var Dialog = SmartChat.Dialog,
		WindowDialog = SmartChat.WindowDialog;

	/*
		Class: eCarList.App.SmartChat.Window
		
		Base class for all SmartChat windows.
	*/
	SmartChat.Window = SmartChat.UI.extend({
		
		/*
			Attributes:
				closeable     - if true, close button included
				minimizable   - if true, minimize button included
				minimized     - if true, window is minimized
				popable       - if true, pop out button included
				title         - window title
				titlebar_icon - if true, display titlebar icon
				toolbar       - window toolbar
				visible       - window visibility
		*/
		Attribute: {
			closeable: { init: true },
			minimizable: { init: true },
			minimized: { init: false },
			popable: { init: true },
			title: { init: 'Window' },
			titlebar_icon: { init: false },
			toolbar: { },
			visible: { init: false }
		},

		/*
			Events:
				CLOSE    - window closed <SmartChat.Window>
				DEBUG    - window debug event fired
				MINIMIZE - window minimized <SmartChat.Window>
				MAXIMIZE - window maximized <SmartChat.Window>
				POP_OUT  - window popped-out <SmartChat.Window>
		*/
		Event: {
			CLOSE: 'WINDOW_CLOSE',
			DEBUG: 'WINDOW_DEBUG',
			MINIMIZE: 'WINDOW_MINIMIZE',
			MAXIMIZE: 'WINDOW_MAXIMIZE',
			POP_OUT: 'WINDOW_POP_OUT'
		},

		HTML: '			<div class="window">				<div class="titlebar">					<div class="icon"></div>					<div class="title"></div>					<div class="button_area">						<button class="minmax_btn" title="Minimize Window"></button>						<button class="popout_btn" title="Pop Out Window"></button>						<button class="close_btn" title="Close Window"></button>					</div>				</div>				<div class="header"></div>				<div class="body"></div>				<div class="footer"></div>			</div>		',

		prototype: {

			/*
				Method: close
				
				Hides window and triggers close event.
			*/
			close: function() {
				this.hide();
				this.trigger(this.klass.Event.CLOSE, this);
			},

			/*
				Method: focus
				
				Hands focus off to window dialog if one exists.
			*/
			focus: function() {
				if (this._dialog) {
					this._dialog.focus();
				}
			},

			/*
				Method: blink_titlebar

				Causes titlebar to blink.

				Parameters:
					count - [optional] number of times for titlebar to blink, if undefined it blinks infinitely
			*/
			blink_titlebar: function(count) {
				if (!this._titlebar_blink_interval) {
					this._titlebar_blink_count = count || -1;
					var self = this;
					this._titlebar_blink_interval = setInterval(function() {
						var $titlebar = this.$('.titlebar');
						if ($titlebar.hasClass('blink')) {
							$titlebar.removeClass('blink');
							if (self._titlebar_blink_count === 0) {
								self.unblink_titlebar();
							}
						} else {
							$titlebar.addClass('blink');
							if (self._titlebar_blink_count > 0) {
								self._titlebar_blink_count--;
							}
						}
					}, 1000);
				}
			},

			/*
				Method: unblink_titlebar

				Stops titlebar blinking.
			*/
			unblink_titlebar: function() {
				if (this._titlebar_blink_interval) {
					clearInterval(this._titlebar_blink_interval);
					delete this._titlebar_blink_interval;
				}
				this.$('.titlebar').removeClass('blink');
			},

			/*
				Method: set_closeable

				Sets closability of this window.
			*/
			set_closeable: function(closeable) {
				this._set('closeable', closeable);
				if (closeable) {
					this.$('.close_btn').show();
				} else {
					this.$('.close_btn').hide();
				}
			},

			/*
				Method: set_minimizable

				Sets minimizability of this window.
			*/
			set_minimizable: function(minimizable) {
				this._set('minimizable', minimizable);
				if (minimizable) {
					this.$('.minmax_btn').show();
				} else {
					this.$('.minmax_btn').hide();
				}
			},

			/*
				Method: set_popable

				Sets popability of this window.
			*/
			set_popable: function(popable) {
				this._set('popable', popable);
				if (popable) {
					this.$('.popout_btn').show();
				} else {
					this.$('.popout_btn').hide();
				}
			},

			/*
				Method: minimize

				Hides window.
			*/
			minimize: function() {
				this._set('minimized', true);
				this.$().addClass('minimized');
				this.$('.minmax_btn').attr('title', 'Maximize Window');
				this.layout();
				this.trigger(this.klass.Event.MINIMIZE, this);
			},

			/*
				Method: maximize
				
				Shows window.
			*/
			maximize: function() {
				this._set('minimized', false);
				this.$().removeClass('minimized');
				this.$('.minmax_btn').attr('title', 'Minimize Window');
				this.layout();
				this.trigger(this.klass.Event.MAXIMIZE, this);
			},

			/*
				Method: set_minimized

				Minimizes/maximizes window
			*/
			set_minimized: function(minimized) {
				if (minimized) {
					this.minimize();
				} else {
					this.maximize();
				}
			},

			/*
				Method: pop_out

				Removes window from DOM.
			*/
			pop_out: function() {
				this.hide();
				this.trigger(this.klass.Event.POP_OUT, this);
			},

			/*
				Method: open_dialog

				Opens a WindowDialog in the context of this window. If another dialog is already open, that
				dialog will be closed first.

				Parameters:
					dialog - dialog to open <SmartChat.WindowDialog>
	
				Returns:
					given dialog object
			*/
			open_dialog: function(dialog) {
				this.close_dialog();
				this._dialog = this._render_child(this._header, dialog);
				this._dialog.focus();
				this.bind_to(this._dialog, Dialog.Event.CLOSE, _on_dialog_close);
				return this._dialog;
			},

			/*
				Method: close_dialog

				Closes the WindowDialog that is currently open in this window. Does nothing if there is no
				open WindowDialog.
			*/
			close_dialog: function() {
				if (this._dialog) {
					this._dialog.close();
				}
			},

			/*
				Method: display_message

				Displays a message dialog in the window.

				Parameters:
					message - dialog message
			*/
			display_message: function(message) {
				var dialog = this.open_dialog(new WindowDialog({
					message: message,
					options: []
				}));
			},

			/*
				Method: confirm

				Convenience method for creating confirm dialogs.

				Parameters:
					confirm_fn - confirm callback
					message    - dialog message
					options    - [optional] dialog options
			*/
			confirm: function(confirm_fn, message, options) {
				var dialog = this.open_dialog(new WindowDialog({
					message: message,
					options: options || ['Cancel', 'OK']
				}));
				this.bind_to(dialog, Dialog.Event.SELECT, function(event, option) {
					if (option === dialog.get_options()[dialog.get_options().length-1]) {
						confirm_fn.call(this);
					}
					dialog.close();
				});
			},

			_render: function() {
				this._super();
				this._titlebar = this.$('.titlebar').get(0);
				this._header = this.$('.header').get(0);
				this._body =  this.$('.body').get(0);
				this._footer = this.$('.footer').get(0);

				if (!this._get('titlebar_icon')) {
					this.$('.titlebar > .icon').hide();
				}

				this.$('.titlebar > .title').html(this._get('title'));
				this.bind('.titlebar', 'mouseover', _on_titlebar_mouseover);
				this.bind('.titlebar', 'mouseout', _on_titlebar_mouseout);
				this.bind('.titlebar', 'click', _on_titlebar_click);

				this.set_closeable(this._get('closeable'));
				this.bind('.close_btn', 'click', _on_close_btn_click);
				
				this.set_minimizable(this._get('minimizable'));
				this.bind('.minmax_btn', 'click', _on_minmax_btn_click);

				this.set_popable(this._get('popable'));
				this.bind('.popout_btn', 'click', _on_popout_btn_click);

				if (this._get('toolbar')) {
					this._render_child(this._header, this._get('toolbar'));
				}

				this.set_minimized(this._get('minimized'));
			},

			/*
				Method: _layout

				Dynamically size window body height based on window height, header, & footer.
			*/
			_layout: function() {
				var body_height;

				if (!this._get('minimized')) {
					var win_height = this.$().innerHeight();
					var title_height = (this._titlebar) ? $(this._titlebar).outerHeight() : 0;
					// IE fails to calculate height correctly if there's nothing in the header/footer
					var header_height = $(this._header).children().length ? $(this._header).height() : 0;
					var footer_height = $(this._footer).children().length ? $(this._footer).height() : 0;
					body_height = win_height - title_height - header_height - footer_height;
				} else {
					body_height = 0;
				}
				$(this._body).height(body_height);
			}
			
		}

	});

	var _on_titlebar_mouseover = function(event) {
		if (event.target.tagName.match(/button/i)) {
			$(event.target).addClass('hover');
		} else {
			this.$('.titlebar').addClass('hover');
		}
	};

	var _on_titlebar_mouseout = function(event) {
		if (event.target.tagName.match(/button/i)) {
			$(event.target).removeClass('hover');
		} else {
			this.$('.titlebar').removeClass('hover');
		}
	};

	var _on_titlebar_click = function(event) {
		if (event.altKey) {
			this.trigger(this.klass.Event.DEBUG);
		} else if (this._get('minimized')) {
			this.maximize();
		}
	};

	var _on_close_btn_click = function(event) {
		event.stopPropagation();
		this.close();
	};

	var _on_minmax_btn_click = function(event) {
		event.stopPropagation();
		if (this._get('minimized')) {
			this.maximize();
		} else {
			this.minimize();
		}
	};

	var _on_popout_btn_click = function(event) {
		event.stopPropagation();
		this.pop_out();
	};

	var _on_dialog_close = function(event) {
		this._dispose_child(this._dialog);
		this._dialog = null;
	};

});
//include eCarList.Module
eCarList.namespace('eCarList.Util', function(Util, $) {

	/*
		Structure: eCarList.Util.Cookie

		Cookie management utility.
	*/
	Util.Cookie = eCarList.Module.extend({

		/*
			Method: read

			Reads the cookie with the given name.

			Parameters:
				name - cookie name

			Returns:
				Cookie value.
		*/
		read: function(name) {
			eCarList.assert(typeof name === 'string', 'invalid name: ' + name);
			var pairs = document.cookie.split(/\s*;\s/);
			for (var i=0; i < pairs.length; i++) {
				var pair = pairs[i].split(/\s*=\s*/);
				if (pair.length === 2 && pair[0] === name) {
					var value = pair[1];
					var unescaped = unescape(pair[1]);
					eCarList.debug('read cookie(' + name + ', ' + value.length + '): ' + unescaped);
					return unescaped;
				}
			}
			if ($.browser.msie) {
				try {
					var win_data = $.evalJSON(window.name || '{}')[name];
					if (!win_data.expires || win_data.expires > (new Date()).getTime()) {
						return unescape(win_data.value);
					}
				} catch(e) { }
			}
			return null;
		},

		/*
			Method: read_object

			Reads the cookie with the given name and interprets it as a list of key/value pairs.

			Parameters:
				name - cookie name

			Returns:
				Object containing cookie key/value pairs if found, null otherwise
		*/
		read_object: function(name) {
			eCarList.assert(typeof name === 'string', 'invalid name: ' + name);
			var value = Util.Cookie.read(name);
			if (value) {
				var object = { };
				var tokens = value.split('&');
				for (var i = 0; (i + 1) < tokens.length; i += 2) {
					object[tokens[i]] = tokens[i+1];
				}
				return object;
			}
			return null;
		},
		
		/*
			Method: write

			Writes a cookie for this document's domain.

			Parameters:
				name    - cookie name
				value   - cookie value
				expires - [optional] cookie expiration date, end of session if null/undefined
				path    - [optional] cookie path, defaults to /
		*/
		write: function(name, value, expires, path) {
			eCarList.assert(name && typeof name === 'string', 'invalid name: ' + name);
			eCarList.assert(typeof value === 'string', 'invalid value: ' + value);
			var cookie_def = name + '=' + escape(value);
			if (expires) {
				if (typeof expires === 'number') {
					var expire_date = new Date();
					expire_date.setTime(expire_date.getTime() + expires);
					expires = expire_date;
				}
				cookie_def += '; expires=' + expires.toGMTString();
			}
			cookie_def += '; path=' + (path || '/');
			if ($.browser.msie) {
				try {
					Util.Cookie._win = Util.Cookie._win || { };
					Util.Cookie._win[name] = {
						expires: (expires) ? expires.getTime() : null,
						value: escape(value)
					};
					window.name = $.toJSON(Util.Cookie._win);
				} catch(e) { }
			} else {
				document.cookie = cookie_def;
			}
			eCarList.debug('writing cookie(' + name + '): ' + value);
		},

		/*
			Method: write_object

			Writes the key/value pairs of the given object to a cookie.

			Parameters:
				name    - cookie name
				object  - cookie key/value pairs
				expires - [optional] cookie expiration date, end of session if null/undefined
				path    - [optional] cookie path, defaults to /
		*/
		write_object: function(name, object, expires, path) {
			eCarList.assert(name && typeof name === 'string', 'invalid name: ' + name);
			eCarList.assert(typeof object === 'object', 'invalid object: ' + object);
			var tokens = [];
			for (var key in object) {
				if (object.hasOwnProperty(key)) {
					tokens.push(key, object[key]);
				}
			}
			var value = tokens.join('&');
			Util.Cookie.write(name, value, expires, path);
		},

		/*
			Method: erase

			Erases a cookie from the current document's domain.

			Parameters:
				name - cookie name
				path - [optional] cookie path, defaults to /
		*/
		erase: function(name, path) {
			eCarList.assert(name && typeof name === 'string', 'invalid name: ' + name);
			var date = new Date();
			date.setTime(date.getTime() - 60000);
			Util.Cookie.write(name, '', date, path);
		},

		read_validated: function(name) {
			var cookie = Util.Cookie.read(name);
			if (!$.browser.msie && !cookie) {
				cookie = Util.Cookie.read(name + '_last');
			}
			return cookie;
		},

		write_validated: function(name, cookie, timeout) {
			var expire = new Date();
			expire.setTime(expire.getTime() + timeout);
			Util.Cookie.write(name, cookie, expire);
		},

		erase_validated: function(name) {
			Util.Cookie.erase(name);
			if (!$.browser.msie) {
				Util.Cookie.erase(name + '_last');
			}
		},

		validate: function(name, cookie, timeout) {
			if (!$.browser.msie) {
				var expire = new Date();
				expire.setTime(expire.getTime() + 2 * timeout);
				Util.Cookie.write(name + '_last', cookie, expire);
				Util.Cookie.erase(name);
			}
		}


	});

});
//include eCarList.Module
/*jslint forin: true*/
eCarList.namespace('eCarList.Util', function(Util, $) {

	/*
		Structure: eCarList.Util.DataDumper

		JavaScript data dumper.
	*/
	Util.DataDumper = eCarList.Module.extend({

		/*
			Method: dump

			Serializes given values to a human readable format for debugging.

			Parameters:
				values... values to dump

			Returns:
				Serialized ata
		*/
		dump: function() {
			var config = {
				indent: '',
				arrays: [],
				functions: [],
				objects: []
			};

			var output = [];
			for (var i = 0; i < arguments.length; i++) {
				output.push(_dump_value(arguments[i], config));
			}

			return output.join('\n');
		}
	});

	var _dump_value = function(value, config) {
		var ref;
		if (typeof value === 'object') {
			try {
				if (value === null) {
					return 'null';
				} else if (value === window) {
					return 'Window';
				} else if (value === $) {
					return 'jQuery';
				} else if (_is_node(value)) {
					return 'DOMNode';
				} else if (value instanceof RegExp) {
					// RegExp is an object in Firefox
					return '/' + value.source + '/';
				} else if ($.isArray(value)) {
					if ((ref = $.inArray(value, config.arrays)) < 0) {
						config.arrays.push(value);
						return 'Array#' + (config.arrays.length - 1) + ' ' + _dump_array(value, config);
					} else {
						return 'Array#' + ref;
					}
				} else {
					if ((ref = $.inArray(value, config.objects)) < 0) {
						config.objects.push(value);
						return 'Object#' + (config.objects.length - 1) + ' ' + _dump_object(value, config);
					} else {
						return 'Object#' + ref;
					}
				}
			} catch (e) {
				// ignore obects that don't dump (XMLHttpRequest)
				eCarList.debug('can\'t dump: ' + e);
			}
		} else if (typeof value === 'function') {
			if (value instanceof RegExp) {
				// RegExp is a function in Rhino
				return '/' + value.source + '/'; 
			} else {
				if ((ref = $.inArray(value, config.functions)) < 0) {
					config.functions.push(value);
					return 'Function#' + (config.functions.length - 1) + ' ' + _dump_object(value, config);
				} else {
					return 'Function#' + ref;
				}
			}
		} else if (typeof value === 'string') {
			return '"' + value + '"';
		} else if ((typeof value).match(/boolean|number/)) {
			return value;
		}
	};

	var _is_node = function(obj) {
		return typeof Node === "object" ? obj instanceof Node : 
			typeof obj === "object" && typeof obj.nodeType === "number" && typeof obj.nodeName==="string";
	};

	var _dump_array = function(array, config) {
		var entries = new Array(array.length);
		config.indent += '  ';
		for (var i = 0; i < array.length; i++) {
			entries[i] = config.indent + _dump_value(array[i], config);
		}
		config.indent = config.indent.substr(0, config.indent.length - 2);
		return (entries.length === 0) ? '[ ]' : '[\n' + entries.join(',\n') + '\n' + config.indent + ']';
	};

	var _dump_object = function(obj, config) {
		var pairs = [];
		config.indent += '  ';
		for (var attr in obj) {
			var str = _dump_value(obj[attr], config);
			if (str) {
				pairs.push(config.indent + attr + ': ' + str);
			}
		}
		config.indent = config.indent.substr(0, config.indent.length - 2);
		return (pairs.length === 0) ? '{ }' : '{\n' + pairs.join(',\n') + '\n' + config.indent + '}';
	};

});
/*jslint forin: false*/
/*
 * jQuery JSON Plugin
 * version: 2.1 (2009-08-14)
 *
 * This document is licensed as free software under the terms of the
 * MIT License: http://www.opensource.org/licenses/mit-license.php
 *
 * Brantley Harris wrote this plugin. It is based somewhat on the JSON.org 
 * website's http://www.json.org/json2.js, which proclaims:
 * "NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.", a sentiment that
 * I uphold.
 *
 * It is also influenced heavily by MochiKit's serializeJSON, which is 
 * copyrighted 2005 by Bob Ippolito.
 */
 
(function($) {
    /** jQuery.toJSON( json-serializble )
        Converts the given argument into a JSON respresentation.

        If an object has a "toJSON" function, that will be used to get the representation.
        Non-integer/string keys are skipped in the object, as are keys that point to a function.

        json-serializble:
            The *thing* to be converted.
     **/
    $.toJSON = function(o)
    {
        if (typeof(JSON) == 'object' && JSON.stringify)
            return JSON.stringify(o);
        
        var type = typeof(o);
    
        if (o === null)
            return "null";
    
        if (type == "undefined")
            return undefined;
        
        if (type == "number" || type == "boolean")
            return o + "";
    
        if (type == "string")
            return $.quoteString(o);
    
        if (type == 'object')
        {
            if (typeof o.toJSON == "function") 
                return $.toJSON( o.toJSON() );
            
            if (o.constructor === Date)
            {
                var month = o.getUTCMonth() + 1;
                if (month < 10) month = '0' + month;

                var day = o.getUTCDate();
                if (day < 10) day = '0' + day;

                var year = o.getUTCFullYear();
                
                var hours = o.getUTCHours();
                if (hours < 10) hours = '0' + hours;
                
                var minutes = o.getUTCMinutes();
                if (minutes < 10) minutes = '0' + minutes;
                
                var seconds = o.getUTCSeconds();
                if (seconds < 10) seconds = '0' + seconds;
                
                var milli = o.getUTCMilliseconds();
                if (milli < 100) milli = '0' + milli;
                if (milli < 10) milli = '0' + milli;

                return '"' + year + '-' + month + '-' + day + 'T' +
                             hours + ':' + minutes + ':' + seconds + 
                             '.' + milli + 'Z"'; 
            }

            if (o.constructor === Array) 
            {
                var ret = [];
                for (var i = 0; i < o.length; i++)
                    ret.push( $.toJSON(o[i]) || "null" );

                return "[" + ret.join(",") + "]";
            }
        
            var pairs = [];
            for (var k in o) {
                var name;
                var type = typeof k;

                if (type == "number")
                    name = '"' + k + '"';
                else if (type == "string")
                    name = $.quoteString(k);
                else
                    continue;  //skip non-string or number keys
            
                if (typeof o[k] == "function") 
                    continue;  //skip pairs where the value is a function.
            
                var val = $.toJSON(o[k]);
            
                pairs.push(name + ":" + val);
            }

            return "{" + pairs.join(", ") + "}";
        }
    };

    /** jQuery.evalJSON(src)
        Evaluates a given piece of json source.
     **/
    $.evalJSON = function(src)
    {
        if (typeof(JSON) == 'object' && JSON.parse)
            return JSON.parse(src);
        return eval("(" + src + ")");
    };
    
    /** jQuery.secureEvalJSON(src)
        Evals JSON in a way that is *more* secure.
    **/
    $.secureEvalJSON = function(src)
    {
        if (typeof(JSON) == 'object' && JSON.parse)
            return JSON.parse(src);
        
        var filtered = src;
        filtered = filtered.replace(/\\["\\\/bfnrtu]/g, '@');
        filtered = filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']');
        filtered = filtered.replace(/(?:^|:|,)(?:\s*\[)+/g, '');
        
        if (/^[\],:{}\s]*$/.test(filtered))
            return eval("(" + src + ")");
        else
            throw new SyntaxError("Error parsing JSON, source is not valid.");
    };

    /** jQuery.quoteString(string)
        Returns a string-repr of a string, escaping quotes intelligently.  
        Mostly a support function for toJSON.
    
        Examples:
            >>> jQuery.quoteString("apple")
            "apple"
        
            >>> jQuery.quoteString('"Where are we going?", she asked.')
            "\"Where are we going?\", she asked."
     **/
    $.quoteString = function(string)
    {
        if (string.match(_escapeable))
        {
            return '"' + string.replace(_escapeable, function (a) 
            {
                var c = _meta[a];
                if (typeof c === 'string') return c;
                c = a.charCodeAt();
                return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
            }) + '"';
        }
        return '"' + string + '"';
    };
    
    var _escapeable = /["\\\x00-\x1f\x7f-\x9f]/g;
    
    var _meta = {
        '\b': '\\b',
        '\t': '\\t',
        '\n': '\\n',
        '\f': '\\f',
        '\r': '\\r',
        '"' : '\\"',
        '\\': '\\\\'
    };
})(eCarList.jQuery);
/**
 * ==DOMCached local storage library, version 0.1c-jquery==
 * DOMCached is a simple wrapper library for the use of DOM Storage provided by the modern browsers.
 * This library is designed after the hugely popular "memcached" caching system, providing similar
 * "caching" options in JavaScript in the form of local storage.
 *
 * While the original DOM Storage provides only methods to save and read string values, DOMCached
 * can input and output any JSON compatible objects. DOMCached includes also support for namespaces 
 * and data expiring.
 *
 * (c) 2009 Andris Reinman, FlyCom
 *         www.andrisreinman.com
 *
 *  DOMCached is freely distributable under the terms of a MIT-style license.
 *  For details, see the DOMCached web site: http://www.domcached.com/
 * 
 * NB! DOMCached requires jquery-json (http://code.google.com/p/jquery-json/) 
 * to convert from/to JSON strings.
 * 
**/

/**
 * Usage:
 * if(!(key = $.DOMCached.get("key", "my_ns"))){
 *     key = load_data_from_server()
 *     $.DOMCached.set("key","value", false, "my_ns");
 * }
 */

(function($) {

	$.DOMCached = {
		/* Version number */
		version: "0.1c-jquery",
	
		/*
		 * This is the object, that holds the cached values
		 * @param {Object} storage 
		 */
		storage: {},
	
		/*
		 * This is the object, that holds the actual storage object (localStorage or globalStorage['domain'])
		 * @param {Object} storage 
		 */
		storage_service: false,
	
	 	/**
 		 * Initialization function. Detects if the browser supports DOM Storage and behaves accordingly
	 	 * @returns undefined
 		 */
		init: function(){
			if("localStorage" in window){
				this.storage_service = localStorage;
			}else if("globalStorage" in window){
				this.storage_service = globalStorage[document.domain];
			}else{
				/* If the browser is IE7 or older, use userData behavior as a storage instead */
				if("addBehavior" in document.createElement('div')){
					// add a link element to the header, this will be the storage element
					document.write('<link id="elm_domcached" style="behavior:url(#default#userData)"/>');
					$('#elm_domcached')[0].load("domcached");
					try{
					var data = $('#elm_domcached')[0].getAttribute("domcached");
					}catch(E){var data = "{}"}
					this.storage_service = {dom_storage:{}};
					if(data && data.length){
						this.storage_service.dom_storage = data;
					}
				}else{
					return;
				}
			}
			if("dom_storage" in this.storage_service && this.storage_service.dom_storage){
				try{
					this.storage = $.evalJSON(this.storage_service.dom_storage);
				}catch(E){this.storage_service.dom_storage = "{}";}
			}else{
				this.storage_service.dom_storage = "{}";
			}
		},
	
	 	/**
 		 * This functions provides the "save" mechanism to store the DOMCached.storage object to the DOM Storage
	 	 * @returns undefined
 		 */
		save:function(){
			if(this.storage_service){
				try{
					this.storage_service.dom_storage = $.toJSON(this.storage);
				}catch(E){/* probably cache is full, nothing is saved this way*/}
				// If userData is used as the storage engine, additional 
				if($('#elm_domcached').length){
					try{
						$('#elm_domcached')[0].setAttribute("domcached",this.storage_service.dom_storage);
						$('#elm_domcached')[0].save("domcached");
					}catch(E){/* probably cache is full, nothing is saved this way*/}
				}
			}
		},
	
	 	/**
 		 * Sets a key's value, regardless of previous contents in cache.
	 	 * @param {String} key - Key to set. If this value is not set or not a string an exception is raised.
 		 * @param value - Value to set. This can be any value that is JSON compatible (Numbers, Strings, Objects etc.).
	 	 * @param {number} time - Optional expiration time, either relative number of seconds from current time, or an absolute Unix epoch time in milliseconds.
 		 * @param {String} namespace - An optional namespace for the key. This must be string, otherwise an exception is raised.
	 	 * @returns the used value
 		 */
		set: function(key, value, time, namespace){
			namespace = namespace || 'default';
			time = time || false;
			if(time && time<(new Date()).getTime()){
				time = (new Date()).getTime()+Math.ceil(time);
			}
			if(!key || (typeof key != "string" && typeof key != "number")){
				throw new TypeError('Key name must be string or numeric');
			}
			if(typeof namespace != "string" && typeof namespace != "number"){
				throw new TypeError('Namespace name must be string or numeric');
			}
			if(!this.storage[namespace]){
				this.storage[namespace] = {}
			}
			this.storage[namespace][key] = {value: value, time: time};
			this.save();
			return value || true;
		},
		/**
 	 	* Looks up a key in cache
	 	 * @param {String} key - Key to look up. If this value is not string an exception is raised.
 		 * @param {String} namespace - An optional namespace for the key. This must be string, otherwise an exception is raised.
	 	 * @returns the key value or <null> if not found
 		 */
		get: function(key, namespace){
			namespace = namespace || 'default';
			if(typeof namespace != "string" && typeof namespace != "number"){
				throw new TypeError('Namespace name must be string or numeric');
			}
			if(this.storage[namespace] && this.storage[namespace][key]){
			 	if (this.storage[namespace][key].time && 
			 		this.storage[namespace][key].time<(new Date()).getTime()){
			 			this.deleteKey(key, namespace);
			 			return null;
			 	}
				return this.storage[namespace][key].value;
			}
			return null;
		},
		/**
 	 	* Deletes a key from cache.
 	 	* @param {String} key - Key to delete. If this value is not string an exception is raised.
 	 	* @param {String} namespace - An optional namespace for the key. This must be string, otherwise an exception is raised.
 	 	* @returns true
 	 	*/
 		'delete': function(key, namespace){
 			return this.deleteKey(key, namespace);
 		},
		deleteKey: function(key, namespace){
			namespace = namespace || 'default';
			if(typeof namespace != "string" && typeof namespace != "number"){
				throw new TypeError('Namespace name must be string or numeric');
			}
			if(this.storage[namespace] && this.storage[namespace][key]){
				delete this.storage[namespace][key];
				for(var i in this.storage[namespace]){
					if(this.storage[namespace].hasOwnProperty(i)){
						this.save();
						return true;
					}
				}
				delete this.storage[namespace];
				this.save();
				return true;
			}
		},
	
 		/**
	 	 * Sets a key's value, if and only if the item is not already in cache.
 		 * @param {String} key - Key to set. If this value is not set or not a string an exception is raised.
	 	 * @param value - Value to set. This can be any value that is JSON compatible (Numbers, Strings, Objects etc.).
 		 * @param {number} time - Optional expiration time, either relative number of seconds from current time, or an absolute Unix epoch time in milliseconds.
	 	 * @param {String} namespace - An optional namespace for the key. This must be string, otherwise an exception is raised.
 		 * @returns the used value or false if the key was already used
	 	 */
		add: function(key, value, time, namespace){
			namespace = namespace || 'default';
			time = time || false;
			if(time && time<(new Date()).getTime()){
				time = (new Date()).getTime()+Math.ceil(time)
			}
			if(!key || (typeof key != "string" && typeof key != "number")){
				throw new TypeError('Key name must be string or numeric');
			}
			if(typeof namespace != "string" && typeof namespace != "number"){
				throw new TypeError('Namespace name must be string or numeric');
			}
			if(!this.storage[namespace]){
				this.storage[namespace] = {}
			}
			if(this.storage[namespace].hasOwnProperty(key)){
				return false;
			}
			this.storage[namespace][key] = {value: value, time: time};
			this.save();
			return value || true;
		},
	
 		/**
	 	 * Replaces a key's value, failing if item isn't already in cache.
 		 * @param {String} key - Key to set. If this value is not set or not a string an exception is raised.
	 	 * @param value - Value to set. This can be any value that is JSON compatible (Numbers, Strings, Objects etc.).
 		 * @param {number} time - Optional expiration time, either relative number of seconds from current time, or an absolute Unix epoch time in milliseconds.
	 	 * @param {String} namespace - An optional namespace for the key. This must be string, otherwise an exception is raised.
 		 * @returns the used value or false if the key was already used
	 	 */
		replace: function(key, value, time, namespace){
			namespace = namespace || 'default';
			time = time || false;
			if(time && time<(new Date()).getTime()){
				time = (new Date()).getTime()+time
			}
			if(!key || (typeof key != "string" && typeof key != "number")){
				throw new TypeError('Key name must be string or numeric');
			}
			if(typeof namespace != "string" && typeof namespace != "number"){
				throw new TypeError('Namespace name must be string or numeric');
			}
			if(!this.storage[namespace]){
				this.storage[namespace] = {}
			}
			if(!this.storage[namespace].hasOwnProperty(key)){
				return false;
			}
			this.storage[namespace][key] = {value: value, time: time};
			this.save();
			return value || true;
		},
	
 		/**
	 	 * Automically increments a key's value.
 		 * @param {String} key - Key to increment. If this value is not set or not a string an exception is raised.
	 	 * @param {Number} delta - Numric value to increment key by, defaulting to 1.
 		 * @param {String} namespace - An optional namespace for the key. This must be string, otherwise an exception is raised.
	 	 * @param {Number} initial_value - An initial value to be used if the key does not yet exist in the cache. Ignored if the key already exists.
 		 * @returns the new value
	 	 */
		incr: function(key, delta, namespace, initial_value){
			namespace = namespace || 'default';
			delta = delta || 1;
			initial_value = initial_value || 0;
			time = time || false;
			if(time && time<(new Date()).getTime()){
				time = (new Date()).getTime()+time
			}
			if(!key || (typeof key != "string" && typeof key != "number")){
				throw new TypeError('Key name must be string or numeric');
			}
			if(typeof namespace != "string" && typeof namespace != "number"){
				throw new TypeError('Namespace name must be string or numeric');
			}
			if(typeof delta != "number"){
				throw new TypeError('Delta value must be number');
			}
			if(!this.storage[namespace]){
				this.storage[namespace] = {}
			}
			if(!this.storage[namespace].hasOwnProperty(key) || typeof this.storage[namespace][key] != "number"){
				this.storage[namespace][key].value = initial_value;
				this.save();
				return initial_value;
			}
			this.storage[namespace][key].value += delta
			this.save();
			return value || true;
		},
	
 		/**
	 	 * Automically decrements a key's value.
 		 * @param {String} key - Key to decrement. If this value is not set or not a string an exception is raised.
	 	 * @param {Number} delta - Numric value to decrement key by, defaulting to 1.
 		 * @param {String} namespace - An optional namespace for the key. This must be string, otherwise an exception is raised.
 	 	* @param {Number} initial_value - An initial value to be used if the key does not yet exist in the cache. Ignored if the key already exists.
	 	 * @returns the new value
 		 */
		decr: function(key, delta, namespace, initial_value){
			namespace = namespace || 'default';
			delta = delta || 1;
			initial_value = initial_value || 0;
			time = time || false;
			if(time && time<(new Date()).getTime()){
				time = (new Date()).getTime()+time
			}
			if(!key || (typeof key != "string" && typeof key != "number")){
				throw new TypeError('Key name must be string or numeric');
			}
			if(typeof namespace != "string" && typeof namespace != "number"){
				throw new TypeError('Namespace name must be string or numeric');
			}
			if(typeof delta != "number"){
				throw new TypeError('Delta value must be number');
			}
			if(!this.storage[namespace]){
				this.storage[namespace] = {}
			}
			if(!this.storage[namespace].hasOwnProperty(key) || typeof this.storage[namespace][key] != "number"){
				this.storage[namespace][key].value = initial_value;
				this.save();
				return initial_value;
			}
			this.storage[namespace][key].value -= delta
			this.save();
			return value || true;
		},
	
		/**
	 	 * Deletes everything in cache.
 		 * @returns true
	 	 */
		flush_all: function(){
			this.storage = {}
			this.save()
			return true;
		}
	}

	/* Initialization */
	$.DOMCached.init();
})(eCarList.jQuery);
//include eCarList.Module
/*jslint bitwise: false*/
eCarList.namespace('eCarList.Util', function(Util, $) {

	// http://www.broofa.com/Tools/Math.uuid.js

	/*!
		Math.uuid.js (v1.4)
		http://www.broofa.com
		mailto:robert@broofa.com
	
		Copyright (c) 2009 Robert Kieffer
		Dual licensed under the MIT and GPL licenses.
	*/
	
	/*
	 * Generate a random uuid.
	 *
	 * USAGE: Math.uuid(length, radix)
	 *   length - the desired number of characters
	 *   radix  - the number of allowable values for each character.
	 *
	 * EXAMPLES:
	 *   // No arguments  - returns RFC4122, version 4 ID
	 *   >>> Math.uuid()
	 *   "92329D39-6F5C-4520-ABFC-AAB64544E172"
	 * 
	 *   // One argument - returns ID of the specified length
	 *   >>> Math.uuid(15)     // 15 character ID (default base=62)
	 *   "VcydxgltxrVZSTV"
	 *
	 *   // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62)
	 *   >>> Math.uuid(8, 2)  // 8 character ID (base=2)
	 *   "01001010"
	 *   >>> Math.uuid(8, 10) // 8 character ID (base=10)
	 *   "47473046"
	 *   >>> Math.uuid(8, 16) // 8 character ID (base=16)
	 *   "098F4D35"
	 */

	// Private array of chars to use
	var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); 
	
	Util.UUID = eCarList.Module.extend({

		create: function(len, radix) {
			var chars = CHARS, uuid = [];
			radix = radix || chars.length;

			if (len) {
				// Compact form
				for (var i = 0; i < len; i++) {
					uuid[i] = chars[0 | Math.random()*radix];
				}
			} else {
				// rfc4122, version 4 form
				var r;
				
				// rfc4122 requires these characters
				uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
				uuid[14] = '4';
				
				// Fill in random data.  At i==19 set the high bits of clock sequence as
				// per rfc4122, sec. 4.1.5
				for (var j = 0; j < 36; j++) {
					if (!uuid[j]) {
						r = 0 | Math.random()*16;
						uuid[j] = chars[(j === 19) ? (r & 0x3) | 0x8 : r];
					}
				}
			}

			return uuid.join('');
		}

	});

});
/*jslint bitwise: true*/
//include eCarList
//external jquery-json.js
//external domcached.js
//include eCarList.Module
//include eCarList.Util.UUID
eCarList.namespace('eCarList.Util', function(Util, $) {

	/*
		Structure: eCarList.Util.Restorable

		This mixin applies the Memento pattern to a class. The save_as and restore_as
		class methods are used to customize how state information should be captured in
		the memento object. By default, object data is simply copied over to a new object.
	*/
	Util.Restorable = eCarList.Module.extend({

		/*
			Method: save_as (class)

			Customizes the memento capture process for object attributes that match
			the given matcher.

			Parameters:
				matcher - string, regex, or matcher function
				fn      - memento save function to invoke: fn(memento, attr, origin)

			Returns:
				this class
		*/
		save_as: function(matcher, fn) {
			eCarList.assert(matcher, 'invalid matcher: ' + matcher);
			this._savers = this._savers || [];
			this._savers.unshift({
				matcher: _create_matcher(matcher),
				fn: fn
			});
			return this;
		},

		/*
			Method: restore_as (class)

			Customizes the state restoration process for object attributes that match
			the given matcher.

			Parameters:
				matcher - string, regex, or matcher function
				fn      - memento restore function to invoke: fn(origin, attr, memento)

			Returns:
				this class
		*/
		restore_as: function(matcher, fn) {
			eCarList.assert(matcher, 'invalid matcher: ' + matcher);
			this._restorers = this._restorers || [];
			this._restorers.unshift({
				matcher: _create_matcher(matcher),
				fn: fn
			});
			return this;
		},

		/*
			Method: cache

			Caches matching attribute values in DOM storage if available. Since a cache may not
			be available, users should define a fallback save/restore method.

			Parameters:
				matcher - string, regex, or matcher function

			Returns:
				this class
		*/
		cache: function(matcher) {
			eCarList.assert(matcher, 'invalid matcher: ' + matcher);
			this.save_as(matcher, function(memento, attr, origin) {
				if (!$.browser.msie && origin) {
					var key = Util.UUID.create();
					var value = _save.call(this.klass, origin);
					var cached = $.DOMCached.set(key, value, false, 'eCarList.Util.Restorable');
					if (cached) {
						memento[attr] = key;
						return true;
					}
				}
				return false;
			});
			this.restore_as(matcher, function(origin, attr, key) {
				if (!$.browser.msie) {
					var value = $.DOMCached.get(key, 'eCarList.Util.Restorable');
					if (value) {
						origin[attr] = _restore.call(this.klass, origin[attr], value);
						$.DOMCached.deleteKey(key, 'eCarList.Util.Restorable');
						return true;
					}
				}
				return false;
			});
			return this;
		},
		
		/*
			Method: dont_save (class)

			Prevent attributes that match the given matcher from being saved in the memento.

			Parameters:
				matcher - string, regex, or matcher function

			Returns:
				this class
		*/
		dont_save: function(matcher) {
			this.save_as(matcher, function(memento, attr, origin) { });
			return this;
		},

		/*
			Method: save_as_date (class)

			Make matching date attributes restorable.

			Parameters:
				matcher - string, regex, or matcher function

			Returns:
				this class
		*/
		save_as_date: function(matcher) {
			this.save_as(matcher, function(memento, attr, date) {
				memento[attr] = (date) ? date.getTime() : null;
			});
			this.restore_as(matcher, function(origin, attr, time) {
				if (time) {
					origin[attr] = new Date();
					origin[attr].setTime(time);
				}
			});
			return this;
		},

		/*
			Method: save_as_memento (class)

			Make matching restorable attributes restorable.

			Parameters:
				matcher - string, regex, or matcher function

			Returns:
				this class
		*/
		save_as_memento: function(matcher) {
			this.save_as(matcher, function(memento, attr, restorable) {
				memento[attr] = (restorable) ? restorable.save_to_memento() : null;
			});
			this.restore_as(matcher, function(origin, attr, memento) {
				if (memento) {
					origin[attr].restore_from_memento(memento);
				}
			});
			return this;
		},
		
		prototype: {

			/*
				Method: save_to_memento

				Creates a memento of this instance's state so that it can later be restored.

				Returns:
					Memento object.
			*/
			save_to_memento: function() {
				return _save.call(this.klass, this);
			},

			/*
				Method: restore_from_memento

				Restores this instance to a previous state defined by the given memento.

				Parameters:
					memento - memento object that defines previous state
			*/
			restore_from_memento: function(memento) {
				_restore.call(this.klass, this, memento);
			}
		}

	});

	var _create_matcher = function(value) {
		if ($.isFunction(value)) {
			return value;
		} else if (value instanceof RegExp) {
			return function(str) { return str.match(value); };
		} else if (typeof value === 'string') {
			return function(str) { return str === value; };
		} else {
			throw 'invalid matcher: ' + value;
		}
	};

	var _find_tweaks = function(tweaks, attr) {
		return $.grep(tweaks, function(tweak) {
			return tweak.matcher(attr);
		});
	};

	var _find_first_tweak = function(tweaks, attr) {
		for (var i = 0; i < tweaks.length; i++) {
			if (tweaks[i].matcher(attr)) {
				return i;
			}
		}
		return tweaks.length;
	};

	var _sort_attributes = function(tweaks, obj) {
		var attributes = [];

		// grab attributes
		for (var attr in obj) {
			if (obj.hasOwnProperty(attr)) {
				attributes.push(attr);
			}
		}

		// sort based on tweak order, giving priority to earliest defined tweak
		// non-tweaked attributes get priority over tweaked attributes
		attributes.sort(function(a, b) {
			return _find_first_tweak(tweaks, b) - _find_first_tweak(tweaks, a);
		});

		return attributes;
	};

	var _save = function(origin) {
		if (typeof origin === 'object') {
			if (origin === null) {
				return null;
			} else if ($.isArray(origin)) {
				return _save_array.call(this, origin);
			} else {
				return _save_object.call(this, origin);
			}
		} else if (typeof origin === 'function') {
			return undefined;
		} else {
			return origin;
		}
	};

	var _restore = function(origin, memento) {
		if (typeof memento === 'object') {
			if (memento === null) {
				return null;
			} else if ($.isArray(memento)) {
				return _restore_array.call(this, origin, memento);
			} else {
				return _restore_object.call(this, origin, memento);
			}
		} else {
			return memento;
		}
	};

	var _save_array = function(origin) {
		var memento = new Array(origin.length);
		for (var i=0; i < origin.length; i++) {
			memento[i] = _save.call(this, origin[i]);
		}
		return memento;
	};

	var _restore_array = function(origin, memento) {
		origin = origin || [];
		eCarList.assert($.isArray(origin), 'memento & origin type mismatch');
		origin.length = memento.length;
		for (var i=0; i < origin.length; i++) {
			origin[i] = _restore.call(this, origin[i], memento[i]);
		}
		return origin;
	};
	
	var _save_object = function(origin) {
		var memento = { };
		var attributes = _sort_attributes(this._savers, origin);
		for (var i = 0; i < attributes.length; i++) {
			var attr = attributes[i];
			var saved = false;

			// find custom savers for this attribute
			var savers = _find_tweaks(this._savers || [], attr);
			for (var j = 0; !saved && j < savers.length; j++) {
				saved = (savers[j].fn.call(origin, memento, attr, origin[attr]) !== false);
			}

			// no custom saver, take default action
			if (!saved) {
				var attr_memento = _save.call(this, origin[attr]);
				if (typeof attr_memento !== 'undefined') {
					memento[attr] = attr_memento;
				}
			}
		}
		return memento;
	};

	var _restore_object = function(origin, memento) {
		origin = origin || { };
		eCarList.assert(typeof origin === 'object' && !$.isArray(origin),
						'memento & origin type mismatch');
		var attributes = _sort_attributes(this._restorers, memento);
		for (var i = 0; i < attributes.length; i++) {
			var attr = attributes[i];
			var restored = false;
			eCarList.debug('restoring: ' + attr);

			// find custom restorers for this attribute
			var restorers = _find_tweaks(this._restorers || [], attr);
			for (var j = 0; !restored && j < restorers.length; j++) {
				restored = (restorers[j].fn.call(origin, origin, attr, memento[attr]) !== false);
			}

			// no custom restorer, take default action
			if (!restored) {
				origin[attr] = _restore.call(this, origin[attr], memento[attr]);
			}
		}
		return origin;
	};
	
});
//include eCarList.Class
//include eCarList.App.SmartChat.Dialog
//include eCarList.App.SmartChat.FrameController
//include eCarList.App.SmartChat.ModalDialog
//include eCarList.App.SmartChat.UI
//include eCarList.App.SmartChat.Window
//include eCarList.Util.Browser
//include eCarList.Util.Cookie
//include eCarList.Util.DataDumper
//include eCarList.Util.Restorable
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	var Browser = eCarList.Util.Browser,
		Cookie = eCarList.Util.Cookie,
		Dialog = SmartChat.Dialog,
		ModalDialog = SmartChat.ModalDialog,
		Window = SmartChat.Window;

	/*
		Class: eCarList.App.SmartChat.ConsoleBase
		
		Base class for SmartChat consoles.
	*/
	SmartChat.ConsoleBase = SmartChat.UI.extend({

		/*
			Attributes:
				autoload - automatically load/unload console
		*/
		Attribute: {
			autoload: { init: true }
		},

		/*
			Method: save_as_session
	
			Make session attributes that match restorable.
	
			Parameters:
				matcher - string, regex, or matcher fn to use
	
			Returns:
				this class
		*/
		save_as_session: function(matcher) {
			this.save_as(matcher, function(memento, attr, session) {
				if (this._get('autoload') && session) {
					memento[attr] = session.save_to_memento();
				}
			});
			this.restore_as(matcher, function(origin, attr, memento) {
				var session = origin[attr];
				if (this._get('autoload') && session.username === memento.username) {
					session.restore_from_memento(memento);
					if (session.available) {
						session.resume();
					}
				}
			});
			return this;
		},
	
		prototype: {

			/*
				Constructor: init
			*/
			init: function(config) {
				var self = this;
				this._super(config);
				this._dialogs = [];	

				// setup auto load/unload
				if (this._get('autoload')) {
					$(document).ready(function(event) {
						
						// don't bother loading/unloading if there's no session
						if (self._session) {
							var cookie = Cookie.read(self._id);
							var memento = { };
							if (cookie) {
								try {
									memento = JSJaCJSON.parse(cookie);
								} catch(e) { }
							}
							
							if (self.load(window, memento)) {
								window.onbeforeunload = function() {
									if (!self._unloaded) {
										self._suspend({ });
										self._unloaded = true;
									}
								};
								$(window).unload(function(event) {
									if (!self._unloaded) {
										self._suspend({ });
										self._unloaded = true;
									}
								});
							}
						}
					});
					/*
					$(document).ready(function(event) { _on_load.call(self, event); });
					this.bind_to(window, Browser.Event.PAGE_UNLOAD, function(event, unload_event) {
						_on_page_unload.call(this, event, unload_event);
					});
					this.bind_to(window, Browser.Event.SITE_UNLOAD, function(event, unload_event) {
						_on_site_unload.call(this, event, unload_event);
					});
					*/
				}
				
				// debug key sequence: 'alt+ctrl+\'
				this.bind_to(window, 'keyup', function(event) {
					if (event.altKey && event.ctrlKey && event.keyCode === 220) {
						this.report_bug();
					}
				});
			},

			dispose: function() {
				this.close_all_dialogs();
				this._super();
			},

			/*
				Method: load

				Invoked on DOM ready. Verifies cookies are enabled. Creates root container for SmartChat elements.

				Parameters:
					window  - window to load console into
					memento - [optional] memento to use to reload previous state

				Returns:
					True if load is successful, false otherwise.
			*/
			load: function(window, memento) {
				memento = memento || { };
				if (_is_browser_supported.call(this) && _are_cookies_enabled.call(this)) {
					this._root = $('<div class="smartchat"></div>').appendTo(window.document.body).get(0);
					this.render(this._root);
					this.restore_from_memento(memento);
					//Browser.init_unload_events(window);
					return true;
				}
				return false;
			},

			/*
				Method: unload

				Disposes all UI elements while retaining non-UI data.

				Returns:
					Memento object that can be used to restore the state before unloading.
			*/
			unload: function() {
				var memento = this.save_to_memento();
				this.dispose();
				delete this._root;
				return memento;
			},
			
			/*
				Method: _suspend

				Invoked on page unload. Serializes console state to cookie.

				Parameters:
					unload - DOM unload event
			*/
			_suspend: function(unload) {
				var memento = this.save_to_memento();
				var cookie = JSJaCJSON.toString(memento);
				Cookie.write(this._id, cookie, 60000);
			},

			/*
				Method: _resume

				Cancels a previous _suspend call.
			*/
			_resume: function() {
				Cookie.erase(this._id);
			},

			/*
				Method: _exit
			
				Invoked on site unload, or page unload for popups. No state information is preserved.

				Parameters:
					unload - DOM unload event
			*/
			_exit: function(unload) {
				Cookie.erase(this._id);
			},

			/*
				Method: report_bug

				Creates a bug report for this console.
			*/
			report_bug: function() {
				var data = {
					description: prompt('Report SmartChat Bug\n\nProblem Description:')
				};
				if (data.description) {
					data.dump = eCarList.Util.DataDumper.dump(this);
					var cookie = this.save_to_memento();
					data.cookie = eCarList.Util.DataDumper.dump(cookie);
					data.cookie_json = JSJaCJSON.toString(cookie);
					data.cookie_length = escape(data.cookie_json).length;

					var form = $('<form method="post" action="mailto:dhodges@ecarlist.com?subject=SmartChat Bug Report" enctype="text/plain"></form>')
					.appendTo(document.body).get(0);

					for (var attr in data) {
						if (data.hasOwnProperty(attr)) {
							$('<input type="hidden" name="' + attr + '"/>').appendTo(form).val(data[attr]);
						}
					}
					form.submit();
				}
			},

			/*
				Method: can_pop_out

				Determines if this console can be popped out

				Returns:
					True if console can be popped out, false otherwise.
			*/
			can_pop_out: function() {
				return (!$.browser.msie && !this._popup && !this._popup_window);
			},

			/*
				Method: pop_out

				Pop out console into popup window.

				Returns:
					True if console successfully popped out, false otherwise.
			*/
			pop_out: function() {
				if (this.can_pop_out()) {
					// suspend console
					this._suspend();
					
					// attempt to create popup window
					var name = this._id + (new Date()).getTime();
					var attributes = this._get_popup_attributes();
					var definitions = [];
					for (var key in attributes) {
						if (attributes.hasOwnProperty(key)) {
							definitions.push(key + '=' + attributes[key]);
						}
					}

					this._popup_window = window.open('', name, definitions.join(','));
					if (this._popup_window) {
						// popup wasn't blocked, generate content
						this._popup_window.document.open();
						this._popup_window.document.write(this._get_popup_html());
						this._popup_window.document.close();

						// control handed over, destroy this console
						this.dispose();
						return true;
					} else {
						// popup blocked
						this._resume();
					}
					return false;
				}
				return true;
			},

			/*
				Method: _get_popup_attributes

				Returns an object defining the pop_out popup window attributes
			*/
			_get_popup_attributes: function() {
				return {
					menubar: 'no',
					location: 'no',
					resizable: 'no'
				};
			},

			/*
				Method: _get_popup_html

				Generates HTML for the pop_out popup window.
			*/
			_get_popup_html: function() {
				return '';
			},

			/*
				Method: open_dialog

				Renders the given dialog and places it on the top of the dialog stack.

				Parameters:
					dialog - the dialog to open <SmartChat.ModalDialog>
	
				Returns:
					The given dialog instance.
			*/
			open_dialog: function(dialog) {
				// hide dialog at the top of the stack
				if (this._dialogs.length > 0) {
					this._dialogs[this._dialogs.length-1].hide();
				}
				// render new dialog & push it on to the stack
				this._dialogs.push(dialog);
				this._render_child(this._root, dialog);
				this.bind_to(dialog, Dialog.Event.CLOSE, _on_dialog_close);
				dialog.show();
				dialog.focus();

				return dialog;
			},

			/*
				Method: close_dialog

				Closes the specified dialog and removes it from the dialog stack. If no dialog is specified
				the top of the dialog stack is closed.

				Parameters:
					dialog - [optional] dialog to close, top wil be used if undefined <SmartChat.ModalDialog>
			*/
			close_dialog: function(dialog) {
				if (this._dialogs.length === 0) {
					return; // no dialog to close
				} else if (dialog && $.inArray(dialog, this._dialogs) < 0) {
					return; // dialog not managed by this console
				}
				dialog = dialog || this._dialogs[this._dialogs.length-1];
				if (dialog) {
					dialog.close();
				}
			},

			/*
				Method: close_all_dialogs

				Closes all open dialogs.
			*/
			close_all_dialogs: function() {
				while (this._dialogs.length > 0) {
					this.close_dialog(this._dialogs[0]);
				}
			},

			/*
				Method: display_message

				Convenience method for creating message dialogs.

				Parameters:
					title      - dialog title
					message    - dialog message
			*/
			display_message: function(title, message) {
				var dialog = this.open_dialog(new ModalDialog({
					title: title,
					message: message,
					options: ['OK']
				}));
				this.bind_to(dialog, Dialog.Event.SELECT, function(event, option) {
					if (option === 'OK') {
						dialog.close();
					}
				});
			},

			/*
				Method: confirm

				Convenience method for creating confirm dialogs.

				Parameters:
					confirm_fn - confirm callback
					title      - dialog title
					message    - dialog message
					options    - [optional] dialog options
			*/
			confirm: function(confirm_fn, title, message, options) {
				var dialog = this.open_dialog(new ModalDialog({
					title: title,
					message: message,
					options: options || ['Cancel', 'OK']
				}));
				this.bind_to(dialog, Dialog.Event.SELECT, function(event, option) {
					if (option === dialog.get_options()[dialog.get_options().length-1]) {
						confirm_fn.call(this);
					}
					dialog.close();
				});
			}

		}
	}).mix(eCarList.Util.Restorable);
	
	var _are_cookies_enabled = function() {	
		// check if we can read the cookie associated with this console.
		if (Cookie.read(this._id)) {
			return true;
		}

		// attempt to write to this console's cookie
		Cookie.write(this._id, 'test');
		if (Cookie.read(this._id)) {
			Cookie.erase(this._id);
			return true;
		}
		eCarList.debug('cookies are not enabled!');

		return false;
	};

	var _is_browser_supported = function() {
		var version = $.map($.browser.version.split(/\./), function(value) {
			return parseInt(value, 10);
		});

		// blacklist known bad browsers
		if ($.browser.msie && version[0] < 7) {
			return false;
		}
		if ($.browser.safari && version[0] < 522) {
			// safari === webkit
			return false;
		}
		if ($.browser.mozilla && (version[0] < 1 || (version[0] === 1 && version[1] < 8))) {
			return false;
		}

		return true;
	};

	var _on_page_unload = function(event, unload_event) {
		if (this._popup) {
			// page unloads in popups are exits too
			this._exit(unload_event);
		} else {
			this._suspend(unload_event);
		}
	};

	var _on_site_unload = function(event, unload_event) {
		this._exit(unload_event);
	};

	var _on_dialog_close = function(event, dialog) {
		// dispose dialog and remove it from the stack
		var index = $.inArray(dialog, this._dialogs);
		if (index >= 0) {
			this._dialogs.splice(index, 1);
		}
		this._dispose_child(dialog);

		// show dialog on the top of the stack
		if (this._dialogs.length > 0) {
			this._dialogs[this._dialogs.length-1].show();
		}
	};
	
});
//include eCarList.App.SmartChat.Common
//include eCarList.App.SmartChat.Window
eCarList.namespace('eCarList.App.SmartChat.Visitor', function(Visitor, $) {

	/*
		Class: eCarList.App.SmartChat.Visitor.OfferWindow

		Parameters:
			session - visitor session
	*/
	Visitor.OfferWindow = eCarList.App.SmartChat.Window.extend({
		
		/*
			Attributes:
				closeable     - if true, close button included
				title         - window title
				titlebar_icon - if true, display titlebar icon
		*/
		Attribute: {
			closeable: { init: false },
			session: { required: true },
			title: { init: 'Need Help?' },
			titlebar_icon: { init: true },
			hide_animation: { init: { method: 'slideUp', speed: 'slow' } },
			show_animation: { init: { method: 'slideDown', speed: 'slow' } }
		},

		HTML: '			<div class="offer_window window">				<div class="titlebar">					<div class="icon"></div>					<div class="title">Need Help?</div>				</div>				<div class="header"></div>				<div class="body">					<button class="minmax_btn"></button>					<div class="admin_img"/>					<div class="message">Click below to chat with an available representative.</div>					<button class="chat_btn">Chat Now</button>				</div>				<div class="footer"></div>			</div>		',

		prototype: {

			maximize: function() {
				this._super();
				if (this._offer_blink_timeout) {
					clearTimeout(this._offer_blink_timeout);
					delete this._offer_blink_timeout;
				}
				this.unblink_titlebar();

				if (this._rendered) {
					// maximizing after this window has been rendered is equivalent to clicking "Chat Now"
					this._get('session').prepare_chat();
				}
			},

			_render: function() {
				this._super();

				if (this._get('minimized')) {
					// blink titlebar after 8 secs if user never opens window
					var self = this;
					this._offer_blink_timeout = setTimeout(function() {
						self.blink_titlebar(2);
						delete self._offer_blink_timeout;
					}, 8000);
				}

				this.bind('.chat_btn', 'click', _on_chat_btn_click);
				this.bind('.minmax_btn', 'mouseover', _on_minmax_btn_mouseover);
				this.bind('.minmax_btn', 'mouseout', _on_minmax_btn_mouseout);
			},

			_layout: function() { }

		}

	});

	var _on_chat_btn_click = function(event) {
		this._get('session').prepare_chat();
	};

	var _on_minmax_btn_mouseover = function(event) {
		this.$('.minmax_btn').addClass('hover');
	};

	var _on_minmax_btn_mouseout = function(event) {
		this.$('.minmax_btn').removeClass('hover');
	};

});
//include eCarList
//include eCarList.Util.Cookie
//include eCarList.Util.Restorable
//external jsjac.js
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	// check node, domain, & resource equality
	JSJaCJID.prototype.equals = function(jid) {
		return (this.getDomain() === jid.getDomain() 
			&& this.getNode() === jid.getNode()
			&& this.getResource() === jid.getResource());
	};

	// overriding original impl, it strips resources
	JSJaCJID.prototype.isEntity = function(jid) {
		if (jid) {
			if (typeof jid === 'string') {
				jid = new JSJaCJID(jid);
			}
			return (this.bare() === jid.bare());
		}
		return false;
	};

	// return bare jid without resource
	JSJaCJID.prototype.bare = function() {
		return this.clone().removeResource().toString();
	};

	// returns root domain (strips 'conference' subdomain from room domains)
	JSJaCJID.prototype.getRootDomain = function() {
		return this.getDomain().replace(/^conference\./i, '');
	};

	// determine if jid corresponds to a room
	JSJaCJID.prototype.isRoom = function() {
		return (this.getDomain().match(/^conference\./i) !== null);
	};

	// determine if jid corresponds to a system bot
	JSJaCJID.prototype.isSystem = function() {
		return (this.getDomain().match(/^system\./i) !== null);
	};

	// determine if jid corresponds to a visitor
	JSJaCJID.prototype.isVisitor = function() {
		return (this.getDomain().match(/^visitor\./i) !== null);
	};

	// determine if jid corresponds to an admin
	JSJaCJID.prototype.isAdmin = function() {
		return (!this.isSystem() && !this.isVisitor() && !this.isRoom());
	};

	// returns nickname for user, room nick or node
	JSJaCJID.prototype.getNick = function() {
		return (this.isRoom()) ? this.getResource() : this.getNode();
	};

	/*
		Class: JSJaCMessage
	*/
	JSJaCMessage.prototype.setBodyHTML = function(html) {
		this.appendNode('html', { xmlns: 'http://jabber.org/protocol/xhtml-im' })
			.appendChild(this.buildNode('body', { xmlns: 'http://www.w3.org/1999/xhtml' }, html));
	};

	JSJaCMessage.prototype.getBodyHTML = function() {
		return $('html > body', this.getNode()).text();
	};

	/*
		Method: eCarList.Util.Restorable.save_as_jid

		Make jid attributes that match restorable.

		Parameters:
			matcher - string, regex, or matcher fn to use

		Returns:
			this class
	*/
	eCarList.Util.Restorable.save_as_jid = function(matcher) {
		this.save_as(matcher, function(memento, attr, jid) {
			memento[attr] = (jid) ? jid.toString() : null;
		});
		this.restore_as(matcher, function(origin, attr, jid_str) {
			if (jid_str) {
				origin[attr] = new JSJaCJID(jid_str);
			}
		});
		return this;
	};

});
//include eCarList.Module
/*jslint bitwise: false*/
eCarList.namespace('eCarList.Util.Codec', function(Codec, $) {

	/*
		Structure: eCarList.Util.Codec.Base32
	*/
	Codec.Base32 = eCarList.Module.extend({

		/*
			Method: encode

			Converts a byte array to a base32 string.

			Parameters:
				array - byte array

			Returns:
				base32 string
		*/
		encode: function(array) {
			var chars = [];
			var len = array.length;
			for (var i = 0; i < len; i += 5) {
				var b0 = array[i];
				var b1 = array[i+1] || 0;
				var b2 = array[i+2] || 0;
				var b3 = array[i+3] || 0;
				var b4 = array[i+4] || 0;

				var c0 = (b0 >> 3); // b0[0-4]
				var c1 = ((b0 & 7) << 2) | (b1 >> 6); // b0[5-7]b1[0-1]
				var c2 = (i+1 < len) ? (b1 >> 1) & 31 : 32; // b1[2-6]
				var c3 = (i+1 < len) ? ((b1 & 1) << 4) | (b2 >> 4) : 32; // b1[7]b2[0-3]
				var c4 = (i+2 < len) ? ((b2 & 15) << 1) | (b3 >> 7) : 32; // b2[4-7]b3[0]
				var c5 = (i+3 < len) ? (b3 >> 2) & 31 : 32; // b3[1-5]
				var c6 = (i+3 < len) ? ((b3 & 3) << 3) | (b4 >> 5): 32; // b3[6-7]b4[0-2]
				var c7 = (i+4 < len) ? b4 & 31 : 32; // b4[3-7]
				
				chars.push(_charset.charAt(c0), _charset.charAt(c1),
						   _charset.charAt(c2), _charset.charAt(c3),
						   _charset.charAt(c4), _charset.charAt(c5),
						   _charset.charAt(c6), _charset.charAt(c7));
			}
			return chars.join('');
		},

		/*
			Method: decode

			Converts a base32 string to an array of bytes. If necessary given string will be padded.

			Parameters:
				str - base32 string

			Returns:
				byte array
		*/
		decode: function(str) {
			str = str.toUpperCase();

			// pad string
			var padding = '';
			for (var i = 0; i < (str.length % 8); i++) {
				padding += '=';
			}
			if (padding.length > 0) {
				str += padding;
			}

			var array = [];
			for (var j = 0; j < str.length; j += 8) {
				var c0 = _charset.indexOf(str.charAt(j));
				var c1 = _charset.indexOf(str.charAt(j+1));
				var c2 = _charset.indexOf(str.charAt(j+2));
				var c3 = _charset.indexOf(str.charAt(j+3));
				var c4 = _charset.indexOf(str.charAt(j+4));
				var c5 = _charset.indexOf(str.charAt(j+5));
				var c6 = _charset.indexOf(str.charAt(j+6));
				var c7 = _charset.indexOf(str.charAt(j+7));

				var b0 = (c0 << 3) | (c1 >> 2); // c0[0-4]c1[0-2]
				array.push(b0);

				if (c2 === 32 || c3 === 32) { break; }
				var b1 = ((c1 & 3) << 6) | (c2 << 1) | (c3 >> 4); // c1[3-4]c2[0-4]c3[0]
				array.push(b1);

				if (c4 === 32) { break; }
				var b2 = ((c3 & 15) << 4) | (c4 >> 1); // c3[1-4]c4[0-3]
				array.push(b2);

				if (c5 === 32 || c6 === 32) { break; }
				var b3 = ((c4 & 1) << 7) | (c5 << 2) | (c6 >> 3); // c4[4]c5[0-4]c6[0-1]
				array.push(b3);

				if (c7 === 32) { break; }
				var b4 = ((c6 & 7) << 5) | c7; // c6[2-4]c7[0-4]
				array.push(b4);
			}
			return array;
		}
	});

	var _charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
});
/*jslint bitwise: true*/
//include eCarList.Module
/*jslint bitwise: false*/
eCarList.namespace('eCarList.Util.Codec', function(Codec, $) {

	/*
		Structure: eCarList.Util.Codec.Base64
	*/
	Codec.Base64 = eCarList.Module.extend({

		/*
			Method: encode

			Converts a byte array to a base64 string.

			Parameters:
				array - byte array

			Returns:
				base64 string
		*/
		encode: function(array) {
			var chars = [];
			for (var i = 0; i < array.length; i += 3) {
				var b0 = array[i];
				var b1 = array[i+1] || 0;
				var b2 = array[i+2] || 0;

				var c0 = (b0 >> 2); // b0[0-5]
				var c1 = ((b0 & 3) << 4) | (b1 >> 4); // b0[6-7]b1[0-3]
				var c2 = (i+1 < array.length) ? ((b1 & 15) << 2) | (b2 >> 6) : 64; // b1[4-7]b2[0-1]
				var c3 = (i+2 < array.length) ? b2 & 63 : 64; // b2[2-7]
				chars.push(_charset.charAt(c0), _charset.charAt(c1),
						   _charset.charAt(c2), _charset.charAt(c3));
			}
			return chars.join('');
		},

		/*
			Method: decode

			Converts a base64 string to an array of bytes. If necessary given string will be padded.

			Parameters:
				str - base64 string

			Returns:
				byte array
		*/
		decode: function(str) {
			// pad string
			var padding = '';
			for (var i = 0; i < (str.length % 4); i++) {
				padding += '=';
			}
			if (padding.length > 0) {
				str += padding;
			}

			var array = [];
			for (var j = 0; j < str.length; j += 4) {
				var c0 = _charset.indexOf(str.charAt(j));
				var c1 = _charset.indexOf(str.charAt(j+1));
				var c2 = _charset.indexOf(str.charAt(j+2));
				var c3 = _charset.indexOf(str.charAt(j+3));

				var b0 = (c0 << 2) | (c1 >> 4); // c0[0-5]c1[0-1]
				array.push(b0);

				if (c2 === 64) { break; }
				var b1 = ((c1 & 15) << 4) | (c2 >> 2); // c1[2-5]c2[0-3]
				array.push(b1);

				if (c3 === 64) { break; }
				var b2 = ((c2 & 3) << 6) | c3; // c2[4-5]c3[0-5]
				array.push(b2);
			}
			return array;
		}
	});

	var _charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
});
/*jslint bitwise: true*/
//include eCarList
//include eCarList.Util.Codec.Base32
//include eCarList.Util.Codec.Base64
eCarList.namespace('eCarList.Util.Codec', function(Codec, $) {

	/*
		Namespace: eCarList.Util.Codec
	*/

	/*
		Method: base32_to_base64
		
		Convenience function for conversion from base32 to base64.
		
		Parameters:
			str - base32 string
		
		Returns:
			base64 string
	*/
	Codec.base32_to_base64 = function(str) {
		return Codec.Base64.encode(Codec.Base32.decode(str));
	};

	/*
		Method: base64_to_base32
		
		Convenience function for conversion from base64 to base32.
		
		Parameters:
			str - base64 string
		
		Returns:
			base32 string
	*/
	Codec.base64_to_base32 = function(str) {
		return Codec.Base32.encode(Codec.Base64.decode(str));
	};

});
//include eCarList.App.SmartChat.Common
//include eCarList.Class
//include eCarList.Util.Bindable
//include eCarList.Util.Codec
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	var	Codec = eCarList.Util.Codec,
		RoomType = SmartChat.RoomType,
		Show = SmartChat.Show;

	/*
		Class: eCarList.App.SmartChat.Room
		
		Chat room, contained in session.rooms.
		
		Properties:
			jid      - room jabber id <JSJaCJID>
			members  - array of room Members
			name     - room name
			priority - room's priority, lower === higher priority
			type     - room type <SmartChat.RoomType>
	*/
	SmartChat.Room = eCarList.Class.extend({

		Event: {
			NAME_CHANGE: 'NAME_CHANGE'
		},

		/*
			Method: create_team_node
			
			Creates a jabber id node for the team room associated with the specified team_eid.

			Parameters:
				team_eid - team's eid

			Returns:
				team room jabber id node
		*/
		create_team_node: function(team_eid) {
			// normalize team_eid
			
			var norm_team_eid = Codec.base64_to_base32(team_eid).replace(new RegExp('=+', 'g'), '').toLowerCase();
			return (RoomType.TEAM + '|' + norm_team_eid);
		},

		/*
			Method: create_private_node

			Creates a jabber id node for the private room associated with the specified visitor_node.

			Parameters:
				visitor_node

			Returns:
				private room jabber id
		*/
		create_private_node: function(visitor_node) {
			if (!visitor_node || typeof visitor_node !== 'string') {
				throw 'invalid visitor_node: ' + visitor_node;
			}
			return (RoomType.PRIVATE + '|' + visitor_node);
		},

		/*
			Method: get_jid_info

			Retrieves room information from a chat room jabber id.

			Parameters:
				jid - room jabber id

			Returns:
				Room info: type, team_eid for team room, visitor_node for private rooms
		*/
		get_jid_info: function(jid) {
			if (!jid.isRoom()) {
				throw 'cannot get room info for user JIDs';
			}

			var node = jid.getNode();
			var tokens = node.split(/\|/, 2);
			if (tokens.length < 2) {
				throw 'invalid room node: ' + node;
			}

			var info = {
				type: tokens[0]
			};

			if (info.type === RoomType.TEAM) {
				info.team_eid = Codec.base32_to_base64(tokens[1]).replace(new RegExp('=+', 'g'), '');
				if (!info.team_eid) {
					throw 'invalid team_eid found in node: ' + node;
				}
			} else if (info.type === RoomType.PRIVATE) {
				info.visitor_node = tokens[1];
				if (!info.visitor_node) {
					throw 'invalid visitor_node found in node: ' + node;
				}
			} else {
				throw 'invalid type found in node: ' + node;
			}

			return info;
		},
		
		prototype: {

			/*
				Constructor: init

				Parameters:
					jid - room jabber id <JSJaCJID>
			*/
			init: function(jid) {
				eCarList.assert(jid instanceof JSJaCJID, 'invalid jid: ' + jid); 
				this.jid = jid;
				this.members = [];
				this.priority = 0;

				var room_info = this.klass.get_jid_info(this.jid);
				for (var attr in room_info) {
					if (room_info.hasOwnProperty(attr)) {
						this[attr] = room_info[attr];
					}
				}
			},

			/*
				Method: update_name

				Updates the name of this room and fires name change event.

				Parameters:
					name - new room name
			*/
			update_name: function(name) {
				this.name = name;
				this.trigger(this.klass.Event.NAME_CHANGE, name);
			},

			/*
				Method: get_member
	
				Returns the member associated with the given jid or nick.

				Parameters;
					jid - member's jabber id
					- or -
					nick - member's nick

				Returns:
					Member object if found, null otherwise.			
			*/
			get_member: function() {
				var jid, nick;
				if (arguments[0] === 'string') {
					nick = arguments[0];
				} else {
					jid = arguments[0];
				}

				if (!jid && !nick) {
					throw 'invalid nick/jid given';
				}

				for (var i = 0; i < this.members.length; i++) {
					var member = this.members[i];
					if ((jid && member.jid.isEntity(jid)) || (nick && member.nick === nick)) {
						return member;
					}
				}

				return null;
			},
			
			/*
				Method: get_admins
				
				Returns an array of admin room members.
			*/
			get_admins: function() {
				return $.grep(this.members, function(member) {
					return member.jid.isAdmin();
				});
			},
			
			/*
				Method: get_available_admins
				
				Returns an array of available admin room members.
			*/
			get_available_admins: function() {
				return $.grep(this.members, function(member) {
					return (member.jid.isAdmin() && member.room_presence.show !== Show.EXTENDED_AWAY
							&& member.room_presence.show !== Show.DO_NOT_DISTURB);
				});
			},
			
			/*
				Method: get_visitors
				
				Returns an array of room members that are visitors.
			*/
			get_visitors: function() {
				return $.grep(this.members, function(member) {
					return member.jid.isVisitor();
				});
			},

			/*
				Method: add_member
				
				Creates a new Member object for the given jid and nick and adds it to the members array.
				
				Parameters:
					jid      - member jabber id
					nick     - member nick
					presence - member presence <SmartChat.Presence>
				
				Returns:
					Newly created member.
			*/
			add_member: function(jid, nick, presence) {
				var member = {
					nick: nick,
					jid: jid,
					room_jid: this.jid,
					room_presence: presence
				};

				this.members.push(member);
				return member;
			},

			/*
				Method: remove_member

				Removes member from members array.

				Parameters:
					jid - member's jabber id

				Returns:
					Removed member object if it was present in members array.
			*/
			remove_member: function(jid) {
				for (var i = 0; i < this.members.length; i++) {
					var member = this.members[i];
					if (member.jid.equals(jid)) {
						this.members.splice(i, 1);
						return member;
					}
				}
				return null;
			}

		}
	}).mix(eCarList.Util.Bindable);

});
//include eCarList.Module
eCarList.namespace('eCarList.Util', function(Util, $) {

	/*
		Structure: eCarList.Util.Stateful

		The Stateful mixin can be used to apply the State pattern to an eCarList.Class.
	*/
	Util.Stateful = eCarList.Module.extend({

		/*
			Events:
				STATE_CHANGE - Fired when this classes's state change (if this class is Bindable)
		*/
		Event: {
			STATE_CHANGE: 'STATE_CHANGE'
		},
		
		/*
			Method: define_state (class)

			Defines a new state for this instance. Any number of prototype objects can be given with
			each successive prototype overriding the preceding ones.

			Parameters:
				name          - name of the state
				...prototypes - state prototypes
		*/
		define_state: function(name) {
			// install _states obj
			if (!this._states) {
				this._states = { };
			}

			// gather prototypes
			var prototypes = [ { } ];
			for (var i = 1; i < arguments.length; i++) {
				prototypes.push(arguments[i]);
			}
			var prototype = $.extend.apply(null, prototypes);
			
			this._states[name] = prototype;
			
			// create proxy methods
			for (var method in prototype) {
				if ($.isFunction(prototype[method])) {
					if (typeof this.prototype[method] === 'undefined') {
						this.prototype[method] = _create_state_method(this, method);
					}
				}
			}
		},

		/*
			Method: extend_state (class)

			Extends an existing state for this instance. Any number of prototype objects can be given with
			each successive prototype overriding the preceding ones.

			Parameters:
				name          - name of the state
				...prototypes - state prototypes
		*/
		extend_state: function(name) {
			eCarList.assert(this._states && this._states[name], 'state ' + name + ' has not been defined');
			var define_args = [name, this._states[name]];
			for (var i = 1; i < arguments.length; i++) {
				define_args.push(arguments[i]);
			}
			this.define_state.apply(this, define_args);
		},
		
		prototype: {

			/*
				Constructor: init

				Default constructor for Stateful classes.

				Parameters:
					state - initial state for this instance.
			*/
			init: function(state) {
				this.init_state(state);
			},

			/*
				Method: init_state

				Establishes the initial state for this instance.

				Parameters:
					state - initial state for this instance.
			*/
			init_state: function(state) {
				eCarList.assert(state && this.klass._states && this.klass._states[state],
								'invalid state: ' + state);
				this.state = state;
				_configure_state_timeout.call(this, this.state);
			},
			
			/*
				Method: get_states
				
				Retrieves all defined states.
				
				Returns:
					Array of defined state names.
			*/
			get_states: function() {
				var states = [];
				if (this.klass._states) {
					for (var state in this.klass._states) {
						if (this.klass._states.hasOwnProperty(state)) {
							states.push(state);
						}
					}
				}
				return states;
			},
			
			/*
				Method: in_state
				
				Returns true if this instance is in ANY of the given state values.
				
				Parameters:
					...states - states to check
				
				Returns:
					True if this instance is in one of ...states, false otherwise.
			*/
			in_state: function() {
				for (var i = 0; i < arguments.length; i++) {
					if (this.state === arguments[i]) {
						return true;
					}
				}
				return false;
			},

			/*
				Method: change_state
				
				Changes the state of this instance and fires a Stateful.Event.STATE_CHANGE event
				(if this instance is Bindable).

				Parameters:
					state - the new state
				
				Returns:
					True if the state is changed, false otherwise.
			*/
			change_state: function(state) {
				eCarList.assert(state && this.klass._states && this.klass._states[state],
								'invalid state: ' + state);
				if (this.state !== state) {
					this.state = state;
					_configure_state_timeout.call(this, this.state);
					eCarList.debug('change_state: ' + state);
					if (this.mix_of(Util.Bindable)) {
						this.trigger(Util.Stateful.Event.STATE_CHANGE, this.state);
					}
					return true;
				}
				return false;
			}
		}
	});
	
	var _create_state_method = function(klass, method) {
		return function() {
			eCarList.assert(this.state, 'state is undefined');

			var state_prototype = klass._states[this.state];
			eCarList.assert(state_prototype, 'state prototype missing');

			var state_method = state_prototype[method];
			eCarList.assert(state_method, 'Invalid operation (' + method
							+ ') called for state (' + this.state + ')');

			return state_method.apply(this, arguments);
		};
	};

	var _configure_state_timeout = function(state) {
		// clear old timeout
		if (this._state_timeout_id) {
			clearTimeout(this._state_timeout_id);
			delete this._state_timeout_id;
		}

		// establish new timeout
		var prototype = this.klass._states[state];
		if (typeof prototype.timeout === 'number' && prototype.timeout >= 0
			&& $.isFunction(prototype._on_state_timeout)) {
			eCarList.debug('configuring timeout[' + prototype.timeout + '] for state[' + state + ']');

			var self = this;
			this._state_timeout_id = setTimeout(function() {
				self._on_state_timeout();
			}, prototype.timeout);
		}
	};

});
//include eCarList.App.SmartChat.Common
//include eCarList.App.SmartChat.JSJaCUtils
//include eCarList.App.SmartChat.Room
//include eCarList.Util.Bindable
//include eCarList.Util.Restorable
//include eCarList.Util.Stateful

/*
	Structure: Presence

	Properties:
		show       - user availability <SmartChat.Show>
		status     - user status message
		login_date - user login date
		history    - [visitors] array of visitor locations
		referrer   - [visitors] http referrer details
*/

/*
	Structure: Contact

	Members of the user's roster contained in session.roster.
	
	Properties:
		available  - true if user is online, false otherwise
		group      - contact roster group
		jid        - contact jabber id <JSJaCJID>
		presence   - contact presence <SmartChat.Presence>
*/

/*
	Structure: Member
	
	Properties:
		nick     - room nickname
		jid      - room member jid <JSJaCJID>
		room_jid - room jid <JSJaCJID>
		presence - member presence <SmartChat.Presence>
*/

/*
	Structure: Message
	
	Properties:
		with_jid - jabber id of the other user/room <JSJaCJID>
		date     - message send/receipt date
		from     - message sender nick (undefined if sender is me or room)
		to       - message recipient nick (undefined if recipient is me)
		body     - message body html
*/

/*
	Structure: Invite

	Properties: 
		with_jid - jabber id of the other user/room <JSJaCJID>
		body     - message body html
		from     - message sender nick
		from_jid - message sender <JSJaCJID>
		reason   - invite reason
		room_jid - jabber id of the invited room <JSJaCJID>
*/

/*
	Structure: Decline

	Properties: 
		with_jid - jabber id of the other user/room <JSJaCJID>
		body     - message body html
		from     - message sender nick
		from_jid - message sender <JSJaCJID>
		reason   - invite reason
		room_jid - jabber id of the invited room <JSJaCJID>
*/

eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	var	MessageType = SmartChat.MessageType,
		Room = SmartChat.Room,
		RoomType = SmartChat.RoomType,
		Show = SmartChat.Show;

	/*
		Class: eCarList.App.SmartChat.Session
		
		Session encapsulates the management of the XMPP session via JSJaC.
		
		Properties:
			archive    - object that maps message parties to arrays of messages
			available  - true if connection is active, false otherwise
			http_base  - http bind url
			jid        - session user's jabber ID <JSJaCJID>
			nick       - session user's preferred nick name
			presence   - session user's presence <SmartChat.Presence>
			room_nicks - object that maps room jids to room nicknames
			rooms      - object that maps room jids to room objects
			roster     - object that maps roster contact jids to contact objects.
			secret     - session_id for ECL admins or visitor password
	*/
	SmartChat.Session = eCarList.Class.extend({

		JSJAC_LOG_LEVEL: 2,

		Event: {
			ARCHIVE_DATA_CHANGE: 'archive_data_change',
			AUTH_FAULT: 'auth_fault',
			CHAT_STATE_CHANGE: 'chat_state_change',
			CONNECT: 'connect',
			CONNECTION_FAULT: 'connection_fault',
			CONTACT_ADD: 'contact_add',
			CONTACT_CHANGE: 'contact_change',
			DISCONNECT: 'disconnect',
			INVITE_DECLINE: 'invite_decline',
			INVITE_RECEIPT: 'invite_receipt',
			JID_CONFLICT: 'jid_conflict',
			MEMBER_ADD: 'member_add',
			MEMBER_CHANGE: 'member_change',
			MEMBER_JOIN: 'member_join',
			MEMBER_LEAVE: 'member_leave',
			MEMBER_REMOVE: 'member_remove',
			MESSAGE_ADD: 'message_add',
			NICK_CHANGE: 'nick_change',
			RECONNECT: 'reconnect',
			ROOM_ADD: 'room_add',
			ROOM_INVITE: 'room_invite',
			ROOM_REMOVE: 'room_remove',
			ROOM_ROSTER_CHANGE: 'room_roster_change',
			ROSTER_CHANGE: 'roster_change',
			SESSION_REPLACE: 'session_replace',
			USER_CHANGE: 'user_change'
		},

		State: {
			FAULT: 'FAULT'
		},

		ChatState: {
			ACTIVE: 'active',
			COMPOSING: 'composing',
			PAUSED: 'paused',
			GONE: 'gone'
		},

		prototype: {

			/*
				Constructor: init
			*/
			init: function(properties) {
				this.available = false;
				this.archive = { };
				this.http_base = window.location.protocol + '//' + window.location.hostname + SmartChat.BIND_PATH;
				this.jid = null;
				this.nick = 'anon';
				this.presence = {
					show: Show.NONE,
					status: ''
				};
				this.room_nicks = { };
				this.rooms = { };
				this.roster = { };

				for (var attr in properties) {
					if (properties.hasOwnProperty(attr)) {
						this[attr] = properties[attr];
					}
				}
				//_init_connection.call(this);
			},

			reset: function() {
				this._connection.dispose();
				_init_connection.call(this);
			},

			/*
				Method: connect
			
				Establishes an XMPP HTTP Binding connection.

				Parameters:
					show   - [optional] the initial show value <SmartChat.Show>
			*/
			connect: function(show) {
				this.connect_as('lol_' + (new Date()).getTime().toString(36), show);
			},

			/*
				Method: connect_as

				Establishes an XMPP HTTP Binding connection with the specified resource.
			
				Parameters:
					resource - jabber id resource to use
					show     - [optional] the initial show value <SmartChat.Show>
			*/
			connect_as: function(resource, show) {
				if (!resource) { 
					throw 'invalid resource: ' + resource;
				}

				if (show || show === '') {
					this.presence.show = show;
				}
				
				// establish a unique resource for each connection
				this.jid.setResource(resource);

				// create a new _connection (cause the old one might suck)
				if (this._connection) {
					this._connection.dispose();
					delete this._connection;
				}
				_init_connection.call(this);

				this._connection.connect({
					domain:   this.jid.getDomain(),
					username: this.jid.getNode(),
					resource: this.jid.getResource(),
					pass:     this.secret,
					register: false
				});
			},

			/*
				Method: with_connection

				Invokes given callback once after a connection becomes available. If a connection is currently
				available the callback is invoked immediately.

				Parameters:
					fn    - connected callback
					scope - [optional] context to invoke callback under, this session by default
			*/
			with_connection: function(fn, scope) {
				if (this._connection && this._connection.connected()) {
					fn.call(scope || this);
				} else {
					this.bind_once(this.klass.Event.CONNECT, function(event) {
						fn.call(this);
					}, scope || this);
				}
			},

			/*
				Method: with_presence

				Invokes given callback after a user's presence information becomes available. If it is already
				available, the callback is invoked immedately. 

				Parameters:
					fn    - presence callback
					scope - [optional] context to invoke callback under, this session by default
			*/
			with_presence: function(fn, scope) {
				if (this.presence.login_date) {
					fn.call(scope || this);
				} else {
					this.bind_once(this.klass.Event.USER_CHANGE, function(event) {
						fn.call(this);
					}, scope || this);
				}
			},

			/*
				Method: suspend
	
				Suspends an existing XMPP HTTP Binding connection, if currently connected.

				Returns:
					True if connection is successfully suspended, false otherwise.
			*/
			suspend: function() {
				if (this._connection && this._connection.connected()) {
					eCarList.debug('suspending jsjac connection...');
					this._connection.suspend();
					return true;
				}
				return false;
			},

			/*
				Method: resume

				Resumes a previously suspended connection

				Returns:
					True if connection is successfully resumed, false otherwise.
			*/
			resume: function() {
				if (this.available) {
					try {
						_init_connection.call(this);
						if (this._connection.resume()) {
							return true;
						}
						this.available = false;
					} catch (e) {
						// ignore missing cookie exception
					}
				}
				return false;
			},

			/*
				Method: disconnect

				Destroys an existing XMPP HTTP Binding connection.
			*/
			disconnect: function() {
				delete this.faults;
				delete this.retry_delay;
				if (this._connection && this._connection.connected()) {
					// leave any rooms we're currently in
					for (var bjid in this.rooms) {
						if (this.rooms.hasOwnProperty(bjid)) {
							var jid = this.rooms[bjid].jid;
							this.leave_room(jid);
						}
					}
					this._connection.disconnect();
				}
			},

			/*
				Method: query_roster

				Queries the Jabber server for the connected user's roster.

				Returns:
					True if request is successfully sent to server, false otherwise.
			*/
			query_roster: function() {
				var iq = new JSJaCIQ();
				iq.setFrom(this.jid);
				iq.setIQ(null, 'get', _request_id('roster'));
				iq.setQuery(NS_ROSTER);

				return _send_iq.call(this, iq, function(result) {
					eCarList.debug('roster_result: ' + result.xml());
					this.roster = { };

					// retrieve roster items
					var self = this;
					$('item', result.getQuery()).each(function(i, item) {
						_add_contact.call(self, new JSJaCJID($(item).attr('jid')), $('group', item).text());
					});
					this.trigger(this.klass.Event.ROSTER_CHANGE);
				});
			},

			/*
				Method: get_groups

				Returns:
					An array of groups in this session's roster.
			*/
			get_groups: function() {
				var groups = [];
				var unique = { };
				for (var jid in this.roster) {
					if (this.roster.hasOwnProperty(jid)) {
						var group = this.roster[jid].group;
						if (!unique[group]) {
							groups.push(group);
							unique[group] = true;
						}
					}
				}
				return groups;
			},

			/*
				Method: get_group_contacts

				Parameters:
					group - name of the group

				Returns:
					An array of contacts in the group.
			*/
			get_group_contacts: function(group) {
				var group_contacts = [];
				for (var bare_jid in this.roster) {
					if (this.roster.hasOwnProperty(bare_jid) && this.roster[bare_jid].group === group) {
						group_contacts.push(this.roster[bare_jid]);
					}
				}
				return group_contacts;
			},

			/*
				Method: get_contact_nick

				Determines best nickname to use for the given contact jabber id.

				Parameters:
					contact_jid - jabber id of contact <JSJaCJID>

				Returns:
					contact nickname.
			*/
			get_contact_nick: function(contact_jid) {
				if (contact_jid.isRoom()) {
					return contact_jid.getResource();
				} else {
					// attempt to get nickname from a room, fallback to node
					var nick = this.get_roommate_nick(contact_jid);
					return (nick) ? nick : contact_jid.getNode();
				}
			},

			/*
				Method: get_chat_jid

				Retrieves this session's jabber id for the conversation associated with with_jid. If
				with_jid is a room then this session's room_jid is returned, if with_jid is a person
				then this session's jid is returned.

				Parameters:
					with_jid - jabber id of the other conversation user/room <JSJaCJID>

				Returns
					jabber id of this user for chat with with_jid
			*/
			get_chat_jid: function(with_jid) {
				if (with_jid.isRoom()) {
					return _get_room_jid.call(this, with_jid);
				} else {
					return this.jid;
				}
			},

			/*
				Method: update_nick

				Updates this session's nickname.

				TODO:
					Currently this will only change the user's nickname if this session is offline. Changing
					it while online requires sending presence updates to each room we're participating in which
					will cause the server to generate presence unavail & avail presences for the old & new nicks
					respectively.

				Parameters:
					nick - new nickname

				Returns:
					True if nickname is updated, false otherwise.
			*/
			update_nick: function(nick) {
				if (!this.available) {
					this.nick = nick;
					this.trigger(this.klass.Event.NICK_CHANGE, nick);
					return true;
				}
				return false;
			},

			/*
				Method: ping_presence

				Sends "ping" type presence packet to server to request the latest presence for this user.

				Returns:
					True if request is successfully sent to the server, false otherwise.
			*/
			ping_presence: function() {
				var packet = new JSJaCPresence();
				packet.setType('ping');
				packet.setFrom(this.jid);
				return this._connection.send(packet);
			},

			/*
				Method: update_presence
			
				Updates availability and status message for user and in all current rooms.

				Parameters:
					show   - [optional] the new show value <SmartChat.Show>
					status - [optional] the new status message
			
				Returns:
					True if request is successfully sent to the server, false otherwise.
			*/
			update_presence: function(show, status) {
				var packet = this._create_presence_packet();

				if (show || show === '') {
					packet.setShow(show);
				} else {
					packet.setShow(this.presence.show);
				}

				if (status || status === '') {
					packet.setStatus(status);
				} else {
					packet.setStatus(this.presence.status);
				}

				eCarList.debug('sent_presence: ' + packet.xml());

				if (this._connection.send(packet) && !this.in_state(this.klass.State.FAULT)) {
					for (var bjid in this.rooms) {
						if (this.rooms.hasOwnProperty(bjid)) {
							this.update_room_presence(this.rooms[bjid].jid, show, status);
						}
					}
					return true;
				}
				return false;
			},

			/*
				Method: get_room_nick

				Retrieves nick name for this session in the specified room.

				Parameters:
					jid       - room's jabber id <JSJaCJID>
					- or -
					domain    - [optional] room's jabber host
					room_node - room node

				Returns:
					This user's nickname in room, null if user is not in room.			
			*/
			get_room_nick: function() {
				var room_jid = _get_room_jid.apply(this, arguments);
				var nick = this.room_nicks[room_jid.bare()];
				return nick || null;
			},

			/*
				Method: get_rooms

				Retrieves room collection as array

				Returns:
					Array of rooms contained by this session.
			*/
			get_rooms: function() {
				var array = [];
				for (var bjid in this.rooms) {
					if (this.rooms.hasOwnProperty(bjid)) {
						array.push(this.rooms[bjid]);
					}
				}
				return array;
			},

			/*
				Method: get_max_priority_room

				Returns the room with the highest priority. Public rooms all have a priority of 0,
				private room priorities increment.

				Returns:
					Room with the highest priority, null if session is not a member of any rooms.
			*/
			get_max_priority_room: function() {
				var max_room = null;
				for (var bare_jid in this.rooms) {
					if (this.rooms.hasOwnProperty(bare_jid)) {
						var room = this.rooms[bare_jid];
						if (max_room === null || room.priority > max_room.priority) {
							max_room = room;
						}
					}
				}
				return max_room;
			},

			/*
				Method: get_room
			
				Retrieves the specified room. This session's user must be a current member of the room.

				Parameters:
					jid       - room's jabber id <JSJaCJID>
					- or -
					domain    - [optional] room's jabber host
					room_node - room node

				Returns:
					The room object if this session is a member of the room, null otherwise.
			*/
			get_room: function() {
				var room_jid = _get_room_jid.apply(this, arguments);
				var room = this.rooms[room_jid.bare()];
				return room || null;
			},

			/*
				Method: get_team_room

				Parameters:
					team_eid - team's eid

				Returns:
					The room object if this session is a member of the room, null otherwise.
			*/
			get_team_room: function(team_eid) {
				var room_node = Room.create_team_node(team_eid);
				return this.get_room(SmartChat.ADMIN_DOMAIN, room_node);
			},

			/*
				Method: get_private_room

				Parameters:
					visitor_node - visitor's node

				Returns:
					The room object if this session is a member of the room, null otherwise.
			*/
			get_private_room: function(visitor_node) {
				var room_node = Room.create_private_node(visitor_node);
				return this.get_room(SmartChat.ADMIN_DOMAIN, room_node);
			},

			/*
				Method: get_room_member

				Returns the member object for the specified room jid & member jid.

				Parameters:
					room_jid   - room jabber id <JSJaCJID>
					member_jid - member jabber id <JSJaCJID>

				Returns:
					Member object if found, null otherwise.
			*/
			get_room_member: function(room_jid, member_jid) {
				var room = this.get_room(room_jid);
				return (room) ? room.get_member(member_jid) : null;
			},

			/*
				Method: get_roommate_rooms

				Returns all rooms shared by this session and the given member jid.

				Parameters:
					member_jid - roommate jabber id <JSJaCJID>
	
				Returns:
					An array of rooms shared by this session and member_jid.
			*/
			get_roommate_rooms: function(member_jid) {
				return $.grep(this.get_rooms(), function(room) {
					return room.get_member(member_jid);
				});
			},

			/*
				Method: get_roommate_nick

				Returns the nickname of the specified roommate in the first found room.

				Parameters:
					member_jid - roommate jabber id <JSJaCJID>

				Returns:
					Nickname of roommate in first found room if one exists, null otherwise.
			*/
			get_roommate_nick: function(member_jid) {
				var room = this.get_roommate_rooms(member_jid)[0];
				return (room) ? room.get_member(member_jid).nick : null;
			},

			/*
				Method: join_room

				Joins the specified room. Uses this session's node and the room nickname.

				Request:
					<presence from='crone1@shakespeare.lit/desktop' to='darkcave@chat.shakespeare.lit/firstwitch'>
						<x xmlns='http://jabber.org/protocol/muc'/>
					</presence>

				Parameters:
					jid       - room's jabber id <JSJaCJID>
					- or -
					domain    - [optional] room's jabber host
					room_node - room node

				Returns:
					Bare room jid if request is successfully sent to server, null otherwise.
			*/
			join_room: function() {
				var room_jid = _get_room_jid.apply(this, arguments);

				if (!this.in_state(this.klass.State.FAULT) && this.rooms[room_jid.bare()]) {
					// already in room, just return jid
					return new JSJaCJID(room_jid.bare());
				}

				var packet = this._create_presence_packet();
				packet.setFrom(this.jid);
				packet.setTo(room_jid.toString());
				packet.appendNode('x', { xmlns: NS_MUC });
				eCarList.debug('join_room: ' + packet.xml());
				if (this._connection.send(packet)) {
					return new JSJaCJID(room_jid.bare());
				}
				return null;
			},

			/*
				Method: join_team_room

				Joins the specified team room. Team rooms are unique to team_eid

				Parameters:
					team_eid - team's eid

				Returns:
					Bare room jid if request is successfully sent to server, null otherwise.
			*/
			join_team_room: function(team_eid) {
				var room_node = Room.create_team_node(team_eid);
				return this.join_room(SmartChat.ADMIN_DOMAIN, room_node);
			},

			/*
				Method: join_private_room

				Joins the specified private room. Private rooms are unique to visitor.

				Parameters:
					visitor_node - visitor's node

				Returns:
					Bare room jid if request is successfully sent to server, null otherwise.
			*/
			join_private_room: function(visitor_node) {
				var room_node = Room.create_private_node(visitor_node);
				return this.join_room(SmartChat.ADMIN_DOMAIN, room_node);
			},

			/*
				Method: leave_room

				Leaves the specified room.

				Request:
					<presence from='hag66@shakespeare.lit/pda' to='darkcave@chat.shakespeare.lit/thirdwitch'
						type='unavailable'/>

				Parameters:
					jid       - room's jabber id <JSJaCJID>
					- or -
					domain    - [optional] room's jabber host
					room_node - room node

				Returns:
					Bare room jid if request is successfully sent to server, null otherwise.
			*/
			leave_room: function() {
				var room_jid = _get_room_jid.apply(this, arguments);
				var packet = this._create_presence_packet();
				packet.setFrom(this.jid);
				packet.setTo(room_jid.toString());
				packet.setType('unavailable');
				eCarList.debug('leave_room: ' + packet.xml());
				if (this._connection.send(packet)) {
					return new JSJaCJID(room_jid.bare());
				}
				return null;
			},

			/*
				Method: ping_room_presence

				Sends "ping" type presence packet to server to request the latest presence for everyone in the room.

				Parameters:
					jid       - room's jabber id <JSJaCJID>
					- or -
					domain    - [optional] room's jabber host
					room_node - room node

				Returns:
					True if request is successfully sent to the server, false otherwise.
			*/
			ping_room_presence: function() {
				var room_jid = _get_room_jid.apply(this, arguments);
				var packet = new JSJaCPresence();
				packet.setType('ping');
				packet.setFrom(this.jid);
				packet.setTo(room_jid.bare());
				eCarList.debug('ping_room_presence: ' + packet.xml());
				return this._connection.send(packet);
			},

			/*
				Method: query_room_roster (Deprecated - use ping_room_presence)
	
				Updates room roster. Our mod_muc_room has been customized to return a user_jid attribute
				which specifies the user's actual jid in addition to the uer's room jid.
	
				Request:
					<iq from='hag66@shakespeare.lit/pda' id='disco4' to='darkcave@chat.shakespeare.lit' type='get'>
						<query xmlns='http://jabber.org/protocol/disco#items'/>
					</iq>
	
				Result:
					<iq from='darkcave@chat.shakespeare.lit' id='disco4' to='hag66@shakespeare.lit/pda' type='result'>
						<query xmlns='http://jabber.org/protocol/disco#items'>
							<item user_jid="daniel@ecarlist.com/mac" jid='darkcave@chat.shakespeare.lit/firstwitch'/>
							<item user_jid="oliver@ecarlist.com/smartchat" jid='darkcave@chat.shakespeare.lit/secondwitch'/>
						</query>
					</iq>
	
				Parameters:
					jid       - room's jabber id <JSJaCJID>
					- or -
					domain    - [optional] room's jabber host
					room_node - room node
	
			*/
			query_room_roster: function() {
				var room_jid = _get_room_jid.apply(this, arguments);

				var iq = new JSJaCIQ();
				iq.setFrom(this.jid);
				iq.setIQ(room_jid.bare(), 'get', _request_id('disco'));
				iq.setQuery(NS_DISCO_ITEMS);

				return _send_iq.call(this, iq, function(result) {
					eCarList.debug('room_roster_result: ' + result.xml());

					// if we're not in the room, remove it
					if ($('item[user_jid="' + this.jid + '"]', result.getQuery()).length === 0) {
						_remove_room.call(this, room_jid);
						return;
					}

					// clear roster
					var room = this.get_room(room_jid);
					if (room) {
						room.members.length = 0;
					}

					// fill out roster
					var self = this;
					$('item', result.getQuery()).each(function(i, item) {
						var member_room_jid = new JSJaCJID(item.getAttribute('jid'));
						var member_jid = new JSJaCJID(item.getAttribute('user_jid'));
						_add_room_member.call(self, member_room_jid, member_jid, { }, false);
					});

					this.trigger(this.klass.Event.ROOM_ROSTER_CHANGE, room);
				}, function(code, type, cond) {
					if (code === 404 && type === 'cancel') {
						// room not found, remove it from collection
						_remove_room.call(this, room_jid);
					} else {
						eCarList.error('error ' + code + '/' + type + ':\n' + cond.xml);
					}
				});
			},

			/*
				Method: update_room_presence
	
				Updates availability and status message for user in the specified room.
	
				Parameters:
					room_jid - room jabber id <JSJaCJID>
					show     - [optional] the new show value <SmartChat.SHOW>
					status   - [optional] the new status message
	
				Returns:
					True if request is successfully sent to server, false otherwise
			*/
			update_room_presence: function(room_jid, show, status) {
				var packet = this._create_presence_packet();
				packet.setTo(room_jid);

				if (show || show === '') {
					packet.setShow(show);
				} else {
					packet.setShow(this.presence.show);
				}

				if (status || status === '') {
					packet.setStatus(status);
				} else {
					packet.setStatus(this.presence.status);
				}

				eCarList.debug('sent_room_presence: ' + packet.xml());
				return this._connection.send(packet);
			},

			/*
				Method: send_invite
	
				Sends a room invitation to the specified invitee.
	
				Request:
					<message from='crone1@shakespeare.lit/desktop' to='darkcave@chat.shakespeare.lit'>
						<x xmlns='http://jabber.org/protocol/muc#user'>
							<invite to='hecate@shakespeare.lit'>
								<reason>Hey Hecate, this is the place for all good witches!</reason>
							</invite>
						</x>
					</message>
	
				Parameters:
					room_jid    - room's jabber id <JSJaCJID>
					invitee_jid - invited user's jabber id <JSJaCJID>
					reason      - invite message
	
				Returns:
					True if request is successfully sent to server, false otherwise.
			*/
			send_invite: function(room_jid, invitee_jid, reason) {
				var message = new JSJaCMessage();
				message.setFrom(this.jid);
				message.setTo(room_jid.bare());

				var invite = message.getNode()
				.appendChild(message.buildNode('x', { xmlns: NS_MUC_USER }))
				.appendChild(message.buildNode('invite', { to: invitee_jid.bare() }));
				
				if (reason) {
					invite.appendChild(message.buildNode('reason', { }, reason));
				}
				
				eCarList.debug('sent_invite: ' + message.xml());
				return this._connection.send(message);
			},

			/*
				Method: decline_invite (unsupported)
	
				Sends a room invitation decline to the specified inviter. Invitation declines are not suported
				by ejabberd 2.05, but will be supported by 2.1.0.
	
				Request:
					<message from='hecate@shakespeare.lit/broom' to='darkcave@chat.shakespeare.lit'>
						<x xmlns='http://jabber.org/protocol/muc#user'>
							<decline to='crone1@shakespeare.lit'>
								<reason>Sorry, I'm too busy right now.</reason>
							</decline>
						</x>
					</message>
	
				Parameters:
					room_jid    - room's jabber id <JSJaCJID>
					inviter_jid - inviter user's jabber id <JSJaCJID>
					reason      - decline message
			*/
			decline_invite: function(room_jid, inviter_jid, reason) {
				var message = new JSJaCMessage();
				message.setFrom(this.jid);
				message.setTo(room_jid.bare());
				
				var decline = message.getNode()
				.appendChild(message.buildNode('x', { xmlns: NS_MUC_USER }))
				.appendChild(message.buildNode('decline', { to: inviter_jid.bare() }));

				if (reason) {
					decline.appendChild(message.buildNode('reason', { }, reason));
				}

				eCarList.debug('sent_decline: ' + message.xml());
				return this._connection.send(message);
			},

			/*
				Method: decline_invite_direct
	
				Sends a room invitation decline directly to the specified inviter. Unlike 'decline_invite', the decline is directed to the inviter and does not go through the room. This is non-standard behavior to deal with ejabberd 2.05's lack of support for declining invites.
	
				Parameters:
					room_jid    - room's jabber id <JSJaCJID>
					inviter_jid - inviter user's jabber id <JSJaCJID>
					reason      - decline message
			*/
			decline_invite_direct: function(room_jid, inviter_jid, reason) {
				var message = new JSJaCMessage();
				message.setFrom(this.jid);
				message.setTo(inviter_jid);
				
				var decline = message.getNode()
				.appendChild(message.buildNode('x', { xmlns: 'http://ecarlist.com/protocol/muc#user' }))
				.appendChild(message.buildNode('decline', { room: room_jid.bare() }));

				var body = this.jid + ' declined your invite to the room ' + room_jid.bare();

				if (reason) {
					body += ' (' + reason + ')';
					decline.appendChild(message.buildNode('reason', { }, reason));
				}

				message.setBody(body);

				eCarList.debug('sent_decline_direct: ' + message.xml());
				return this._connection.send(message);
			},
			
			/*
				Method: update_chat_state

				Updates the current chat state for this session in the chat specified
				by with_jid.

				Parameters:
					with_jid   - jabber id of other user/room <JSJaCJID>
					chat_state - new chat state <Session.ChatState>

				Returns:
					True if chat state change message is successfully sent, false otherwise
			*/
			update_chat_state: function(with_jid, chat_state) {
			
				if (_update_contact_chat_state.call(this, with_jid, this.get_chat_jid(with_jid), chat_state)) {
					// create state change message
					var message = new JSJaCMessage();
					if (with_jid.isRoom()) {
						// strip room resource & change type for room messages
						message.setTo(with_jid.bare());
						message.setType('groupchat');
					} else {
						message.setTo(with_jid);
					}

					message.getNode().appendChild(message.buildNode(chat_state, {
						xmlns: 'http://jabber.org/protocol/chatstates' }));

					return this._connection.send(message);
				}
				return false;
			},

			/*
				Method: send_message
	
				Creates message packet and sends to the Jabber server.
	
				Request:
					<message>
						<body>Wow, I&apos;m green with envy!</body>
						<html xmlns='http://jabber.org/protocol/xhtml-im'>
							<body xmlns='http://www.w3.org/1999/xhtml'>
								<p style='font-size:large'>
									<em>Wow</em>, I&apos;m <span style='color:green'>green</span> with
									<strong>envy</strong>!
								</p>
							</body>
						</html>
					</message>
	
				Parameters:
					recipient_jid - the message recipient's jid <JSJaCJID>
					body          - the message body (plain text)
					body_html     - [optional] the message body html
	
				Returns:
					True if request is successfully sent to server, false otherwise.
			*/
			send_message: function(recipient_jid, body, body_html) {
				
				var jsjac_message = new JSJaCMessage();

				if (recipient_jid.isRoom()) {
					// strip room resource & change type for room messages
					jsjac_message.setTo(recipient_jid.bare());
					jsjac_message.setType('groupchat');
				} else {
					jsjac_message.setTo(recipient_jid);
				}

				jsjac_message.setFrom(this.jid);
				jsjac_message.setBody(body_html || body.htmlEnc());
				if (body_html) {
					jsjac_message.setBodyHTML(body_html);
				}

				// when sending a message, chat state is always active
				if (_update_contact_chat_state.call(this, recipient_jid, this.get_chat_jid(recipient_jid),
													this.klass.ChatState.ACTIVE)) {
					jsjac_message.getNode()
					.appendChild(jsjac_message.buildNode(this.klass.ChatState.ACTIVE, { 
						xmlns: 'http://jabber.org/protocol/chatstates' }));
				}

				eCarList.debug('sent_message: ' + jsjac_message.xml());
				if (this._connection.send(jsjac_message)) {

					// only add private messages, room messages get reflected back
					if (!recipient_jid.isRoom()) {
						var message = {
							with_jid: recipient_jid,
							date: new Date(),
							to: recipient_jid.getNick(),
							body: body.htmlEnc(),
							body_html: body_html,
							type: MessageType.MESSAGE
						};
						_add_message.call(this, message);
					}
					return true;
				}
				return false;
			},

			/*
				Method: create_system_message
	
				Creates a "system" message and sends it to this session. System messages are created by sending a
				message to echo@system.ecarlist.com which will echo the message back at us from
				system@system.ecarlist.com.
	
				Parameters:
					body      - body of the message
					body_html - [optional] html version of body
	
				Returns:
					True if request is successfully sent to server, false otherwise.
			*/
			create_system_message: function(body, body_html) {
				var jsjac_message = new JSJaCMessage();
				jsjac_message.setFrom(this.jid);
				jsjac_message.setTo(new JSJaCJID('echo@system.ecarlist.com'));
				jsjac_message.setBody(body_html || body.htmlEnc());
				if (body_html) {
					jsjac_message.setBodyHTML(body_html);
				}

				eCarList.debug('sent_message: ' + jsjac_message.xml());
				return this._connection.send(jsjac_message);
			},

			/*
				Method: archive_data
	
				Associates the given data with the current conversation with with_jid. Each property of the
				given data object will be saved to a field in the archive.
	
				Archive Request:
					<iq type='set' id='form1'>
						<save xmlns='urn:xmpp:archive'>
							<chat with='benvolio@montague.net' start='1469-07-21T03:01:54Z'>
								<x xmlns='jabber:x:data' type='submit'>
									<field var='FORM_TYPE'><value>http://example.com/archiving</value></field>
									<field var='task'><value>1</value></field>
									<field var='important'><value>1</value></field>
									<field var='action_before'><value>1469-07-29T12:00:00Z</value></field>
								</x>
							</chat>
						</save>
					</iq>
	
				Parameters:
					with_jid - jabber id of other conversation participant <JSJaCJID>
					data     - data object to associate with latest convo
			*/
			archive_data: function(with_jid, data) {
				if (_update_archive_data.call(this, with_jid, data)) {

					data = this.archive[with_jid.bare()].data;

					_last_chat.call(this, with_jid, function(chat_with_jid, start_date) {
						var iq = new JSJaCIQ();
						iq.setIQ(null, 'set', _request_id('archive_data'));

						var save_el = iq.getNode().appendChild(iq.buildNode('save', {
							xmlns: 'http://www.xmpp.org/extensions/xep-0136.html#ns'
						}));

						var chat_el = save_el.appendChild(iq.buildNode('chat', {
							start: start_date.jabberDate()
						}));
						chat_el.setAttribute('with', chat_with_jid.toString());

						var x_el = chat_el.appendChild(iq.buildNode('x', {
							xmlns: NS_XDATA,
							type: 'submit'
						}));

						for (var property in data) {
							if (data.hasOwnProperty(property)) {
								var field_el = x_el.appendChild(iq.buildNode('field'));
								field_el.setAttribute('var', property);
								var value_json = JSJaCJSON.toString(data[property]);
								field_el.appendChild(iq.buildNode('value', { }, value_json));
							}
						}

						eCarList.debug('sent_iq: ' + iq.xml());
						var sent = _send_iq.call(this, iq, function(result) {
							eCarList.debug('archive_data_result: ' + result.xml());
							// nothing to do
						});
					});
				}
			},

			/*
				Method: query_archive
	
				Loads archive with all messages from the last chat with jid.
	
				Collection List Request:
					<iq type='get' id='juliet1'>
						<list xmlns='urn:xmpp:archive' with='juliet@capulet.com'>
							<set xmlns='http://jabber.org/protocol/rsm'>
								<max>30</max>
							</set>
						</list>
					</iq>
	
				Collection Request:
					<iq type='get' id='page1'>
						<retrieve xmlns='urn:xmpp:archive' with='juliet@capulet.com/chamber' start='1469-07-21T02:56:15Z'>
							<set xmlns='http://jabber.org/protocol/rsm'>
								<max>100</max>
							</set>
						</retrieve>
					</iq>
	
				Parameters:
					with_jid - archive jid
			*/
			query_archive: function(with_jid) {

				_last_chat.call(this, with_jid, function(chat_with_jid, start_date) {
					var msg_iq = new JSJaCIQ();
					msg_iq.setType('get');
					var retrieve_el = msg_iq.getNode().appendChild(msg_iq.buildNode('retrieve', {
						xmlns: 'http://www.xmpp.org/extensions/xep-0136.html#ns',
						start: start_date.jabberDate()
					}));
					retrieve_el.setAttribute('with', chat_with_jid.toString());
					
					_each_result.call(this, msg_iq, function(msg, msg_index, msg_count) {
						eCarList.debug('_archive_msg: ' + msg.xml);
						if (msg.tagName === 'x') {
							// archive data
							var data = { };
							$('field', msg).each(function() {
								var property = this.getAttribute('var');
								var value_json = $('value', this).text();
								data[property] = JSJaCJSON.parse(value_json);
							});
							_update_archive_data.call(this, with_jid, data);
						} else {
							// archive message
							var date = new Date();
							date.setTime(start_date.getTime() + parseInt(msg.getAttribute('secs'), 10) * 1000);

							var message = {
								with_jid: chat_with_jid,
								date: date,
								body: $('body', msg).text()
							};
							
							if (chat_with_jid.isRoom()) {
								var name = msg.getAttribute('name');
								if (name) {
									message[msg.tagName] = name;
									message.type = MessageType.MESSAGE;
								} else {
									message.type = MessageType.STATUS;
								}
							} else {
								message[msg.tagName] = chat_with_jid.getNick();
								message.type = MessageType.MESSAGE;
							}
							
							_add_message.call(this, message);
						}
						return true;
					});
					
					return false;
				}, { max: 1 });
			},

			/*
				Method: clear_archive
	
				Clears local copy of archive and prevents it from being queried in the future. If no with_jid
				is specified, the entire archive will be cleared.
	
				Parameters:
					with_jid - [optional] jabber id of other conversation participant <JSJaCJID>
			*/
			clear_archive: function(with_jid) {
				if (with_jid) {
					delete this.archive[with_jid.bare()];
				} else {
					for (var bjid in this.archive) {
						if (this.archive.hasOwnProperty(bjid)) {
							delete this.archive[bjid];
						}
					}
				}
			},

			/*
				Method: get_messages
	
				Returns all archived messages that have occurred between session user and with_jid.
	
				Parameters:
					with_jid - jabber id of other conversation partcipant <JSJaCJID>
	
				Returns:
					Array of messages between this user & with_jid.
			*/
			get_messages: function(with_jid) {
				var archive = this.archive[with_jid.bare()];
				return (archive && archive.messages) ? archive.messages : [];
			},

			/*
				Method: get_chat_duration
	
				Determines the length of a conversation in milliseconds.
	
				Parameters:
					with_jid - jabber id of the other conversation participant <JSJaCJID>
	
				Returns:
					chat duration in milliseconds, 0 if no chat for the specified with_jid
			*/
			get_chat_duration: function(with_jid) {
				var messages = this.get_messages(with_jid);
				if (messages.length >= 2) {
					return messages[messages.length-1].date.getTime() - messages[0].date.getTime();
				}
				return 0;
			},

			/*
				Method: get_chat_log
	
				Returns a stripped down version of the chat message archive.
	
				Parameters:
					with_jid - jabber id of other conversation participant.
	
				Returns: 
					Array of log objects, each containing a nickname and message.
			*/
			get_chat_log: function(with_jid) {
				var messages = this.get_messages(with_jid);
				var self = this;
				return $.map(messages, function(message) {
					return {
						nickname: message.from || self.nick,
						date: message.date.getTime(),
						message: $.ecl.string.strip_html(message.body_html || message.body)
					};
				});
			},

			/*
				Method: get_transcript
	
				Creates a chat transcript for the conversation that occurred with with_jid.
	
				Parameters:
					with_jid - jabber id of other conversation participant <JSJaCJID>
	
				Returns:
					Single chat transcript string.
			*/
			get_transcript: function(with_jid) {
				var messages = this.get_messages(with_jid);
				var self = this;
				messages = $.map(messages, function(message) {
					return (message.from || self.nick) + ': ' + (message.body_html || message.body);
				});
				return messages.join('\n');
			},

			/*
				Method: get_archive_data
				
				Returns data that has previously been archived from a conversation with with_jid.
	
				Parameters:
					with_jid - jabber id of other conversation participant <JSJaCJID>
	
				Returns:
					Object containing archived data.
				
			*/
			get_archive_data: function(with_jid) {
				var archive = this.archive[with_jid.bare()];
				return (archive && archive.data) ? archive.data : { };
			},

			/*
				Method: _create_presence_packet
	
				Creates a smartchat presence packet.
			*/
			_create_presence_packet: function() {
				var packet = new JSJaCPresence();
				var smartchat_attr = { xmlns: 'http://ecarlist.com/protocol/presence' };
				if (this.presence && this.presence.login_date) {
					smartchat_attr.login = this.presence.login_date.jabberDate();
				}
				packet.appendNode('smartchat', smartchat_attr);
				return packet;
			},

			/*
				Method: _parse_presence_packet
	
				Parses presence information from presence packet
	
				Parameters:
					packet - presence packet <JSJaCPresence>
				
				Returns:
					Parsed presence object. <SmartChat.Presence>
			*/
			_parse_presence_packet: function(packet) {
				var presence = { };
				
				presence.show = packet.getShow();
				presence.status = packet.getStatus();

				var login = $('smartchat', packet.getNode()).attr('login');
				presence.login_date = (login) ? Date.jab2date(login) : new Date();

				// parse session info
				var session_el = $('smartchat > session', packet.getNode()).each(function() {
					presence.dealer_eid = this.getAttribute('dealer_eid');
					presence.dealer_id = this.getAttribute('dealer_id');
					presence.domain = this.getAttribute('domain');
				});

				// parse profile info
				var profile_el = $('smartchat > profile', packet.getNode()).each(function() {
					presence.email = $(this).attr('email');
					presence.phone = $(this).attr('phone');
				});			

				presence.history = [];
				$('smartchat > history > location', packet.getNode()).each(function() {
					presence.history = presence.history || [];
					var location = $(this).ecl().attr_set('title', 'url', 'dealer_id', 'vehicle_id');
					presence.history.push(location);
				});

				presence.referrer = $('smartchat > referrer', packet.getNode()).attr('url') || '';

				return presence;
			},

			/*
				Method: _get_room_name
	
				Determines a room name for the given room. Subclasses can override this method
				to create appropriate names for their perspective.
	
				Parameters:
					room - room <SmartChat.Room>
	
				Returns room name string
			*/
			_get_room_name: function(room) {
				return (room) ? room.jid.getNode() : null;
			},

			toString: function() {
				return this.jid.toString();
			}

		}
	}).mix(eCarList.Util.Bindable, eCarList.Util.Restorable, eCarList.Util.Stateful)

	.dont_save(/^_/)
	.save_as_date(/^(.+_)?date$/)
	.save_as_jid(/^(.+_)?jid$/)

	.restore_as('state', function(origin, attr, state) {
		this.init_state(state);
		if (this.state === this.klass.State.FAULT) {
			this.reconnect();
		}
	})

	.dont_save('http_base')
	.dont_save('room_nicks')
	.dont_save('roster')

	.save_as('presence', function(memento, attr, presence) {
		memento._presence = 1;
	})

	.restore_as('_presence', function(origin, attr, _presence) {
		this.bind_once(this.klass.Event.CONNECT, function(event) {
			// load this user's presence
			this.ping_presence();
		});
	})

	.cache('presence')

	.save_as('rooms', function(memento, attr, rooms) {
		// marshal rooms jids
		if (this.available || this.in_state(this.klass.State.FAULT)) {
			memento._rooms = [];
			for (var bjid in rooms) {
				if (rooms.hasOwnProperty(bjid)) {
					// make sure we capture room_nick with jid
					var room_jid = _get_room_jid.call(this, rooms[bjid].jid);
					memento._rooms.push(room_jid.toString());
				}
			}
		}
	})

	.restore_as('_rooms', function(origin, attr, _rooms) {

		// unmarshal room info
		for (var i = 0; i < _rooms.length; i++) {
			var room_jid = new JSJaCJID(_rooms[i]);
			this.room_nicks[room_jid.bare()] = room_jid.getNick();
			_add_room_member.call(this, room_jid, this.jid, this.presence, false);
		}

		this.bind_once(this.klass.Event.CONNECT, function(event) {
			// load room rosters
			for (var i = 0; i < _rooms.length; i++) {
				var room_jid = new JSJaCJID(_rooms[i]);
				this.ping_room_presence(room_jid);
			}
		});
	})

	.save_as('archive', function(memento, attr, archive) {
		// marshal "with" jids of any conversations we've had
		if (this.available || this.in_state(this.klass.State.FAULT)) {
			memento._archive = [];
			for (var bjid in archive) {
				if (archive.hasOwnProperty(bjid)) {
					memento._archive.push(bjid);
				}
			}
		}
	})

	.restore_as('_archive', function(origin, attr, _archive) {
		// load archive after connect
		for (var i = 0; i < _archive.length; i++) {
			this.archive[_archive[i]] = { data: { }, messages: [] };
		}
		this.bind_once(this.klass.Event.CONNECT, function(event) {
			for (var j = 0; j < _archive.length; j++) {
				this.query_archive(new JSJaCJID(_archive[j]));
			}
		});
	})

	.cache('archive');

	SmartChat.Session.define_state(SmartChat.Session.State.FAULT, {

		reconnect: function() {
			if (!this.retry_delay) {
				// delay up _fibonacci(#faults) secs up to a max of _fibonacci(10) (55 secs)
				this.retry_delay = 1000 * _fibonacci(Math.min(this.faults || 1, 10));
			}
			var self = this;
			this._retry_interval = setInterval(function() {
				self.retry_delay -= 1000;
				if (self.retry_delay <= 0) {
					eCarList.debug('reconnecting...');
					clearInterval(self._retry_interval);
					delete self.retry_delay;
					//  reconnect
					//self.connect_as(self.jid.getResource());
					SmartChat.Session.prototype.connect_as.call(self, self.jid.getResource());
				} else {
					self.trigger(self.klass.Event.CONNECTION_FAULT, self.retry_delay);
				}
			}, 1000);
			this.trigger(this.klass.Event.CONNECTION_FAULT, this.retry_delay);
		}
	});

	var _static = {
		next_request_id: 1
	};


	var _fibonacci = function(i) {
		if (i <= 0) {
			return 0;
		} else if (i === 1) {
			return 1;
		} else {
			return _fibonacci(i-2) + _fibonacci(i-1);
		}
	};

	/*
		Method: _send_iq

		Provides a simpler interface to handling iq responses & errors.
		
		Parameters:
			iq       - request iq packet <JSJaCIQ>
			fn       - result handler
			error_fn - [optional] error handler function

		Returns:
				True if request is successfully sent to server, false otherwise.
	*/
	var _send_iq = function(iq, fn, error_fn) {
		var self = this;

		error_fn = error_fn || function(code, type, cond) {
			self._error('error ' + code + '/' + type + ':\n' + cond.xml);
		};
		
		return this._connection.sendIQ(iq, {
			error_handler: function(result) {
				var node = result.getNode();
				var code = parseInt($('error', node).attr('code'), 10);
				var type = $('error', node).attr('type');
				var cond = $('error > *', node).get(0);
				error_fn.call(self, code, type, cond);
			},
			default_handler: function(result) {
				fn.call(self, result);
			},
			result_handler: function(result) {
				fn.call(self, result);
			}
		});
	};

	/*
		Method: _each_page
		
		Provides a simple way to iterate over XMPP result set pages.

		Parameters:
			iq   - request iq packet <JSJaCIQ>
			fn   - result page callback, if the callback returns false, iteration is terminated
			page - [optional] object that specifies page size and 'after' offset
	*/
	var _each_page = function(iq, fn, page) {
		page = page || { };

		// clone iq so we'll have a pristine version for future iterations
		var page_iq = iq.clone();

		page_iq.setID(_request_id('rsm'));
		var query = page_iq.getNode().childNodes[0];
		var set = query.appendChild(page_iq.buildNode('set', { xmlns: 'http://jabber.org/protocol/rsm' }));

		// page.max specifies page size
		set.appendChild(page_iq.buildNode('max', { }, page.max || '20'));

		if (page.after) {
			// continue forward iteration
			set.appendChild(page_iq.buildNode('after', { }, page.after));
		} else if (page.before) {
			// continue reverse iteration
			set.appendChild(page_iq.buildNode('before', { }, page.before));
		} else if (page.reverse) {
			// empty before element to start reverse iteration
			set.appendChild(page_iq.buildNode('before'));
		}

		eCarList.debug('_each_page_iq: ' + page_iq.xml());
		return _send_iq.call(this, page_iq, function(result) {
			eCarList.debug('_each_page_result: ' + result.xml());
			var first_index = parseInt($('set > first', result.getNode()).attr('index'), 10);
			var last = $('set > last', result.getNode()).text();
			var count = parseInt($('set > count', result.getNode()).text(), 10);
			var page_size = result.getNode().childNodes[0].childNodes.length - 1;

			if (last && fn.call(this, result, first_index, count)) {
				// continue iteration (if there's more iteratin' to do)
				var done = false;
				if (page.reverse) {
					page.before = last;
					done = (first_index === 0);
				} else {
					page.after = last;
					done = ((first_index + page_size) === count);
				}

				if (!done) {
					_each_page.call(this, iq, fn, page);
				}
			}
		});
	};

	/*
		Method: _each_page_reverse
		
		Provides a simple way to iterate over XMPP result set pages.

		Parameters:
			iq   - request iq packet <JSJaCIQ>
			fn   - result page callback, if the callback returns false, iteration is terminated
			page - [optional] object that specifies page size and 'before/after' offset
	*/
	var _each_page_reverse = function(iq, fn, page) {
		page = page || { };
		page.reverse = true;
		_each_page.call(this, iq, fn, page);
	};

	/*
		Method: _each_result

		Provides a simple way to iterate over XMPP result sets.

		Parameters:
			iq   - request iq packet <JSJaCIQ>
			fn   - result callback, if the callback returns false, iteration is terminated
			page - [optional] object that specifies page size and 'before/after' offset
	*/
	var _each_result = function(iq, fn, page) {
		page = page || { };
		_each_page.call(this, iq, function(result, first_index, count) {
			var query = result.getNode().childNodes[0];
			var done = false;
			var page_size = query.childNodes.length - 1;
			for (var i = 0; !done && i < page_size; i++) {
				var j = (page.reverse) ? page_size - i - 1: i;
				if (!fn.call(this, query.childNodes[j], first_index + j, count)) {
					done = true;
				}
			}
			return !done;
		}, page);
	};

	/*
		Method: _each_result_reverse

		Provides a simple way to iterate over XMPP result sets.

		Parameters:
			iq   - request iq packet <JSJaCIQ>
			fn   - result callback, if the callback returns false, iteration is terminated
			page - [optional] object that specifies page size and 'before/after' offset
	*/
	var _each_result_reverse = function(iq, fn, page) {
		page = page || { };
		page.reverse = true;
		_each_result.call(this, iq, fn, page);
	};

	/*
		Method: _last_chat

		Provides a simple way to retrieve the last chat collection.

		Parameters:
			with_jid - jabber id of conversation participant <JSJaCJID>
			fn       - result callback
	*/
	var _last_chat = function(with_jid, fn) {
		var chat_iq = new JSJaCIQ();
		chat_iq.setType('get');

		var list_el = chat_iq.getNode().appendChild(chat_iq.buildNode('list', {
			xmlns: 'http://www.xmpp.org/extensions/xep-0136.html#ns'
		}));
		list_el.setAttribute('with', with_jid.bare());
		
		_each_result_reverse.call(this, chat_iq, function(chat, chat_index, chat_count) {
			eCarList.debug('_archive_chat: ' + chat.xml);
			var chat_with_jid = new JSJaCJID(chat.getAttribute('with'));
			var start_date = Date.jab2date(chat.getAttribute('start'));

			fn.call(this, chat_with_jid, start_date);

			return false;
		}, { max: 1 });
	};

	var _add_contact = function(jid, group) { 
		var contact = {
			jid: jid,
			group: group,
			available: false,
			presence: {
				show: Show.NONE,
				status: ''
			}
		};
		
		this.roster[jid.bare()] = contact;
		this.trigger(this.klass.Event.CONTACT_ADD, contact);
		return contact;
	};

	/*
		Method: _get_room_jid

		Creates a room jid (with nickname) for the given parameters. 

		Parameters:
			jid       - room's jabber id <JSJaCJID>
			- or -
			domain    - [optional] room's jabber host
			room_node - room node
	*/
	var _get_room_jid = function() {

		// parse args to generate bare_jid	
		var bare_jid;
		if (typeof arguments[0] === 'string') {
			var domain, node;
			if (arguments.length > 1) {
				domain = arguments[0];
				node = arguments[1];
			} else {
				domain = this.jid.getDomain();
				node = arguments[0];
			}
			
			bare_jid = node + '@conference.' + domain;
		} else {
			bare_jid = arguments[0].bare();
		}

		this.room_nicks[bare_jid] = this.room_nicks[bare_jid] || this.nick;

		return new JSJaCJID(bare_jid + '/' + this.room_nicks[bare_jid]);
	};

	var _add_room = function(jid) {
		var room = new Room(jid);

		// assign room priority
		if (room.type === RoomType.PRIVATE) {
			var max_room = this.get_max_priority_room();
			room.priority = (max_room) ? max_room.priority + 1 : 1;
		}

		room.name = this._get_room_name(room);
		
		this.rooms[jid.bare()] = room;
		this.trigger(this.klass.Event.ROOM_ADD, room);
		return room;
	};

	var _remove_room = function(jid) {
		var room = this.rooms[jid.bare()];
		delete this.rooms[jid.bare()];
		delete this.archive[jid.bare()];
		this.trigger(this.klass.Event.ROOM_REMOVE, room);
	};

	var _add_room_member = function(room_jid, member_jid, presence, is_join) {
		// grab room, create if necessary
		var room = this.rooms[room_jid.bare()];
		if (!room) {
			room = _add_room.call(this, new JSJaCJID(room_jid.bare()));
		}

		var member = room.add_member(member_jid, room_jid.getResource(), presence);
		if (member !== null) {
			this.trigger(this.klass.Event.MEMBER_ADD, member);
			if (is_join) {
				this.trigger(this.klass.Event.MEMBER_JOIN, member);
			}
		}

		return member;
	};

	var _remove_room_member = function(room_jid, member_jid, is_leave) {
		var room = this.rooms[room_jid.bare()];
		
		if (room) {
			// remove room member
			var member = room.remove_member(member_jid);
			if (member !== null) {
				_update_contact_chat_state.call(this, room_jid, room_jid, this.klass.ChatState.GONE);
				this.trigger(this.klass.Event.MEMBER_REMOVE, member);
				if (is_leave) {
					this.trigger(this.klass.Event.MEMBER_LEAVE, member);
				}

				// remove room if this session leaves
				if (member.jid.equals(this.jid)) {
					_remove_room.call(this, room_jid);
				}
			}
		}
	};

	var _update_archive_data = function(with_jid, data) {
		var bjid = with_jid.bare();
		this.archive[bjid] = this.archive[bjid] || { data: { }, messages: [] };
		var archived = this.archive[bjid].data;

		// overwrite data properties
		var updated = false;
		for (var property in data) {
			if (data.hasOwnProperty(property) && archived[property] !== data[property]) {
				archived[property] = data[property];
				updated = true;
			}
		}

		if (updated) {
			this.trigger(this.klass.Event.ARCHIVE_DATA_CHANGE, with_jid);
		}

		return updated;
	};

	var _update_contact_chat_state = function(with_jid, contact_jid, chat_state) {
		var bjid = with_jid.bare();
		this.archive[bjid] = this.archive[bjid] || { data: { }, messages: [] };
		var chat = this.archive[bjid];
		chat.chat_state = chat.chat_state || { };
		if (chat.chat_state[contact_jid.toString()] !== chat_state) {
			chat.chat_state[contact_jid.toString()] = chat_state;
			this.trigger(this.klass.Event.CHAT_STATE_CHANGE, {
				with_jid: with_jid,
				contact_jid: contact_jid,
				chat_state: chat_state
			});
			return true;
		}
		return false;
	};

	var _add_message = function(message, quiet) {
		var bjid = message.with_jid.bare();
		
		// filter out anonymous room & decline messages
		if (message.body.match(/this room is not anonymous|declined your invite to the room/i)) {
			return;
		}
		
		if (!this.archive[bjid] || typeof this.archive[bjid] !== 'object') {
			this.archive[bjid] = { data: { }, messages: [] };
		}
		this.archive[bjid].messages.push(message);
		
		this.trigger(this.klass.Event.MESSAGE_ADD, message);
	};

	var _add_invite = function(invite) {
		_add_message.call(this, invite);
		this.trigger(this.klass.Event.INVITE_RECEIPT, invite);
	};

	var _add_decline = function(decline) {
		_add_message.call(this, decline);
		this.trigger(this.klass.Event.INVITE_DECLINE, decline);
	};

	var _init_connection = function() {

		// jabber http-bind base is @ http(s)://$host/jabber
		this._connection = new JSJaCHttpBindingConnection({
			httpbase: this.http_base,
			oDbg: { log: _log }
		});
		
		// register connection handlers
		var self = this;
		this._connection.registerHandler('onConnect', function() {
			_on_connect.call(self);
		});
		this._connection.registerHandler('onDisconnect', function() {
			_on_disconnect.call(self);
		});
		this._connection.registerHandler('onResume', function() {
			_on_resume.call(self);
		});
		this._connection.registerHandler('onStatusChanged', function(status) {
			_on_status_change.call(self, status);
		});
		this._connection.registerHandler('onError', function(error) {
			_on_error.call(self, error);
		});
        this._connection.registerHandler('message', function(message) {
			_on_message.call(self, message);
        });
		this._connection.registerHandler('presence', function(packet) {
			_on_presence.call(self, packet);
		});
	};

	var _log = function(msg, level) {
		if (level <= SmartChat.Session.JSJAC_LOG_LEVEL) {
			switch (level) {
			case 0:
				eCarList.warn(msg);
				break;
			case 1:
				eCarList.error(msg);
				break;
			case 2:
				eCarList.info(msg);
				break;
			case 4:
				eCarList.debug(msg);
				break;
			default:
				eCarList.debug(msg);
			}
		}
	};

	var _request_id = function(type) {
		var request_id = ((type) ? type + '_' : '') + _static.next_request_id;
		_static.next_request_id = (_static.next_request_id + 1) % 1024;
		return request_id;
	};

	/*
		Method: _configure_room

		Configures chat room with default settings.

		Request:
			<iq from='crone1@shakespeare.lit/desktop' id='create1' to='darkcave@chat.shakespeare.lit' type='set'>
				<query xmlns='http://jabber.org/protocol/muc#owner'>
					<x xmlns='jabber:x:data' type='submit'/>
				</query>
			</iq>

		Response:
			<iq from='darkcave@chat.shakespeare.lit' id='reg2' to='hag66@shakespeare.lit/pda' type='result'/>

		Parameters:
			room_jid - room JabberID <JSJacJID>

		Returns:
			True if request is successfully sent to server, false otherwise.
	*/
	var _configure_room = function(room_jid) {
		var iq = new JSJaCIQ();
		iq.setFrom(this.jid.toString());
		iq.setIQ(room_jid.bare(), 'set', _request_id('room'));

		var x = iq.buildNode('x', { xmlns: NS_XDATA, type: 'submit' });
		iq.setQuery(NS_MUC_OWNER).appendChild(x);

		return _send_iq.call(this, iq, function(result) {
			eCarList.debug('configure_room_result: ' + result.xml());
			// NOTE: rooms sometime take a few millisecs to unlock, wait before we add the room
			// TODO: something better, query for room existence?
			var self = this;
			setTimeout(function() {
				_add_room_member.call(self, room_jid, self.jid, self.presence, true);
			}, 500);
		});
	};

	//TODO: less naive impl - use discovery to determine current room members
	var _resolve_nick_conflict = function(room_jid) {
		// prepend _# to nick, if nick already has _# -> increment #
		var nick = this.room_nicks[room_jid.bare()];
		var nick_match = nick.match(/(.*?)#(\d+)$/);
		if (nick_match) {
			nick = nick_match[1] + '#' + (parseInt(nick_match[2], 10) + 1);
		} else {
			nick = nick + '#2';
		}

		// use this new nick for this room and as our preferred nick
		this.room_nicks[room_jid.bare()] = nick;
		this.nick = nick;

		// try to rejoin room
		this.join_room(room_jid);
	};

	//
	// JSJaC Connection Handlers
	// 

	var _on_connect = function() {
		eCarList.debug('_on_connect');
		
		this.available = true;
		this.presence.login_date = new Date();
		//this.query_roster();
		this.update_presence();

		if (this.in_state(this.klass.State.FAULT)) {
			_on_reconnect.call(this);
		} else {
			this.trigger(this.klass.Event.CONNECT);
		}
	};

	var _on_reconnect = function() {
		// re-join chat rooms
		for (var bjid in this.rooms) {
			if (this.rooms.hasOwnProperty(bjid)) {
				var room = this.rooms[bjid];
				this.join_room(room.jid);
			}
		}

		// return to previous state
		this.change_state(this.fault_state);
		delete this.fault_state;
		delete this.faults;

		this.trigger(this.klass.Event.RECONNECT);
	};

	var _on_disconnect = function() {
		eCarList.debug('_on_disconnect');
		eCarList.debug('status: ' + this._connection.status());

		this.available = false;

		if (this._connection.status() !== 'aborted' && !this.in_state(this.klass.State.FAULT)) {
			// reset session info
			for (var bjid in this.rooms) {
				if (this.rooms.hasOwnProperty(bjid)) {
					_remove_room.call(this, this.rooms[bjid].jid);
				}
			}

			this.archive = { };
			this.room_nicks = { };
			this.rooms = { };
			this.roster = { };
			this.presence.show = Show.NONE;
			this.presence.status = '';
			this.trigger(this.klass.Event.DISCONNECT);
		}
	};

	var _on_resume = function() {
		eCarList.debug('_on_resume');
		// reload roster
		//this.query_roster();
		this.trigger(this.klass.Event.CONNECT);
	};
	
	var _on_status_change = function(status) {
		eCarList.debug('_on_status: ' + status);
	};

	var _on_message = function(packet) {
		eCarList.debug('_on_message: ' + packet.xml());
		
		var message = {
			with_jid: packet.getFromJID(),
			date: new Date(),
			body: packet.getBody().htmlEnc(),
			body_html: packet.getBody()
		};
		
		if ($('invite', packet.getNode()).length > 0) {
			// invite message
			message.type = MessageType.INVITE;
			message.from = packet.getFromJID().getNick();
			message.room_jid = packet.getFromJID();
			message.from_jid = new JSJaCJID($('invite', packet.getNode()).attr('from'));
			message.reason = $('reason', packet.getNode()).text();
			_add_invite.call(this, message);
		} else if ($('decline', packet.getNode()).length > 0) {
			// decline invite message (indirect - coming from room, direct - coming from user)
			var indirect = packet.getFromJID().isRoom();
			message.type = MessageType.DECLINE;
			message.from = packet.getFromJID().getNick();
			message.room_jid = (indirect) ? packet.getFromJID() 
				: new JSJaCJID($('decline', packet.getNode()).attr('room'));
			message.from_jid = (indirect) ? new JSJaCJID($('decline', packet.getNode()).attr('from')) 
				: packet.getFromJID();
			message.reason = $('reason', packet.getNode()).text();
			_add_decline.call(this, message);
		} else if ($('status[code="100"]', packet.getNode()).length > 0) {
			// non-anonymous room warning
			message.type = MessageType.STATUS;
			_add_message.call(this, message);
		} else if ($('error[code="404"]', packet.getNode()).length > 0) {
			// sent a message to a missing room, get rid of it
			_remove_room.call(this, packet.getFromJID());
		} else if ($(this.klass.ChatState.COMPOSING, packet.getNode()).length) {
			// composing state change message
			_update_contact_chat_state.call(this, message.with_jid, packet.getFromJID(), 
											this.klass.ChatState.COMPOSING);
		} else if ($(this.klass.ChatState.PAUSED, packet.getNode()).length) {
			// paused state change message
			_update_contact_chat_state.call(this, message.with_jid, packet.getFromJID(),
											this.klass.ChatState.PAUSED);
		} else {
			// normal message
			_update_contact_chat_state.call(this, message.with_jid, packet.getFromJID(),
											this.klass.ChatState.ACTIVE);
			if (message.body) {
				message.type = MessageType.MESSAGE;
				message.from = this.get_contact_nick(packet.getFromJID());
				message.from_jid = packet.getFromJID();
				_add_message.call(this, message);
			}
		}
		
	};

	var _on_presence = function(packet) {
		eCarList.debug('_on_presence: ' + packet.xml());
		var jid = packet.getFromJID();
		var presence = this._parse_presence_packet(packet);
		if (jid.isRoom()) {
			_on_room_presence.call(this, packet, jid, presence);
		} else {
			_on_roster_presence.call(this, packet, jid, presence);
		}
	};

	var _on_roster_presence = function(packet, jid, presence) {
		if (jid.isEntity(this.jid)) {
			// session user presence
			if (jid.getResource() !== this.jid.getResource()) {
				// diff resource, another this account is logged in more than once
				var login_date = this.presence.login_date || new Date();
				if (presence.login_date && presence.login_date.getTime() < login_date.getTime()) {
					// other smartchat resource was here first, bail out
					this.disconnect();
					this.trigger(this.klass.Event.JID_CONFLICT, jid.getResource());
				}
				return;
			}

			this.presence = presence;
			this.trigger(this.klass.Event.USER_CHANGE);
		} else {
			// roster contact presence
			var contact = this.roster[jid.bare()];
			if (packet.getType() === 'unavailable') {
				contact.available = false;
			} else {
				contact.available = true;
			}
			contact.jid = jid;
			contact.presence = presence;
			this.trigger(this.klass.Event.CONTACT_CHANGE, contact);
		}
	};

	var _on_room_presence = function(packet, room_jid, presence) {
		if ($('status[code="201"]', packet.getNode()).length > 0) {
			// new room, it needs to be configured
			_configure_room.call(this, room_jid);
		} else if ($('conflict', packet.getNode()).length > 0) {
			// nickname conflict
			_resolve_nick_conflict.call(this, room_jid);
		} else if (packet.getType() === 'error') {
			// presence error
			if ($('error[code="404"]', packet.getNode()).length > 0) {
				// 404, room not found, remove it
				_remove_room.call(this, room_jid);
			}
		} else {
			// room member presence info
			var item = $('item', packet.getNode()).get(0);
			var jid_attr = $(item).attr('jid');
			var member_jid = (jid_attr) ? new JSJaCJID(jid_attr) : null;
			if (packet.getType() === 'unavailable') {
				if (!$('status:contains("Replaced")', packet.getNode()).length) {
					// ignore presence if it's an "Replaced by new connection" status
					_remove_room_member.call(this, room_jid, member_jid, true);
				}
			} else {
				var member = this.get_room_member(room_jid, member_jid);
				if (member) {
					// current member, update presence
					member.room_presence = presence;
					this.trigger(this.klass.Event.MEMBER_CHANGE, member);
				} else {
					// add new member
					var is_join = (packet.getType() !== 'ping');
					member = _add_room_member.call(this, room_jid, member_jid, presence, is_join);
				}
			}

		}
	};

	var _on_error = function(error_xml) {
		var error = {
			code: parseInt($(error_xml).attr('code'), 10),
			type: $(error_xml).attr('type'),
			cond: $(error_xml).children().get(0).nodeName,
			status: (this._connection) ? this._connection.status() : null
		};
		eCarList.error('xmpp error - code: ' + error.code + ' type: ' + error.type
					   + ' cond: ' + error.cond + '\n' + error_xml.xml);
		
		var error_class = Math.floor(error.code / 100);
		switch(Math.floor(error.code / 100)) {
		case 5:
			if (error.status === 'session-terminate-conflict') {
				this.trigger(this.klass.Event.SESSION_REPLACE, error);
			} else {
				// increment faults & change to fault state
				if (!this.in_state(this.klass.State.FAULT)) {
					this.fault_state = this.state;
				}
				this.faults = (this.faults || 0) + 1;
				this.change_state(this.klass.State.FAULT);
				this.reconnect();
			}
			break;
		case 4:
			this.trigger(this.klass.Event.AUTH_FAULT, error);
			break;
		}
	};

});
/*!
   SoundManager 2: Javascript Sound for the Web
   --------------------------------------------
   http://schillmania.com/projects/soundmanager2/

   Copyright (c) 2008, Scott Schiller. All rights reserved.
   Code licensed under the BSD License:
   http://schillmania.com/projects/soundmanager2/license.txt

   V2.95a.20090717
*/

var soundManager = null;

function SoundManager(smURL,smID) {
 
  this.flashVersion = 8;           // version of flash to require, either 8 or 9. Some API features require Flash 9.
  this.debugMode = true;           // enable debugging output (div#soundmanager-debug, OR console if available + configured)
  this.useConsole = true;          // use firebug/safari console.log()-type debug console if available
  this.consoleOnly = false;        // if console is being used, do not create/write to #soundmanager-debug
  this.waitForWindowLoad = false;  // force SM2 to wait for window.onload() before trying to call soundManager.onload()
  this.nullURL = 'null.mp3';       // path to "null" (empty) MP3 file, used to unload sounds (Flash 8 only)
  this.allowPolling = true;        // allow flash to poll for status update (required for whileplaying() events, peak, sound spectrum functions to work.)
  this.useFastPolling = false;     // uses 1 msec flash timer interval (vs. default of 20) for higher callback frequency, best combined with useHighPerformance
  this.useMovieStar = false;	   // enable support for Flash 9.0r115+ (codename "MovieStar") MPEG4 audio + video formats (AAC, M4V, FLV, MOV etc.)
  this.bgColor = '#ffffff';	       // movie (.swf) background color, '#000000' useful if showing on-screen/full-screen video etc.
  this.useHighPerformance = false; // position:fixed flash movie can help increase js/flash speed, minimize lag
  this.flashLoadTimeout = 1000;    // msec to wait for flash movie to load before failing (0 = infinity)
  this.wmode = null;	   		   // mode to render the flash movie in - null, transparent, opaque (last two allow layering of HTML on top)
  this.allowFullScreen = true;     // enter full-screen (via double-click on movie) for flash 9+ video

  this.defaultOptions = {
    'autoLoad': false,             // enable automatic loading (otherwise .load() will be called on demand with .play(), the latter being nicer on bandwidth - if you want to .load yourself, you also can)
    'stream': true,                // allows playing before entire file has loaded (recommended)
    'autoPlay': false,             // enable playing of file as soon as possible (much faster if "stream" is true)
    'onid3': null,                 // callback function for "ID3 data is added/available"
    'onload': null,                // callback function for "load finished"
    'whileloading': null,          // callback function for "download progress update" (X of Y bytes received)
    'onplay': null,                // callback for "play" start
    'onpause': null,               // callback for "pause"
    'onresume': null,              // callback for "resume" (pause toggle)
    'whileplaying': null,          // callback during play (position update)
    'onstop': null,                // callback for "user stop"
    'onfinish': null,              // callback function for "sound finished playing"
    'onbeforefinish': null,        // callback for "before sound finished playing (at [time])"
    'onbeforefinishtime': 5000,    // offset (milliseconds) before end of sound to trigger beforefinish (eg. 1000 msec = 1 second)
    'onbeforefinishcomplete':null, // function to call when said sound finishes playing
    'onjustbeforefinish':null,     // callback for [n] msec before end of current sound
    'onjustbeforefinishtime':200,  // [n] - if not using, set to 0 (or null handler) and event will not fire.
    'multiShot': true,             // let sounds "restart" or layer on top of each other when played multiple times, rather than one-shot/one at a time
    'multiShotEvents': false,      // fire multiple sound events (currently onfinish() only) when multiShot is enabled
    'position': null,              // offset (milliseconds) to seek to within loaded sound data.
    'pan': 0,                      // "pan" settings, left-to-right, -100 to 100
    'volume': 100                  // self-explanatory. 0-100, the latter being the max.
  };

  this.flash9Options = {           // flash 9-only options, merged into defaultOptions if flash 9 is being used
    'isMovieStar': null,	  	   // "MovieStar" MPEG4 audio/video mode. Null (default) = auto detect MP4, AAC etc. based on URL. true = force on, ignore URL
    'usePeakData': false,          // enable left/right channel peak (level) data
    'useWaveformData': false,      // enable sound spectrum (raw waveform data) - WARNING: CPU-INTENSIVE: may set CPUs on fire.
    'useEQData': false,            // enable sound EQ (frequency spectrum data) - WARNING: Also CPU-intensive.
    'onbufferchange': null,	   	   // callback for "isBuffering" property change
    'ondataerror': null			   // callback for waveform/eq data access error (flash playing audio in other tabs/domains)
  };

  this.movieStarOptions = {        // flash 9.0r115+ MPEG4 audio/video options, merged into defaultOptions if flash 9 + movieStar mode is enabled
    'onmetadata': null,		   	   // callback for when video width/height etc. are received
    'useVideo': false,		   	   // if loading movieStar content, whether to show video
    'bufferTime': null		   	   // seconds of data to buffer before playback begins (null = flash default of 0.1 seconds - if AAC playback is gappy, try up to 3 seconds)
  };

  // jslint global declarations
  /*global SM2_DEFER, sm2Debugger, alert, console, document, navigator, setTimeout, window */

  var SMSound = null; // defined later

  var _s = this;
  this.version = null;
  this.versionNumber = 'V2.95a.20090717';
  this.movieURL = null;
  this.url = null;
  this.altURL = null;
  this.swfLoaded = false;
  this.enabled = false;
  this.o = null;
  this.id = (smID||'sm2movie');
  this.oMC = null;
  this.sounds = {};
  this.soundIDs = [];
  this.muted = false;
  this.isFullScreen = false; // set later by flash 9+
  this.isIE = (navigator.userAgent.match(/MSIE/i));
  this.isSafari = (navigator.userAgent.match(/safari/i));
  this.debugID = 'soundmanager-debug';
  this.debugURLParam = /([#?&])debug=1/i;
  this.specialWmodeCase = false;
  this._onready = [];
  this._debugOpen = true;
  this._didAppend = false;
  this._appendSuccess = false;
  this._didInit = false;
  this._disabled = false;
  this._windowLoaded = false;
  this._hasConsole = (typeof console != 'undefined' && typeof console.log != 'undefined');
  this._debugLevels = ['log','info','warn','error'];
  this._defaultFlashVersion = 8;
  this._oRemoved = null;
  this._oRemovedHTML = null;

  var _$ = function(sID) {
    return document.getElementById(sID);
  };

  this.filePatterns = {
	flash8: /\.mp3(\?.*)?$/i,
	flash9: /\.mp3(\?.*)?$/i
  };

  this.netStreamTypes = ['aac','flv','mov','mp4','m4v','f4v','m4a','mp4v','3gp','3g2']; // Flash v9.0r115+ "moviestar" formats
  this.netStreamPattern = new RegExp('\\.('+this.netStreamTypes.join('|')+')(\\?.*)?$','i');

  this.filePattern = null;
  this.features = {
	buffering: false,
    peakData: false,
    waveformData: false,
    eqData: false,
    movieStar: false
  };

  this.sandbox = {
    'type': null,
    'types': {
      'remote': 'remote (domain-based) rules',
      'localWithFile': 'local with file access (no internet access)',
      'localWithNetwork': 'local with network (internet access only, no local access)',
      'localTrusted': 'local, trusted (local + internet access)'
    },
    'description': null,
    'noRemote': null,
    'noLocal': null
  };

  this._setVersionInfo = function() {
    if (_s.flashVersion != 8 && _s.flashVersion != 9) {
      alert('soundManager.flashVersion must be 8 or 9. "'+_s.flashVersion+'" is invalid. Reverting to '+_s._defaultFlashVersion+'.');
      _s.flashVersion = _s._defaultFlashVersion;
    }
    _s.version = _s.versionNumber+(_s.flashVersion==9?' (AS3/Flash 9)':' (AS2/Flash 8)');
    // set up default options
	if (_s.flashVersion > 8) {
	  _s.defaultOptions = _s._mergeObjects(_s.defaultOptions,_s.flash9Options);
	  _s.features.buffering = true;
	}
    if (_s.flashVersion > 8 && _s.useMovieStar) {
      // flash 9+ support for movieStar formats as well as MP3
      _s.defaultOptions = _s._mergeObjects(_s.defaultOptions,_s.movieStarOptions);
      _s.filePatterns.flash9 = new RegExp('\\.(mp3|'+_s.netStreamTypes.join('|')+')(\\?.*)?$','i');
      _s.features.movieStar = true;
    } else {
      _s.useMovieStar = false;
      _s.features.movieStar = false;
    }
    _s.filePattern = _s.filePatterns[(_s.flashVersion!=8?'flash9':'flash8')];
    _s.movieURL = (_s.flashVersion==8?'soundmanager2.swf':'soundmanager2_flash9.swf');
    _s.features.peakData = _s.features.waveformData = _s.features.eqData = (_s.flashVersion>8);
  };

  this._overHTTP = (document.location?document.location.protocol.match(/http/i):null);
  this._waitingforEI = false;
  this._initPending = false;
  this._tryInitOnFocus = (this.isSafari && typeof document.hasFocus == 'undefined');
  this._isFocused = (typeof document.hasFocus != 'undefined'?document.hasFocus():null);
  this._okToDisable = !this._tryInitOnFocus;

  this.useAltURL = !this._overHTTP; // use altURL if not "online"

  var flashCPLink = 'http://www.macromedia.com/support/documentation/en/flashplayer/help/settings_manager04.html';

  this.strings = {
    notReady: 'Not loaded yet - wait for soundManager.onload() before calling sound-related methods',
    appXHTML: 'soundManager._createMovie(): appendChild/innerHTML set failed. May be app/xhtml+xml DOM-related.',
    localFail: 'soundManager: Loading this page from local/network file system (not over HTTP?) Flash security likely restricting JS-Flash access. Consider adding current URL to "trusted locations" in the Flash player security settings manager at '+flashCPLink+', or simply serve this content over HTTP.',
    waitFocus: 'soundManager: Special case: Waiting for focus-related event..',
    waitImpatient: 'soundManager: Getting impatient, still waiting for Flash.. ;)',
    waitForever: 'soundManager: Waiting indefinitely for Flash...',
    needFunction: 'soundManager.onready(): Function object expected'
  };

  // --- public methods ---
  
  this.supported = function() {
    return (_s._didInit && !_s._disabled);
  };

  this.getMovie = function(smID) {
    return _s.isIE?window[smID]:(_s.isSafari?_$(smID)||document[smID]:_$(smID));
  };

  this.loadFromXML = function(sXmlUrl) {
    try {
      _s.o._loadFromXML(sXmlUrl);
    } catch(e) {
      _s._failSafely();
      return true;
    }
  };

  this.createSound = function(oOptions) {
	var _cs = 'soundManager.createSound(): ';
    if (!_s._didInit) {
	  throw _s._complain(_cs+_s.strings.notReady,arguments.callee.caller);
	}
    if (arguments.length == 2) {
      // function overloading in JS! :) ..assume simple createSound(id,url) use case
      oOptions = {'id':arguments[0],'url':arguments[1]};
    }
    var thisOptions = _s._mergeObjects(oOptions); // inherit SM2 defaults
    var _tO = thisOptions; // alias
    if (_tO.id.toString().charAt(0).match(/^[0-9]$/)) { // hopefully this isn't buggy regexp-fu. :D
	  var complaint = _cs+'Warning: Sound ID "'+_tO.id+'" should be a string, starting with a non-numeric character';
	  _s._wD(complaint,2);
	}
    _s._wD(_cs+_tO.id+' ('+_tO.url+')',1);
    if (_s._idCheck(_tO.id,true)) {
      _s._wD(_cs+_tO.id+' exists',1);
      return _s.sounds[_tO.id];
    }
    if (_s.flashVersion > 8 && _s.useMovieStar) {
	  if (_tO.isMovieStar === null) {
	    _tO.isMovieStar = (_tO.url.match(_s.netStreamPattern)?true:false);
	  }
	  if (_tO.isMovieStar) {
	    _s._wD(_cs+'using MovieStar handling');
	  }
	  if (_tO.isMovieStar && (_tO.usePeakData || _tO.useWaveformData || _tO.useEQData)) {
	    _s._wD('Warning: peak/waveform/eqData features unsupported for non-MP3 formats');
	    _tO.usePeakData = false;
		_tO.useWaveformData = false;
		_tO.useEQData = false;
	  }
    }
    _s.sounds[_tO.id] = new SMSound(_tO);
    _s.soundIDs[_s.soundIDs.length] = _tO.id;
    // AS2:
    if (_s.flashVersion == 8) {
      _s.o._createSound(_tO.id,_tO.onjustbeforefinishtime);
    } else {
      _s.o._createSound(_tO.id,_tO.url,_tO.onjustbeforefinishtime,_tO.usePeakData,_tO.useWaveformData,_tO.useEQData,_tO.isMovieStar,(_tO.isMovieStar?_tO.useVideo:false),(_tO.isMovieStar?_tO.bufferTime:false));
    }
    if (_tO.autoLoad || _tO.autoPlay) {
      // TODO: does removing timeout here cause problems?
        if (_s.sounds[_tO.id]) {
          _s.sounds[_tO.id].load(_tO);
        }
    }
    if (_tO.autoPlay) {
	  _s.sounds[_tO.id].play();
	}
    return _s.sounds[_tO.id];
  };

  this.createVideo = function(oOptions) {
    if (arguments.length==2) {
      oOptions = {'id':arguments[0],'url':arguments[1]};
    }
    if (_s.flashVersion >= 9) {
      oOptions.isMovieStar = true;
      oOptions.useVideo = true;
    } else {
      _s._wD('soundManager.createVideo(): flash 9 required for video. Exiting.',2);
      return false;
    }
    if (!_s.useMovieStar) {
      _s._wD('soundManager.createVideo(): MovieStar mode not enabled. Exiting.',2);
    }
    return _s.createSound(oOptions);
  };

  this.destroySound = function(sID,bFromSound) {
    // explicitly destroy a sound before normal page unload, etc.
    if (!_s._idCheck(sID)) {
      return false;
    }
    for (var i=0; i<_s.soundIDs.length; i++) {
      if (_s.soundIDs[i] == sID) {
	    _s.soundIDs.splice(i,1);
        continue;
      }
    }
    // conservative option: avoid crash with ze flash 8
    // calling destroySound() within a sound onload() might crash firefox, certain flavours of winXP + flash 8??
    // if (_s.flashVersion != 8) {
      _s.sounds[sID].unload();
    // }
    if (!bFromSound) {
      // ignore if being called from SMSound instance
      _s.sounds[sID].destruct();
    }
    delete _s.sounds[sID];
  };

  this.destroyVideo = this.destroySound;

  this.load = function(sID,oOptions) {
    if (!_s._idCheck(sID)) {
      return false;
    }
    _s.sounds[sID].load(oOptions);
  };

  this.unload = function(sID) {
    if (!_s._idCheck(sID)) {
      return false;
    }
    _s.sounds[sID].unload();
  };

  this.play = function(sID,oOptions) {
    if (!_s._didInit) {
	  throw _s._complain('soundManager.play(): '+_s.strings.notReady,arguments.callee.caller);
	}
    if (!_s._idCheck(sID)) {
      if (typeof oOptions != 'Object') {
		oOptions = {url:oOptions}; // overloading use case: play('mySound','/path/to/some.mp3');
	  }
      if (oOptions && oOptions.url) {
        // overloading use case, creation + playing of sound: .play('someID',{url:'/path/to.mp3'});
        _s._wD('soundController.play(): attempting to create "'+sID+'"',1);
        oOptions.id = sID;
        _s.createSound(oOptions);
      } else {
        return false;
      }
    }
    _s.sounds[sID].play(oOptions);
  };

  this.start = this.play; // just for convenience

  this.setPosition = function(sID,nMsecOffset) {
    if (!_s._idCheck(sID)) {
      return false;
    }
    _s.sounds[sID].setPosition(nMsecOffset);
  };

  this.stop = function(sID) {
    if (!_s._idCheck(sID)) {
	  return false;
	}
    _s._wD('soundManager.stop('+sID+')',1);
    _s.sounds[sID].stop(); 
  };

  this.stopAll = function() {
    _s._wD('soundManager.stopAll()',1);
    for (var oSound in _s.sounds) {
      if (_s.sounds[oSound] instanceof SMSound) {
		_s.sounds[oSound].stop(); // apply only to sound objects
	  }
    }
  };

  this.pause = function(sID) {
    if (!_s._idCheck(sID)) {
	  return false;
	}
    _s.sounds[sID].pause();
  };

  this.pauseAll = function() {
    for (var i=_s.soundIDs.length; i--;) {
      _s.sounds[_s.soundIDs[i]].pause();
    }
  };

  this.resume = function(sID) {
    if (!_s._idCheck(sID)) {
	  return false;
	}
    _s.sounds[sID].resume();
  };

  this.resumeAll = function() {
    for (var i=_s.soundIDs.length; i--;) {
      _s.sounds[_s.soundIDs[i]].resume();
    }
  };

  this.togglePause = function(sID) {
    if (!_s._idCheck(sID)) {
	  return false;
	}
    _s.sounds[sID].togglePause();
  };

  this.setPan = function(sID,nPan) {
    if (!_s._idCheck(sID)) {
	  return false;
	}
    _s.sounds[sID].setPan(nPan);
  };

  this.setVolume = function(sID,nVol) {
    if (!_s._idCheck(sID)) {
	  return false;
	}
    _s.sounds[sID].setVolume(nVol);
  };

  this.mute = function(sID) {
	if (typeof sID != 'string') {
	  sID = null;
	}
    if (!sID) {
      _s._wD('soundManager.mute(): Muting all sounds');
      for (var i=_s.soundIDs.length; i--;) {
        _s.sounds[_s.soundIDs[i]].mute();
      }
      _s.muted = true;
    } else {
      if (!_s._idCheck(sID)) {
	    return false;
	  }
      _s._wD('soundManager.mute(): Muting "'+sID+'"');
      _s.sounds[sID].mute();
    }
  };

  this.muteAll = function() {
    _s.mute();
  };

  this.unmute = function(sID) {
    if (typeof sID != 'string') {
	  sID = null;
	}
    if (!sID) {
      _s._wD('soundManager.unmute(): Unmuting all sounds');
      for (var i=_s.soundIDs.length; i--;) {
        _s.sounds[_s.soundIDs[i]].unmute();
      }
      _s.muted = false;
    } else {
      if (!_s._idCheck(sID)) {
		return false;
	  }
      _s._wD('soundManager.unmute(): Unmuting "'+sID+'"');
      _s.sounds[sID].unmute();
    }
  };

  this.unmuteAll = function() {
    _s.unmute();
  };

  this.toggleMute = function(sID) {
    if (!_s._idCheck(sID)) {
	return false;
    }
    _s.sounds[sID].toggleMute();
  };

  this.getMemoryUse = function() {
    if (_s.flashVersion == 8) {
      // not supported in Flash 8
      return 0;
    }
    if (_s.o) {
      return parseInt(_s.o._getMemoryUse(),10);
    }
  };

  this.disable = function(bNoDisable) {
    // destroy all functions
    if (typeof bNoDisable == 'undefined') {
      bNoDisable = false;
    }
    if (_s._disabled) {
	  return false;
    }
    _s._disabled = true;
    _s._wD('soundManager.disable(): Shutting down',1);
    for (var i=_s.soundIDs.length; i--;) {
      _s._disableObject(_s.sounds[_s.soundIDs[i]]);
    }
    _s.initComplete(bNoDisable); // fire "complete", despite fail
    // _s._disableObject(_s); // taken out to allow reboot()
  };

  this.canPlayURL = function(sURL) {
    return (sURL?(sURL.match(_s.filePattern)?true:false):null);	
  };

  this.getSoundById = function(sID,suppressDebug) {
    if (!sID) {
	  throw new Error('SoundManager.getSoundById(): sID is null/undefined');
	}
    var result = _s.sounds[sID];
    if (!result && !suppressDebug) {
      _s._wD('"'+sID+'" is an invalid sound ID.',2);
      // soundManager._wD('trace: '+arguments.callee.caller);
    }
    return result;
  };

  this.onready = function(oMethod,oScope) {
    // queue a callback, with optional scope
    // a status object will be passed to your handler
	/*
    soundManager.onready(function(oStatus) {
	  alert('SM2 init success: '+oStatus.success);
	});
	*/
    if (oMethod && oMethod instanceof Function) {
      if (_s._didInit) {
        _s._wD('soundManager.onready(): Queueing handler');
      }
      if (!oScope) {
        oScope = window;
      }
      _s._addOnReady(oMethod,oScope);
      _s._processOnReady();
      return true;
	} else {
	  throw _s.strings.needFunction;
	}
  };

  this.oninitmovie = function() {
    // called after SWF has been appended to the DOM via JS (or retrieved from HTML)
    // this is a stub for your own scripts.
  };

  this.onload = function() {
    // window.onload() equivalent for SM2, ready to create sounds etc.
    // this is a stub for your own scripts.
    soundManager._wD('soundManager.onload()',1);
  };

  this.onerror = function() {
    // stub for user handler, called when SM2 fails to load/init
  };

  // --- "private" methods ---

  this._idCheck = this.getSoundById;

  this._complain = function(sMsg,oCaller) {
    // Try to create meaningful custom errors, w/stack trace to the "offending" line
    var sPre = 'Error: ';
    if (!oCaller) {
      return new Error(sPre+sMsg);
    }
    var e = new Error(''); // make a mistake.
    var stackMsg = null;
    if (e.stack) {
      // potentially dangerous: Try to return a meaningful stacktrace where provided (Mozilla)
  	  try {
	    var splitChar = '@';
	    var stackTmp = e.stack.split(splitChar);
	    stackMsg = stackTmp[4]; // try to return only the relevant bit, skipping internal SM2 shiz
	  } catch(ee) {
	    // oops.
	    stackMsg = e.stack;
	  }
	}
	if (typeof console != 'undefined' && typeof console.trace != 'undefined') {
	  console.trace();
	}
	var errorDesc = sPre+sMsg+'. \nCaller: '+oCaller.toString()+(e.stack?' \nTop of stacktrace: '+stackMsg:(e.message?' \nMessage: '+e.message:''));
	// See JS error/debug/console output for real error source, stack trace / message detail where possible.
	return new Error(errorDesc);
  };

  var _doNothing = function() {
    return false;
  };

  _doNothing._protected = true;

  this._disableObject = function(o) {
    for (var oProp in o) {
      if (typeof o[oProp] == 'function' && typeof o[oProp]._protected == 'undefined') {
		o[oProp] = _doNothing;
	  }
    }
    oProp = null;
  };

  this._failSafely = function(bNoDisable) {
    // general failure exception handler
    if (typeof bNoDisable == 'undefined') {
      bNoDisable = false;
    }
    if (!_s._disabled || bNoDisable) {
      _s._wD('soundManager: Failed to initialise.',2);
      _s.disable(bNoDisable);
    }
  };
  
  this._normalizeMovieURL = function(smURL) {
    var urlParams = null;
    if (smURL) {
      if (smURL.match(/\.swf(\?.*)?$/i)) {
        urlParams = smURL.substr(smURL.toLowerCase().lastIndexOf('.swf?')+4);
        if (urlParams) {
          return smURL; // assume user knows what they're doing
        }
      } else if (smURL.lastIndexOf('/') != smURL.length-1) {
        smURL = smURL+'/';
      }
    }
    return(smURL && smURL.lastIndexOf('/')!=-1?smURL.substr(0,smURL.lastIndexOf('/')+1):'./')+_s.movieURL;
  };

  this._getDocument = function() {
    return (document.body?document.body:(document.documentElement?document.documentElement:document.getElementsByTagName('div')[0]));
  };

  this._getDocument._protected = true;

  this._setPolling = function(bPolling,bHighPerformance) {
    if (!_s.o || !_s.allowPolling) {
	  return false;
	}
    _s.o._setPolling(bPolling,bHighPerformance);
  };

  this._createMovie = function(smID,smURL) {
    var specialCase = null;
    var remoteURL = (smURL?smURL:_s.url);
    var localURL = (_s.altURL?_s.altURL:remoteURL);
    if (_s.debugURLParam.test(window.location.href.toString())) {
      _s.debugMode = true; // allow force of debug mode via URL
    }
    if (_s._didAppend && _s._appendSuccess) {
	  return false; // ignore if already succeeded
	}
    _s._didAppend = true;
	
    // safety check for legacy (change to Flash 9 URL)
    _s._setVersionInfo();
    _s.url = _s._normalizeMovieURL(_s._overHTTP?remoteURL:localURL);
    smURL = _s.url;

    if (_s.useHighPerformance && _s.useMovieStar && _s.defaultOptions.useVideo === true) {
      specialCase = 'soundManager note: disabling highPerformance, not applicable with movieStar mode + useVideo';
      _s.useHighPerformance = false;
    }

    _s.wmode = (!_s.wmode && _s.useHighPerformance && !_s.useMovieStar?'transparent':_s.wmode);
    if (_s.wmode !== null && _s.flashLoadTimeout !== 0 && !_s.useHighPerformance && !_s.isIE && navigator.platform.match(/win32/i)) {
	  _s.specialWmodeCase = true;
      // extra-special case: movie doesn't load until scrolled into view when using wmode = anything but 'window' here
      // does not apply when using high performance (position:fixed means on-screen), OR infinite flash load timeout
      _s._wD('soundManager note: Removing wmode, preventing off-screen SWF loading issue');
      _s.wmode = null;
    }

    if (_s.flashVersion == 8) {
      _s.allowFullScreen = false;
    }

    var oEmbed = {
      name: smID,
      id: smID,
      src: smURL,
      width: '100%',
      height: '100%',
      quality: 'high',
      allowScriptAccess: 'always',
      bgcolor: _s.bgColor,
      pluginspage: 'http://www.macromedia.com/go/getflashplayer',
      type: 'application/x-shockwave-flash',
      wmode: _s.wmode,
      allowfullscreen: (_s.allowFullScreen?'true':'false')
    };

    if (!_s.wmode) {
	  delete oEmbed.wmode; // don't write empty attribute
    }

    var oMovie = null;
    var tmp = null;

    if (_s.isIE) {
      // IE is "special".
      oMovie = document.createElement('div');
      var movieHTML = '<object id="'+smID+'" data="'+smURL+'" type="application/x-shockwave-flash" width="100%" height="100%"><param name="movie" value="'+smURL+'" /><param name="AllowScriptAccess" value="always" /><param name="quality" value="high" />'+(_s.wmode?'<param name="wmode" value="'+_s.wmode+'" /> ':'')+'<param name="bgcolor" value="'+_s.bgColor+'" /><param name="allowFullScreen" value="'+(_s.allowFullScreen?'true':'false')+'" /><!-- --></object>';
    } else {
      oMovie = document.createElement('embed');
      for (tmp in oEmbed) {
	    if (oEmbed.hasOwnProperty(tmp)) {
          oMovie.setAttribute(tmp,oEmbed[tmp]);
	    }
      }
    }

    var oD = document.createElement('div');
    oD.id = _s.debugID+'-toggle';
    var oToggle = {
      position: 'fixed',
      bottom: '0px',
      right: '0px',
      width: '1.2em',
      height: '1.2em',
      lineHeight: '1.2em',
      margin: '2px',
      textAlign: 'center',
      border: '1px solid #999',
      cursor: 'pointer',
      background: '#fff',
      color: '#333',
      zIndex: 10001
    };

    oD.appendChild(document.createTextNode('-'));
    oD.onclick = _s._toggleDebug;
    oD.title = 'Toggle SM2 debug console';

    if (navigator.userAgent.match(/msie 6/i)) {
      oD.style.position = 'absolute';
      oD.style.cursor = 'hand';
    }

    for (tmp in oToggle) {
 	  if (oToggle.hasOwnProperty(tmp)) {
        oD.style[tmp] = oToggle[tmp];
	  }
    }

    var oTarget = _s._getDocument();

    if (oTarget) {
       
      _s.oMC = _$('sm2-container')?_$('sm2-container'):document.createElement('div');

      if (!_s.oMC.id) {
        _s.oMC.id = 'sm2-container';
        _s.oMC.className = 'movieContainer';
        // "hide" flash movie
        var s = null;
        var oEl = null;
        if (_s.useHighPerformance) {
          s = {
 	    position: 'fixed',
 	    width: '8px',
            height: '8px', // must be at least 6px for flash to run fast. odd? yes.
            bottom: '0px',
            left: '0px',
	    overflow: 'hidden'
	    // zIndex:-1 // sit behind everything else - potentially dangerous/buggy?
          };
        } else {
          s = {
            position: 'absolute',
	    width: '8px',
            height: '8px',
            top: '-9999px',
            left: '-9999px'
          };
        }
        var x = null;
        for (x in s) {
		  if (s.hasOwnProperty(x)) {
            _s.oMC.style[x] = s[x];
		  }
        }
        try {
		  if (!_s.isIE) {
    	    _s.oMC.appendChild(oMovie);
		  }
          oTarget.appendChild(_s.oMC);
		  if (_s.isIE) {
			oEl = _s.oMC.appendChild(document.createElement('div'));
			oEl.className = 'sm2-object-box';
			oEl.innerHTML = movieHTML;
          }
          _s._appendSuccess = true;
        } catch(e) {
          throw new Error(_s.strings.appXHTML);
        }
      } else {
        // it's already in the document.
        _s.oMC.appendChild(oMovie);
		if (_s.isIE) {
		  oEl = _s.oMC.appendChild(document.createElement('div'));
		  oEl.className = 'sm2-object-box';
		  oEl.innerHTML = movieHTML;
        }
        _s._appendSuccess = true;
      }

      if (!_$(_s.debugID) && ((!_s._hasConsole||!_s.useConsole)||(_s.useConsole && _s._hasConsole && !_s.consoleOnly))) {
        var oDebug = document.createElement('div');
        oDebug.id = _s.debugID;
        oDebug.style.display = (_s.debugMode?'block':'none');
        if (_s.debugMode && !_$(oD.id)) {
          try {
            oTarget.appendChild(oD);
          } catch(e2) {
            throw new Error(_s.strings.appXHTML);
          }
          oTarget.appendChild(oDebug);
        }
      }
      oTarget = null;
    }

    if (specialCase) {
      _s._wD(specialCase);
    }

    _s._wD('-- SoundManager 2 '+_s.version+(_s.useMovieStar?', MovieStar mode':'')+(_s.useHighPerformance?', high performance mode, ':', ')+((_s.useFastPolling?'fast':'normal')+' polling mode')+(_s.wmode?', wmode: '+_s.wmode:'')+' --',1);
    _s._wD('soundManager._createMovie(): Trying to load '+smURL+(!_s._overHTTP && _s.altURL?' (alternate URL)':''),1);
  };

  // aliased to this._wD()
  this._writeDebug = function(sText,sType,bTimestamp) {
    if (!_s.debugMode) {
	  return false;
	}
    if (typeof bTimestamp != 'undefined' && bTimestamp) {
      sText = sText + ' | '+new Date().getTime();
    }
    if (_s._hasConsole && _s.useConsole) {
      var sMethod = _s._debugLevels[sType];
      if (typeof console[sMethod] != 'undefined') {
	    console[sMethod](sText);
      } else {
        console.log(sText);
      }
      if (_s.useConsoleOnly) {
	return true;
      }
    }
    var sDID = 'soundmanager-debug';
    try {
      var o = _$(sDID);
      if (!o) {
		return false;
	  }
      var oItem = document.createElement('div');
      if (++_s._wdCount%2===0) {
	    oItem.className = 'sm2-alt';
      }
      // sText = sText.replace(/\n/g,'<br />');
      if (typeof sType == 'undefined') {
        sType = 0;
      } else {
        sType = parseInt(sType,10);
      }
      oItem.appendChild(document.createTextNode(sText));
      if (sType) {
        if (sType >= 2) {
		  oItem.style.fontWeight = 'bold';
		}
        if (sType == 3) {
		  oItem.style.color = '#ff3333';
		}
      }
      // o.appendChild(oItem); // top-to-bottom
      o.insertBefore(oItem,o.firstChild); // bottom-to-top
    } catch(e) {
      // oh well
    }
    o = null;
  };
  this._writeDebug._protected = true;
  this._wdCount = 0;
  this._wdCount._protected = true;
  this._wD = this._writeDebug;

  this._wDAlert = function(sText) { alert(sText); };

  if (window.location.href.indexOf('debug=alert')+1 && _s.debugMode) {
    _s._wD = _s._wDAlert;
  }

  this._toggleDebug = function() {
    var o = _$(_s.debugID);
    var oT = _$(_s.debugID+'-toggle');
    if (!o) {
	  return false;
	}
    if (_s._debugOpen) {
      // minimize
      oT.innerHTML = '+';
      o.style.display = 'none';
    } else {
      oT.innerHTML = '-';
      o.style.display = 'block';
    }
    _s._debugOpen = !_s._debugOpen;
  };

  this._toggleDebug._protected = true;

  this._debug = function() {
    _s._wD('--- soundManager._debug(): Current sound objects ---',1);
    for (var i=0,j=_s.soundIDs.length; i<j; i++) {
      _s.sounds[_s.soundIDs[i]]._debug();
    }
  };

  this._debugTS = function(sEventType,bSuccess,sMessage) {
    // troubleshooter debug hooks
    if (typeof sm2Debugger != 'undefined') {
	  try {
	    sm2Debugger.handleEvent(sEventType,bSuccess,sMessage);
	  } catch(e) {
	    // oh well	
	  }
    }
  };

  this._debugTS._protected = true;

  this._mergeObjects = function(oMain,oAdd) {
    // non-destructive merge
    var o1 = {}; // clone o1
    for (var i in oMain) {
	  if (oMain.hasOwnProperty(i)) {
        o1[i] = oMain[i];
	  }
    }
    var o2 = (typeof oAdd == 'undefined'?_s.defaultOptions:oAdd);
    for (var o in o2) {
      if (o2.hasOwnProperty(o) && typeof o1[o] == 'undefined') {
		o1[o] = o2[o];
	  }
    }
    return o1;
  };

  this.createMovie = function(sURL) {
    if (sURL) {
      _s.url = sURL;
    }
    _s._initMovie();
  };

  this.go = this.createMovie; // nice alias

  this._initMovie = function() {
    // attempt to get, or create, movie
    if (_s.o) {
	  return false; // may already exist
    }
    _s.o = _s.getMovie(_s.id); // (inline markup)
    if (!_s.o) {
      if (!_s.oRemoved) {
        // try to create
        _s._createMovie(_s.id,_s.url);
      } else {
        // try to re-append removed movie after reboot()
        if (!_s.isIE) {
          _s.oMC.appendChild(_s.oRemoved);
        } else {
          _s.oMC.innerHTML = _s.oRemovedHTML;
        }
        _s.oRemoved = null;
        _s._didAppend = true;
      }
      _s.o = _s.getMovie(_s.id);
    }
    if (_s.o) {
      _s._wD('soundManager._initMovie(): Got '+_s.o.nodeName+' element ('+(_s._didAppend?'created via JS':'static HTML')+')',1);
      if (_s.flashLoadTimeout>0) {
        _s._wD('soundManager._initMovie(): Waiting for ExternalInterface call from Flash..');
      }
    }
    if (typeof _s.oninitmovie == 'function') {
	  setTimeout(_s.oninitmovie,1);
    }
  };

  this.waitForExternalInterface = function() {
    if (_s._waitingForEI) {
	  return false;
	}
    _s._waitingForEI = true;
    if (_s._tryInitOnFocus && !_s._isFocused) {
      _s._wD(_s.strings.waitFocus);
      return false;
    }
    if (_s.flashLoadTimeout>0) {
      if (!_s._didInit) {
        _s._wD(_s.strings.waitImpatient);
      }
      setTimeout(function() {
        if (!_s._didInit) {
          _s._wD('soundManager: No Flash response within reasonable time after document load.\nPossible causes: Loading '+_s.movieURL+' failed, Flash version under '+_s.flashVersion+', no support, flash blocked or JS-Flash security error.',2);
          if (!_s._overHTTP) {
          _s._wD(_s.strings.localFail,2);
        }
        _s._debugTS('flashtojs',false,': Timed out'+(_s._overHTTP)?' (Check flash security or flash blockers)':' (No plugin/missing SWF?)');
      }
      // if still not initialized and no other options, give up
      if (!_s._didInit && _s._okToDisable) {
	_s._failSafely(true); // don't disable, for reboot()
      }
    },_s.flashLoadTimeout);
    } else if (!_s._didInit) {
      _s._wD(_s.strings.waitForever);
    }
  };

  this.handleFocus = function() {
    if (_s._isFocused || !_s._tryInitOnFocus) {
	  return true;
	}
    _s._okToDisable = true;
    _s._isFocused = true;
    _s._wD('soundManager.handleFocus()');
    if (_s._tryInitOnFocus) {
      // giant Safari 3.1 hack - assume window in focus if mouse is moving, since document.hasFocus() not currently implemented.
      window.removeEventListener('mousemove',_s.handleFocus,false);
    }
    // allow init to restart
    _s._waitingForEI = false;
    setTimeout(_s.waitForExternalInterface,500);
    // detach event
    if (window.removeEventListener) {
      window.removeEventListener('focus',_s.handleFocus,false);
    } else if (window.detachEvent) {
      window.detachEvent('onfocus',_s.handleFocus);
    }
  };

  this.initComplete = function(bNoDisable) {
    if (_s._didInit) {
	  return false;
	}
    _s._didInit = true;
    _s._wD('-- SoundManager 2 '+(_s._disabled?'failed to load':'loaded')+' ('+(_s._disabled?'security/load error':'OK')+') --',1);
    if (_s._disabled || bNoDisable) {
      _s._wD('soundManager.initComplete(): calling soundManager.onerror()',1);
	  _s._processOnReady();
      _s._debugTS('onload',false);
      _s.onerror.apply(window);
      return false;
    } else {
	  _s._debugTS('onload',true);
    }
    if (_s.waitForWindowLoad && !_s._windowLoaded) {
      _s._wD('soundManager: Waiting for window.onload()');
      if (window.addEventListener) {
        window.addEventListener('load',_s._initUserOnload,false);
      } else if (window.attachEvent) {
        window.attachEvent('onload',_s._initUserOnload);
      }
      return false;
    } else {
      if (_s.waitForWindowLoad && _s._windowLoaded) {
        _s._wD('soundManager: Document already loaded');
      }
      _s._initUserOnload();
    }
  };

  this._addOnReady = function(oMethod,oScope) {
    _s._onready.push({
	  'method': oMethod,
	  'scope': (oScope||null),
	  'fired': false
	});
  };

  this._processOnReady = function() {
	if (!_s._didInit) {
	  // not ready yet.
	  return false;
	}
    var status = {
	  success: (!_s._disabled)
    };
    var queue = [];
    for (var i=0, j=_s._onready.length; i<j; i++) {
	  if (_s._onready[i].fired !== true) {
		queue.push(_s._onready[i]);
	  }
    }
	if (queue.length) {
	  _s._wD('soundManager: Firing '+queue.length+' onready() item'+(queue.length>1?'s':''));
	  for (i=0, j=queue.length; i<j; i++) {
	    if (queue[i].scope) {
		  queue[i].method.apply(queue[i].scope,[status]);
	    } else {
		  queue[i].method(status);
	    }
	    queue[i].fired = true;
	  }
	}
  };

  this._initUserOnload = function() {
    window.setTimeout(function() {
      _s._processOnReady();
      _s._wD('soundManager.initComplete(): calling soundManager.onload()',1);
      // call user-defined "onload", scoped to window
      _s.onload.apply(window);
      _s._wD('soundManager.onload() complete',1);
    });
  };

  this.init = function() {
    _s._wD('-- soundManager.init() --');
    // called after onload()
    _s._initMovie();
    if (_s._didInit) {
      _s._wD('soundManager.init(): Already called?');
      return false;
    }
    // event cleanup
    if (window.removeEventListener) {
      window.removeEventListener('load',_s.beginDelayedInit,false);
    } else if (window.detachEvent) {
      window.detachEvent('onload',_s.beginDelayedInit);
    }
    try {
      _s._wD('Attempting to call Flash from JS..');
      _s.o._externalInterfaceTest(false); // attempt to talk to Flash
      if (!_s.allowPolling) {
	    _s._wD('Polling (whileloading()/whileplaying() support) is disabled.',1);
	  }
      _s._setPolling(true,_s.useFastPolling?true:false);
	  if (!_s.debugMode) {
		_s.o._disableDebug();
	  }
      _s.enabled = true;
      _s._debugTS('jstoflash',true);
    } catch(e) {
	  _s._wD('js/flash exception: '+e.toString());
	  _s._debugTS('jstoflash',false);
      _s._failSafely(true); // don't disable, for reboot()
      _s.initComplete();
      return false;
    }
    _s.initComplete();
  };

  this.beginDelayedInit = function() {
    _s._wD('soundManager.beginDelayedInit()');
    _s._windowLoaded = true;
    setTimeout(_s.waitForExternalInterface,500);
    setTimeout(_s.beginInit,20);
  };

  this.beginInit = function() {
    if (_s._initPending) {
	  return false;
	}
    _s.createMovie(); // ensure creation if not already done
    _s._initMovie();
    _s._initPending = true;
    return true;
  };

  this.domContentLoaded = function() {
    _s._wD('soundManager.domContentLoaded()');
    if (document.removeEventListener) {
	  document.removeEventListener('DOMContentLoaded',_s.domContentLoaded,false);
	}
    _s.go();
  };

  this._externalInterfaceOK = function() {
    // callback from flash for confirming that movie loaded, EI is working etc.
    if (_s.swfLoaded) {
	  return false;
	}
    _s._wD('soundManager._externalInterfaceOK()');
    _s._debugTS('swf',true);
    _s._debugTS('flashtojs',true);
    _s.swfLoaded = true;
    _s._tryInitOnFocus = false;

    if (_s.isIE) {
      // IE needs a timeout OR delay until window.onload - may need TODO: investigating
      setTimeout(_s.init,100);
    } else {
      _s.init();
    }

  };

  this._setSandboxType = function(sandboxType) {
    var sb = _s.sandbox;
    sb.type = sandboxType;
    sb.description = sb.types[(typeof sb.types[sandboxType] != 'undefined'?sandboxType:'unknown')];
    _s._wD('Flash security sandbox type: '+sb.type);
    if (sb.type == 'localWithFile') {
      sb.noRemote = true;
      sb.noLocal = false;
      _s._wD('Flash security note: Network/internet URLs will not load due to security restrictions. Access can be configured via Flash Player Global Security Settings Page: http://www.macromedia.com/support/documentation/en/flashplayer/help/settings_manager04.html',2);
    } else if (sb.type == 'localWithNetwork') {
      sb.noRemote = false;
      sb.noLocal = true;
    } else if (sb.type == 'localTrusted') {
      sb.noRemote = false;
      sb.noLocal = false;
    }
  };

  this.reboot = function() {
    // attempt to reset and init SM2
    _s._wD('soundManager.reboot()');
    if (_s.soundIDs.length) {
      _s._wD('Destroying '+_s.soundIDs.length+' SMSound objects...');
    }
    for (var i=_s.soundIDs.length; i--;) {
      _s.sounds[_s.soundIDs[i]].destruct();
    }
    // trash ze flash
    try {
      if (_s.isIE) {
        _s.oRemovedHTML = _s.o.innerHTML;
      }
      _s.oRemoved = _s.o.parentNode.removeChild(_s.o);
      _s._wD('Flash movie removed.');
    } catch(e) {
      // uh-oh.
      _s._wD('Warning: Failed to remove flash movie.',2);
    }
    _s.enabled = false;
    _s._didInit = false;
    _s._waitingForEI = false;
    _s._initPending = false;
    _s._didAppend = false;
    _s._appendSuccess = false;
    _s._disabled = false;
    _s._waitingforEI = true;
    _s.swfLoaded = false;
    _s.soundIDs = {};
    _s.sounds = [];
    _s.o = null;
    for (i=_s._onready.length; i--;) {
	  _s._onready[i].fired = false;
    }
    _s._wD('soundManager: Rebooting...');
    window.setTimeout(soundManager.beginDelayedInit,20);
  };

  this.destruct = function() {
    _s._wD('soundManager.destruct()');
    _s.disable(true);
  };
  
  // SMSound (sound object)
  
  SMSound = function(oOptions) {
  var _t = this;
  this.sID = oOptions.id;
  this.url = oOptions.url;
  this.options = _s._mergeObjects(oOptions);
  this.instanceOptions = this.options; // per-play-instance-specific options
  this._iO = this.instanceOptions; // short alias

  // assign property defaults (volume, pan etc.)
  this.pan = this.options.pan;
  this.volume = this.options.volume;

  this._lastURL = null;

  this._debug = function() {
    if (_s.debugMode) {
    var stuff = null;
    var msg = [];
    var sF = null;
    var sfBracket = null;
    var maxLength = 64; // # of characters of function code to show before truncating
    for (stuff in _t.options) {
      if (_t.options[stuff] !== null) {
        if (_t.options[stuff] instanceof Function) {
	      // handle functions specially
	      sF = _t.options[stuff].toString();
	      sF = sF.replace(/\s\s+/g,' '); // normalize spaces
	      sfBracket = sF.indexOf('{');
	      msg[msg.length] = ' '+stuff+': {'+sF.substr(sfBracket+1,(Math.min(Math.max(sF.indexOf('\n')-1,maxLength),maxLength))).replace(/\n/g,'')+'... }';
	    } else {
	      msg[msg.length] = ' '+stuff+': '+_t.options[stuff];
	    }
      }
    }
    _s._wD('SMSound() merged options: {\n'+msg.join(', \n')+'\n}');
    }
  };

  this._debug();

  this.id3 = {
   /* 
    Name/value pairs set via Flash when available - see reference for names:
    http://livedocs.macromedia.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00001567.html
    (eg., this.id3.songname or this.id3['songname'])
   */
  };

  this.resetProperties = function(bLoaded) {
    _t.bytesLoaded = null;
    _t.bytesTotal = null;
    _t.position = null;
    _t.duration = null;
    _t.durationEstimate = null;
    _t.loaded = false;
    _t.playState = 0;
    _t.paused = false;
    _t.readyState = 0; // 0 = uninitialised, 1 = loading, 2 = failed/error, 3 = loaded/success
    _t.muted = false;
    _t.didBeforeFinish = false;
    _t.didJustBeforeFinish = false;
    _t.isBuffering = false;
    _t.instanceOptions = {};
    _t.instanceCount = 0;
    _t.peakData = {
      left: 0,
      right: 0
    };
    _t.waveformData = {
	  left: [],
	  right: []
    };
    _t.eqData = [];
  };

  _t.resetProperties();

  // --- public methods ---

  this.load = function(oOptions) {
    if (typeof oOptions != 'undefined') {
      _t._iO = _s._mergeObjects(oOptions);
      _t.instanceOptions = _t._iO;
    } else {
      oOptions = _t.options;
      _t._iO = oOptions;
      _t.instanceOptions = _t._iO;
      if (_t._lastURL && _t._lastURL != _t.url) {
        _s._wD('SMSound.load(): Using manually-assigned URL');
        _t._iO.url = _t.url;
        _t.url = null;
      }
    } 

    if (typeof _t._iO.url == 'undefined') {
      _t._iO.url = _t.url;
    }

    _s._wD('soundManager.load(): '+_t._iO.url,1);
    if (_t._iO.url == _t.url && _t.readyState !== 0 && _t.readyState != 2) {
      _s._wD('soundManager.load(): current URL already assigned.',1);
      return false;
    }
    _t.url = _t._iO.url;
    _t._lastURL = _t._iO.url;
    _t.loaded = false;
    _t.readyState = 1;
    _t.playState = 0; // (oOptions.autoPlay?1:0); // if autoPlay, assume "playing" is true (no way to detect when it actually starts in Flash unless onPlay is watched?)
    try {
      if (_s.flashVersion==8) {
        _s.o._load(_t.sID,_t._iO.url,_t._iO.stream,_t._iO.autoPlay,(_t._iO.whileloading?1:0));
      } else {
        _s.o._load(_t.sID,_t._iO.url,_t._iO.stream?true:false,_t._iO.autoPlay?true:false); // ,(_tO.whileloading?true:false)
        if (_t._iO.isMovieStar && _t._iO.autoLoad && !_t._iO.autoPlay) {
          // special case: MPEG4 content must start playing to load, then pause to prevent playing.
          _t.pause();
        }
      }
    } catch(e) {
      _s._wD('SMSound.load(): Exception: JS-Flash communication failed, or JS error.',2);
      _s._debugTS('onload',false);
      _s.onerror();
      _s.disable();
    }

  };

  this.unload = function() {
    // Flash 8/AS2 can't "close" a stream - fake it by loading an empty MP3
    // Flash 9/AS3: Close stream, preventing further load
    if (_t.readyState !== 0) {
      _s._wD('SMSound.unload(): "'+_t.sID+'"');
      if (_t.readyState != 2) { // reset if not error
        _t.setPosition(0,true); // reset current sound positioning
      }
      _s.o._unload(_t.sID,_s.nullURL);
      // reset load/status flags
      _t.resetProperties();
    }
  };

  this.destruct = function() {
    // kill sound within Flash
    _s._wD('SMSound.destruct(): "'+_t.sID+'"');
    _s.o._destroySound(_t.sID);
    _s.destroySound(_t.sID,true); // ensure deletion from controller
  };

  this.play = function(oOptions) {
    if (!oOptions) {
	  oOptions = {};
    }
    _t._iO = _s._mergeObjects(oOptions,_t._iO);
    _t._iO = _s._mergeObjects(_t._iO,_t.options);
    _t.instanceOptions = _t._iO;
    if (_t.playState == 1) {
      var allowMulti = _t._iO.multiShot;
      if (!allowMulti) {
        _s._wD('SMSound.play(): "'+_t.sID+'" already playing (one-shot)',1);
        return false;
      } else {
        _s._wD('SMSound.play(): "'+_t.sID+'" already playing (multi-shot)',1);
      }
    }
    if (!_t.loaded) {
      if (_t.readyState === 0) {
        _s._wD('SMSound.play(): Attempting to load "'+_t.sID+'"',1);
        // try to get this sound playing ASAP
        _t._iO.stream = true;
        _t._iO.autoPlay = true;
        // TODO: need to investigate when false, double-playing
        // if (typeof oOptions.autoPlay=='undefined') _tO.autoPlay = true; // only set autoPlay if unspecified here
        _t.load(_t._iO); // try to get this sound playing ASAP
      } else if (_t.readyState == 2) {
        _s._wD('SMSound.play(): Could not load "'+_t.sID+'" - exiting',2);
        return false;
      } else {
        _s._wD('SMSound.play(): "'+_t.sID+'" is loading - attempting to play..',1);
      }
    } else {
      _s._wD('SMSound.play(): "'+_t.sID+'"');
    }
    if (_t.paused) {
      _t.resume();
    } else {
      _t.playState = 1;
      if (!_t.instanceCount || _s.flashVersion > 8) {
		_t.instanceCount++;
	  }
      _t.position = (typeof _t._iO.position != 'undefined' && !isNaN(_t._iO.position)?_t._iO.position:0);
      if (_t._iO.onplay) {
		_t._iO.onplay.apply(_t);
	  }
      _t.setVolume(_t._iO.volume,true); // restrict volume to instance options only
      _t.setPan(_t._iO.pan,true);
      _s.o._start(_t.sID,_t._iO.loop||1,(_s.flashVersion==9?_t.position:_t.position/1000));
    }
  };

  this.start = this.play; // just for convenience

  this.stop = function(bAll) {
    if (_t.playState == 1) {
      _t.playState = 0;
      _t.paused = false;
      // if (_s.defaultOptions.onstop) _s.defaultOptions.onstop.apply(_s);
      if (_t._iO.onstop) {
		_t._iO.onstop.apply(_t);
	  }
      _s.o._stop(_t.sID,bAll);
      _t.instanceCount = 0;
      _t._iO = {};
      // _t.instanceOptions = _t._iO;
    }
  };

  this.setPosition = function(nMsecOffset,bNoDebug) {
    if (typeof nMsecOffset == 'undefined') {
      nMsecOffset = 0;
    }
    var offset = Math.min(_t.duration,Math.max(nMsecOffset,0)); // position >= 0 and <= current available (loaded) duration
    _t._iO.position = offset;
    if (!bNoDebug) {
      // _s._wD('SMSound.setPosition('+nMsecOffset+')'+(nMsecOffset != offset?', corrected value: '+offset:''));
    }
    _s.o._setPosition(_t.sID,(_s.flashVersion==9?_t._iO.position:_t._iO.position/1000),(_t.paused||!_t.playState)); // if paused or not playing, will not resume (by playing)
  };

  this.pause = function() {
    if (_t.paused || _t.playState === 0) {
	  return false;
	}
    _s._wD('SMSound.pause()');
    _t.paused = true;
    _s.o._pause(_t.sID);
    if (_t._iO.onpause) {
	  _t._iO.onpause.apply(_t);
	}
  };

  this.resume = function() {
    if (!_t.paused || _t.playState === 0) {
	  return false;
	}
    _s._wD('SMSound.resume()');
    _t.paused = false;
    _s.o._pause(_t.sID); // flash method is toggle-based (pause/resume)
    if (_t._iO.onresume) {
	  _t._iO.onresume.apply(_t);
	}
  };

  this.togglePause = function() {
    _s._wD('SMSound.togglePause()');
    if (_t.playState === 0) {
      _t.play({position:(_s.flashVersion==9?_t.position:_t.position/1000)});
      return false;
    }
    if (_t.paused) {
      _t.resume();
    } else {
      _t.pause();
    }
  };

  this.setPan = function(nPan,bInstanceOnly) {
    if (typeof nPan == 'undefined') {
      nPan = 0;
    }
    if (typeof bInstanceOnly == 'undefined') {
      bInstanceOnly = false;
    }
    _s.o._setPan(_t.sID,nPan);
    _t._iO.pan = nPan;
    if (!bInstanceOnly) {
      _t.pan = nPan;
    }
  };

  this.setVolume = function(nVol,bInstanceOnly) {
    if (typeof nVol == 'undefined') {
      nVol = 100;
    }
    if (typeof bInstanceOnly == 'undefined') {
      bInstanceOnly = false;
    }
    _s.o._setVolume(_t.sID,(_s.muted&&!_t.muted)||_t.muted?0:nVol);
    _t._iO.volume = nVol;
    if (!bInstanceOnly) {
      _t.volume = nVol;
    }
  };

  this.mute = function() {
    _t.muted = true;
    _s.o._setVolume(_t.sID,0);
  };

  this.unmute = function() {
    _t.muted = false;
    var hasIO = typeof _t._iO.volume != 'undefined';
    _s.o._setVolume(_t.sID,hasIO?_t._iO.volume:_t.options.volume);
  };

  this.toggleMute = function() {
    if (_t.muted) {
      _t.unmute(); 
    } else {
      _t.mute();
    }
  };

  // --- "private" methods called by Flash ---

  this._whileloading = function(nBytesLoaded,nBytesTotal,nDuration) {
    if (!_t._iO.isMovieStar) {
      _t.bytesLoaded = nBytesLoaded;
      _t.bytesTotal = nBytesTotal;
      _t.duration = Math.floor(nDuration);
      _t.durationEstimate = parseInt((_t.bytesTotal/_t.bytesLoaded)*_t.duration,10);
      if (_t.durationEstimate === undefined) {
	    // reported bug?
	    _t.durationEstimate = _t.duration;
      }
      if (_t.readyState != 3 && _t._iO.whileloading) {
  	    _t._iO.whileloading.apply(_t);
      }
    } else {
      _t.bytesLoaded = nBytesLoaded;
      _t.bytesTotal = nBytesTotal;
      _t.duration = Math.floor(nDuration);
      _t.durationEstimate = _t.duration;
      if (_t.readyState != 3 && _t._iO.whileloading) {
	_t._iO.whileloading.apply(_t);
      }
    }
  };

  this._onid3 = function(oID3PropNames,oID3Data) {
    // oID3PropNames: string array (names)
    // ID3Data: string array (data)
    _s._wD('SMSound._onid3(): "'+this.sID+'" ID3 data received.');
    var oData = [];
    for (var i=0,j=oID3PropNames.length; i<j; i++) {
      oData[oID3PropNames[i]] = oID3Data[i];
      // _s._wD(oID3PropNames[i]+': '+oID3Data[i]);
    }
    _t.id3 = _s._mergeObjects(_t.id3,oData);
    if (_t._iO.onid3) {
      _t._iO.onid3.apply(_t);
    }
  };

  this._whileplaying = function(nPosition,oPeakData,oWaveformDataLeft,oWaveformDataRight,oEQData) {

    if (isNaN(nPosition) || nPosition === null) {
      return false; // Flash may return NaN at times
    }
    if (_t.playState === 0 && nPosition > 0) {
      // can happen at the end of a video where nPosition == 33 for some reason, after finishing.???
      // can also happen with a normal stop operation. This resets the position to 0.
      // _s._writeDebug('Note: Not playing, but position = '+nPosition);
      nPosition = 0;	
    }
    _t.position = nPosition;
	if (_t._iO.usePeakData && typeof oPeakData != 'undefined' && oPeakData) {
	  _t.peakData = {
	   left: oPeakData.leftPeak,
	   right: oPeakData.rightPeak
	  };
	}
	if (_t._iO.useWaveformData && typeof oWaveformDataLeft != 'undefined' && oWaveformDataLeft) {
	  _t.waveformData = {
	   left: oWaveformDataLeft.split(','),
	   right: oWaveformDataRight.split(',')
	  };
	}
	if (_t._iO.useEQData && typeof oEQData != 'undefined' && oEQData) {
	  _t.eqData = oEQData;
	}
    if (_t.playState == 1) {
	  // special case/hack: ensure buffering is false (instant load from cache, thus buffering stuck at 1?)
	  if (_t.isBuffering) {
		_t._onbufferchange(0);
	  }
      if (_t._iO.whileplaying) {
	    _t._iO.whileplaying.apply(_t); // flash may call after actual finish
      }
      if (_t.loaded && _t._iO.onbeforefinish && _t._iO.onbeforefinishtime && !_t.didBeforeFinish && _t.duration-_t.position <= _t._iO.onbeforefinishtime) {
        _s._wD('duration-position &lt;= onbeforefinishtime: '+_t.duration+' - '+_t.position+' &lt= '+_t._iO.onbeforefinishtime+' ('+(_t.duration-_t.position)+')');
        _t._onbeforefinish();
      }
    }
  };

  this._onload = function(bSuccess) {
    bSuccess = (bSuccess==1?true:false);
    _s._wD('SMSound._onload(): "'+_t.sID+'"'+(bSuccess?' loaded.':' failed to load? - '+_t.url),(bSuccess?1:2));
    if (!bSuccess) {
      if (_s.sandbox.noRemote === true) {
        _s._wD('SMSound._onload(): Reminder: Flash security is denying network/internet access',1);
      }
      if (_s.sandbox.noLocal === true) {
        _s._wD('SMSound._onload(): Reminder: Flash security is denying local access',1);
      }
    }
    _t.loaded = bSuccess;
    _t.readyState = bSuccess?3:2;
    if (_t._iO.onload) {
      _t._iO.onload.apply(_t);
    }
  };

  this._onbeforefinish = function() {
    if (!_t.didBeforeFinish) {
      _t.didBeforeFinish = true;
      if (_t._iO.onbeforefinish) {
        _s._wD('SMSound._onbeforefinish(): "'+_t.sID+'"');
        _t._iO.onbeforefinish.apply(_t);
      }
    }
  };

  this._onjustbeforefinish = function(msOffset) {
    // msOffset: "end of sound" delay actual value (eg. 200 msec, value at event fire time was 187)
    if (!_t.didJustBeforeFinish) {
      _t.didJustBeforeFinish = true;
      if (_t._iO.onjustbeforefinish) {
        _s._wD('SMSound._onjustbeforefinish(): "'+_t.sID+'"');
        _t._iO.onjustbeforefinish.apply(_t);
      }
    }
  };

  this._onfinish = function() {
    // sound has finished playing

    // TODO: calling user-defined onfinish() should happen after setPosition(0)
    // OR: onfinish() and then setPosition(0) is bad.
    if (_t._iO.onbeforefinishcomplete) {
      _t._iO.onbeforefinishcomplete.apply(_t);
    }
    // reset some state items
    _t.didBeforeFinish = false;
    _t.didJustBeforeFinish = false;
    if (_t.instanceCount) {
      _t.instanceCount--;
      if (!_t.instanceCount) {
        // reset instance options
        // _t.setPosition(0);
        _t.playState = 0;
        _t.paused = false;
        _t.instanceCount = 0;
        _t.instanceOptions = {};
      }
	  if (!_t.instanceCount || _t._iO.multiShotEvents) {
        // fire onfinish for last, or every instance
        if (_t._iO.onfinish) {
          _s._wD('SMSound._onfinish(): "'+_t.sID+'"');
          _t._iO.onfinish.apply(_t);
        }
	  }
    } else {
	  if (_t.useVideo) {
	    // video has finished
	    // may need to reset position for next play call, "rewind"
	    // _t.setPosition(0);
	  }
      // _t.setPosition(0);
    }

  };

  this._onmetadata = function(oMetaData) {
    // movieStar mode only
    _s._wD('SMSound.onmetadata()');
    // Contains a subset of metadata. Note that files may have their own unique metadata.
    // http://livedocs.adobe.com/flash/9.0/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00000267.html
    if (!oMetaData.width && !oMetaData.height) {
	  _s._wD('No width/height given, assuming defaults');
	  oMetaData.width = 320;
	  oMetaData.height = 240;
    }
    _t.metadata = oMetaData; // potentially-large object from flash
    _t.width = oMetaData.width;
    _t.height = oMetaData.height;
    if (_t._iO.onmetadata) {
      _s._wD('SMSound._onmetadata(): "'+_t.sID+'"');
      _t._iO.onmetadata.apply(_t);
    }
    _s._wD('SMSound.onmetadata() complete');
  };

  this._onbufferchange = function(bIsBuffering) {
    if (_t.playState === 0) {
      // ignore if not playing
      return false;
    }
    if (bIsBuffering == _t.isBuffering) {
      // ignore initial "false" default, if matching
	  _s._wD('_onbufferchange: ignoring false default / loaded sound');
      return false;
    }
    _t.isBuffering = (bIsBuffering==1?true:false);
    if (_t._iO.onbufferchange) {
      _s._wD('SMSound._onbufferchange(): '+bIsBuffering);
      _t._iO.onbufferchange.apply(_t);
    }
  };

  this._ondataerror = function(sError) {
	// flash 9 wave/eq data handler
	if (_t.playState > 0) { // hack: called at start, and end from flash at/after onfinish().
	  _s._wD('SMSound._ondataerror(): '+sError);
	  if (_t._iO.ondataerror) {
	    _t._iO.ondataerror.apply(_t);	
	  }
	} else {
	  // _s._wD('SMSound._ondataerror(): ignoring');
	}
  };

  }; // SMSound()

  this._onfullscreenchange = function(bFullScreen) {
    _s._wD('onfullscreenchange(): '+bFullScreen);
    _s.isFullScreen = (bFullScreen==1?true:false);
    if (!_s.isFullScreen) {
	  // attempt to restore window focus after leaving full-screen
	  try {
		window.focus();
		_s._wD('window.focus()');
	  } catch(e) {
	    // oh well
	  }
    }
  };

  // register a few event handlers
  if (window.addEventListener) {
    window.addEventListener('focus',_s.handleFocus,false);
    window.addEventListener('load',_s.beginDelayedInit,false);
    window.addEventListener('unload',_s.destruct,false);
    if (_s._tryInitOnFocus) {
      window.addEventListener('mousemove',_s.handleFocus,false); // massive Safari focus hack
    }
  } else if (window.attachEvent) {
    window.attachEvent('onfocus',_s.handleFocus);
    window.attachEvent('onload',_s.beginDelayedInit);
    window.attachEvent('unload',_s.destruct);
  } else {
    // no add/attachevent support - safe to assume no JS -> Flash either.
    _s._debugTS('onload',false);
    soundManager.onerror();
    soundManager.disable();
  }

  if (document.addEventListener) {
	document.addEventListener('DOMContentLoaded',_s.domContentLoaded,false);
  }

} // SoundManager()

// set var SM2_DEFER = true; in your own script to prevent auto start-up
if (typeof SM2_DEFER == 'undefined' || !SM2_DEFER) {
  soundManager = new SoundManager();
}//external soundmanager2.js
//include eCarList.Module
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	/*
		Structure: SmartChat.Sound
		
		Sound playing utility.
	*/
	SmartChat.Sound = eCarList.Module.extend({
		enabled: true
	});

	var _sounds = {
		chat_request: 'chat_request.mp3',
		message_receipt: 'message_receipt.mp3'
	};

	var _init_soundManager = function() {
		if (window.soundManager) {
			soundManager.url = SmartChat.IMAGE_BASE;
			soundManager.flashVersion = 9;
			soundManager.debugMode = false;
		
			soundManager.onload = function() {
				for (var id in _sounds) {
					if (_sounds.hasOwnProperty(id)) {
						var sound = soundManager.createSound({
							id: id,
							url: SmartChat.IMAGE_BASE + '/' + _sounds[id],
							volume: 20
						});
						SmartChat.Sound[id] = _create_sound_fn(sound);
					}
				}
			};
		} else {
			// soundManager not included, don't play anything
			for (var id in _sounds) {
				if (_sounds.hasOwnProperty(id)) {
					SmartChat.Sound[id] = _noop;
				}
			}
		}
	};

	var _noop = function() {
	};

	var _create_sound_fn = function(sound) {
		return function() {
			if (SmartChat.Sound.enabled) {
				sound.play();
			}
		};
	};

	_init_soundManager();
});
//include eCarList.Class
//include eCarList.App.SmartChat.Common
//include eCarList.App.SmartChat.Session
//include eCarList.App.SmartChat.Sound
//include eCarList.App.SmartChat.UI
//include eCarList.Util.Browser
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	var Browser = eCarList.Util.Browser,
		ChatPanel = SmartChat.ChatPanel,
		MessageType = SmartChat.MessageType,
		Session = SmartChat.Session,
		Sound = SmartChat.Sound,
		UI = SmartChat.UI;

	/*
		Class: eCarList.App.SmartChat.ChatPanel
	
		Properties:
			session  - user session <SmartChat.Session>
			with_jid - recipient jabber id (room or person) <JSJaCJID>
	*/
	SmartChat.ChatPanel = SmartChat.UI.extend({

		/*
			Attributes:
				base_href - [optional] base href to use for relative links
				session   - user session <SmartChat.Session>
				with_jid  - recipient jabber id (room or person) <JSJaCJID>
		*/
		Attribute: {
			base_href: { },
			session: { required: true },
			with_jid: { required: true }
		},

		HTML: '			<div class="chat_panel">				<div class="history">					<div class="notification"></div>				</div>				<textarea class="message_input"></textarea>			</div>		',
		
		prototype: {
			
			init: function(config) {
				this._super(config);
				this._init_date = new Date();
			},

			/*
				Method: dispose

				Make sure title is no longer blinking
			*/
			dispose: function() {
				_unblink_title.call(this);
				this._super();
			},

			/*
				Method: set_enabled
				
				Enables or disables chat panel input.

				Parameters:
					enabled - enable/disable option
			*/
			set_enabled: function(enabled) {
				if (enabled && this._get('session').available) {
					this._super(true);
					this.$('.message_input').ecl().disabled(false);
				} else {
					this._super(false);
					this.$('.message_input').ecl().disabled(true);
				}
			},

			/*
				Method: focus

				Gives focus to this chat panel's message input area.
			*/
			focus: function() {
				var history = this.$('.history').get(0);
				history.scrollTop = history.scrollHeight;

				if (this._get('enabled')) {
					this.$('.message_input').get(0).focus();
				}
			},

			/*
				Method: add_message
			
				Adds a chat message to this chat panel view.

				Parameters:
					message - message to add
			*/
			add_message: function(message) {

				var is_me = (message.with_jid.isRoom())
					? message.from === this._get('session').room_nicks[message.with_jid.bare()]
					: message.to;

				var sender_class;
				if (is_me) {
					sender_class = 'to';
				} else if (message.with_jid.isSystem()) {
					sender_class = 'system';
				} else {
					sender_class = 'from';
				}
				
				var html = '<div class="message"><span class="' + sender_class + '">'
					+ (is_me ? 'me' : message.from) + ':</span> ' + (message.body_html || message.body) + '</div>';

				// determine index for new message
				var msg_index = 0;
				var new_msg_time = message.date.getTime();
				//eCarList.debug('new_msg_time: ' + new_msg_time + ' msg: ' + message.body);
				this.$('.history > .message').each(function() {
					var msg_time = parseInt($(this).data('time') || '0', 10);
					//eCarList.debug('msg_time: ' + msg_time + ' msg: ' + $(this).text());
					if (new_msg_time > msg_time) {
						msg_index++;
						return true;
					}
					return false;
				});
				
				// add message html to history
				if (msg_index === 0) {
					this.$('.history').prepend(html);
				} else {
					_$message_at.call(this, msg_index-1).after(html);
				}

				// attach time to message
				_$message_at.call(this, msg_index).data('time', new_msg_time);

				// adjust link href & targets in new entry
				var self = this;
				$('a', _$message_at.call(this, msg_index).get(0)).each(function() {
					var href = $(this).attr('href');
					var retarget = true;
					var rel_match = href.match(/^rel-href:(\/?(.*))/);
					if (rel_match) {
						// convert rel-url: protocol to local url
						if (self._get('base_href')) {
							href = self._get('base_href').replace(/\/$/, '') + '/' + rel_match[2];
						} else {
							href = rel_match[1];
							retarget = false;
						}
						$(this).attr('href', href);
					}

					if (retarget) {
						$(this).attr('target', '_blank');
					}
				});

				_scroll_history.call(this);

				// play msg sound for newly received messages
				// adding a sec to deal with case where chat panel was created due to arrival of new message
				if (!is_me && this._init_date.getTime() < (message.date.getTime() + 1000)) {
					Sound.message_receipt();
					_blink_title.call(this, message.from);
				}
			},

			/*
				Method: add_status_message

				Adds a status message to this chat panel view.

				Parameters:
					status - the status message text
			*/
			add_status_message: function(status) {
				this.$('.history > div.notification').before('<div class="status message">' + status + '</div>');
				this.$('.history > div.status:last-child').data('time', (new Date()).getTime());
				_scroll_history.call(this);
			},

			/*
				Method: set_notification

				Displays a notification message at the end of the conversation history. Notifications are
				automatically cleared after 30 secs.

				Parameters:
					notification - notification text to display
			*/
			set_notification: function(notification) {
				this.clear_notification();
				this.$('.history > div.notification').text(notification);
				_scroll_history.call(this);
				var self = this;
				this._notification_timeout = setTimeout(function() {
					self.clear_notification();
				}, 30000);
			},

			/*
				Method: clear_notification

				Clears any notification message from the end of the conversation history.
			*/
			clear_notification: function() {
				if (this._notification_timeout) {
					clearTimeout(this._notification_timeout);
					delete this._notification_timeout;
				}
				this.$('.history > div.notification').text('');
			},

			/*
				Method: get_input_text

				Returns:
					The current text in the message input box.
			*/
			get_input_text: function() {
				return this.$('.message_input').val();
			},
			
			/*
				Method: set_input_text
				
				Sets the text of the message input box.

				Parameters:
					text - new text of the message input box.
			*/
			set_input_text: function(text) {
				this.$('.message_input').val(text);
			},

			_render: function() {
				this._super();
				var session = this._get('session');
				
				// load history (only include MESSAGEs)
				var with_jid = this._get('with_jid');
				if (with_jid) {
					var messages = session.get_messages(with_jid);
					for (var i=0; i < messages.length; i++) {
						var message = messages[i];
						if (message.type === MessageType.MESSAGE) {
							this.add_message(message);
						}
					}
				}
				this.set_enabled(this._get('enabled'));

				// bind events
				this.bind('.message_input', 'keyup', _on_input_keyup);
				this.bind('.message_input', 'focus', _on_input_focus);
				this.bind_to(session, Session.Event.CONNECT, _on_connect);
				this.bind_to(session, Session.Event.RECONNECT, _on_reconnect);
				this.bind_to(session, Session.Event.DISCONNECT, _on_disconnect);
				this.bind_to(session, Session.Event.CONNECTION_FAULT, _on_connection_fault);
				this.bind_to(session, Session.Event.MEMBER_JOIN, _on_member_join);
				this.bind_to(session, Session.Event.MEMBER_LEAVE, _on_member_leave);
				this.bind_to(session, Session.Event.MESSAGE_ADD, _on_message_add);
				this.bind_to(session, Session.Event.CHAT_STATE_CHANGE, _on_chat_state_change);
			},

			_layout: function() {
				this._super();

				var width = this.$().width();
				var height = this.$().height();

				// input_area_height = input_height + input_border + input_margin + input_area_border
				// history_height = div_height - (input_height + history_padding);
				var history_height = height - 61;
				this.$('.history').height((history_height < 0) ? 0 : history_height);

				// input_width = div_width - (input_margin + input_border + input_padding);
				this.$('.message_input').width(width - 12);
			}

		}

	});

	var _stylize_text = function(text) {
		// disallow user tags
		text = text.htmlEnc();
		// make *blah* bold
		text = text.replace(/\b\*([^\*]+)\*\b/, '<span style="font-weight: bold">$1</span>');
		// make _blah_ italic
		text = text.replace(/\b_([^_]+)_\b/, '<span style="font-style: italic">$1</span>');
		// replace email addresses with links
		text = text.replace(/\b([A-Z0-9\._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,4})\b/i, '<a href="mailto:$1">$1</a>');
		// replace URLs with links
		text = text.replace(/\b(https?:\/\/[\-\w]+(\.\w[\-\w]*)+(:\d+)?[\w\.\!\?\;=\/#\&\+\%]*)/ig,
							'<a href="$1">$1</a>');
		return text;
	};

	var _$message_at = function(index) {
		return this.$('.history > div.message:eq(' + index +')');
	};

	var _scroll_history = function() {
		var history = this.$('.history').get(0);
		history.scrollTop = history.scrollHeight;
	};

	var _blink_title = function(from) {
		Browser.blink_title('chat_panel_' + this._id, from + ' says...');
	};

	var _unblink_title = function() {
		Browser.unblink_title('chat_panel_' + this._id);
	};

	var _on_input_focus = function(event) {
		_unblink_title.call(this);
	};

	var _on_input_keyup = function(event) {
		var session = this._get('session');
		var with_jid = this._get('with_jid');

		_unblink_title.call(this);
		if ((event.keyCode === 13 || event.keyCode === 14) && !event.shiftKey) {
			var body = $.trim(this.$('.message_input').val());
			var body_html = _stylize_text.call(this, body);
			if (body.length === 0 || session.send_message(with_jid, body, body_html)) {
				message_sent = true;
				this.$('.message_input').val('');
			}
		}

		// update chat state
		if (this.$('.message_input').val().length) {
			session.update_chat_state(with_jid, Session.ChatState.COMPOSING);

			// reset timeout for "paused" chat state, 5 secs after last input
			if (this._compose_timeout) {
				clearTimeout(this._compose_timeout);
			}
			var self = this;
			this._compose_timeout = setTimeout(function() {
				session.update_chat_state(with_jid, Session.ChatState.PAUSED);
				delete self._compose_timeout;
			}, 5000);
		} else {
			session.update_chat_state(with_jid, Session.ChatState.ACTIVE);
			if (this._compose_timeout) {
				clearTimeout(this._compose_timeout);
				delete this._compose_timeout;
			}
		}
	};

	var _on_connect = function(event) {
		this.set_enabled(true);
	};

	var _on_reconnect = function(event) {
		this.set_enabled(true);
	};

	var _on_disconnect = function(event) {
		this.set_enabled(false);
	};

	var _on_connection_fault = function(event) {
		this.set_enabled(false);
	};

	var _on_member_join = function(event, member) {
		if (member.room_jid.isEntity(this._get('with_jid'))) {
			if (member.nick !== this._get('session').nick) {
				this.add_status_message(member.nick + ' has joined the chat session.');
			}
		}
	};

	var _on_member_leave = function(event, member) {
		if (member.room_jid.isEntity(this._get('with_jid'))) {
			if (member.nick !== this._get('session').nick) {
				this.add_status_message(member.nick + ' has left the chat session.');
			}
		}
	};

	var _on_message_add = function(event, message) {
		if ((message.with_jid.isSystem() || message.with_jid.isEntity(this._get('with_jid')))
			&& message.type === MessageType.MESSAGE) {
			// only add messages
			this.add_message(message);
		}
	};

	var _on_chat_state_change = function(event, state_change) {
		var session = this._get('session');
		var with_jid = this._get('with_jid');
		if (state_change.with_jid.isEntity(with_jid)
			&& !state_change.contact_jid.equals(session.get_chat_jid(with_jid))) {
			var notification;
			if (state_change.chat_state === Session.ChatState.COMPOSING) {
				notification = session.get_contact_nick(state_change.contact_jid) + ' is typing...';
			} else if (state_change.chat_state === Session.ChatState.PAUSED) {
				notification = session.get_contact_nick(state_change.contact_jid) + ' has entered text.';
			}
			if (notification) {
				this.set_notification(notification);
			} else {
				this.clear_notification();
			}
		}
	};

});
//include eCarList.App.SmartChat.Common
//include eCarList.App.SmartChat.WindowDialog
eCarList.namespace('eCarList.App.SmartChat.Visitor', function(Visitor, $) {

	var Form = SmartChat.Form;

	/*
		Class: SmartChat.VisitorNameDialog
	*/
	Visitor.VisitorNameDialog = eCarList.App.SmartChat.WindowDialog.extend({
		
		Attribute: {
			message: { init: 'Please enter your name and click "Start Chat" to begin.' },
			options: { init: ['Start Chat'] },
			session: { required: true },
			style_class: { init: 'visitor_name_dialog window_dialog' }
		},

		BODY_HTML: '			<div class="form">				<label>Name:</label><input class="text name_input" type="text" maxlength="32"/><br/>			</div>		',

		prototype: {

			focus: function() {
				this._super();
				this.$('.name_input').get(0).focus();
			},

			get_name: function() {
				return Form.sanitize_name(this.$('.name_input').val());
			},

			request_chat: function() {
				var team_eid;
				var session = this._get('session');
				if (session.teams.length === 1) {
					team_eid = session.teams[0].eid;
				} else if (session.teams.length > 1) {
					team_eid = this.$('input:radio[name="team"]:checked').val();
				}

				if (team_eid) {
					session.request_chat(this.get_name() || 'Visitor', team_eid);
					this.close();
				}
			},

			_render: function() {
				this._super();
				var session = this._get('session');
				
				if (session.teams.length > 1) {
					for (var i = 0; i < session.teams.length; i++) {
						var team = session.teams[i];
						var label = (i === 0) ? 'Support:' : '';
						var html = '<label>' + label + '</label><input type="radio" name="team" value="' + team.eid + '"/> ' + team.name + '<br/>';
						this.$('.form').append(html);
					}
					// select first option by default
					this.$('input[name="team"]').val([session.teams[0].eid]);
				}

				this.bind('.name_input', 'blur', _on_input_blur);
				this.bind('.name_input', 'keyup', _on_input_keyup);
				this.bind(this.klass.Event.SELECT, _on_option_select);
			}

		}
	});

	var _on_option_select = function(event, option) {
		this.request_chat();
	};

	var _on_input_blur = function(event) {
		Form.sanitize_name(this.$('.name_input'));
	};

	var _on_input_keyup = function(event) {
		if (event.keyCode === 13 || event.keyCode === 14) {
			this.request_chat();
		}
	};

});
//include eCarList.Module
//include eCarList.App.SmartChat.Common
eCarList.namespace('eCarList.App.SmartChat', function(SmartChat, $) {

	var Form = SmartChat.Form;
	
	/*
		Structure: eCarList.App.SmartChat.AdminSite
		
		Admin app web services interface.
	*/
	SmartChat.AdminSite = eCarList.Module.extend({

		get_vehicle_edit_url: function(vehicle_id) {
			return _get_url('/cgi-bin/admin2.cgi?mode=updt&id=' + vehicle_id);
		},

		get_vehicle_leads_url: function(vehicle_id) {
			return _get_url('/cgi-bin/admin2.cgi?mode=v_m&id=' + vehicle_id);
		},

		/*
			Method: get_carfax_url

			Attempts to pull carfax url from admin page.

			HTML:
				<div id="contentSectionRight">
					...
					<a href="http://www.carfaxonline.com/cfm/Display_Dealer_Report.cfm?partner=ECR_0&UID=c405956&vin=WA1EY74L57D027929" target=_blank>View CarFax History Report</a>
					...
				</div>

			Returns:
				String containing carfax url if found, null otherwise.
		*/
		get_carfax_url: function() {
			var url = $('div#contentSectionRight a:contains(\'View CarFax History Report\')').attr('href');
			return url || null;
		},

		/*
			Method: get_vehicle_detail

			Attempts to retrive vehicle details from the current admin page.

			HTML:
				<div id="subNavigationBoxVehicle"><img...></div>
				<div class="subNavigationBoxText">
					<ul>
						<li>2007 Audi Q7 Premium</li>
						<li>VIN: WA1EY74L57D027929</li>
						<li>Stock: 027929</li>
						<li>ID: 100229</li>
					</ul>
				</div>

			Returns:
				Object containing vehicle details if this is a valid admin vehicle page, null otherwise.
		*/
		get_vehicle_detail: function() {
			var detail_div = $('#subNavigationBoxVehicle').next('div.subNavigationBoxText').get(0);
			if (detail_div) {
				var vehicle_detail = { };
				$('li', detail_div).each(function() {
					var text = $(this).text();
					var keypair_match = text.match(/^([A-Z]+):\s*(.*)/i);
					if (keypair_match) {
						vehicle_detail[keypair_match[1].toLowerCase()] = keypair_match[2];
					} else {
						vehicle_detail.name = text;
					}
				});
				return vehicle_detail;
			}
			return null;
		},

		/*
			Method: log_chat_session

			Submit chat session log to eCarList backed.
		*/
		log_chat_session: function(session_log, completed) {

			var post = {
				d_id: session_log.dealer_id,
				mode: 'vehicle_question',
				lead_type: session_log.lead_type || '28',
				chat_session: JSJaCJSON.toString({
					status: completed ? 'completed' : 'cancelled',
					user_eid: session_log.admin_user_eid,
					dealer_eid: session_log.dealer_eid,
					domain: session_log.domain,
					phone: session_log.phone,
					email: session_log.email,
					duration_seconds: Math.floor(session_log.duration / 1000),
					log: session_log.chat_log,
					urls: (session_log.history) 
						? $.map(session_log.history, function(location) { return location.url; })
						: []
				})
			};

			if (completed) {
				post.subject = 'Chat Lead';
				post.name = session_log.name;

				post.email =  session_log.email;
				if (!Form.validate_email(post.email)) {
					// fake email if invalid one given
					post.email = 'unknown@ecarlist.com';
				}

				post.phone = session_log.phone.replace(/\D+/g, '');
				if (!Form.validate_phone(post.phone)) {
					// fake phone if invalid one given
					post.phone = '5555555555';
				}

				if (session_log.vehicle_id) {
					post.id = session_log.vehicle_id;
				}

				post.body = '-- Chat Lead --\n';
				post.body += '    Name: ' + session_log.name + '\n';
				post.body += '   Email: ' + session_log.email + '\n';
				post.body += '   Phone: ' + session_log.phone + '\n';
				if (session_log.admin_nick) {
					post.body += '   Admin: ' + session_log.admin_nick + '\n';
				}
				if (session_log.duration) {
					var min = Math.floor(session_log.duration / 60000);
					var sec = Math.floor((session_log.duration % 60000) / 1000);
					post.body += 'Duration: ' + min + ' min ' + sec + ' secs\n';
				}

				var transcript = $.map(session_log.chat_log, function(log) {
					return log.nickname + ': ' + log.message;
				}).join('\n');
				post.body += '\n-- Conversation --\n' + transcript;

				var pages_visited = $.map(session_log.history, function(location) {
					return location.title + ' (' + location.url + ')';
				}).join('\n');
				post.body += '\n\n-- Pages Visited --\n' + pages_visited;
			}
			
			// HACK: disable real ajax since we don't want to pollute prod DB
			//alert(_get_url('/cgi-bin/ajax'); + '\n' + JSJaCJSON.toString(post));
			$.post(_get_url('/cgi-bin/ajax'), post);
		}

	});

	/*
		Prepends host info to url if we're on the desktop
	*/
	var _get_url = function(url) {
		if (window.location.protocol === 'app:') {
			// fix url for desktop app
			url = 'https://www.ecarlist.com' + url;
		}
		return url;
	};

	/*
		Structure: Lead

		Contains all of attributes of the lead that are to be sent to the LeadService.

		Properties:
			dealer_id  - dealer id
			name       - lead name
			email      - email address
			phone      - phone number
			vehicle_id - [optional] vechicle id
			transcript - conversation transcript
	*/

});
//include eCarList.App.SmartChat.AdminSite
//include eCarList.App.SmartChat.ChatService
//include eCarList.App.SmartChat.Common
//include eCarList.App.SmartChat.Session

/*
	Class: SmartChat.VisitorSession
		
	Properties:
		private_jid - private room jabber id <JSJaCJID>
		state       - current visitor state
		teams       - avail support teams
		team_eid    - support team responsible for handling chat request
		team_jid    - support team room jabber id <JSJaCJID>
*/
eCarList.namespace('eCarList.App.SmartChat.Visitor', function(Visitor, $) {

	var AdminSite = eCarList.App.SmartChat.AdminSite,
		ChatService = eCarList.App.SmartChat.ChatService,
		Session = eCarList.App.SmartChat.Session;

	/*
		Class: eCarList.App.SmartChat.VisitorSession

		Parameters:
			dealer_eid - site dealer_eid
			dealer_id  - site dealer id
	*/
	Visitor.VisitorSession = eCarList.App.SmartChat.Session.extend({

		CHAT_REQUEST_TIMEOUT: 180000, // 3 min

		Event: {
			CHAT_ACCEPT: 'visitor_session_chat_accept',
			CHAT_COMPLETE: 'visitor_session_chat_complete',
			CHAT_OFFER: 'visitor_session_chat_offer',
			CHAT_PREPARE: 'visitor_session_chat_prepare',
			CHAT_REQUEST: 'visitor_session_chat_request'
		},

		State: {
			IDLE: 'IDLE',
			OFFERED: 'OFFERED',
			PREPARING: 'PREPARING',
			CONNECTING: 'CONNECTING',
			REQUESTING: 'REQUESTING',
			REQUESTED: 'REQUESTED',
			ACCEPTING: 'ACCEPTING',
			ACTIVE: 'ACTIVE',
			UNAVAILABLE: 'UNAVAILABLE'
		},
		
		prototype: {
			/*
				Constructor: init
			*/
			init: function(properties) {
				this._super(properties);
				this.init_state(this.klass.State.IDLE);
				this.nick = this.nick || 'Visitor';
				this.presence.history = [];
				//this._require('dealer_eid', 'dealer_id');
				_init_handlers.call(this);
			},

			/*
				Method: connect (Overrides SmartChat.Session#connect)
	
				Retrieves new visitor credentials from the server if this session does not alrady
				have credentials. After credentials are retrieved, the session is established.
			*/
			connect: function() {
				ChatService.create_visitor(function(visitor) {
					this.jid = new JSJaCJID({
						node: visitor.node,
						domain: eCarList.App.SmartChat.VISITOR_DOMAIN
					});
					this.secret = visitor.password;
					this.klass.base.connect.call(this);
				}, this);
			},
	
			/*
				Method: update_location (mod)
	
				Updates this visitor's presence information.
			*/
			update_location: function() {
				var location = {
					title: document.title || window.location.toString(),
					url: window.location.toString().toLowerCase()
				};
	
				var path = window.location.pathname.substring(1).split('/');
	
				// parse dealer id & vehicle id from vehicle pages
				if (path.length >= 3 && path[0] === 'web') {
					if (path[1] === 'vehicle') {
						if (path[2].match(/\d+/)) {
							location.vehicle_id = path[2];
						}
					} else if (path[2] === 'vehicle') {
						if (path[1].match(/\d+/)) {
							location.dealer_id = path[1];
						}
						if (path[3].match(/\d+/)) {
							location.vehicle_id = path[3];
						}
					}
				}
	
				this.presence.history.unshift(location);
	
				// get rid of any other instances of this location
				for (var i = this.presence.history.length - 1; i >= 1; i--) {
					var past_location = this.presence.history[i];
					if (past_location.url === location.url) {
						this.presence.history.splice(i, 1);
					}
				}
				
				// only update referrer if it hasn't been defined
				if (!this.presence.referrer &&  document.referrer) {
					this.presence.referrer = document.referrer;
				}
	
				if (this.available) {
					this.update_presence();
				}
			},
	
			/*
				Method: elicit_lead
				
				Manages system's dialogue with visitor.
			*/
			elicit_lead: function(stimulus) {
				stimulus = stimulus || 'none';
				var dialogue = this.get_dialogue()[stimulus];
				
				if (dialogue) {
					var messages = [];
	
					if (dialogue.response) {
						messages.push(dialogue.response);
					}
	
					var prompts = dialogue.prompts;
					if (prompts) {
						if (!this.presence.email && !this.presence.phone && prompts.email_and_phone) {
							messages.push(prompts.email_and_phone);
						} else if (!this.presence.email && prompts.email) {
							messages.push(prompts.email);
						} else if (!this.presence.phone && prompts.phone) {
							messages.push(prompts.phone);
						} else if (prompts.none) {
							messages.push(prompts.none);
						}
					}
	
					if (messages.length > 0) {
						var message = messages.join(' ').replace('$nick', $.ecl.string.first_name(this.nick));
						this.create_system_message(message);
					}
				}
			},
			
			/*
				Message: send_message (Overrides SmartChat.Session#send_message)
	
				Searches message body for an email address or phone number to add to the visitor's presence data.
	
				Parameters:
					recipient_jid - the message recipient's jid <JSJaCJID>
					body          - the message body
					body_html     - [optional] the message body in html
	
				Returns:
					True if request is successfully sent to server, false otherwise.
			*/
			send_message: function(recipient_jid, body, body_html) {
				if (this._super(recipient_jid, body, body_html)) {
	
					var entries = [];
	
					// try to find an email address in the body
					if (!this.presence.email) {
						var email_match = body.match(/\b[A-Z0-9\._%+\-]+@[A-Z0-9\.\-]+\.[A-Z]{2,4}\b/i);
						if (email_match) {
							this.presence.email = email_match[0];
							entries.push('email');
						}
					}
					
					// try to find an phone # in a message
					if (!this.presence.phone) {
						var phone_match = body.match(/\b(1\s*[\-\/\.]?)?(\((\d{3})\)|(\d{3}))\s*[\-\/\.]?\s*(\d{3})\s*[\-\/\.]?\s*(\d{4})\s*(([xX]|[eE][xX][tT])\.?\s*(\d+))*\b/);
						if (phone_match) {
							this.presence.phone = phone_match[0];
							entries.push('phone');
						}
					}
					
					var stimulus;
					if (entries.length > 0) {
						this.update_presence();
						stimulus = entries.join('_and_') + '_entry';
					}
					this.elicit_lead(stimulus);
	
					return true;
				}
				return false;
			},
	
			/*
				Method: create_session_log
	
				Creates a SmartChat.ChatSessionLog from all information collected from visitor.
	
				Returns:
					Newly created SmartChat.ChatSessionLog if sufficient information has been provided, null otherwise.
			*/
			create_session_log: function() {
				var duration = this.get_chat_duration(eCarList.App.SmartChat.SYSTEM_JID);
				var chat_log = this.get_chat_log(eCarList.App.SmartChat.SYSTEM_JID);
	
				// there shouldn't be a private_jid, but just in case
				if (this.private_jid) {
					duration += this.get_chat_duration(this.private_jid);
					chat_log = chat_log.concat(this.get_chat_log(this.private_jid));
				}
				
				var session_log = {
					dealer_eid: this.dealer_eid,
					dealer_id: this.dealer_id,
					domain: window.location.hostname,
					name: this.nick,
					email: this.presence.email || '',
					phone: this.presence.phone || '',
					duration: duration,
					history: this.presence.history,
					chat_log: chat_log
				};
				
				return session_log;
			},
	
			/*
				Method: validate_lead
	
				Verifies that a valid lead can be created for visitor.
	
				Returns:
					True if we have an email or phone number for the visitor, false otherwise.
			*/
			validate_lead: function() {
				return (this.presence.email || this.presence.phone);
			},
	
			/*
				Method: resume
			*/	
			resume: function() {
				if (!this._super()) {
					this.change_state(this.klass.State.IDLE);
					return false;
				}
				return true;
			},
			
			/*
				Method: _create_presence_packet
	
				Adds visitor presence information to prsence packet
			*/
			_create_presence_packet: function() {
				var presence = this._super();
				var smartchat_el = $('smartchat', presence.getNode()).get(0);
	
				// session info - dealer_eid, dealer_id, team
				smartchat_el.appendChild(presence.buildNode('session', {
					dealer_eid: this.dealer_eid,
					dealer_id: this.dealer_id,
					domain: window.location.hostname
				}));
	
				// profile info - email, phone
				if (this.presence.email || this.presence.phone) {
					smartchat_el.appendChild(presence.buildNode('profile', {
						email: this.presence.email || '',
						phone: this.presence.phone || ''
					}));
				}
	
				// navigation history
				var history_el = smartchat_el.appendChild(presence.buildNode('history'));
				if (this.presence.history) {
					for (var i = 0; i < this.presence.history.length; i++) {
						history_el.appendChild(presence.buildNode('location', this.presence.history[i]));
					}
				}
	
				// http referrer
				smartchat_el.appendChild(presence.buildNode('referrer', {
					url: this.presence.referrer || ''
				}));
				
				return presence;
			}
	
		}
	});

	var _init_handlers = function() {
		this.bind(Session.Event.CONNECT, this._on_connect);
		this.bind(Session.Event.INVITE_RECEIPT, this._on_invite_receipt);
		this.bind(Session.Event.MEMBER_ADD, this._on_member_add);
		this.bind(Session.Event.MEMBER_REMOVE, this._on_member_remove);
	};

	/*
		State: BaseState

		Defines state operations default implementations.
	*/
	var BaseState = {

		/*
			Method: get_dialogue

			Deafult dialogue is empty.
		*/
		get_dialogue: function() {
			return { };
		},

		/*
			Method: end_chat

			Cancels the active chat request/session.
		*/
		end_chat: function() {
			// disconnect & kill chat request
			delete this.team_jid;
			delete this.private_jid;
			this.disconnect();
			this.change_state(this.klass.State.OFFERED);
		},

		_on_connect: function(event) { },

		_on_invite_receipt: function(event, invite) {
			// by default decline any invites
			this.decline_invite_direct(invite.room_jid, invite.from_jid);
		},

		_on_member_add: function(event, member) { },
		_on_member_remove: function(event, member) { }
	};

	/*
		State: OfflineState

		Defines operations common among offline states (Idle, Offered, Preparing)
	*/
	var OfflineState = {

		/*
			Method: prepare_chat

			Prepares to request a new chat conversation with one of the specified teams.

			Parameters:
				teams - [optional] support teams

			Returns:
				True if we have a valid set of teams.
		*/
		prepare_chat: function(teams) {
			this.teams = teams || this.teams;
			if (this.teams && this.teams.length > 0) {
				this.change_state(this.klass.State.PREPARING);
				return true;
			}
			return false;
		},

		/*
			Method: request_chat

			Requests a new conversation with a member of the specified support team. A new connection will
			be established if one is not already available.

			Parameters:
				nick     - visitor nickname
				team_eid - support team eid
		*/
		request_chat: function(nick, team_eid) {
			this.nick = nick;
			this.team_eid = team_eid;

			// create connection if we're not already connected
			if (!this._connection || !this._connection.connected()) {
				this.change_state(this.klass.State.CONNECTING);
				this.connect();
			} else {
				this.change_state(this.klass.State.REQUESTING);
				this.contact_rep();
			}
		},

		/*
			Method: end_chat

			Reset to idle
		*/
		end_chat: function() {
			this.change_state(this.klass.State.OFFERED);
		}

	};

	/*
		State: Idle

		Visitors are initially idle. This state signifies that the user is not communicating with an admin
		and is not attempting to communicate with an admin.
	*/
	Visitor.VisitorSession.define_state(Visitor.VisitorSession.State.IDLE, BaseState, OfflineState, {

		/*
			Method: offer_chat

			Updates visitor state and fires offer event when after delay.

			Parameters:
				teams  - support teams
				delay - [optional] delay until chat is offered, no delay if undefined
		*/
		offer_chat: function(teams, delay) {
			// cancel any existing offer timeout
			if (this._offer_timeout) {
				clearTimeout(this._offer_timeout);
				this._offer_timeout = null;
			}

			var self = this;
			var offer = function() {
				self._offer_timeout = null;
				if (self.state === self.klass.State.IDLE) {
					self.teams = teams;
					self.trigger(self.klass.Event.CHAT_OFFER, teams);
					self.change_state(self.klass.State.OFFERED);
				}
			};

			if (delay) {
				this._offer_timeout = setTimeout(offer, delay);
			} else {
				offer();
			}
		}

	});

	/*
		State: Offered

		The visitor has been offered a chat 
	*/
	Visitor.VisitorSession.define_state(Visitor.VisitorSession.State.OFFERED, BaseState, OfflineState, {
		/*
			Method: offer_chat

			Chat has already been offered, just update team info

			Parameters:
				teams  - support teams
		*/
		offer_chat: function(teams) {
			this.teams = teams;
		}
	});

	/*
		State: Preparing

		The PREPARING state occurs after the user has indicated that he/she wants to chat. During this state
		we collect the visitor's name.
	*/
	Visitor.VisitorSession.define_state(Visitor.VisitorSession.State.PREPARING, BaseState, OfflineState);

	/*
		State: Connecting

		The Connecting state occurs while waiting for a new connection to be established.
	*/
	Visitor.VisitorSession.define_state(Visitor.VisitorSession.State.CONNECTING, BaseState, {

		_on_connect: function(event) {
			this.change_state(this.klass.State.REQUESTING);
			this.contact_rep();
		}

	});

	/*
		State: Requesting

		The Requesting state occurs after the user has requested a chat, but has yet to arrive in the
		support team room.
	*/
	Visitor.VisitorSession.define_state(Visitor.VisitorSession.State.REQUESTING, BaseState, {

		timeout:  20000, // 20 secs

		/*
			Method: contact_rep

			Joins team room to wait for help.
		*/
		contact_rep: function() {
			this.team_jid = this.join_team_room(this.team_eid);
		},

		_on_member_add: function(event, member) {
			if (member.jid.isEntity(this.jid) && member.room_jid.isEntity(this.team_jid)) {
				// arrived in team room, chat officially requested
				var room = this.get_room(member.room_jid);
				if (room.get_available_admins().length > 0) {
					// wait for admin
					this.change_state(this.klass.State.REQUESTED);
					this.trigger(this.klass.Event.CHAT_REQUEST, this.team_eid);
					this.elicit_lead('name_entry');
				} else {
					// no available admins
					this.change_state(this.klass.State.UNAVAILABLE);
					this.leave_room(this.team_jid);
					this.elicit_lead('admins_unavailable');
				}
			}
		},

		_on_state_timeout: function() {
			// switch to preparing & retry
			// NOTE: it may be better to throw up an error and let the user resolve
			this.change_state(this.klass.State.PREPARING);
			this.request_chat(this.nick, this.team_eid);
		},

		_on_reconnect: function() {
			// try contacting a rep again
			this.contact_rep();
		}

	});

	/*
		State: Requested

		The REQUESTED state occurs after the visitor has entered a support team room and is waiting to
		be invited to a private room.
	*/
	Visitor.VisitorSession.define_state(Visitor.VisitorSession.State.REQUESTED, BaseState, {

		timeout: Visitor.VisitorSession.CHAT_REQUEST_TIMEOUT,

		/*
			Method: end_chat

			When ending chat from the Requested state, the visitor is responsible for submitting their own lead.
		*/
		end_chat: function() {
			AdminSite.log_chat_session(this.create_session_log(), this.validate_lead());
			BaseState.end_chat.call(this);
		},

		/*
			Method: get_dialogue

			Requested state dialogue.
		*/
		get_dialogue: function() {
			var none_prompt = 'Someone will contact you as soon as possible. Feel free to continue browsing through our inventory.';
			return {
				name_entry: {
					response: 'Thanks $nick!',
					prompts: {
						email: 'While we contact a representative, please provide us with your email address so that we can send you a transcript of this chat session.',
						phone: 'While we contact a representative, please provide us with your phone number so we can have someone call you if necessary.',
						none: none_prompt
					}
				},
				email_entry: {
					response: 'Thanks!',
					prompts: {
						phone: 'We are still contacting a representative. To better serve you, can we have your phone number in case we get disconnected?',
						none: none_prompt
					}
				},
				phone_entry: {
					response: 'Thanks!',
					prompts: {
						email: 'While we wait, what is your email address?',
						none: none_prompt
					}
				},
				email_and_phone_entry: {
					response: 'Thanks!',
					prompts: {
						none: none_prompt
					}
				},
				visitor_exit: {
					prompts: {
						email: 'Before you close this chat, please provide us with your email address so that we may be of futher assistance.'
					}
				}
			};
		},

		_on_invite_receipt: function(event, invite) {
			// accept first invite
			this.change_state(this.klass.State.ACCEPTING);
			this.accept_private_room(invite.room_jid);
		},

		_on_state_timeout: function() {
			// timed out waiting for invite, go unavail
			this.change_state(this.klass.State.UNAVAILABLE);
			this.leave_room(this.team_jid);
			this.elicit_lead('admins_busy');
		},

		_on_reconnect: function() {
			// nothing to do
			// maybe i elicited a lead, maybe i didn't, we'll just wait to see if someone helps us
		}
		
	});

	/*
		State: Accepting

		The ACCEPTING state occurs after the visitor has received an invite to a private room and is attempting
		to join.
	*/
	Visitor.VisitorSession.define_state(Visitor.VisitorSession.State.ACCEPTING, BaseState, {

		accept_private_room: function(room_jid) {
			this.private_jid = this.join_room(room_jid);
		},

		_on_member_add: function(event, member) {
			if (member.jid.isEntity(this.jid) && member.room_jid.isEntity(this.private_jid)) {
				// arrived in private room, leave team room, chat officially accepted
				this.change_state(this.klass.State.ACTIVE);
				this.leave_room(this.team_jid);
				this.trigger(this.klass.Event.CHAT_ACCEPT);
			}
		},

		_on_reconnect: function() {
			// make sure we've attempted to join private room
			this.accept_private_room(this.private_jid);
		}
		
	});

	/*
		State: Active

		The ACTIVE state occurs once the visitor has arrived in the private chat room and is ready to be assisted.
	*/
	Visitor.VisitorSession.define_state(Visitor.VisitorSession.State.ACTIVE, BaseState, {
		
		get_admins: function() {
			var room = this.get_room(this.private_jid);
			return (room) ? room.get_admins() : [];
		},

		_on_reconnect: function() {
			// nothing to do
		}

	});

	/*
		State: Unavailable

		The Unavailable state occurs when there are no available admins for the visitor to communicate with.
		All communication responsiblity is handed over to "system".
	*/
	Visitor.VisitorSession.define_state(Visitor.VisitorSession.State.UNAVAILABLE, BaseState, {

		/*
			Method: get_dialogue

			Unavailable state dialogue.
		*/
		get_dialogue: function() {
			var default_prompts = {
				email: 'Please leave us your email address and we\'ll have a representative contact you as soon as possible.',
				phone: 'Please leave us your phone number and we\'ll have a representative contact you as soon as possible.',
				email_and_phone: 'Please leave us your email address and/or phone number and we\'ll have a representative contact you as soon as possible.',
				none: 'We\'ll have a representative contact you as soon as possible.'
			};

			var default_response = 'Thanks $nick, someone will contact you as soon as possible.';

			return {
				admins_busy: {
					response: 'Sorry $nick, all representatives are currently busy.',
					prompts: default_prompts
				},
				admins_unavailable: {
					response: 'Sorry $nick, there are currently no available representatives.',
					prompts: default_prompts
				},
				email_entry: {
					response: default_response
				},
				phone_entry: {
					response: default_response
				},
				email_and_phone_entry: {
					response: default_response
				},
				visitor_exit: {
					prompts: {
						email: 'Before you close this chat, please provide us with your email address so that we may be of futher assistance.'
					}
				}
			};
		},

		/*
			Method: end_chat

			When ending chat from the Unavailable state, the visitor is responsible for submitting their own lead.
		*/
		end_chat: function() {
			AdminSite.log_chat_session(this.create_session_log(), this.validate_lead());
			BaseState.end_chat.call(this);
		},

		_on_reconnect: function() {
			// nothing to do - we're at some point in the unavail dialogue
		}

	});

	/*
		State: FAULT
	*/
	Visitor.VisitorSession.extend_state(Visitor.VisitorSession.State.FAULT, {

		_on_connect: function() { }

	});
	
});
//include eCarList.App.SmartChat.ChatPanel
//include eCarList.App.SmartChat.Common
//include eCarList.App.SmartChat.Visitor.VisitorNameDialog
//include eCarList.App.SmartChat.Visitor.VisitorSession
//include eCarList.App.SmartChat.Window
eCarList.namespace('eCarList.App.SmartChat.Visitor', function(Visitor, $) {

	var ChatPanel = eCarList.App.SmartChat.ChatPanel,
		VisitorNameDialog = eCarList.App.SmartChat.Visitor.VisitorNameDialog,
		VisitorSession = eCarList.App.SmartChat.Visitor.VisitorSession;

	/*
		Class: eCarList.App.SmartChat.Visitor.VisitorChatWindow

		Properties:
	*/
	Visitor.VisitorChatWindow = eCarList.App.SmartChat.Window.extend({

		/*
			Attributes:
				base_href     - [optional] base href to use for chat window
				minimizable   - if true, minimize button included
				popup         - [optional] if true, this window is in a popup
				session       - visitor session <SmartChat.VisitorSession>
				title         - window title
				titlebar_icon - if true, display titlebar icon
				visible       - window visibility
				with_jid      - conversation participant jid <JSJaCJID>
		*/
		Attribute: {
			base_href: { },
			minimizable: { init: false },
			popup: { init: false },
			session: { required: true },
			style_class: { init: 'visitor_chat_window' },
			title: { init: 'SmartChat&trade;' },
			titlebar_icon: { init: true },
			visible: { init: true },
			with_jid: { init: null }
		},

		prototype: {

			/*
				Method: close

				Make sure we have all we need from visitor before they close.
			*/
			close: function() {
				var session = this._get('session');
				this._close_attempts = (this._close_attempts || 0) + 1;

				var close_now = function() {
					this.klass.base.close.call(this);
					session.end_chat();
				};

				if (this._get('minimized')) {
					this.maximize();
				}

				if (session.in_state(VisitorSession.State.ACTIVE) && session.get_admins().length) {
					// visitor leaving admin chat session with support personel
					this.confirm(function() {
						close_now.call(this);
					}, 'Are you sure you want to end chat?');
				} else if (session.in_state(VisitorSession.State.REQUESTED, VisitorSession.State.UNAVAILABLE)
						   && !session.validate_lead() && this._close_attempts < 2) {
					// visitor leaving system chat without giving us a lead, beg for it
					session.elicit_lead('visitor_exit');
				} else {
					// visitor leaving 
					close_now.call(this);
				}
			},

			show: function(animate) {
				this._super(animate);
				if (this._chat_panel) {
					this._chat_panel.focus();
				}
			},

			open_name_dialog: function() {
				this.open_dialog(new VisitorNameDialog({ session: this._get('session') }));
			},

			_render: function() {
				this._super();

				var session = this._get('session');
				this._chat_panel = this._render_child(this._body, new ChatPanel({
					base_href: this._get('base_href'),
					session: session,
					with_jid: this._get('with_jid') || eCarList.App.SmartChat.SYSTEM_JID
				}));

				// display name dialog if we're preparing and we don't have a nick yet
				if (session.in_state(VisitorSession.State.PREPARING)) {
					this.open_name_dialog();
				}

				this.bind_to(session, VisitorSession.Event.CONNECTION_FAULT, _on_connection_fault);
				this.bind_to(session, VisitorSession.Event.RECONNECT, _on_reconnect);
				this.bind_to(session, VisitorSession.Event.MEMBER_ADD, _on_members_change);
				this.bind_to(session, VisitorSession.Event.MEMBER_REMOVE, _on_members_change);
				this.bind_to(session, VisitorSession.Event.STATE_CHANGE, _on_visitor_state_change);
			}
		}

	});

	var _on_connection_fault = function(event, retry_delay) {
		var retry_secs = Math.ceil(retry_delay / 1000);
		this.display_message('Communication Error. Retrying in ' + retry_secs + ' secs.');
	};

	var _on_reconnect = function(event) {
		this.close_dialog();
	};

	var _on_members_change = function(event) {
		// only enable thie chat panel if there's someone to talk to
		var session = this._get('session');
		if (session.in_state(VisitorSession.State.ACTIVE)) {
			this._chat_panel.set_enabled(session.get_admins().length > 0);
		}
	};

	var _on_visitor_state_change = function(event, state) {
		// reset close attempts
		delete this._close_attempts;
		if (state === VisitorSession.State.CONNECTING) {
			this._chat_panel.add_status_message('connecting...');
		} else if (state === VisitorSession.State.ACTIVE) {
			var session = this._get('session');
			// switch to private room jid
			this._chat_panel.set_with_jid(session.private_jid);
			var room = session.get_room(session.private_jid);
			var admin = room.get_admins()[0];
			var messages = session.get_messages(session.private_jid);
			if (admin && messages.length <= 1) {
				// checking messages.length to avoid sending this message on reconnects
				this._chat_panel.add_status_message(admin.nick + ' has joined.');
			}
		}
	};

});
//include eCarList.Module
eCarList.namespace('eCarList.Util', function(Util, $) {

	/*
		Structure: eCarList.Util.ColorParser

		Utility for parsing all types of color values.

		Adaptation of RGBColor:
			URL: http://www.phpied.com/rgb-color-parser-in-javascript/
			Author: Stoyan Stefanov <sstoo@gmail.com>
			
	*/
	Util.ColorParser = eCarList.Module.extend({

		parse: function(color_string) {

			// strip any leading #
			if (color_string.charAt(0) === '#') { // remove # if any
				color_string = color_string.substr(1,6);
			}

			color_string = color_string.replace(/ /g,'');
			color_string = color_string.toLowerCase();

			// before getting into regexps, try simple matches
			// and overwrite the input
			var simple_colors = {
				aliceblue: 'f0f8ff',
				antiquewhite: 'faebd7',
				aqua: '00ffff',
				aquamarine: '7fffd4',
				azure: 'f0ffff',
				beige: 'f5f5dc',
				bisque: 'ffe4c4',
				black: '000000',
				blanchedalmond: 'ffebcd',
				blue: '0000ff',
				blueviolet: '8a2be2',
				brown: 'a52a2a',
				burlywood: 'deb887',
				cadetblue: '5f9ea0',
				chartreuse: '7fff00',
				chocolate: 'd2691e',
				coral: 'ff7f50',
				cornflowerblue: '6495ed',
				cornsilk: 'fff8dc',
				crimson: 'dc143c',
				cyan: '00ffff',
				darkblue: '00008b',
				darkcyan: '008b8b',
				darkgoldenrod: 'b8860b',
				darkgray: 'a9a9a9',
				darkgreen: '006400',
				darkkhaki: 'bdb76b',
				darkmagenta: '8b008b',
				darkolivegreen: '556b2f',
				darkorange: 'ff8c00',
				darkorchid: '9932cc',
				darkred: '8b0000',
				darksalmon: 'e9967a',
				darkseagreen: '8fbc8f',
				darkslateblue: '483d8b',
				darkslategray: '2f4f4f',
				darkturquoise: '00ced1',
				darkviolet: '9400d3',
				deeppink: 'ff1493',
				deepskyblue: '00bfff',
				dimgray: '696969',
				dodgerblue: '1e90ff',
				feldspar: 'd19275',
				firebrick: 'b22222',
				floralwhite: 'fffaf0',
				forestgreen: '228b22',
				fuchsia: 'ff00ff',
				gainsboro: 'dcdcdc',
				ghostwhite: 'f8f8ff',
				gold: 'ffd700',
				goldenrod: 'daa520',
				gray: '808080',
				green: '008000',
				greenyellow: 'adff2f',
				honeydew: 'f0fff0',
				hotpink: 'ff69b4',
				indianred : 'cd5c5c',
				indigo : '4b0082',
				ivory: 'fffff0',
				khaki: 'f0e68c',
				lavender: 'e6e6fa',
				lavenderblush: 'fff0f5',
				lawngreen: '7cfc00',
				lemonchiffon: 'fffacd',
				lightblue: 'add8e6',
				lightcoral: 'f08080',
				lightcyan: 'e0ffff',
				lightgoldenrodyellow: 'fafad2',
				lightgrey: 'd3d3d3',
				lightgreen: '90ee90',
				lightpink: 'ffb6c1',
				lightsalmon: 'ffa07a',
				lightseagreen: '20b2aa',
				lightskyblue: '87cefa',
				lightslateblue: '8470ff',
				lightslategray: '778899',
				lightsteelblue: 'b0c4de',
				lightyellow: 'ffffe0',
				lime: '00ff00',
				limegreen: '32cd32',
				linen: 'faf0e6',
				magenta: 'ff00ff',
				maroon: '800000',
				mediumaquamarine: '66cdaa',
				mediumblue: '0000cd',
				mediumorchid: 'ba55d3',
				mediumpurple: '9370d8',
				mediumseagreen: '3cb371',
				mediumslateblue: '7b68ee',
				mediumspringgreen: '00fa9a',
				mediumturquoise: '48d1cc',
				mediumvioletred: 'c71585',
				midnightblue: '191970',
				mintcream: 'f5fffa',
				mistyrose: 'ffe4e1',
				moccasin: 'ffe4b5',
				navajowhite: 'ffdead',
				navy: '000080',
				oldlace: 'fdf5e6',
				olive: '808000',
				olivedrab: '6b8e23',
				orange: 'ffa500',
				orangered: 'ff4500',
				orchid: 'da70d6',
				palegoldenrod: 'eee8aa',
				palegreen: '98fb98',
				paleturquoise: 'afeeee',
				palevioletred: 'd87093',
				papayawhip: 'ffefd5',
				peachpuff: 'ffdab9',
				peru: 'cd853f',
				pink: 'ffc0cb',
				plum: 'dda0dd',
				powderblue: 'b0e0e6',
				purple: '800080',
				red: 'ff0000',
				rosybrown: 'bc8f8f',
				royalblue: '4169e1',
				saddlebrown: '8b4513',
				salmon: 'fa8072',
				sandybrown: 'f4a460',
				seagreen: '2e8b57',
				seashell: 'fff5ee',
				sienna: 'a0522d',
				silver: 'c0c0c0',
				skyblue: '87ceeb',
				slateblue: '6a5acd',
				slategray: '708090',
				snow: 'fffafa',
				springgreen: '00ff7f',
				steelblue: '4682b4',
				tan: 'd2b48c',
				teal: '008080',
				thistle: 'd8bfd8',
				tomato: 'ff6347',
				turquoise: '40e0d0',
				violet: 'ee82ee',
				violetred: 'd02090',
				wheat: 'f5deb3',
				white: 'ffffff',
				whitesmoke: 'f5f5f5',
				yellow: 'ffff00',
				yellowgreen: '9acd32'
			};

			for (var key in simple_colors) {
				if (color_string === key) {
					color_string = simple_colors[key];
				}
			}
			// end of simple type-in colors

			// array of color definition objects
			var color_defs = [
				{
					re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
					example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],
					process: function (bits){
						return {
							red: parseInt(bits[1], 10),
							green: parseInt(bits[2], 10),
							blue: parseInt(bits[3], 10)
						};
					}
				},
				{
					re: /^(\w{2})(\w{2})(\w{2})$/,
					example: ['#00ff00', '336699'],
					process: function (bits){
						return {
							red: parseInt(bits[1], 16),
							green: parseInt(bits[2], 16),
							blue: parseInt(bits[3], 16)
						};
					}
				},
				{
					re: /^(\w{1})(\w{1})(\w{1})$/,
					example: ['#fb0', 'f0f'],
					process: function (bits){
						return {
							red: parseInt(bits[1] + bits[1], 16),
							green: parseInt(bits[2] + bits[2], 16),
							blue: parseInt(bits[3] + bits[3], 16)
						};
					}
				}
			];

			// search through the definitions to find a match
			var color = null;
			for (var i = 0; i < color_defs.length; i++) {
				var re = color_defs[i].re;
				var processor = color_defs[i].process;
				var bits = re.exec(color_string);
				if (bits) {
					color = processor(bits);
					break;
				}
			}

			if (color) {
				// validate/cleanup values
				color.red = (color.red < 0 || isNaN(color.red)) 
					? 0 : ((color.red > 255) ? 255 : color.red);
				color.green = (color.green < 0 || isNaN(color.green))
					? 0 : ((color.green > 255) ? 255 : color.green);
				color.blue = (color.blue < 0 || isNaN(color.blue))
					? 0 : ((color.blue > 255) ? 255 : color.blue);

				// rgb(#,#,#)
				color.rgb = 'rgb(' + color.red + ', ' + color.green + ', ' + color.blue + ')';
				
				// #RRGGBB
				color.hex = '#' + _hex(color.red) + _hex(color.green) + _hex(color.blue);
			}

			return color;
		}

	});

	var _hex = function(value) {
		var hex = value.toString(16);
		if (hex.length === 1) {
			hex = '0' + hex;
		}
		return hex;
	};

});
//include eCarList.Class
eCarList.namespace('eCarList.Util', function(Util, $) {

	/*
		Class: eCarList.Util.Theme

		Utility for allowing customers to define the set of colors used in a UI. Theme designers
		should extend this class and map style properties to css selectors for components in the
		 UI with the static Rule object.

		Rule: {
			chat_window: {
				border: {
					'.smartchat .chat_window': 'border-color'
				}, 
				background: {
					'.smartchat .window .header': 'background-color',
					'.smartchat .window .footer': [ 'background-color', 'border-color' ],
					'.smartchat .window .controls: Theme.url_property('background-image')
					}
				}
			}
		}
	*/
	Util.Theme = eCarList.Class.extend({

		/*
			UI rule descriptors.
		*/
		Rule: { },

		/*
			Function: url_property

			Creates a style declaration function that wraps the given style value with 'url()'.

			Parameters:
				property - name of the url'd property

			Returns:
				style declaration function
		*/
		url_property: function(property) {
			return function(url) {
				var properties = { };
				properties[property] = 'url("' + url + '")';
				return properties;
			};
		},

		prototype: {

			/*
				Method: apply

				Applies the given style using the Theme rules defined by this theme. The given style
				object can make global style declarations and ui-specific style declarations.

				Example:
					style = {
						background: '#9C6',   // global style declaration
						text: '#360',         // global style declaration
						
						chat_window: {
							background: '#FFF',  // chat_window style declaration (overrides global)
							border: '#360'       // chat_window style declaration
						}
					}

				Parameters:
					style - user-defined style
			*/
			apply: function(style) {
				eCarList.assert(style && typeof style === 'object', 'invalid style object: ' + style);
				var css_rules = [];

				_each_property_rule.call(this, function(ui, property, rule) {
					var property_style = _get_property_style.call(this, style, ui, property);
					if (property_style) {
						for (var selector in rule) {
							if (rule.hasOwnProperty(selector)) {
								css_rules.push(_apply_rule.call(this, selector, rule[selector], property_style));
							}
						}
					}
				});

				// add rules to dom
				var html = '<style>\n' + css_rules.join('\n') + '\n</style>';
				$('head').append(html);
			}
		}

	});

	var _each_property_rule = function(fn) {
		for (var ui in this.klass.Rule) {
			if (this.klass.Rule.hasOwnProperty(ui)) {
				var ui_rules = this.klass.Rule[ui];
				for (var property in ui_rules) {
					if (ui_rules.hasOwnProperty(property)) {
						var property_rule = ui_rules[property];
						fn.call(this, ui, property, property_rule);
					}
				}
			}
		}
	};

	var _get_property_style = function(style, ui, property) {
		if (style[ui] && style[ui][property]) {
			// found ui-specific style
			return style[ui][property];
		} else if (style[property]) {
			// found global style
			return style[property];
		}
		// no style
		return null;
	};

	var _apply_rule = function(selector, declaration, style) {
		var definitions = [];
		if ($.isFunction(declaration)) {
			var properties = declaration(style);
			for (var attr in properties) {
				if (properties.hasOwnProperty(attr)) {
					definitions.push(attr + ': ' + properties[attr] + ';');
				}
			}
		} else if ($.isArray(declaration)) {
			for (var i = 0; i < declaration.length; i++) {
				definitions.push(declaration[i] + ': ' + style + ';');
			}
		} else if (typeof declaration === 'string') {
			definitions.push(declaration + ': ' + style + ';');
		}

		// convert property map into a css declaration
		return selector + ' {\n' + definitions.join('\n') + '\n}';
	};

});
//include eCarList.Util.ColorParser
//include eCarList.Util.Theme
eCarList.namespace('eCarList.App.SmartChat.Visitor', function(Visitor, $) {

	var ColorParser = eCarList.Util.ColorParser,
		Theme = eCarList.Util.Theme;

	var _button_property = function(color) {
		var properties = { };
		if (typeof color === 'object') {
			if (color.background) {
				properties['background-color'] = color.background;
				properties['border-color'] = color.background;
			}
			if (color.text) {
				properties['color'] = color.text;
			}
		} else if (typeof color === 'string') {
			properties['background-color'] = color;
			properties['border-color'] = color;
			properties['color'] = _use_white(color) ? '#FFF' : '#000';
		}
		return properties;
	};

	/*
		Class: eCarList.App.SmartChat.Visitor.VisitorTheme
	*/
	Visitor.VisitorTheme = eCarList.Util.Theme.extend({

		Rule: {

			offer: {
				background: {
					'.smartchat .offer_window': 'background-color',
					'.smartchat .offer_window button.minmax_btn': function(color) {
						var y = _use_white(color) ? -10 : 0;
						return { 'background-position': '-46px ' + y + 'px' };
					},
					'.smartchat .offer_window button.minmax_btn.hover': function(color) {
						var y = _use_white(color) ? -10 : 0;
						return { 'background-position': '-34px ' + y + 'px' };
					}
				},
				text: {
					'.smartchat .offer_window': 'color'
				},
				image: {
					'.smartchat .offer_window .admin_img': Theme.url_property('background-image')
				},
				button: {
					'.smartchat .offer_window .chat_btn': _button_property
				}
			},
			
			tab: {
				background: {
					'.smartchat .offer_window.minimized .titlebar': 'background-color'
				},
				text: {
					'.smartchat .offer_window.minimized .titlebar .title': 'color'
				},
				image: {
					'.smartchat .offer_window.minimized .titlebar .icon': Theme.url_property('background-image')
				}
			},

			chat: {
				border: {
					'.smartchat .visitor_chat_window': 'border-color',
					'.smartchat .visitor_chat_window .titlebar': 'border-bottom-color'
				},
				background: {
					'.smartchat .visitor_chat_window .titlebar': 'background-color',
					'.smartchat .visitor_chat_window .titlebar .icon': function(color) {
						var y = _use_white(color) ? -15 : 0;
						return { 'background-position': '0px ' + y + 'px' };
					},
					'.smartchat .visitor_chat_window .titlebar button.close_btn': function(color) {
						var y = _use_white(color) ? -10 : 0;
						return { 'background-position': '-46px ' + y + 'px' };
					},
					'.smartchat .visitor_chat_window .titlebar button.close_btn.hover': function(color) {
						var y = _use_white(color) ? -10 : 0;
						return { 'background-position': '-34px ' + y + 'px' };
					},
					'.smartchat .visitor_chat_window .titlebar button.minmax_btn': function(color) {
						var y = _use_white(color) ? -10 : 0;
						return { 'background-position': '-19px ' + y + 'px' };
					},
					'.smartchat .visitor_chat_window .titlebar button.minmax_btn.hover': function(color) {
						var y = _use_white(color) ? -10 : 0;
						return { 'background-position': '-2px ' + y + 'px' };
					}
				},
				text: {
					'.smartchat .visitor_chat_window .titlebar .title': 'color'
				},
				button: {
					'.smartchat .visitor_chat_window button': _button_property
				}
			}
		}

	});

	// decide to use white or black based on the color's intensity
	var _use_white = function(color_string) {
		var color = ColorParser.parse(color_string);
		if (color) {
			var intensity = color.red + color.green + color.blue;
			if (intensity < 380) {
				return true;
			}
		}
		return false;
	};

});
//include eCarList.App.SmartChat.ChatService
//include eCarList.App.SmartChat.Common
//include eCarList.App.SmartChat.ConsoleBase
//include eCarList.App.SmartChat.Visitor.OfferWindow
//include eCarList.App.SmartChat.Session
//include eCarList.App.SmartChat.Visitor.VisitorChatWindow
//include eCarList.App.SmartChat.Visitor.VisitorSession
//include eCarList.App.SmartChat.Visitor.VisitorTheme
//include eCarList.App.SmartChat.Window

/*
	Function: SmartChat.load

	Parameters:
		dealer_eid  - site dealer_eid
		dealer_id   - site dealer_id
		page_config - page configuration
*/
eCarList.App.SmartChat.load = function(dealer_eid, dealer_id, page_config) {
	var console = new eCarList.App.SmartChat.Visitor.VisitorConsole({
		dealer_eid: dealer_eid,
		dealer_id: dealer_id,
		page_config: page_config
	});
};

eCarList.namespace('eCarList.App.SmartChat.Visitor', function(Visitor, $) {

	var ChatService = eCarList.App.SmartChat.ChatService,
		OfferWindow = eCarList.App.SmartChat.Visitor.OfferWindow,
		Session = eCarList.App.SmartChat.Session,
		Show = eCarList.App.SmartChat.Show,
		SmartChat = eCarList.App.SmartChat,
		VisitorChatWindow = eCarList.App.SmartChat.Visitor.VisitorChatWindow,
		VisitorSession = eCarList.App.SmartChat.Visitor.VisitorSession,
		VisitorTheme = eCarList.App.SmartChat.Visitor.VisitorTheme,
		Window = eCarList.App.SmartChat.Window;

	/*
		Class: eCarList.App.SmartChat.VisitorConsole
	
		Root container for visitor UI.
	*/
	Visitor.VisitorConsole = eCarList.App.SmartChat.ConsoleBase.extend({
		
		/*
			Attributes:
				delaer_eid  - dealer_eid
				dealer_id   - dealer id
				page_config - page configuration
		*/
		Attribute: {
			dealer_eid: { required: true },
			dealer_id: { required: true },
			page_config: { }
		},

		prototype: {
			init: function(config) {
				this._super(config);
				this._id = 'lol_vc_20100207';
				this._init_date = new Date();

				// session must be created before base.load
				this._session = new VisitorSession({
					dealer_eid: this._get('dealer_eid'),
					dealer_id: this._get('dealer_id')
				});
			},

			can_pop_out: function() {
				//if (VisitorConsole.base.can_pop_out.call(this)) {
				//	return !this._session.in_state(VisitorSession.State.IDLE, VisitorSession.State.UNAVAILABLE);
				//}
				return false;
			},

			pop_out: function() {
				// make sure chat window is up
				_open_chat_window.call(this);
				if (this._super()) {
					_close_all_windows.call(this); // necessary?
					return true;
				}
				return false;
			},

			_get_popup_attributes: function() {
				return $.extend(this._super(), {
					width: '300',
					height: '300'
				});
			},

			_get_popup_html: function() {
				var base_href = window.location.protocol + '//' + window.location.hostname;
				return '			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">			<html>				<head>					<title>SmartChat</title>					<link rel="stylesheet" type="text/css" href="' + SmartChat.CSS_BASE + '/smartchat.css"/>					<script type="text/javascript" src="' + SmartChat.JS_BASE + '/jquery-1.3.2.js"></script>					<script type="text/javascript" src="' + SmartChat.JS_BASE + '/jsjac.uncompressed.js"></script>					<script type="text/javascript" src="' + SmartChat.JS_BASE + '/soundmanager2.js"></script>					<script type="text/javascript" src="' + SmartChat.JS_BASE + '/smartchat-common.js"></script>					<script type="text/javascript" src="' + SmartChat.JS_BASE + '/smartchat-session.js"></script>					<script type="text/javascript" src="' + SmartChat.JS_BASE + '/smartchat.js"></script>					<script type="text/javascript">						var console = new SmartChat.VisitorConsole({ dealer_eid: \'' + this._get('dealer_eid') + '\', dealer_id: \'' + this._get('dealer_id') + '\', _popup: true, _base_href: \'' + base_href + '\' });					</script>				</head>				<body class="smartchat_popup"></body>			</html>			';
			},

			dispose: function() {
				this._super();
				this._session.dispose();
			},
			
			load: function(window, memento) {
				var page_config = this._get('page_config');
				
				// load theme
				if (page_config && page_config.style) {
					var theme = new VisitorTheme();
					theme.apply(page_config.style);
				}

				if (this._super(window, memento)) {
					if (this._session.in_state(VisitorSession.State.IDLE, VisitorSession.State.OFFERED)) {
						this._session.disconnect();
						// retrieve page configuration from server
						//_configure_page.call(this);
						if (page_config) {
							this._session.offer_chat(page_config.teams, page_config.offer_delay);
						}
					}

					// update location after receiving initial presence
					this._session.with_presence(function() {
						this.update_location();
					});

					var self = this;
					$(window).bind('blur', function(event) { _on_focus_change.call(self, event, false); });
					$(window).bind('focus', function(event) { _on_focus_change.call(self, event, true); });
					
					// init smartchat buttons
					_init_buttons.call(this);
					return true;
				}
				return false;
			},
			
			_suspend: function(unload) {
				if (this._session.state === VisitorSession.State.IDLE) {
					_close_all_windows.call(this);
				} else if (this._session.state === VisitorSession.State.UNAVAILABLE) {
					// visitor talkin to system, end chat if we have enough for a lead
					if (this._session.validate_lead()) {
						_close_all_windows.call(this); // closing window will force end_chat()
					}
				}
				this._super(unload);
				this._session.suspend();
			},

			_resume: function() {
				this._super();
				this._session.resume();
			},
			
			_exit: function(unload) {
				this._super(unload);
				
				if (this._session.state === VisitorSession.State.ACTIVE) {
					if (this._popup) {
						// user is unloading popup, beg them to stay
						unload.message = 'You are currently in a conversation, if you close this window your conversation will be terminated. Click "Cancel" to continue your conversation.';
					} else if (!this.pop_out()) {
						// failed to auto popout chat, suggest they popout before leaving
						unload.message = 'Your current conversation will be terminated if you leave this page without popping out the chat window. Click "Cancel" and click on the pop-out (^) button on the "Chat" window before leaving this page.';
					}
				} else {
					// user is doing nothing or patiently waiting for on a chat request
					// kill everything, pretend we never met this user
					this._session.end_chat();
					_close_all_windows.call(this);
				}
			},
			
			_render: function() {
				this._super();
				this.bind_to(this._session, Session.Event.MESSAGE_ADD, _on_message_add);
				this.bind_to(this._session, VisitorSession.Event.STATE_CHANGE, _on_visitor_state_change);
			}

		}

	})

	.dont_save(/^_/)
	.save_as_session('_session')

	.save_as('_offer_window', function(memento, attr, _offer_window) {
		if (_offer_window) {
			memento._offer_window = (_offer_window.get_minimized()) ? 'min' : 'max';
		}
	})

	.restore_as('_offer_window', function(origin, attr, _offer_window) {
		_open_offer_window.call(this, _offer_window === 'min', false);
	})

	.save_as('_chat_window', function(memento, attr, _chat_window) {
		if (_chat_window) {
			memento._chat_window = (_chat_window.get_minimized()) ? 'min' : 'max';
		}
	})

	.restore_as('_chat_window', function(origin, attr, _chat_window) {
		_open_chat_window.call(this, _chat_window === 'min');
	});

	var _init_buttons = function() {
		var self = this;
		$('button.smartchat').text('Need Help?').show().click(function(event) {
			_on_chat_btn_click.call(self, event);
		});
		$('.smartchat_button').click(function(event) {
			_on_chat_btn_click.call(self, event);
			return false;
		});
	};

	/*
	var _configure_page = function() {
		ChatService.configure_page(window.location.pathname, function(page_config) {
			if (this._session.in_state(VisitorSession.State.IDLE, VisitorSession.State.OFFERED)) {
				this._session.offer_chat(page_config.teams, page_config.offer_delay);
			}
		}, this);
	};
	*/

	var _open_offer_window = function(minimized, animate) {
		// close chat window if it's open (i don't know why it would be open)
		if (this._chat_window) {
			this._chat_window.close();
		}
		
		// render & open offer window
		if (!this._offer_window) {
			this._offer_window = this._render_child(this._root, new OfferWindow({
				session: this._session,
				minimized: minimized,
				visible: false
			}));
			this.bind_to(this._offer_window, Window.Event.CLOSE, _on_offer_window_close);
		}
		this._offer_window.show(animate);

		return this._offer_window;
	};

	var _open_chat_window = function(minimized) {
		// close offer window if it's open
		if (this._offer_window) {
			this._offer_window.close();
		}

		// render & open chat window
		if (!this._chat_window) {
			this._chat_window = this._render_child(this._root, new VisitorChatWindow({ 
				session: this._session,
				with_jid: this._session.private_jid,
				closeable: !this._popup,
				minimizable: !this._popup,
				minimized: minimized || false,
				popable: this.can_pop_out(),
				popup: this._popup,
				base_href: this._base_href
			}));
			this.bind_to(this._chat_window, Window.Event.CLOSE, _on_chat_window_close);
			this.bind_to(this._chat_window, Window.Event.POP_OUT, _on_chat_window_pop_out);
			this.bind_to(this._chat_window, Window.Event.DEBUG, _on_debug);
		}

		if (!minimized) {
			this._chat_window.maximize();
		}

		return this._chat_window;
	};

	var _close_all_windows = function() {
		if (this._popup) {
			window.close();
		} else {
			if (this._offer_window) {
				this._offer_window.close();
			}

			if (this._chat_window) {
				this._chat_window.close();
			}
		}
	};

	var _on_debug = function(event) {
		this.report_bug();
	};

	var _on_focus_change = function(event, focus) {
		var show = (focus) ? Show.NONE : Show.AWAY;
		if (this._session.available && this._session.presence.show !== show) {
			this._session.update_presence(show);
		}
	};

	var _on_chat_btn_click = function(event) {
		if (this._popup_window) {
			// focus popup
			this._popup_window.focus();
		} else if (this._session.in_state(VisitorSession.State.IDLE, VisitorSession.State.OFFERED)) {
			// prepare chat
			this._session.prepare_chat();
		} else {
			// open chat window
			_open_chat_window.call(this);
		}
	};

	var _on_visitor_state_change = function(event, state) {
		switch(state) {
		case VisitorSession.State.OFFERED:
			_open_offer_window.call(this, false, true);
			break;
		case VisitorSession.State.PREPARING:
		case VisitorSession.State.CONNECTING:
		case VisitorSession.State.REQUESTING:
		case VisitorSession.State.ACTIVE:
			_open_chat_window.call(this);
			break;
		}

		// update popability
		if (this._chat_window) {
			this._chat_window.set_popable(this.can_pop_out());
		}
	};
	
	var _on_offer_window_close = function(event, window) {
		this._dispose_child(this._offer_window);
		this._offer_window = null;
	};

	var _on_chat_window_close = function(event, window) {
		this._dispose_child(this._chat_window);
		this._chat_window = null;
		_open_offer_window.call(this).minimize();
	};

	var _on_chat_window_pop_out = function(event, window) {
		this._dispose_child(this._chat_window);
		this.pop_out();
	};

	var _on_message_add = function(event, message) {
		// open window if message date is after console init date
		if (this._session.state === VisitorSession.State.ACTIVE
			&& this._init_date.getTime() < message.date.getTime()) {
			_open_chat_window.call(this);
		}
	};

});
