/*

Copyright 2008-2015 Clipperz Srl

This file is part of Clipperz, the online password manager.
For further information about its features and functionalities please
refer to http://www.clipperz.com.

* Clipperz is free software: you can redistribute it and/or modify it
  under the terms of the GNU Affero General Public License as published
  by the Free Software Foundation, either version 3 of the License, or 
  (at your option) any later version.

* Clipperz is distributed in the hope that it will be useful, but 
  WITHOUT ANY WARRANTY; without even the implied warranty of 
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  See the GNU Affero General Public License for more details.

* You should have received a copy of the GNU Affero General Public
  License along with Clipperz. If not, see http://www.gnu.org/licenses/.

*/

"use strict";
Clipperz.Base.module('Clipperz.PM.DataModel');

//if (typeof(Clipperz) == 'undefined') { Clipperz = {}; }
//if (typeof(Clipperz.PM) == 'undefined') { Clipperz.PM = {}; }
//if (typeof(Clipperz.PM.DataModel) == 'undefined') { Clipperz.PM.DataModel = {}; }


//#############################################################################

Clipperz.PM.DataModel.User = function (args) {
	args = args || {};

	Clipperz.PM.DataModel.User.superclass.constructor.apply(this, arguments);

	this._username = args.username || null;
	this._getPassphraseFunction = args.getPassphraseFunction || null;

	this._data = null;

	this._connection = null;
	this._connectionVersion = 'current';

	this._accountInfo = null;
	this._serverData = null;
//	this._serverLockValue = null;
	this._transientState = null;

	this._deferredLocks = {
		'passphrase':			new MochiKit.Async.DeferredLock(),
		'serverData':			new MochiKit.Async.DeferredLock(),
//		'recordsIndex':			new MochiKit.Async.DeferredLock(),
//		'directLoginsIndex':	new MochiKit.Async.DeferredLock()
//		'preferences':			new MochiKit.Async.DeferredLock()
//		'oneTimePasswords':		new MochiKit.Async.DeferredLock()
		'__syntaxFix__': 'syntax fix'
	};

	this._usedOTP = null;

	return this;
}

Clipperz.Base.extend(Clipperz.PM.DataModel.User, Object, {

	'toString': function () {
		return "Clipperz.PM.DataModel.User - " + this.username();
	},

	//-------------------------------------------------------------------------

	'username': function () {
		return this._username;
	},

	'setUsername': function (aValue) {
		this._username = aValue;
	},

	//-------------------------------------------------------------------------

	'setUsedOTP': function(aOTP) {
		this._usedOTP = aOTP;

		return aOTP;
	},

	'resetUsedOTP': function(aOTP) {
		this._usedOTP = null;
	},

	'markUsedOTP': function(aOTP) {
		var result;
		var oneTimePasswordKey;

		if (this._usedOTP) {
			oneTimePasswordKey = Clipperz.PM.DataModel.OneTimePassword.computeKeyWithPassword(
				Clipperz.PM.DataModel.OneTimePassword.normalizedOneTimePassword(this._usedOTP)
			);

			result = Clipperz.Async.callbacks("User.markUsedOTP", [ // NOTE: fired also when passphrase looks exactly like OTP
				MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
				MochiKit.Base.methodcaller('markOTPAsUsed', oneTimePasswordKey),
				MochiKit.Base.method(this,'saveChanges'), // Too 'heavy'?
				MochiKit.Base.method(this, 'resetUsedOTP')
			], {'trace': false});
		} else {
			result = MochiKit.Async.succeed();
		}

		return result;
	},

	//-------------------------------------------------------------------------

//	this.setSubscription(new Clipperz.PM.DataModel.User.Subscription(someServerData['subscription']));
	'accountInfo': function () {
		return this._accountInfo;
	},

	'setAccountInfo': function (aValue) {
		this._accountInfo = aValue;
	},

	//-------------------------------------------------------------------------

	'displayName': function() {
		return "" + this.username() + "";
	},
	
	//-------------------------------------------------------------------------

	'data': function () {
		if (this._data == null) {
			this._data = new Clipperz.KeyValueObjectStore(/*{'name':'User.data [1]'}*/);
		};
		
		return this._data;
	},

	//-------------------------------------------------------------------------
/*
	'serverLockValue': function () {
		return this._serverLockValue;
	},
	
	'setServerLockValue': function (aValue) {
		this._serverLockValue = aValue;
	},
*/
	//-------------------------------------------------------------------------

	'transientState': function () {
		if (this._transientState == null) {
			this._transientState = {}
		}
		
		return this._transientState;
	},

	'resetTransientState': function (isCommitting) {
		this._transientState = null;
	},

	//-------------------------------------------------------------------------

	'deferredLockForSection': function(aSectionName) {
		return this._deferredLocks[aSectionName];
	},

	//-------------------------------------------------------------------------

	'getPassphrase': function() {
		return this._getPassphraseFunction();
	},

	'getPassphraseFunction': function () {
		return this._getPassphraseFunction;
	},

	'setPassphraseFunction': function (aFunction) {
		this._getPassphraseFunction = aFunction;
	},

	//-------------------------------------------------------------------------

	'getCredentials': function () {
		return Clipperz.Async.collectResults("User; get username and passphrase", {
			'username': MochiKit.Base.method(this, 'username'),
			'password': MochiKit.Base.method(this, 'getPassphrase')
		}, {trace:false})();
	},

	//-------------------------------------------------------------------------

	'changePassphrase': function (aNewValueCallback) {
		return this.updateCredentials(this.username(), aNewValueCallback);
	},

	//.........................................................................

	'updateCredentials': function (aUsername, aPassphraseCallback) {
		var	deferredResult;

		deferredResult = new Clipperz.Async.Deferred("User.updateCredentials", {trace:false});
		deferredResult.addMethod(this.connection(), 'ping');
		deferredResult.collectResults({
			'newUsername': MochiKit.Base.partial(MochiKit.Async.succeed, aUsername),
			'newPassphrase': aPassphraseCallback,
			'user': MochiKit.Base.method(this, 'prepareRemoteDataWithKeyFunction', aPassphraseCallback),
			'oneTimePasswords': [
				MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
				MochiKit.Base.methodcaller('getEncryptedOTPData', aPassphraseCallback),
				function (otps) {
					var result;
					var otpRefs;
					var i, c;

					result = {};
					otpRefs = MochiKit.Base.keys(otps);
					c = otpRefs.length;
					for (i=0; i<c; i++) {
						result[otpRefs[i]] = {}
						result[otpRefs[i]]['data'] = otps[otpRefs[i]]['data'];
						result[otpRefs[i]]['version'] = otps[otpRefs[i]]['version'];
					}

					return result;
				}
			]
		});

		deferredResult.addMethod(this.connection(), 'updateCredentials');
		deferredResult.callback();
		
		return deferredResult;
	},

	//-------------------------------------------------------------------------

	'initialSetupWithNoData': function () {
		this._serverData = {
			'version': '0.1',
			'statistics': "",
			'header': {
				'data': null,
				'version': Clipperz.PM.Crypto.encryptingFunctions.currentVersion,

				'recordsIndex': new Clipperz.PM.DataModel.User.Header.RecordIndex({
					'retrieveKeyFunction':			MochiKit.Base.method(this, 'getPassphrase'),
					'recordsData':					{'data':null, 'index':{}},
					'recordsStats':					null,
					'directLoginsData':				{'data':null, 'index':{}},
					'attachmentsData':				{'data':null, 'index':{}},
					'encryptedDataVersion':			Clipperz.PM.Crypto.encryptingFunctions.currentVersion,
					'retrieveRecordDetailFunction':	MochiKit.Base.method(this, 'getRecordDetail')
				}),
				'preferences': new Clipperz.PM.DataModel.User.Header.Preferences({
					'name':	'preferences',
					'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase')
				}),
				'oneTimePasswords': new Clipperz.PM.DataModel.User.Header.OneTimePasswords({
					'connection': this.connection(),
					'name':	'oneTimePasswords',
					'username': this.username(),
					'passphraseCallback': MochiKit.Base.method(this, 'getPassphrase')
				})
			}
		};
		
//		this._serverLockValue = Clipperz.PM.Crypto.randomKey();
	},

	//.........................................................................

	'registerAsNewAccount': function () {
		var deferredResult;

		deferredResult = new Clipperz.Async.Deferred("User.registerAsNewAccount", {trace:false});
		deferredResult.addMethod(this, 'initialSetupWithNoData')
		deferredResult.addMethod(this, 'getPassphrase');
		deferredResult.addMethod(this, 'prepareRemoteDataWithKey');
		deferredResult.addMethod(this.connection(), 'register');
		deferredResult.callback();
		
		return deferredResult;
	},
	
	'deleteAccount': function() {
		var deferredResult;
		
		deferredResult = new MochiKit.Async.Deferred("User.deleteAccount", {trace:false});
		deferredResult.addCallback(MochiKit.Base.method(this.connection(), 'message'), 'deleteUser');
		deferredResult.addCallback(MochiKit.Base.method(this, 'resetAllLocalData'));
		deferredResult.callback();

		return deferredResult;
	},
	
	'resetAllLocalData': function() {
		var deferredResult;
		
		deferredResult = new MochiKit.Async.Deferred("User.resetAllLocalData", {trace:false});
		deferredResult.addCallback(MochiKit.Base.method(this, 'deleteAllCleanTextData'));
		deferredResult.addCallback(MochiKit.Base.method(this, function() {
			this.resetConnection();
			this.setUsername("");
			this._getPassphraseFunction = function() { return ""; };
			this._serverData = null;
		}));
		
		deferredResult.callback();
		
		return deferredResult;	
	},

	//-------------------------------------------------------------------------

	'login': function () {
		var deferredResult;
		var oneTimePasswordReference;

		deferredResult = new Clipperz.Async.Deferred("User.login", {trace:false});
		deferredResult.addMethod(this, 'getPassphrase');
		deferredResult.addCallback(Clipperz.PM.DataModel.OneTimePassword.isValidOneTimePasswordValue);

		deferredResult.addCallback(Clipperz.Async.deferredIf("Is the passphrase an OTP", [
			MochiKit.Base.method(this,'getPassphrase'),
			MochiKit.Base.method(this,'setUsedOTP'),
			MochiKit.Base.method(this, 'getCredentials'),
			MochiKit.Base.method(this.connection(), 'redeemOneTimePassword'),
			function (aPassphrase) {
				return MochiKit.Base.partial(MochiKit.Async.succeed, aPassphrase);
			},
			MochiKit.Base.method(this, 'setPassphraseFunction')
		], []));

		deferredResult.addBoth(MochiKit.Base.method(this, 'loginWithPassphrase'));
		deferredResult.addBothPass(MochiKit.Base.method(this, 'resetUsedOTP'));
		
		deferredResult.callback();
		
		return deferredResult;
	},

	'loginWithPassphrase': function () {
		var deferredResult;

		deferredResult = new Clipperz.Async.Deferred("User.loginWithPassphrase", {trace:false});

		deferredResult.addMethod(this, 'getPassphrase');
		deferredResult.addMethod(this.connection(), 'login', false);
		deferredResult.addMethod(this, 'setupAccountInfo');
		deferredResult.addMethod(this, 'markUsedOTP');
		deferredResult.addErrback (MochiKit.Base.method(this, 'handleConnectionFallback'));

		deferredResult.callback();

		return deferredResult;
	},

	//.........................................................................

	'handleConnectionFallback': function(aValue) {
		var result;

		if (aValue instanceof MochiKit.Async.CancelledError) {
			result = aValue;
		} else if ((aValue['isPermanent'] === true) || (Clipperz.PM.Connection.communicationProtocol.fallbackVersions[this.connectionVersion()] == null)) {
			result = Clipperz.Async.callbacks("User.handleConnectionFallback - failed", [
				MochiKit.Base.method(this.data(), 'removeValue', 'passphrase'),
				MochiKit.Base.method(this, 'setConnectionVersion', 'current'),
				MochiKit.Base.partial(MochiKit.Async.fail, aValue)
			], {trace:false});
		} else {
			this.setConnectionVersion(Clipperz.PM.Connection.communicationProtocol.fallbackVersions[this.connectionVersion()]);
			result = new Clipperz.Async.Deferred("User.handleConnectionFallback - retry");
			result.addMethod(this, 'loginWithPassphrase');
			result.callback();
		}

		return result;
	},

	//-------------------------------------------------------------------------

	'setupAccountInfo': function (aValue) {
		this.setAccountInfo(new Clipperz.PM.DataModel.User.AccountInfo(aValue['accountInfo']));
	},

	//-------------------------------------------------------------------------

	'lock': function () {
		return Clipperz.Async.callbacks("User.lock", [
			MochiKit.Base.method(this.connection(), 'logout'),
			MochiKit.Base.method(this, 'deleteAllCleanTextData'),
			MochiKit.Base.method(this, 'setPassphraseFunction', function() {throw("No passphrase set.")})
		], {trace:false});
	},

	'unlock': function (aPassphraseDelegate) {
		this.setPassphraseFunction(aPassphraseDelegate);
		return this.getPreferences();	//	TODO: make this more explicit.
	},

	//-------------------------------------------------------------------------

	'logout': function () {
		return Clipperz.Async.callbacks("User.logout", [
			MochiKit.Base.method(this, 'deleteAllCleanTextData'),
			MochiKit.Base.method(this.connection(), 'logout')
		], {trace:false});
	},

	//-------------------------------------------------------------------------

	'headerFormatVersion': function(anHeader) {
		var result;
		
		if (anHeader.charAt(0) == '{') {
			var	headerData;
			
			headerData = Clipperz.Base.evalJSON(anHeader);
			result = headerData['version'];
		} else {
			result = 'LEGACY';
		}
		
		return result;
	},

	//-------------------------------------------------------------------------

	'unpackServerData': function (someServerData) {
		var unpackedData;
		var headerVersion;

		var	recordsIndex;
		var preferences;
		var oneTimePasswords;

		headerVersion = this.headerFormatVersion(someServerData['header']);
		switch (headerVersion) {
			case 'LEGACY':
				var	legacyHeader;
				
				legacyHeader = new Clipperz.PM.DataModel.User.Header.Legacy({
					'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase'),
					'remoteData': {
						'data': someServerData['header'],
						'version': someServerData['version'],
						'recordsStats': someServerData['recordsStats']
					},
//					'encryptedDataKeypath':			'data',
//					'encryptedVersionKeypath':		'version',
					'retrieveRecordDetailFunction':	MochiKit.Base.method(this, 'getRecordDetail')
				});

				recordsIndex		= legacyHeader;
				preferences			= legacyHeader;
				oneTimePasswords	= legacyHeader;
				break;
			case '0.1':
				var	headerData;

//console.log("Server data", someServerData);
				headerData = Clipperz.Base.evalJSON(someServerData['header']);

//console.log("headerData", headerData);
				recordsIndex = new Clipperz.PM.DataModel.User.Header.RecordIndex({
					'retrieveKeyFunction':			MochiKit.Base.method(this, 'getPassphrase'),
					'recordsData':					headerData['records'],
					'recordsStats':					someServerData['recordsStats'],
					'directLoginsData':				headerData['directLogins'],
					'attachmentsData':				headerData['attachments'] || {'data': null, 'index':{}},
					'encryptedDataVersion':			someServerData['version'],
					'retrieveRecordDetailFunction':	MochiKit.Base.method(this, 'getRecordDetail')
				});

				//	Still missing a test case that actually fais with the old version of the code, where the check for undefined was missing
				if (typeof(headerData['preferences']) != 'undefined') {
					preferences	= new Clipperz.PM.DataModel.User.Header.Preferences({
						'name':	'preferences',
						'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase'),
						'remoteData': {
							'data': headerData['preferences']['data'],
							'version': someServerData['version']
						}
					});
				} else {
					preferences	= new Clipperz.PM.DataModel.User.Header.Preferences({
						'name':	'preferences',
						'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase')
					});
				}

				if (typeof(headerData['oneTimePasswords']) != 'undefined') {
					oneTimePasswords = new Clipperz.PM.DataModel.User.Header.OneTimePasswords({
						'name':	'oneTimePasswords',
						'connection': this.connection(),
						'username': this.username(),
						'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase'),
						'remoteData': {
							'data': headerData['oneTimePasswords']['data'],
							'version': someServerData['version']
						}
					});
				} else {
					oneTimePasswords = new Clipperz.PM.DataModel.User.Header.OneTimePasswords({
						'name':	'OneTimePasswords',
						'connection': this.connection(),
						'username': this.username(),
						'retrieveKeyFunction': MochiKit.Base.method(this, 'getPassphrase')
					});
				}
				
				break;
		}

		unpackedData = {
			'version': someServerData['version'],
			'statistics': someServerData['statistics'],
			'header': {
				'data': someServerData['header'],
				'version': headerVersion,
				
				'recordsIndex': recordsIndex,
				'preferences': preferences,
				'oneTimePasswords': oneTimePasswords
			}
		};

		this._serverData = unpackedData;
		
		return this._serverData;
	},

	//-------------------------------------------------------------------------

	'getServerData': function() {
		var deferredResult;

		deferredResult = new Clipperz.Async.Deferred("User.getServerData", {trace:false});
		deferredResult.acquireLock(this.deferredLockForSection('serverData'));
		deferredResult.addCallback(MochiKit.Base.bind(function(aResult) {
			var innerDeferredResult;
		
			innerDeferredResult = new Clipperz.Async.Deferred("User.getUserDetails.innerDeferred", {trace:false});
			if (this._serverData == null) {
				innerDeferredResult.addCallbackPass(MochiKit.Signal.signal, this, 'loadingUserDetails');
				innerDeferredResult.addMethod(this.connection(), 'message', 'getUserDetails');
				innerDeferredResult.addMethod(this, 'unpackServerData');
				innerDeferredResult.addCallbackPass(MochiKit.Signal.signal, this, 'loadedUserDetails');
			}

			innerDeferredResult.addCallback(MochiKit.Base.bind(function () {
				return this._serverData;
			},this));
			innerDeferredResult.callback();

			return innerDeferredResult;
		}, this));
		deferredResult.releaseLock(this.deferredLockForSection('serverData'));
		deferredResult.callback();

		return deferredResult;
	},

	//-------------------------------------------------------------------------

	'connectionVersion': function() {
		return this._connectionVersion;
	},
	
	'setConnectionVersion': function(aValue) {
		if (this._connectionVersion != aValue) {
			this.resetConnection();
		}
		this._connectionVersion = aValue;
	},

	//-------------------------------------------------------------------------

	'connection': function() {
		if ((this._connection == null) && (this.connectionVersion() != null) ){
			this._connection = new Clipperz.PM.Connection.communicationProtocol.versions[this.connectionVersion()]({
				getCredentialsFunction: MochiKit.Base.method(this, 'getCredentials')
			});
		}
				
		return this._connection;
	},

	'resetConnection': function(aValue) {
		if (this._connection != null) {
			this._connection.reset();
		}

		this._connection = null;
	},

	//=========================================================================
	
	'getHeaderIndex': function (aKey) {
		return Clipperz.Async.callbacks("User.getHeaderIndex", [
			MochiKit.Base.method(this, 'getServerData'),
			MochiKit.Base.itemgetter('header'),
			MochiKit.Base.itemgetter(aKey)
		], {trace:false})
	},

	//=========================================================================

	'getTags': function (shouldIncludeArchivedCards) {
		var	archivedCardsFilter =	(shouldIncludeArchivedCards || false)
									?	MochiKit.Async.succeed
									:	MochiKit.Base.partial(MochiKit.Base.filter, function (someTags) {
											return someTags.indexOf(Clipperz.PM.DataModel.Record.archivedTag) == -1;
										});

		return Clipperz.Async.callbacks("User.getTags", [
			MochiKit.Base.method(this, 'getRecords'),
			MochiKit.Base.partial(MochiKit.Base.map, MochiKit.Base.methodcaller('tags')),
			Clipperz.Async.collectAll,
			archivedCardsFilter,
			MochiKit.Base.flattenArray,
			MochiKit.Iter.groupby,
			function (someGroups) {
				return  MochiKit.Iter.reduce(function(aCollector, aGroup) {
					var currentValue = aCollector[aGroup[0]] ? aCollector[aGroup[0]] : 0;
					aCollector[aGroup[0]] = MochiKit.Iter.list(aGroup[1]).length + currentValue;

					return aCollector;
				}, someGroups, {});
			}
		], {trace:false});
	},

	//=========================================================================

	'recordWithData': function (recordData) {
//console.log("recordWithData", recordData)
		return Clipperz.Async.callbacks("User.recordWithData", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
			MochiKit.Base.methodcaller('records'),
			MochiKit.Base.itemgetter(recordData['reference']),
			MochiKit.Base.methodcaller('setRemoteData', recordData),
		], {trace:false});
		
	},

	//=========================================================================

	'getRecords': function () {
		return Clipperz.Async.callbacks("User.getRecords", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
			MochiKit.Base.methodcaller('records'),
			MochiKit.Base.values
		], {trace:false});
	},

	'getRecordsLoadingAllData': function () {
		return Clipperz.Async.callbacks("User.getRecordsLoadingAllData", [
			MochiKit.Base.method(this.connection(), 'message', 'getAllRecordDetails'),
			MochiKit.Base.values,
			MochiKit.Base.partial(MochiKit.Base.map, MochiKit.Base.method(this, 'recordWithData')),
			MochiKit.Base.method(this, 'getRecords'),
		], {trace:false});
	},

	'getRecordsInfo': function (someInfo) {
		return Clipperz.Async.callbacks("User.getRecordsInfo", [
			MochiKit.Base.method(this, 'getRecords'),
			MochiKit.Base.partial(MochiKit.Base.map, Clipperz.Async.collectResults("collectResults", someInfo, {trace:false})),
			Clipperz.Async.collectAll,
		], {trace:false});
	},

	'recordWithLabel': function (aLabel) {
		return Clipperz.Async.callbacks("User.recordWithLabel", [
			MochiKit.Base.method(this, 'getRecords'),
			MochiKit.Base.partial(Clipperz.Async.deferredFilter, function (aRecord) {
				return Clipperz.Async.callbacks("User.recordWithLabel - check record label", [
					MochiKit.Base.methodcaller('label'),
					MochiKit.Base.partial(MochiKit.Base.operator.eq, aLabel)
				], {trace:false}, aRecord);
			}),
			function (someFilteredResults) {
				var result;

				switch (someFilteredResults.length) {
					case 0:
						result = null;
						break;
					case 1:
						result = someFilteredResults[0];
						break;
					default:
Clipperz.log("Warning: User.recordWithLabel('" + aLabel + "') is returning more than one result: " + someFilteredResults.length);
						result = someFilteredResults[0];
						break;
				}
				
				return result;
			}
		], {trace:false});
	},

	//-------------------------------------------------------------------------

	'getRecord': function (aRecordReference) {
		return Clipperz.Async.callbacks("User.getRecord", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
			MochiKit.Base.methodcaller('records'),
			MochiKit.Base.itemgetter(aRecordReference),
			
			Clipperz.Async.deferredIf("record != null", [
				MochiKit.Base.operator.identity
			], [
				function () { throw "Record does not exists"}
			])
		], {trace:false});
	},

	//-------------------------------------------------------------------------

	'getRecordDetail': function (aRecord) {
		// return this.connection().message('getRecordDetail', {reference: aRecordReference});

		return Clipperz.Async.callbacks("User.getRecordDetail", [
			MochiKit.Base.method(this.connection(), 'message', 'getRecordDetail', {reference: aRecord.reference()}),
			function(someInfo) { aRecord.setAttachmentServerStatus(someInfo['attachmentStatus']); return someInfo;}, // Couldn't find a better way...
		], {trace:false});
	},

	//-------------------------------------------------------------------------

	'deleteRecord': function (aRecord) {
		return Clipperz.Async.callbacks("User.deleteRecord", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
			MochiKit.Base.methodcaller('deleteRecord', aRecord)
		], {trace:false});
	},

	//-------------------------------------------------------------------------

	'createNewRecord': function () {
		return Clipperz.Async.callbacks("User.createNewRecord", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
			MochiKit.Base.methodcaller('createNewRecord')
		], {trace:false});
	},

	//.........................................................................

	'createNewRecordFromJSON': function(someJSON) {
		var deferredResult;

		//	TODO: how do we handle attachments?
		deferredResult = new Clipperz.Async.Deferred("User.createNewRecordFromJSON", {trace:false});
		deferredResult.collectResults({
			'recordIndex': MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
			'newRecord': [
				MochiKit.Base.method(this, 'createNewRecord'),
				MochiKit.Base.methodcaller('setUpWithJSON', someJSON),
			]
		});
		deferredResult.addCallback(function (someInfo) {
			var	record = someInfo['newRecord'];
			var	recordIndex = someInfo['recordIndex'];
			
			return MochiKit.Base.map(function (aDirectLogin) {
				var	configuration = JSON.stringify({
					'page': {'title': aDirectLogin['label']},
					'form': aDirectLogin['formData'],
					'version': '0.2' // correct?
				});

				return Clipperz.Async.callbacks("User.createNewRecordFromJSON__inner", [
					//	TODO: check if we should invoke the create new direct login methon on Record instead of calling it directly on the index
					MochiKit.Base.method(recordIndex, 'createNewDirectLogin', record),
					MochiKit.Base.methodcaller('setLabel', aDirectLogin['label']),
					MochiKit.Base.methodcaller('setBookmarkletConfiguration', configuration),
					MochiKit.Base.methodcaller('setBindings', aDirectLogin['bindingData'], someJSON['currentVersion']['fields']),
				], {'trace': false});
			}, MochiKit.Base.values(someJSON.data.directLogins));
		});
		deferredResult.addCallback(Clipperz.Async.collectAll);
		deferredResult.callback();

		return deferredResult;
	},

	//-------------------------------------------------------------------------

	'cloneRecord': function (aRecord) {
		var	result;
		var	user = this;
		
		return Clipperz.Async.callbacks("User.cloneRecord", [
			MochiKit.Base.method(this, 'hasPendingChanges'),
			Clipperz.Async.deferredIf("User has pending changes", [
				MochiKit.Async.fail
			], [
				MochiKit.Base.method(user, 'createNewRecord'),
				MochiKit.Base.methodcaller('setUpWithRecord', aRecord),
			])
		], {trace:false});
	},

	//=========================================================================

	'getDirectLogins': function() {
		var deferredResult;
		
		deferredResult = new Clipperz.Async.Deferred("User.getDirectLogins", {trace:false});
		deferredResult.addMethod(this, 'getRecords');
		deferredResult.addCallback(MochiKit.Base.map, MochiKit.Base.compose(MochiKit.Base.values, MochiKit.Base.methodcaller('directLogins')));
		deferredResult.addCallback(MochiKit.Base.flattenArray);
		deferredResult.callback();

		return deferredResult;
	},

	//=========================================================================

	'getOneTimePasswords': function () {
		return Clipperz.Async.callbacks("User.getOneTimePasswords", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
			MochiKit.Base.methodcaller('oneTimePasswords'),
			MochiKit.Base.values
		], {trace:false});
	},

	'getOneTimePasswordsDetails': function() {
		return Clipperz.Async.callbacks("User.getOneTimePasswords", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
			MochiKit.Base.methodcaller('oneTimePasswordsDetails', this.connection()),
		], {trace:false});
	},

	//-------------------------------------------------------------------------

	'createNewOTP': function () {
		var messageParameters;

		messageParameters = {};
		return Clipperz.Async.callbacks("User.createNewOTP", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
			MochiKit.Base.methodcaller('createNewOTP', this.username(), MochiKit.Base.method(this, 'getPassphrase')),
			MochiKit.Base.methodcaller('encryptedData'),
			MochiKit.Base.partial(function(someParameters, someOTPEncryptedData) {
				someParameters['oneTimePassword'] = someOTPEncryptedData;
			}, messageParameters),
			MochiKit.Base.method(this, 'getPassphrase'),
			MochiKit.Base.method(this, 'prepareRemoteDataWithKey'),
			MochiKit.Base.partial(function(someParameters, someEncryptedRemoteData) {
				someParameters['user'] = someEncryptedRemoteData;
			}, messageParameters),
			MochiKit.Base.method(this.connection(), 'message', 'addNewOneTimePassword', messageParameters)
		], {trace:false});
	},

	'deleteOTPs': function (aList) {
		var messageParameters;

		messageParameters = {};
		return Clipperz.Async.callbacks("User.deleteOTPs", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
			MochiKit.Base.methodcaller('deleteOTPs', aList),
			MochiKit.Base.partial(function(someParameters, aList) {
				someParameters['oneTimePasswords'] = aList
			}, messageParameters),
			MochiKit.Base.method(this, 'getPassphrase'),
			MochiKit.Base.method(this, 'prepareRemoteDataWithKey'),
			MochiKit.Base.partial(function(someParameters, someEncryptedRemoteData) {
				someParameters['user'] = someEncryptedRemoteData;
			}, messageParameters),
			MochiKit.Base.method(this.connection(), 'message', 'updateOneTimePasswords', messageParameters)			
		], {trace:false});
	},

	'changeOTPLabel': function (aReference, aLabel) {
		return Clipperz.Async.callbacks("User.changeOTPLabel", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
			MochiKit.Base.methodcaller('changeOTPLabel', aReference, aLabel),
			MochiKit.Base.method(this,'saveChanges') // Too 'heavy'? Should be moved to MainController to prevent glitch in the UI? 
		], {trace:false});
	},

	//=========================================================================

	'invokeMethodNamedOnHeader': function (aMethodName, aValue) {
		return Clipperz.Async.collectResults("User.invokeMethodNamedOnHeader [" + aMethodName + "]", {
			'recordIndex': [
				MochiKit.Base.method(this, 'getHeaderIndex', 'recordsIndex'),
				MochiKit.Base.methodcaller(aMethodName, aValue)
			],
			'preferences': [
				MochiKit.Base.method(this, 'getHeaderIndex', 'preferences'),
				MochiKit.Base.methodcaller(aMethodName, aValue)
			],
			'oneTimePasswords': [
				MochiKit.Base.method(this, 'getHeaderIndex', 'oneTimePasswords'),
				MochiKit.Base.methodcaller(aMethodName, aValue)
			]//,
//			'statistics': [
//				MochiKit.Base.method(this, 'getStatistics'),
//				MochiKit.Base.methodcaller(aMethodName, aValue)
//			]
		}, {trace:false})();
	},

	//-------------------------------------------------------------------------

	'invokeMethodNamedOnRecords': function (aMethodName, aValue) {
		return Clipperz.Async.callbacks("User.invokeMethodNamedOnRecords[" + aMethodName + "]", [
			MochiKit.Base.method(this, 'getRecords'),
			MochiKit.Base.partial(MochiKit.Base.map, MochiKit.Base.methodcaller(aMethodName, aValue)),
			Clipperz.Async.collectAll
		], {trace:false});
	},

	//-------------------------------------------------------------------------

	'getPreferences': function() {
		return Clipperz.Async.callbacks("User.getPreferences", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'preferences'),
			MochiKit.Base.methodcaller('getPreferences')
		], {trace:false});
	},

	'getPreference': function(aKey) {
		return Clipperz.Async.callbacks("User.getPreference", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'preferences'),
			MochiKit.Base.methodcaller('getPreference', aKey)
		], {trace:false});
	},

	setPreferences: function(anObject) {
		return Clipperz.Async.callbacks("User.setPreferences", [
			MochiKit.Base.method(this, 'getHeaderIndex', 'preferences'),
			MochiKit.Base.methodcaller('setPreferences', anObject),
			MochiKit.Base.method(this, 'saveChanges')
		], {trace:false});
	},


	//-------------------------------------------------------------------------
/*
	'addNewAttachment': function(anAttachment) {
		console.log("Adding attachment", anAttachment);
	},
*/
	'uploadAttachment': function(aRecordReference, anAttachmentReference, someData) {
		// TODO: pass a callback to handle onProgress events (and modify Connection accordingly)
		this.connection().message('uploadAttachment', {
			'recordReference': aRecordReference,
			'attachmentReference': anAttachmentReference,
			'data': someData
		});
	},

	//=========================================================================

	'hasPendingChanges': function () {
		var	deferredResult;
		
		deferredResult = new Clipperz.Async.Deferred("User.hasPendingChanges", {trace:false});
		deferredResult.collectResults({
			'header': [
				MochiKit.Base.method(this, 'invokeMethodNamedOnHeader', 'hasPendingChanges'),
				MochiKit.Base.values
			],
			'records': MochiKit.Base.method(this, 'invokeMethodNamedOnRecords', 'hasPendingChanges')
		});
		deferredResult.addCallback(Clipperz.Async.or);
		deferredResult.callback();
//		recordsIndex		= legacyHeader;
//		preferences			= legacyHeader;
//		oneTimePasswords	= legacyHeader;
		
		return deferredResult;
	},

	//=========================================================================

	'commitTransientState': function () {
		return Clipperz.Async.callbacks("User.commitTransientState", [
			MochiKit.Base.method(this, 'invokeMethodNamedOnHeader', 'commitTransientState'),
			MochiKit.Base.method(this, 'invokeMethodNamedOnRecords', 'commitTransientState'),

			MochiKit.Base.method(this, 'transientState'),
//			MochiKit.Base.itemgetter('lock'),
//			MochiKit.Base.method(this, 'setServerLockValue'),
			MochiKit.Base.method(this, 'resetTransientState', true)
		], {trace:false});
	},

	//-------------------------------------------------------------------------

	'revertChanges': function () {
		return Clipperz.Async.callbacks("User.revertChanges", [
			MochiKit.Base.method(this, 'invokeMethodNamedOnHeader', 'revertChanges'),
			MochiKit.Base.method(this, 'invokeMethodNamedOnRecords', 'revertChanges'),
			MochiKit.Base.method(this, 'resetTransientState', false),
		], {trace:false});
	},

	//=========================================================================

	'deleteAllCleanTextData': function () {
		return Clipperz.Async.callbacks("User.deleteAllCleanTextData", [
			MochiKit.Base.method(this, 'invokeMethodNamedOnRecords', 'deleteAllCleanTextData'),
			MochiKit.Base.method(this, 'invokeMethodNamedOnHeader',  'deleteAllCleanTextData'),

			MochiKit.Base.method(this.data(), 'removeAllData'),
			MochiKit.Base.method(this, 'resetTransientState', false)
		], {trace:false});
	},

	//-------------------------------------------------------------------------

	'hasAnyCleanTextData': function () {
		var	deferredResult;
		
		deferredResult = new Clipperz.Async.Deferred("User.hasAnyCleanTextData", {trace:false});
		deferredResult.collectResults({
			'header':	[
				MochiKit.Base.method(this, 'invokeMethodNamedOnHeader',  'hasAnyCleanTextData'),
				MochiKit.Base.values
			],
			'records':	MochiKit.Base.method(this, 'invokeMethodNamedOnRecords', 'hasAnyCleanTextData'),
			'data':		MochiKit.Base.bind(function () {
							return MochiKit.Async.succeed(! this.data().isEmpty());
						}, this),
			'transientState':	MochiKit.Base.bind(function () {
							return MochiKit.Async.succeed(MochiKit.Base.keys(this.transientState()).length != 0);
						}, this)
		});
		deferredResult.addCallback(Clipperz.Async.or);
		deferredResult.callback();

		return deferredResult;
	},

	//=========================================================================

	'prepareRemoteDataWithKey': function (aKey /*, aCurrentKey*/) {
		var deferredResult;
		var	result;

		result = {};
		deferredResult = new Clipperz.Async.Deferred("User.prepareRemoteDataWithKey", {trace:false});
		deferredResult.addMethod(this, 'invokeMethodNamedOnHeader', 'prepareRemoteDataWithKey', aKey /*, aCurrentKey*/);
		deferredResult.addCallback(MochiKit.Base.bind(function (aResult, someHeaderPackedData) {
			var header;

			header = {};
			header['records']			= someHeaderPackedData['recordIndex']['records'];
			header['directLogins']		= someHeaderPackedData['recordIndex']['directLogins'];
			header['attachments']		= someHeaderPackedData['recordIndex']['attachments'];
			header['preferences']		= {'data': someHeaderPackedData['preferences']['data']};
			header['oneTimePasswords']	= {'data': someHeaderPackedData['oneTimePasswords']['data']};
			header['version']			= '0.1';

			aResult['header']		= Clipperz.Base.serializeJSON(header);
			aResult['statistics']	= this._serverData['statistics'];	//	"someHeaderPackedData['statistics']['data']";
			
			return aResult;
		}, this), result);
		deferredResult.addCallback(Clipperz.Async.setItem, result, 'version', Clipperz.PM.Crypto.encryptingFunctions.currentVersion);
//		deferredResult.addCallback(Clipperz.Async.setItem, result, 'lock', this.serverLockValue());
		deferredResult.callback();

		return deferredResult;
	},

	'prepareRemoteDataWithKeyFunction': function(aKeyFunction) {
		return new Clipperz.Async.callbacks("User.prepareRemoteDataWithKeyFunction", [
			aKeyFunction,
			MochiKit.Base.method(this, 'prepareRemoteDataWithKey')
		], {'trace': false})
	},

	//=========================================================================

	'saveChanges': function () {
		var	deferredResult;
		var messageParameters;
		
		messageParameters = {};

		deferredResult = new Clipperz.Async.Deferred("User.saveChangaes", {trace:false});

		deferredResult.addMethod(this, 'getHeaderIndex', 'recordsIndex');
		deferredResult.addCallback(MochiKit.Base.methodcaller('prepareRemoteDataForChangedRecords'));
		deferredResult.addCallback(Clipperz.Async.setItem, messageParameters, 'records');

		deferredResult.addMethod(this, 'getPassphrase');
		deferredResult.addMethod(this, 'prepareRemoteDataWithKey');
		deferredResult.addCallback(Clipperz.Async.setItem, messageParameters, 'user');

		deferredResult.addCallback(MochiKit.Async.succeed, messageParameters);
		deferredResult.addMethod(this.connection(), 'message', 'saveChanges');
		deferredResult.addCallback(MochiKit.Base.update, this.transientState())

		deferredResult.addMethod(this, 'commitTransientState');
		deferredResult.addErrbackPass(MochiKit.Base.method(this, 'revertChanges'));

		deferredResult.callback();

		return deferredResult;
	},

	//=========================================================================
	__syntaxFix__: "syntax fix"
});

//-----------------------------------------------------------------------------

Clipperz.PM.DataModel.User.registerNewAccount = function (anUsername, aPassphraseFunction) {
	var	deferredResult;
	var user;
	
	user = new Clipperz.PM.DataModel.User({'username':anUsername, 'getPassphraseFunction':aPassphraseFunction});

	deferredResult = new Clipperz.Async.Deferred("Clipperz.PM.DataModel.User.registerNewAccount", {trace:false});
	deferredResult.addMethod(user, 'registerAsNewAccount');
	deferredResult.addMethod(user, 'login');
	deferredResult.addCallback(MochiKit.Async.succeed, user);
	deferredResult.callback();
	
	return deferredResult;
}

//-----------------------------------------------------------------------------

Clipperz.PM.DataModel.User.exception = {
	LoginFailed:				new MochiKit.Base.NamedError("Clipperz.PM.DataModel.User.exception.LoginFailed"),
	CredentialUpgradeFailed:	new MochiKit.Base.NamedError("Clipperz.PM.DataModel.User.exception.CredentialUpgradeFailed") 
};
	
//-----------------------------------------------------------------------------
