	if(!com) var com = {};
	if(!com.qwidget) com.qwidget = {};

	com.qwidget.Model = Class.extend({
		url: '',
		dataOut: {},
		dataPass: {},
		callback: undefined,
		view: undefined,
		qwidMap: {},
		cqq: com.qwidget.qwids,
		moreData: false,
		page: 0,
		init: function()
		{
			var that = this;
			this.callback = function (dataIn)
			{
				that.updateMap(dataIn);
				if (that.view)
					that.view.refresh(that.qwidMap);
			}
		},
		refresh: function ()
		{
			this.updateDataOut();
			com.qwidget.ajax(com.qwidget.servicesUrl + this.url, this.dataOut, this.dataPass, this.callback);
		},
		updateMap: function ()
		{
			com.qwidget.warn('com.qwidget.Model.updateMap not over-ridden?');
		},
		dumpMap: function ()
		{
			com.qwidget.log('Model.dumpMap')
			for (x in this.qwidMap)
				com.qwidget.log('\t' + x + ': ' + this.qwidMap[x] + '\n');
		},
		updateDataOut: function ()
		{
		},
		changePage: function (mode)
		{
			// DRY - can this go in the dialog base class?
			switch (mode)
			{
			 case 'previous':
				if (this.page > 0)
					this.page--;
				break;
			 case 'next':
				if (this.moreData)
					this.page++;
				break;
			 default:
				com.qwidget.error('Model.page - mode is undefined');
				break;
			}
			this.refresh();
		}
	});

	com.qwidget.AccountModel = com.qwidget.Model.extend({
		credentials: undefined,
		init: function (credentials, view)
		{
			if (!credentials || !view) {com.qwidget.error('AccountModel.init - credentials and/or view undefined');}
			
			this.credentials = credentials;
			this.url = '/member/get.jsp';
			this.view = view;
			this._super();
		},
		updateCredentials: function(credentials)
		{
			this.credentials = credentials;
		},
		updateDataOut: function ()
		{
			var error = function (msg) {com.qwidget.error('AccountModel.updateDataOut - ' + msg);}

			// this data out supports the get.jsp call...
			this.dataOut =
			{
				id: this.credentials.memberId,
				email: this.credentials.memberEmail,
				password: this.credentials.memberPassword
			};
		},
		createAccount: function (qwidMap)
		{
			this.createUpdateAccount(qwidMap, 'create');
		},
		updateAccount: function (qwidMap)
		{
			this.createUpdateAccount(qwidMap, 'update');
		},
		createUpdateAccount: function (qwidMap, mode)
		{
			var error = function (msg) {com.qwidget.error('AccountModel.updateAccount - ' + msg);}
			var warn = function (msg) {com.qwidget.warn('AccountModel.updateAccount - ' + msg);}

			if (qwidMap[this.cqq.account_displayName] == '') {alert('Username is required'); return;}

			var emailNew = qwidMap[this.cqq.account_email];
			if (emailNew == '') {alert('Email Address is required'); return;}

			var passwordNew = qwidMap[this.cqq.account_password];
			var passwordConfirm = qwidMap[this.cqq.account_passwordConfirm];
			var autoLogIn = qwidMap[this.cqq.account_autoLogIn];
			
			var credentials = com.qwidget.AccountManager.credentials();
			
			switch (mode)
			{
			 case 'create':
				if (passwordNew == '') {alert("Password required"); return;}
				credentials.memberEmail = emailNew;
				credentials.memberPassword = passwordNew;
				qwidMap[this.cqq.account_responseNotification] = true; // default to notify during account creation...
				break;
			 case 'update':
				if (passwordNew != '' && passwordNew != passwordConfirm) {alert("Password does not match Confirm Password"); return;}
				if (autoLogIn)
					com.qwidget.AccountManager.save();
				break;
			 default:
				error('unknown mode - ' + mode);
				break;
			}
			
		   	var dataOut = {
				memberId: 				credentials.memberId, // not used for create
				email: 					credentials.memberEmail,
				password: 				credentials.memberPassword,
				newEmail: 				emailNew,
				newPassword: 			passwordNew,
				displayName: 			qwidMap[this.cqq.account_displayName],
				firstName: 				qwidMap[this.cqq.account_firstName],
				lastName: 				qwidMap[this.cqq.account_lastName],
				city: 					qwidMap[this.cqq.account_city],
				stateId: 				qwidMap[this.cqq.account_stateId],
				// TODO? - add state?
				postCode: 				qwidMap[this.cqq.account_postalCode],
				countryId: 				qwidMap[this.cqq.account_countryId],
				// gender property name is mismatched on webservices side...
				genderId: 				3, // create - 1: male, 2: female, 3: unspecified
				gender: 				3, // update - 1: male, 2: female, 3: unspecified
				contactPreferenceId: 	(qwidMap[this.cqq.account_responseNotification] ? 3 : 6) // map true -> immediate and false -> never
			};
			
			var dataPass = {};
			
			for (var i in dataOut)
			{
				if (!dataOut[i])
				{
					warn('dataOut['+i+'] undefined');
					dataOut[i] = '';
					//return;
				}
			}

			var that = this;
			url = com.qwidget.servicesUrl + (mode == 'create' ? '/member/create.jsp' : '/member/update.jsp')
			com.qwidget.ajax(url, dataOut, dataPass,
				function (dataIn, dataPass)
				{
					switch (dataIn.status)
					{
					 case 200:
						// that.view.hide ? that.view.hide() : error('view.hide undefined');
						if (mode == 'create')
							com.qwidget.AccountManager.logIn({memberId: dataIn.id, memberEmail: dataIn.email, memberPassword: passwordNew, memberDisplayName: dataIn.displayName}, autoLogIn)
						that.view.accountCreatedUpdated ? that.view.accountCreatedUpdated() : error('view.accountCreatedUpdated undefined');
						break;
					 case 900:
						that.view.displayNameInUse ? that.view.displayNameInUse() : error('view.displayNameInUse undefined');
						break;
					 case 901:
						that.view.emailInUse ? that.view.emailInUse() : error('view.emailInUse undefined');
						break;
					 case 902:
						that.view.usernameTooShort ? that.view.usernameTooShort() : error('view.usernameTooShort undefined');
						break;
					 case 903:
						that.view.usernameNonAlphanumeric ? that.view.usernameNonAlphanumeric() : error('view.usernameNonAlphanumeric undefined');
						break;
					 default:
						error('unknown dataIn.status: ' + dataIn.status + ': ' + dataIn.statusText);
						break;
					}
				}
			);
		},
		updateMap: function (dataIn)
		{
			this.qwidMap = {};
			this.qwidMap[this.cqq.account_firstName] 			= dataIn.firstName 	 || '';
			this.qwidMap[this.cqq.account_lastName] 			= dataIn.lastName 	 || '';
			this.qwidMap[this.cqq.account_displayName] 			= dataIn.displayName || '';
			this.qwidMap[this.cqq.account_image]	 			= dataIn.imageUrl 	 ? com.qwidget.imagesUrl + dataIn.imageUrl + 'medium.jpeg' : '';
			this.qwidMap[this.cqq.account_email] 				= dataIn.email 		 || '';
			this.qwidMap[this.cqq.account_city] 				= dataIn.city 		 || '';
			this.qwidMap[this.cqq.account_country] 				= dataIn.countryName || '';
			this.qwidMap[this.cqq.account_countryId] 			= dataIn.countryId 	 || '';
			this.qwidMap[this.cqq.account_countryCode] 			= dataIn.countryCode || '';
			this.qwidMap[this.cqq.account_state] 				= dataIn.stateName 	 || '';
			this.qwidMap[this.cqq.account_stateId] 				= dataIn.stateId 	 || '';
			this.qwidMap[this.cqq.account_stateCode] 			= dataIn.stateCode 	 || '';
			this.qwidMap[this.cqq.account_postalCode] 			= dataIn.postCode 	 || '';
			this.qwidMap[this.cqq.account_responseNotification]	= dataIn.contactPreferenceId == 3 ? true : false; 
			if (com.qwidget.AccountManager.isLoggedIn())
			{
				var credentials = com.qwidget.AccountManager.credentials();
				this.qwidMap[this.cqq.account_password]				= credentials.memberPassword;
				this.qwidMap[this.cqq.account_passwordConfirm]		= credentials.memberPassword;
			}
		}
	});

	com.qwidget.MessagesModel = com.qwidget.Model.extend({
		showRead: true,
		showUnRead: true,
		showArchived: false,
		page: 0,
		messagesPerPage: 7,
		moreMessages: true,
		init: function (view)
		{
			this.view = view;
			
			this._super();
		},
		refresh: function ()
		{
			this._super();
		},
		toggleShowArchived: function ()
		{
			this.showArchived = this.showArchived ? false : true;
			this.refresh();
			return this.showArchived;
		},
		updateDataOut: function ()
		{
			var credentials = com.qwidget.AccountManager.credentials();
			this.dataOut =
			{
				email:			credentials.memberEmail,
				password:		credentials.memberPassword,
				showRead: 		this.showRead,
				showUnRead: 	this.showUnRead,
				showArchived: 	this.showArchived,
				offset: 		this.page * 7,
				count: 			this.messagesPerPage + 1
			};
			return credentials.memberId;
		},
		messageItem: function (qwidMap, envelope, threadId, isArchived)
		{
			qwidMap[this.cqq.message_threadId] = threadId;
			
			var credentials = com.qwidget.AccountManager.credentials();
			var member = envelope.toMember.id == credentials.memberId ? envelope.fromMember : envelope.toMember;
			
			qwidMap[this.cqq.message_item_id]			= envelope.message.id;
			qwidMap[this.cqq.message_item_name] 		= member.displayName 						|| '{displayName}';
			qwidMap[this.cqq.message_item_photo]		= member.imageUrl ? com.qwidget.imagesUrl + member.imageUrl + 'small.jpeg' : '';
			qwidMap[this.cqq.message_item_location]		= member.city 								|| '';
			qwidMap[this.cqq.message_item_subject] 		= envelope.message.subject 					|| '{subject}';
			var messageBody = envelope.message.body || '{body}';
			qwidMap[this.cqq.message_item_preview] 		= (messageBody.length < 70 ? messageBody : messageBody.slice(0, 60) + '...');
			qwidMap[this.cqq.message_item_date] 		= envelope.message.createdOn.slice(0, 10) 	|| '{date}';
			// always mark read if this message is from the viewer
			qwidMap[this.cqq.message_item_read]			= envelope.fromMember.id == credentials.memberId ? true: envelope.message.read;
			// mark repliedTo if this message is from the viewer...
			qwidMap[this.cqq.message_item_repliedTo]	= envelope.fromMember.id == credentials.memberId ? true : false;
			qwidMap[this.cqq.message_item_archived]		= isArchived;
		},
		messageDetail: function (qwidMap, envelope)
		{
			var formatDetailFrom = function (member)
			{
				var name = member.displayName || '{from.displayName}';
				var city = member.city || '';
				var country = member.countryName || '';
				return name + (city != '' ? ' - ' : '') + city + (country != '' ? ', ' : '') + country;
			}
			qwidMap[this.cqq.message_detail_id]			= envelope.message.id;
			qwidMap[this.cqq.message_detail_responseId]	= envelope.message.responseId;
			qwidMap[this.cqq.message_detail_from] 		= formatDetailFrom(envelope.fromMember);
			qwidMap[this.cqq.message_detail_fromId] 	= envelope.fromMember.id;
			qwidMap[this.cqq.message_detail_photo]		= envelope.fromMember.imageUrl ? com.qwidget.imagesUrl + envelope.fromMember.imageUrl + 'medium.jpeg' : '';
			qwidMap[this.cqq.message_detail_to] 		= envelope.toMember.displayName				|| '{to.displayName}';
			qwidMap[this.cqq.message_detail_toId] 		= envelope.toMember.id;
			qwidMap[this.cqq.message_detail_subject] 	= envelope.message.subject 					|| '{subject}';
			qwidMap[this.cqq.message_detail_body] 		= envelope.message.body 					|| '{body}';
			qwidMap[this.cqq.message_detail_date] 		= envelope.message.createdOn.slice(0, 10) 	|| '{date}';
		}
	});
	com.qwidget.InboxMessagesModel = com.qwidget.MessagesModel.extend({  // DRY me!
		init: function (view)
		{
			this.url = '/discussion/message/find_threads.jsp';
			this._super(view);
		},
		updateMap: function (dataIn)
		{
			this.qwidMap = {};
			var qwidMaps = [];
			if (dataIn.status == undefined)
			{
				this.moreData = dataIn.hasMoreData;
				var threads = dataIn.threads;
				for (var i = 0; i < threads.length; i++)
				{
					var envelopes = threads[i].messages;
					var qwidMap = {};
					// the first envelope may be from the user - if so skip it
					//var credentials = com.qwidget.AccountManager.credentials();
					//if (envelopes[0].fromMember.id == credentials.memberId)
					//	envelopes.shift();
					// use first envelope for the message item list
					this.messageItem(qwidMap, envelopes[0], threads[i].threadId, threads[i].isArchived);
					var messageHistory = [];
					for (var j = 0; j < envelopes.length; j++)
					{
						if (j == 0)
						{
							// use first envelope for the message detail
							this.messageDetail(qwidMap, envelopes[0]);
						}
						else
						{
							// put any other envelopes in message history
							var history = {};
							this.messageDetail(history, envelopes[j]);
							messageHistory.push(history);
						}
					}
					qwidMap[this.cqq.message_detail_history] = messageHistory;
					qwidMaps.push(qwidMap);
				}
			}
			else
			{
				// web services returns 404 if there are no messages 
				// wouldn't it be preferable to return an empty array in messages?
				com.qwidget.log('MessagesModel.updateMap - status: '+dataIn.status+' ('+dataIn.statusText+')')
			}
			this.qwidMap[this.cqq.messages_data] = qwidMaps;
		},
		updateDataOut: function ()
		{
			var memberId = this._super();
			this.dataOut.toMemberId = memberId;
		}
	});
	com.qwidget.OutboxMessagesModel = com.qwidget.MessagesModel.extend({
		// DRY me!
		init: function (view)
		{
			this.url = '/discussion/message/find.jsp';
			this._super(view);
		},
		// updateMap: function(dataIn)
		// {
		// 	this._super(dataIn, 'outbox');
		// },
		updateMap: function (dataIn)
		{
			this.qwidMap = {};
			var qwidMaps = [];
			if (dataIn.status == undefined)
			{
				var envelopes = dataIn.messages; // TODO? - shouldn't this be envelopes or some such?
				this.moreData = dataIn.hasMoreData;
				for (var i = 0; i < Math.min(envelopes.length, this.messagesPerPage); i++)
				{
					var envelope = envelopes[i];
					var qwidMap = {};
					
					this.messageItem(qwidMap, envelope, undefined, envelope.message.archivedSent);
					this.messageDetail(qwidMap, envelope);
			
					qwidMaps.push(qwidMap);
				}
			}
			else
			{
				// web services returns 404 if there are no messages 
				// wouldn't it be preferable to return an empty array in messages?
				com.qwidget.log('MessagesModel.updateMap - status: '+dataIn.status+' ('+dataIn.statusText+')')
			}
			this.qwidMap[this.cqq.messages_data] = qwidMaps;
		},
		updateDataOut: function ()
		{
			var memberId = this._super();
			this.dataOut.fromMemberId = memberId;
		}
	});

	
	com.qwidget.SurveyModel = com.qwidget.Model.extend({
		questionId: 0,
		memberAnswerId: 0,
		init: function (questionId, view)
		{
			this.questionId = questionId;
			var that = this;
			this.dataOut = {id: questionId};
			if (com.qwidget.AccountManager.isLoggedIn())
			{
				var credentials = com.qwidget.AccountManager.credentials();
				this.dataOut['memberId'] = credentials.memberId;
			}
			else
			{
				com.qwidget.log("SurveyModel.init - user not logged in");
			}
			this.url = '/question/get.jsp';
			this.view = view;
			this._super();
		},
		updateMap: function (dataIn)
		{
			this.qwidMap = {};
			this.qwidMap[this.cqq.survey_questionId] = this.questionId;
			this.qwidMap[this.cqq.survey_question]	= dataIn.body;
			this.qwidMap[this.cqq.survey_data]	= dataIn.answers; // TODO? - process answer data here...
			this.memberAnswerId = dataIn.memberAnswerId;
			this.qwidMap[this.cqq.survey_memberAnswerId] = this.memberAnswerId;
		},
		questionAnswered: function ()
		{
			return this.memberAnswerId > 0;
		}
	});

	com.qwidget.ResponsesModel = com.qwidget.Model.extend({
		//totalResponses: -1,
		//page: 0,
		init: function (questionId, view)
		{
			var that = this;
			this.dataOut = {email: '', password: '', memberId: com.qwidget.guestUserId, questionId: questionId, offset: 0, count: 3};
			this.filterUrls =
			{
				recent: 	'/response/find_most_recent.jsp',
				agree: 		'/response/find_most_agree.jsp',	// TODO - need to replace find_most_popular.jsp with find_most_agree.jsp...
				disagree: 	'/response/find_most_disagree.jsp'	// TODO - need to replace find_random.jsp with find_most_disagree.jsp...
			}
			this.url = this.filterUrls.recent; // default to most recent
			this.view = view;
			this._super();
		},
		updateMap: function (dataIn)
		{
			this.qwidMap = {};
			
			var getAnswer = function (id)
			{
				// TODO - pass the answer text in the json call...
				var answers = ['Maybe', 'Yes', 'No'];
				return answers[id%3];
			}
			var responses = [];
			
			if (dataIn.responses) // webservice doesn't return a response object when there are no responses...
			{
				for (var i = 0; i < dataIn.responses.length; i++)
				{
					var r = dataIn.responses[i];
				
					if (r.memberId == com.qwidget.guestUserId)
					{
						com.qwidget.info('ResponsesModel.updateMap - skipped guest user...'); // should we get responses from guest users?
						continue; // skip responses from guest users
					}
				
					this.moreData = r.hasMoreData;
				
					var stateCode = '';
					switch (r.stateCode)
					{
					case undefined:
					case '__':
						break;
					default:
						stateCode = r.stateCode;
						break;
					}

					var response = {}; // the response data to be sent to the view
					response[this.cqq.response_id]				= r.id;
					response[this.cqq.responder_item_memberId] 	= r.memberId;
					response[this.cqq.responder_item_name]		= r.displayName;
					response[this.cqq.responder_item_location] 	= (r.city ? r.city + ', ' : '') + stateCode + '<br/>' + (r.countryName ? r.countryName : ''); 
					response[this.cqq.responder_item_photo] 	= (r.imageUrl ? com.qwidget.imagesUrl + r.imageUrl + 'medium.jpeg' : '');
					response[this.cqq.responder_item_answer] 	= getAnswer(r.answerId);
					response[this.cqq.responder_item_comment]	= r.comment;
					response[this.cqq.responder_item_permalink]	= r.permalink;
					response[this.cqq.response_agreeCount] 		= r.agreeCount;
					response[this.cqq.response_disagreeCount] 	= r.disagreeCount;
					response[this.cqq.response_memberReplyDate] = (r.lastReplyDate == null ? undefined : r.lastReplyDate);
					response[this.cqq.response_reply_data]		= {responseId: r.id, toMemberId: r.memberId};
					if (com.qwidget.AccountManager.isLoggedIn())
						response[this.cqq.response_viewerAgreed] 	= r.responseOpinions.length == 0 ? undefined : (r.responseOpinions[0].agree == 1 ? true : false); // what should we do if responseOpinions.length > 1?
					else
						response[this.cqq.response_viewerAgreed]	= com.qwidget.GuestResponseOpinions.responseOpinion(r.id);
				
					responses.push(response);
				}
			}
			this.qwidMap[this.cqq.responses_data] = responses;
		},
		updateDataOut: function ()
		{
			var credentials = com.qwidget.AccountManager.credentials();
			this.dataOut.memberId = credentials.memberId;
			this.dataOut.offset = this.page * 3;
			this.count = 3;
		},
		refresh: function (filter)
		{
			var error = function (msg) {com.qwidget.error('ResponsesModel.refresh - ' + msg);}

			if (!filter)
				filter = 'recent';

			// select service url for the specified filter
			if (this.filterUrls[filter])
				this.service_url = this.filterUrls[filter];
			else
				return error('unknown filter: ' + filter);

			this._super();
		},
		filter: function (mode)
		{
			this.url = this.filterUrls[mode];
			if (this.url == undefined)
			{
				com.qwidget.error('ResponsesModel.filter - unknown mode: ' + mode);
				this.url = this.filterUrls.recent;
			}
			this.page = 0;
			this.refresh();
		},
		agreeDisagreeFlag: function (responseId, responderId, mode, forceSwap)
		{
			var url = com.qwidget.servicesUrl;
			var dataOut = undefined;
			var dataPass = {
				mode: mode
			};
			var credentials = com.qwidget.AccountManager.credentials();
			switch (mode)
			{
			 case 'agree':
			 case 'disagree':
				url += '/response/opinion/create.jsp';
				var dataOut = {
					memberId: 	credentials.memberId,
					email: 		credentials.memberEmail,
					password: 	credentials.memberPassword,
					responseId: responseId,
					agree: 		(mode == 'agree' ? 1 : 0),
					forceSwap: 	forceSwap
				};
				break;
			 case 'flag':
				url += '/member/flag/create.jsp';
				dataOut = {
					memberId: 	credentials.memberId,
					email: 		credentials.memberEmail,
					password: 	credentials.memberPassword,
					flaggerId: 	com.qwidget.AccountManager.memberId,
					flaggeeId: 	responderId,
					reason: 	'Flagged via the responses list.'
				};
				break;
			 default:
				com.qwidget.error('ResponsesMode.agreeDisagreeFlag - unknown mode: ' + mode);
				return;
				break;
			}
			var callback = function (dataIn, dataPass)
			{
				if (dataPass.mode == 'flag')
					alert("You have flagged this user and/or response as inappropriate. Thanks for the helping us keep the conversations here sane.  We will review the username, picture and response and moderate as necessary.");
			}
			com.qwidget.ajax(url, dataOut, dataPass, callback);
			{
				// TODO - remove an agree/disagree for the anonymous user...
				//com.qwidget.ajax(url, dataOut, dataPass, callback);
				com.qwidget.GuestResponseOpinions.store(responseId, mode == 'agree');
			}
		}
	});
	
	com.qwidget.ResultsModel = com.qwidget.Model.extend({
		init: function (questionId, view)
		{
			var that = this;
			this.dataOut = {id: questionId};
			this.url = '/question/get.jsp';
			this.view = view;
			this._super();
		},
		updateMap: function (dataIn)
		{
			var answers = dataIn.answers;
			var results = [];
			var grandTotal = 0;
			for (var i = 0; i < answers.length; i++)
			{
				var answer = answers[i];
				var result = {};
				result[this.cqq.results_list_item_answer] = answer.text
				result[this.cqq.results_list_item_total] = answer.responseCount;
				result[this.cqq.results_list_item_percent] = 0;
				results.push(result);
				grandTotal += answer.responseCount;
			}
			// touch up the percentages
			if (grandTotal > 0)
			{
				for (var i = 0; i < results.length; i++)
				{
					var pct = (results[i][this.cqq.results_list_item_total])/(grandTotal/100);
					results[i][this.cqq.results_list_item_percent] = (pct+0.05+"").replace(/^(.*\..).*$/,"$1%"); // something like ssprintf(s, "%f.1%%", x)
				}
			}
			this.qwidMap[this.cqq.results_data] = results;
		}
	});
	
