/** 
 * @projectDescription	The Deeppurple library helps Adobe collect metrics and perform multivariate experiments
 * @namespace adobe.Deeppurple
 * @author Chris Lechner clechner@adobe.com
 */

// This code must be run *after* the adobe namespace was initialized
if (!adobe) abort();

/**
 * Class: adobe.Deeppurple
 */
adobe.Deeppurple = Class.create({
	/**
	 * @classDescription Adds a method to adobe.Cookie
	 * @type {Object}
	 * @constructor
	 */
	initialize: function() {
		// Temporarily extend adobe.Cookie
		adobe.Cookie = Class.create(adobe.Cookie,{});
		
		// Add methods
		adobe.Cookie.addMethods({
			isSupported: function() {
				if (this.cookiesupport != undefined) return this.cookiesupport;
				adobe.Cookie.set("cookietest",true);
				this.cookiesupport = adobe.Cookie.get("cookietest") ? true : false;
				adobe.Cookie.remove("cookietest");
				return this.cookiesupport;
			}
		});
		
		// Replace the original instance
		adobe.Cookie = new adobe.Cookie();
	}
});

// Initialize the base deeppurple object
adobe.Deeppurple = new adobe.Deeppurple();

/**
 * Class: adobe.Deeppurple.ABTest
 */
adobe.Deeppurple.ABTest = Class.create({
	/**
	 * @classDescription Facilitates multivariate experimentation
	 * @param {Integer} The current time as UTC integer (POSIX)
	 * @param {String} The name of this AB experiment
	 * @param {Object} [rules] Override the default AB testing constraints, if applicable
	 * @type {Object}
	 * @constructor
	 */
	initialize: function(now,project,rules) {
		// Internal properties
		this.now = now;
		this.project = project;
		this.valid = undefined;
		this.id = false;
		
		// Test constraints
		this.rules = $H({
			requirecookies: false,
			start: 1243580400,	// TODO: May 29th 2009 00:00:00
			stop: 1243666799,	// TODO: May 29th 2009 23:59:59
			segments: $H({
				A: 0.9,
				B: 0.1
			})
		});
	},
	
	/**
	 * Executes the test
	 * @return {Object} Returns instance of ABTest
	 */
	run: function() {
		if (this.isValid()) this.segment();
		return this;
	},
	
	/**
	 * Compares the supplied "now" timestamp to the start and stop range.
	 * 
	 * @return {Boolean} Returns true if the test should be run
	 */
	isValid: function() {
		if (this.rules.get('requirecookies') && !adobe.Cookie.isSupported()) return false;
		if (this.valid == undefined) this.valid = (this.now >= this.rules.get('start') && this.now < this.rules.get('stop')) ? true : false;
		return this.valid;
	},
	
	/**
	 * Returns test segment for this user
	 * 
	 * Segment weights are percentages of the total user base and can be turned into a structure
	 * that indexes each test by the space it should occupy in probabilty.  The methodology
	 * consists of generating a random number between 0 and 1, then iteratively comparing it
	 * to the boundaries of the index.
	 * 
	 * @param {String} [id] Override the segmentation behavior, if desired
	 * @return {String} The segment which this user falls into
	 */
	segment: function(id) {
		// This test must be valid in order to split up the users
		if (!this.isValid()) return false;
		
		// Override the test segmentation behavior
		if (id != undefined && this.rules.get('segments').keys().indexOf(id) > 0) this.id = id;
		
		// Try to get the segment id from the cookie
		if (adobe.Cookie.isSupported()) {
			if (id = adobe.Cookie.get(this.project + 'Segment')) this.id = id;
		}
		
		// Return the segment id if it was previously set
		if (this.id != false) return this.id;
		
		// Get a random number
		var sample = Math.random();
		var pointer = 0;
		var index = $H({});
		
		// Build the index
		this.rules.get('segments').each(function(pair) {
			if (pair.value <= 0) return;
			index.set(pair.key, $H({ start: pointer, stop: pointer + pair.value }));
			pointer += pair.value;
		});
		
		// Now test the random sample
		this.rules.get('segments').each(function(pair) {
			if (pair.value <= 0) return;
			if (sample >= index.get(pair.key).get('start') && sample < index.get(pair.key).get('stop')) {
				this.id = pair.key;
				$break;
			}
		}.bind(this));
		
		// Set a cookie so we can skip this test later
		adobe.Cookie.set(this.project + 'Segment',this.id,false,'/','.adobe.com');
		
		// Return false if we still don't have a test segment id
		return this.id;
	},
	
	/**
	 * Generates fire() closures based on test conditions
	 * @param {String} Condition name
	 * @param {Object} Hash of arguments from the particular alias
	 */
	eventHelper: function(condition, args) {
		var condition = condition;
		var eventName = 'ABTest:' + this.project + ':' + condition;
		var closure = { fire: function() {} };
		var segment = this.segment();
		var valid = this.isValid();
		var fire = false;
		var memo = { valid: valid, condition: condition, segment: segment };
		
		// Validate segment id input and append segment to event name
		if (condition.indexOf('Equals') > 0) {
			if (args.segment == undefined || this.rules.get('segments').keys().indexOf(args.segment) < 0) return closure;
			eventName += ':' + args.segment;
		}
		
		// Execute the conditions and update the closure function as needed
		if (condition == 'onValid' && valid) fire = true; else
		if (condition == 'onNotValid' && !valid) fire = true; else
		if (condition == 'onEqualsSegment' && valid && segment == args.segment) fire = true; else
		if (condition == 'onNotEqualsSegment' && valid && segment != args.segment) fire = true;
		
		// Override the closure if the condition was true
		if (fire) closure.fire = function(name) {
			if (name != undefined) eventName = name;
			document.fire(eventName, memo);
		}.bind(this);
		
		// Return the closure
		return closure;
	},
	
	/**
	 * Returns the closure from eventHelper()
	 * @see eventHelper
	 * @return {Object} a fireable closure
	 */
	onValid: function() { return this.eventHelper('onValid', {}) },
	
	/**
	 * Returns the closure from eventHelper()
	 * @see eventHelper
	 * @return {Object} a fireable closure
	 */
	onNotValid: function() { return this.eventHelper('onNotValid', {}) },
	
	/**
	 * Returns the closure from eventHelper()
	 * @see eventHelper
	 * @return {Object} a fireable closure
	 */
	onEqualsSegment: function(id) { return this.eventHelper('onEqualsSegment', { segment: id }) },
	
	/**
	 * Returns the closure from eventHelper()
	 * @see eventHelper
	 * @return {Object} a fireable closure
	 */
	onNotEqualsSegment: function(id) { return this.eventHelper('onNotEqualsSegment', { segment: id }) }
});


/**
 * Class: adobe.Deeppurple.Event
 */
adobe.Deeppurple.Event = Class.create({
	/**
	 * @classDescription Represents an event in deeppurple
	 * @param {String} The name of this event
	 * @param {String} The URI which pings should be sent to
	 * @type {Object}
	 * @constructor
	 */
	initialize: function(project,name) {
		// Internal properties
		this.project = project;
		this.name = name;
		this.base64 = new Base64;
		this.md5 = MD5;
		
		// Define properties and internal field methods
		this.properties = $H({ currentEvent: this.name });
	},
		
	/**
	 * Collects event properties.  The 'properties' argument should be a hash where each key and value is either a property name and value or a method name and argument. 
	 * 
	 * @param {Object} A hash of the property names and values to be executed
	 * @return void
	 */
	run: function(properties) {
		// Iterate over properties and collect values
		properties.each(function(pair) {
			this[pair.key] ? this.properties.set(pair.key,this[pair.key](pair.value)) : this.properties.set(pair.key,pair.value);
		}.bind(this));
	},
	
	/**
	 * Saves applicable properties to the previous coookie and send the current properties as a ping
	 * @return void
	 */
	send: function() {
		if (adobe.Cookie.isSupported()) adobe.Cookie.set(this.project + 'Previous',this.serialize(),false,'/','.adobe.com');
		$$('body').each(function(element) { element.insert(new Element('img',{ id: this.name + 'Ping', height: 0, width: 0, src: this.toURL() },'DOM'))}.bind(this));
	},
	
	/**
	 * Generates unique session id's and assigns them to a cookie.  If cookies are not enabled, returns "nocookies"
	 * @return string
	 */
	session: function(prefix) {
		var session = false;
		var cookie = this.project + 'Session';
		
		// Try to get the session id from cookie
		if (!adobe.Cookie.isSupported()) return 'nocookies';
		if (session = adobe.Cookie.get(cookie)) return session;
		
		// Generate a new session
		session = MD5.hex(this.now + ':' + Math.random() + ':' + Math.random() + ':' + Math.random());
		if (prefix != undefined) session = prefix + ':' + session;
		
		// Set the cookie
		adobe.Cookie.set(cookie,session,false,'/','.adobe.com');
		this.properties.set("newsession",true);
		return session;
	},
	
	/**
	 * @return {String} a base64 encoded JSON string representing the current properties
	 */
	serialize: function() {
		// Remove the previous data to prevent the cookie from getting too large
		var props = this.properties.clone();
		if (props.get('previous')) props.unset('previous');
		
		// base64 encode the properties as a JSON string
		return this.base64.encode($H(props).toJSON());
	},
	
	/**
	 * @param {Object} a base64 encoded JSON string
	 * @return {Object} the object resulting from deserialzing a base64 encoded JSON string
	 */
	deserialize: function(input) {
		try {
			return $H(this.base64.decode(input).evalJSON());
		} catch(error) {
			return {};
		}
	},
	
	/**
	 * @return {String} the ping URL encoded with local properties
	 */
	toURL: function() {
		// set the URL
		var url = this.base64.decode('aHR0cDovL3d3dy5hZG9iZS5jb20vZGVlcHB1cnBsZS9GbGFzaEFCLmdpZj8=');
	
		// join the properties
		this.properties.each(function(pair) {
			if (pair.value != undefined) url += pair.key + "=" + pair.value + "&";
		});
		
		// remove the trailing ampersand
		return url.slice(0,-1);
	},
	
	/**
	 * Resets local parameters
	 * @return	void
	 */
	reset: function() {
		this.properties = $H({});
	}
});

/**
 *  Base64 encode / decode
 *  http://www.webtoolkit.info/
 */
var Base64 = Class.create({
 	initialize: function() {},
	
	// private property
	_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
 
	// public method for encoding
	encode : function (input) {
		var output = "";
		var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
		var i = 0;
 
		input = this._utf8_encode(input);
 
		while (i < input.length) {
 
			chr1 = input.charCodeAt(i++);
			chr2 = input.charCodeAt(i++);
			chr3 = input.charCodeAt(i++);
 
			enc1 = chr1 >> 2;
			enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
			enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
			enc4 = chr3 & 63;
 
			if (isNaN(chr2)) {
				enc3 = enc4 = 64;
			} else if (isNaN(chr3)) {
				enc4 = 64;
			}
 
			output = output +
			this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
			this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
 
		}
 
		return output;
	},
 
	// public method for decoding
	decode : function (input) {
		var output = "";
		var chr1, chr2, chr3;
		var enc1, enc2, enc3, enc4;
		var i = 0;
 
 		if (input == undefined) return {};
		
		input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
 
		while (i < input.length) {
 
			enc1 = this._keyStr.indexOf(input.charAt(i++));
			enc2 = this._keyStr.indexOf(input.charAt(i++));
			enc3 = this._keyStr.indexOf(input.charAt(i++));
			enc4 = this._keyStr.indexOf(input.charAt(i++));
 
			chr1 = (enc1 << 2) | (enc2 >> 4);
			chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
			chr3 = ((enc3 & 3) << 6) | enc4;
 
			output = output + String.fromCharCode(chr1);
 
			if (enc3 != 64) {
				output = output + String.fromCharCode(chr2);
			}
			if (enc4 != 64) {
				output = output + String.fromCharCode(chr3);
			}
 
		}
 
		output = this._utf8_decode(output);
 
		return output;
 
	},
 
	// private method for UTF-8 encoding
	_utf8_encode : function (string) {
		string = string.replace(/\r\n/g,"\n");
		var utftext = "";
 
		for (var n = 0; n < string.length; n++) {
 
			var c = string.charCodeAt(n);
 
			if (c < 128) {
				utftext += String.fromCharCode(c);
			}
			else if((c > 127) && (c < 2048)) {
				utftext += String.fromCharCode((c >> 6) | 192);
				utftext += String.fromCharCode((c & 63) | 128);
			}
			else {
				utftext += String.fromCharCode((c >> 12) | 224);
				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
				utftext += String.fromCharCode((c & 63) | 128);
			}
 
		}
 
		return utftext;
	},
 
	// private method for UTF-8 decoding
	_utf8_decode : function (utftext) {
		var string = "";
		var i = 0;
		var c = c1 = c2 = 0;
 
		while ( i < utftext.length ) {
 
			c = utftext.charCodeAt(i);
 
			if (c < 128) {
				string += String.fromCharCode(c);
				i++;
			}
			else if((c > 191) && (c < 224)) {
				c2 = utftext.charCodeAt(i+1);
				string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
				i += 2;
			}
			else {
				c2 = utftext.charCodeAt(i+1);
				c3 = utftext.charCodeAt(i+2);
				string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
				i += 3;
			}
 
		}
 
		return 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.
 */

var MD5 = (function() {
/*
 * 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 [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 * MD5.chrsz);

  var ipad = [], opad = [];
  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 * MD5.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 = [], chrsz = MD5.chrsz;
  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 = "", chrsz = MD5.chrsz;
  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 = MD5.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 += MD5.b64pad;
      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
    }
  }
  return str;
}

  return {
/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
    hexcase: 0, // hex output format. 0 - lowercase; 1 - uppercase
    b64pad: "", // base-64 pad character. "=" for strict RFC compliance
    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
 */
    hex:function( s ) { 
      return binl2hex( core_md5( str2binl( s ), s.length * MD5.chrsz ) );
    },

    base64:function( s ) {
      return binl2b64( core_md5( str2binl( s ), s.length * MD5.chrsz ) );
    },

    string:function( s ) {
      return binl2str( core_md5( str2binl( s ), s.length * MD5.chrsz ) );
    },

    hmac:{
      hex:function( key, data ) {
        return binl2hex( core_hmac_md5( key, data ) );
      },

      base64:function( key, data ) {
        return binl2b64( core_hmac_md5( key, data ) );
      },

      string:function( key, data ) {
        return binl2str( core_hmac_md5( key, data ) );
      }
    },

    test:function() { // Perform a simple self-test to see if the VM is working
      return this.hex("abc") == "900150983cd24fb0d6963f7d28e17f72";
    }
  };
})();
