/*
Description:
	O24S.auth.Login
		Backbone for logging in
	O24S.auth.LoginBox
		TODO: Manual translations
Translation needed:
	"Select employee", "Username", "Password", "Please wait", "Login", "Login failed", "Unknown error"
Dependencies:
	none
 */

Ext.namespace("O24S", "O24S.auth");

O24S.auth.Login = function(config){
	Ext.apply(this,config);
}

O24S.auth.Login.prototype = {
	url: '/data/authentication/authenticate.aspx',
	sessionAbondonUrl: '/script/system/session/removeidentity.asp',
	singleTemplate: "Username={0}&Password={1}&Password8={2}",
	doubleTemplate: "Username={0}&Password={1}&Password8={2}&OptionalUsername={3}&OptionalPassword={4}&OptionalPassword8={5}",
	username: false,
	password: false,
	optuname: false,
	optpword: false,
	callback: false,
	xmlPathUser: 'Office24Seven Authenticate Client Users User',
	xmlPathClientName: 'Office24Seven Authenticate Client Name',
	xmlPathSuccess: 'Office24Seven Success',
	xmlPathPassportId: 'Office24Seven Authenticate Session Passport @id',
	xmlPathChangePassword: 'Office24Seven Authenticate ChangePassword',
	xmlPathPending: 'Office24Seven Session Passport EmailAddresses EmailAddress State @id',
	xmlPathRequiredAccessLevelType: 'Office24Seven Authenticate RequiredAccessLevel @type',
	xmlPathRequiredAccessLevelText: 'Office24Seven Authenticate RequiredAccessLevel Text',


	// Error codes
	FAILURE_UNKNOWN: 0001,
	FAILURE_HASH_FAILED: 0002,
	FAILURE_LOGIN_FAILED: 0003,
	FAILURE_MAIL_PENDING: 0004,
	FAILURE_PINCODE_REQUIRED: 0005,
	FAILURE_NO_REGISTERED_PHONENUMBER: 0006,
	FAILURE_NO_COMMUNITY_ACCOUNT: 0007,

	// Success codes
	SUCCESS_LOGIN: 1001,
	SUCCESS_EMPLOYEE_LIST: 1002,
	SUCCESS_PASSPORT: 1003,
	SUCCESS_REDIRECT: 1004,

/**
 *
 * @param config An object that can contain the following properties
 *
 *  username    required
 *  password    required
 *  optuname    optional
 *  optpword    optional
 *  callback    recommended :)
 *  cbScope     optional
 *  url         optional
 *
 */

	authenticate: function(config){
		Ext.apply(this,config);
		if(!this.username && !this.password){
			return false;
		};
		// Awesome password-lowercaser-checker at 01:24
		if(this.username.indexOf('@') == -1){
			this.password = this.password.toLowerCase();
			this.username = this.username.toLowerCase();
			if(this.optpword != false){
				this.optpword = this.optpword.toLowerCase();
				//alert(O24S.auth.Md5.hex_md5(this.optpword));
			};
		};
		// Hash-experiment
		// OUT
		// this.hashDealer.call(this,this.hashRecieved);
		// IN
		this.hashRecieved();
		// END
	},

	hashRecieved: function(hash){
		var oam = O24S.auth.Md5;

		// Hash-experiment
		// OUT
		// if(!hash){
		//	this.onError(this.FAILURE_HASH_FAILED);
		// }
		// END

		var post,level;
		if(this.optuname && this.optpword){
			// Hash-experiment
			// OUT
			// var eight = oam.hashCode8(hash, this.password);
			// var eightOpt = oam.hashCode8(hash, this.optpword);
			// var sixteen = oam.hashCode16(hash, this.password);
			// var sixteenOpt = oam.hashCode16(hash, this.optpword);
			// IN
			var eight = oam.hex_md5_hax(this.password, 8);
			var eightOpt = oam.hex_md5_hax(this.optpword, 8);
			var sixteen = oam.hex_md5_hax(this.password, 16);
			var sixteenOpt = oam.hex_md5_hax(this.optpword, 16);
			// END

			post = String.format(this.doubleTemplate, this.username, sixteen, eight, this.optuname, sixteenOpt, eightOpt);
			//alert("Password:\n8: " + O24S.auth.Md5.hex_md5_hax(this.password,8) + "\n16: " + O24S.auth.Md5.hex_md5_hax(this.password,16) + "\nOptional:\n8: " + O24S.auth.Md5.hex_md5_hax(this.optpword,8) + "\n16: " + O24S.auth.Md5.hex_md5_hax(this.optpword,16));
			level = 2;
		}else{
			// Hash-experiment
			// OUT
			// var eight = oam.hashCode8(hash, this.password);
			// var sixteen = oam.hashCode16(hash, this.password);
			// IN
			var eight = oam.hex_md5_hax(this.password, 8);
			var sixteen = oam.hex_md5_hax(this.password, 16)
			post = String.format(this.singleTemplate, this.username, sixteen, eight);
			//post = String.format(this.singleTemplate, this.username, this.password, this.password);
			//alert("Password:\n8: " + O24S.auth.Md5.hex_md5_hax(this.password,8) + "\n16: " + O24S.auth.Md5.hex_md5_hax(this.password,16));
			level = 1;
		};

		Ext.Ajax.request({
			url: this.url,
			method: 'post',
			params: post,
			scope: this,
			disableErrorHandling: true,
			success: function(result,options){
				this.traverseXml(result.responseXML, level);
			},
			failure: function(result,options){
				this.onError(result);
			}
		})
	},

	hashDealer: function(callback){
		if(!(typeof(callback)=="function"))
			return false;
		Ext.Ajax.request({
			url: '/data/authentication/gethash.aspx',
			method: 'get',
			scope: this,
			success: function(result, options) {
				callback.call(this, result.responseText);
			},
			failure: function(result, options){
				callback.call(this, false);
			}
		});
	},

	traverseXml: function(xml,level){
		var ed = Ext.DomQuery;
		// CHECK FOR USERS
		var nodes = ed.select(this.xmlPathUser, xml);

		if(nodes.length > 0 && level==1){
			var users = [];
			
			for(var s = 0; s < nodes.length; s++){
				var id = ed.selectValue('@id', nodes[s], false);
				var firstname = ed.selectValue('FirstName', nodes[s], '');
				var lastname = ed.selectValue('LastName', nodes[s], '');

				if(id){
					users.push([firstname+" "+lastname, id]);
				}
			}
			this.performCallback(this.SUCCESS_EMPLOYEE_LIST, users, ed.selectValue(this.xmlPathClientName, xml, false));
			
			return;
		};

		var success = Ext.DomQuery.selectValue(this.xmlPathSuccess, xml, 'none');
		var passport = Ext.DomQuery.selectValue(this.xmlPathPassportId, xml, false);
		var pending = Ext.DomQuery.selectValue(this.xmlPathPending, xml, 0);
		var requiredtype = Number(Ext.DomQuery.selectValue(this.xmlPathRequiredAccessLevelType, xml, 0));
		var requiredtext = Ext.DomQuery.selectValue(this.xmlPathRequiredAccessLevelText, xml);
		var typeOverride = false;
			
		if(requiredtype > 0){
			switch(requiredtype){
				case 1:
					typeOverride = this.FAILURE_PINCODE_REQUIRED;
					break;
				case 2:
					typeOverride = this.FAILURE_NO_REGISTERED_PHONENUMBER;
					break;
				case 3:
					typeOverride = this.FAILURE_NO_COMMUNITY_ACCOUNT;
					break;
			}
		
		}
		
		// CHECK FOR PASSPORT
		if(passport!=false && success.toLowerCase()=='true'){
			// we have passport
			if(Ext.DomQuery.selectValue(this.xmlPathChangePassword, xml, 'false') == 'true'){
				Ext.Ajax.request(
                    {
                        url: "/Data/Authentication/ChangePassword.aspx",
                        params:
                        {
                            OldPassword: O24S.auth.Md5.hex_md5_hax(this.password, 8),
							OldPassword8: O24S.auth.Md5.hex_md5_hax(this.password, 8),
                            NewPassword: O24S.auth.Md5.hex_md5_hax(this.password, 16)
                        }
				});
			};
			Ext.Ajax.request({
				url:this.sessionAbondonUrl,
				method:'get', 
				callback: function(){
					this.performCallback(typeOverride || this.SUCCESS_PASSPORT, this.SUCCESS_PASSPORT, requiredtext)
				}, 
				scope: this});
			return;
		}

		// CHECK FOR LOGIN
		if(success.toLowerCase()=='true'){
			// we have singlelogin... ahem.... !passport
			Ext.Ajax.request({
				url:this.sessionAbondonUrl,
				method:'get', 
				callback: function(){
					this.performCallback(typeOverride || this.SUCCESS_LOGIN, this.SUCCESS_LOGIN, requiredtext)
				}, 
				scope: this});
			return;
		}
		
		if(success.toLowerCase() == 'false' && (pending==1 || pending=='1')){
			this.performCallback(this.FAILURE_MAIL_PENDING);
			return;
		}

		if(success.toLowerCase()=='false'){
			this.performCallback(this.FAILURE_LOGIN_FAILED);
			return;
		}
/*

		// CHECK FOR SINGELOGIN

		if(success && level==1){
			// we have a singlelogin
			this.performCallback(this.SUCCESS_LOGIN);
			return;
		}
		// CHECK FOR DOUBLELOGIN
		if(success && level==2){
			// we have doublelogin
			this.performCallback(this.SUCCESS_LOGIN);
			return;
		}
*/
		this.performCallback(this.FAILURE_UNKNOWN);
	},

	performCallback: function(type,data,optional){
		if(this.callback){
			if(this.cbScope){
				this.callback.call(this.cbScope,type,data,optional);
			}else{
				this.callback(type,data,optional);
			}
		}
	},

	onError: function(result){
		// TODO: general errorhandling
		this.performCallback(this.FAILURE_LOGIN_FAILED, result);
	},
	
	encode: function(hash, string, bits){
		return O24S.auth.Md5.hex_md5_hax(hash+O24S.auth.Md5.hex_md5_hax(string, bits), 16);
	},

	destroy: function(){
		this.username = "";
		this.password = "";
		this.optuname = "";
		this.optpword = "";
		this.callback = "";
		this.cbScope = "";
		this.url = "";
	}
}

O24S.auth.LoginBox = function(config){
	Ext.apply(this,config);
	config.items =
		[
			this.topText = new Ext.Panel({
				border: false,
				html: "&nbsp;",
				bodyStyle: config.topTextStyle || 'background: transparent; text-align: right; padding-bottom: 2px;'
			}),
			{
				xtype: 'textfield',
				allowBlank: false,
				invalidClass : '',
				fieldLabel: Translation.get('Username'),
				bodyStyle: 'padding-top: 2px; padding-bottom: 2px;',
				id: 'loginbox-username',
				width: this.usernameWidth,
				preventMark: true,
				tabIndex: 1,
				selectOnFocus: true,
				listeners:  {
					        	'specialkey':{
							        fn: function(field,e){ if(e.getKey() == e.ENTER) this.onLogin()},
							        scope: this
						        }
							}
			},
			{
				xtype: 'textfield',
				inputType: 'password',
				invalidClass : '',
				allowBlank: false,
				fieldLabel: Translation.get('Password'),
				id: 'loginbox-password',
				width: this.passwordWidth,
				preventMark: true,
				tabIndex: 2,
				selectOnFocus: true,
				listeners:  {
					            'specialkey':{
							        fn: function(field,e){ if(e.getKey() == e.ENTER) this.onLogin()},
							        scope: this
						        }
							}
			},
			// This was the only way to solve this
			// and i am NOT proud of it
			(this.checkBox?
					{
						xtype: 'checkbox',
						value: true,
						id: 'remember-me',
						boxLabel: 'Remember me',
						labelSeparator: '',
						tabIndex: 4
					}:
					{
						border: false,
						bodyStyle: 'background: transparent;',
						height: 0
					}),
			this.bottomText = new Ext.Panel({
				border: false,
				html: '&nbsp;',
				bodyStyle: 'font-family: tahoma, arial, helvetica, sans-serif; font-size: 11px; background: transparent; text-align: center; padding-top:2px;',
				cls: 'overridden-color'
			}),
			(this.forgotLink?
					{
						border: false,
						bodyStyle: 'font-family: tahoma, arial, helvetica, sans-serif; font-size: 11px; background: transparent; text-align: center; padding-top: 7px;',
						html: '<a class="overridden-color" style="cursor: pointer;" href="#" onclick="shortHandForgot()">Forgot your Community password?</a>'
					}:
					{
						borde: false,
						bodyStyle: 'background: transparent; border: 0px;',
						html: '&nbsp;',
						height: 0
					})					
		];

	config.buttons = config.buttons==undefined ? [{ text: this.title, handler: this.onLogin, scope: this, tabIndex: 5}] : config.buttons;
	if(config.buttonTitleOverride){
		config.buttons[0].text = config.buttonTitleOverride;
	}
	O24S.auth.LoginBox.superclass.constructor.call(this,config);
	this.login = new O24S.auth.Login({url: this.url});
	if(config.login)
		Ext.apply(this.login,config.login);
	if(Ext.util.CSS.getRule(".login-window-icon")==null){
		Ext.util.CSS.createStyleSheet('.login-window-icon{ background-image: url("/media/icons/ns/16x16/gif/keys.gif")}');
	};
}


function shortHandForgot(){
	Ext.Ajax.request({
		method: 'post',
		url: "/data/authentication/forgotpassword.aspx",
		xmlData: "<Office24Seven></Office24Seven>",
		callback: Ext.emptyFn	
	})
}

Ext.extend(O24S.auth.LoginBox, Ext.Window, {
	login: false,
	width: 270,
	height: 154,
	comboBoxWidth: 165,
	modal: true,
	title: 'Login',
	buttonAlign: 'center',
	checkBox: false,
	employeeCombo: false,
	iconCls: "login-window-icon",
	allowPassport: true,
	forgotLink: false,
	center: true,
	layout: 'form',
	labelWidth: 80,
	border: false,
	callback: false,
	usernameWidth: 165,
	passwordWidth: 165,
	callbackScope: false,
	autoShowOnInit: false,
	pinWin: false,
	url: '/data/authentication/authenticate.aspx',

	focusUsername: function(){
		this.getComponent('loginbox-username').focus(true,true);
	},
	
	focusPassword: function(){
		this.getComponent('loginbox-password').focus(true,true);
	},

	setUsername: function(value){
		this.getComponent('loginbox-username').setValue(value);		
	},
	
	setDisabled: function(bool){
		this.getComponent('loginbox-username').setDisabled(bool);
		this.getComponent('loginbox-password').setDisabled(bool);
		if(this.buttons)
			this.buttons[0].setDisabled(bool);
		if(this.checkBox)
			this.getComponent('remember-me').setDisabled(bool);
	},

	setTopText: function(text){
		this.topText.body.dom.innerHTML = text;
	},

	setBottomText: function(text){
		if(text=="Please wait"){
			text = '&nbsp;<img src="/media/js/ext/ext-2.0/resources/images/default/shared/loading-balls.gif"/>&nbsp;';
		}else{
			text = Translation.get(text);
		}
		this.bottomText.body.dom.innerHTML = text;
	},

	onLogin: function(){
   	    var username = this.getComponent('loginbox-username').getValue();
		var password = this.getComponent('loginbox-password').getValue();

		if(this.employeeCombo && this.employeeCombo.isValid()){
			this.setDisabled(true);
			this.setBottomText('Please wait');
			var loginObj = [];
			loginObj.optuname = this.employeeCombo.getValue();
			loginObj.optpword = password;
			loginObj.callback = this.loginCallback;
			loginObj.scope = this;
			this.login.authenticate(loginObj);
		}else if(username && password){
			this.setDisabled(true);
			this.setBottomText('Please wait');
			var loginObj = [];
			loginObj.username = username;
			loginObj.password = password;
			loginObj.callback = this.loginCallback;
			loginObj.cbScope = this;
			this.login.authenticate(loginObj);
		}		
	},

	loginCallback: function(type, data, optional){
		switch(type){
			case this.login.SUCCESS_EMPLOYEE_LIST:
				this.setTopText(optional);
				this.setBottomText("Select employee");
				this.setDisabled(false);
				this.getComponent('loginbox-username').setWidth(this.comboBoxWidth);
				this.employeeCombo = new Ext.form.ComboBox({
					store: new Ext.data.SimpleStore({
						fields: ['name','id'],
						data: data,
						sortInfo: {field:'name'}
					}),
					displayField: 'name',
					valueField: 'id',
					shadow: false,
					forceSelection: true,
					editable: true,
					tabIndex: 1,
					transform:  'loginbox-username',
					mode: 'local',
					invalidClass : '',
					triggerAction: 'all',
					width: this.comboBoxWidth});
				//Ext.get('loginbox-username',true).remove();
				this.getComponent('loginbox-password').setValue('');
				this.employeeCombo.focus();
				break;
			case this.login.SUCCESS_LOGIN:
				this.performCallback(type);
				break;
			case this.login.SUCCESS_PASSPORT:
			   	if(this.allowPassport){
					this.performCallback(type);
			    }else{
					this.setBottomText("Login failed");
					this.setDisabled(false);
					this.getComponent('loginbox-password').focus();
				}
				break;
			case this.login.FAILURE_UNKNOWN:
				this.setBottomText("Unknown error");
				this.setDisabled(false);
				break;
			case this.login.FAILURE_MAIL_PENDING:
				document.location = "/login/confirm.aspx";
				break;
			case this.login.FAILURE_LOGIN_FAILED:
				if(data && data.status && data.status != 403 && Ext.DomQuery.selectValue("Office24Seven Error DetailedDescription", data.responseXML, false) != "Invalid username or password"){
					//var errorHandler = O24S.ErrorPanel.createAjaxRequestFailure();
					//errorHandler(data);
					O24S.Atlantis.addXML(data.responseXML);
					this.setBottomText("Error");
					this.setDisabled(false);
					break;
				}
				this.setBottomText("Login failed");
				this.setDisabled(false);
				if(this.employeeCombo)
					this.employeeCombo.focus();
				else
					this.getComponent('loginbox-username').focus();
				break;
			case this.login.FAILURE_PINCODE_REQUIRED:
				this.pinWin = new Ext.Window({
					layout: 'form',
					width: 184,
					modal: true,
					title: 'Pincode required',
					hideAction: 'close',
					height: 160,
					resizable: false,
					closable: false,
					bodyStyle: 'padding: 5px;',
					defaults: {
						border: false,
						bodyStyle: 'background-color: transparent;'	
					},
					items: [
						{
							html: optional || "Please enter pin code sent to your registered phone-number",
							style: 'padding-bottom: 10px'
						},
						this.pinField = new Ext.form.TextField({
							xtype: 'textfield',
							hideLabel: true,
							anchor: '100%',
							ref: '../pin',
							emptyText: 'Pincode',
							maxLength: 4,
							minLength: 4,
							maxLengthText: '',
							minLengthText: '',
							invalidText: ''
						})
					],
					buttons: [
						{
							text: 'OK',
							handler: function(){
								if(this.pinField.isValid()){
									this.pinWin.getEl().mask('Please wait');
									Ext.Ajax.request({
										url: this.url + "?pincode=" + this.pinField.getValue(),
										scope: this,
										disableErrorHandling: true,
										success: function(){									
											this.pinWin.hide();
											this.loginCallback(data);											
										},
										failure: function(){
											this.pinWin.getEl().unmask();
											this.pinField.markInvalid();										
										}
									})
								}
							},
							scope: this
						},
						{
							text: 'Cancel',
							handler: function(){
								this.pinWin.hide();
								this.loginCallback(data);
							},
							scope: this
						}			
					]
				});
				
				this.pinWin.show();
				break;
			case this.login.FAILURE_NO_REGISTERED_PHONENUMBER:
				var retfunc = function(){this.loginCallback(data);}.createDelegate(this);
				Ext.Msg.alert(	"Two factor authentication", 
								optional || "The client you are trying to log in to (set as default client on your Community-account) is set up to require two factor authentication (pincode by SMS). You dont have a phonenumber registered and confirmed on your Community-account so you cannot log in to the client.",
								retfunc);				
				return;
				break;
			case this.login.FAILURE_NO_COMMUNITY_ACCOUNT:
				var retfunc = function(){
						this.setBottomText("This login is disabled");
						this.setDisabled(false);
					};
				Ext.Msg.alert( "Two factor authentication",
								optional || "The client you are trying to log in to is set up to require two factor authentication (pincode by SMS). This requires a Community-account.",
								retfunc);
				return;
				break;
		}
	},

	initComponent: function(){

		O24S.auth.LoginBox.superclass.initComponent.call(this);
		if(this.checkBox)
			this.height+=20;
		if(this.autoShowOnInit) this.show();
	},

	destroy: function(){
		this.login.username = "";
		this.login.password = "";
		this.login.optuname = "";
		this.login.optpword = "";
		//this.login.destroy();
		O24S.auth.LoginBox.superclass.destroy.call(this);
	},

	performCallback: function(type){
		if(this.callback){
			if(this.callbackScope){
				this.callback.call(this.callbackScope, this, type);
			}else{
				this.callback(this, type);
			}
		}else{
			this.destroy();
		}
	}
});

O24S.auth.Md5 = function()
{
    return {
        // 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.

        // hex output format. 0 - lowercase; 1 - uppercase
        hexcase: 0,

        // base-64 pad character. "=" for strict RFC compliance
        b64pad: "",

        //bits per input character. 8 - ASCII; 16 - Unicode
        chrsz: 16,

        md5Encode: function(s)
        {
            return this.hex_md5(s);
        },

        // These are the functions you'll usually want to call
        // They take string arguments and return either hex or base-64 encoded strings
	    hex_md5_hax: function(s, bits){
		    this.chrsz = bits;
			return this.hex_md5(s);    
	    },

	    hashCode8: function(hash, password){
			return this.hex_md5_hax(hash+this.hex_md5_hax(password,8),16);
	    },

	    hashCode16: function(hash, password){
			return this.hex_md5_hax(hash+this.hex_md5_hax(password,16),16);
	    },

        hex_md5: function(s)
        {
            return this.binl2hex(this.core_md5(this.str2binl(s), s.length * this.chrsz));
        },
        b64_md5: function(s)
        {
            return this.binl2b64(this.core_md5(this.str2binl(s), s.length * this.chrsz));
        },
        str_md5: function(s)
        {
            return this.binl2str(this.core_md5(this.str2binl(s), s.length * this.chrsz));
        },
        hex_hmac_md5: function(key, data)
        {
            return this.binl2hex(this.core_hmac_md5(key, data));
        },
        b64_hmac_md5: function(key, data)
        {
            return this.binl2b64(this.core_hmac_md5(key, data));
        },
        str_hmac_md5: function(key, data)
        {
            return this.binl2str(this.core_hmac_md5(key, data));
        },

        //Perform a simple self-test to see if the VM is working
        md5_vm_test: function()
        {
          return this.hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
        },

        //Calculate the MD5 of an array of little-endian words, and a bit length
        core_md5: function(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 = this.md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
            d = this.md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
            c = this.md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
            b = this.md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
            a = this.md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
            d = this.md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
            c = this.md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
            b = this.md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
            a = this.md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
            d = this.md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
            c = this.md5_ff(c, d, a, b, x[i+10], 17, -42063);
            b = this.md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
            a = this.md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
            d = this.md5_ff(d, a, b, c, x[i+13], 12, -40341101);
            c = this.md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
            b = this.md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

            a = this.md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
            d = this.md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
            c = this.md5_gg(c, d, a, b, x[i+11], 14,  643717713);
            b = this.md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
            a = this.md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
            d = this.md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
            c = this.md5_gg(c, d, a, b, x[i+15], 14, -660478335);
            b = this.md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
            a = this.md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
            d = this.md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
            c = this.md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
            b = this.md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
            a = this.md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
            d = this.md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
            c = this.md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
            b = this.md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

            a = this.md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
            d = this.md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
            c = this.md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
            b = this.md5_hh(b, c, d, a, x[i+14], 23, -35309556);
            a = this.md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
            d = this.md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
            c = this.md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
            b = this.md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
            a = this.md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
            d = this.md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
            c = this.md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
            b = this.md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
            a = this.md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
            d = this.md5_hh(d, a, b, c, x[i+12], 11, -421815835);
            c = this.md5_hh(c, d, a, b, x[i+15], 16,  530742520);
            b = this.md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

            a = this.md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
            d = this.md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
            c = this.md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
            b = this.md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
            a = this.md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
            d = this.md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
            c = this.md5_ii(c, d, a, b, x[i+10], 15, -1051523);
            b = this.md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
            a = this.md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
            d = this.md5_ii(d, a, b, c, x[i+15], 10, -30611744);
            c = this.md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
            b = this.md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
            a = this.md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
            d = this.md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
            c = this.md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
            b = this.md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

            a = this.safe_add(a, olda);
            b = this.safe_add(b, oldb);
            c = this.safe_add(c, oldc);
            d = this.safe_add(d, oldd);
          }

          return Array(a, b, c, d);
        },

        //These functions implement the four basic operations the algorithm uses.
        md5_cmn: function(q, a, b, x, s, t)
        {
          return this.safe_add(this.bit_rol(this.safe_add(this.safe_add(a, q), this.safe_add(x, t)), s),b);
        },
        md5_ff: function(a, b, c, d, x, s, t)
        {
          return this.md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
        },
        md5_gg: function(a, b, c, d, x, s, t)
        {
          return this.md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
        },
        md5_hh: function(a, b, c, d, x, s, t)
        {
          return this.md5_cmn(b ^ c ^ d, a, b, x, s, t);
        },
        md5_ii: function(a, b, c, d, x, s, t)
        {
          return this.md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
        },

        //Calculate the HMAC-MD5, of a key and some data
        core_hmac_md5: function(key, data)
        {
          var bkey = this.str2binl(key);
          if(bkey.length > 16) bkey = this.core_md5(bkey, key.length * this.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 = this.core_md5(ipad.concat(this.str2binl(data)), 512 + data.length * this.chrsz);
          return this.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.
        safe_add: function(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.
        bit_rol: function(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.
        str2binl: function(str)
        {
          var bin = Array();
          var mask = (1 << this.chrsz) - 1;
          for(var i = 0; i < str.length * this.chrsz; i += this.chrsz)
            bin[i>>5] |= (str.charCodeAt(i / this.chrsz) & mask) << (i%32);
          return bin;
        },

        //Convert an array of little-endian words to a string
        binl2str: function(bin)
        {
          var str = "";
          var mask = (1 << this.chrsz) - 1;
          for(var i = 0; i < bin.length * 32; i += this.chrsz)
            str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
          return str;
        },

        //Convert an array of little-endian words to a hex string.
        binl2hex: function(binarray)
        {
          var hex_tab = this.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
        binl2b64: function(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 += this.b64pad;
              else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
            }
          }
          return str;
        }
    };
}();

