OC.Contacts = OC.Contacts || {};


(function(window, $, OC) {
	'use strict';
	/**
	* An item which binds the appropriate html and event handlers
	* @param parent the parent ContactList
	* @param id The integer contact id.
	* @param metadata An metadata object containing and 'owner' string variable, a 'backend' string variable and an integer 'permissions' variable.
	* @param data the data used to populate the contact
	* @param listtemplate the jquery object used to render the contact list item
	* @param fulltemplate the jquery object used to render the entire contact
	* @param detailtemplates A map of jquery objects used to render the contact parts e.g. EMAIL, TEL etc.
	*/
	var Contact = function(parent, id, metadata, data, listtemplate, dragtemplate, fulltemplate, detailtemplates) {
		//console.log('contact:', id, metadata, data); //parent, id, data, listtemplate, fulltemplate);
		this.parent = parent,
			this.storage = parent.storage,
			this.id = id,
			this.metadata = metadata,
			this.data = data,
			this.$dragTemplate = dragtemplate,
			this.$listTemplate = listtemplate,
			this.$fullTemplate = fulltemplate;
			this.detailTemplates = detailtemplates;
			this.displayNames = {};
			this.sortOrder = contacts_sortby || 'fn';
		this.undoQueue = [];
		this.multi_properties = ['EMAIL', 'TEL', 'IMPP', 'ADR', 'URL'];
	};

	Contact.prototype.metaData = function() {
		return {
			contactId: this.id,
			addressBookId: this.metadata.parent,
			backend: this.metadata.backend
		};
	};

	Contact.prototype.getDisplayName = function() {
		return this.displayNames[this.sortOrder];
	};

	Contact.prototype.setDisplayMethod = function(method) {
		if(this.sortOrder === method) {
			return;
		}
		this.sortOrder = method;
		// ~30% faster than jQuery.
		try {
			this.$listelem.get(0).firstElementChild.getElementsByClassName('nametext')[0].innerHTML = escapeHTML(this.displayNames[method]);
			this.setThumbnail();
		} catch(e) {
			var $elem = this.$listelem.find('.nametext').text(escapeHTML(this.displayNames[method]));
			$elem.text(escapeHTML(this.displayNames[method]));
		}
	};

	Contact.prototype.getId = function() {
		return this.id;
	};

	Contact.prototype.getOwner = function() {
		return this.metadata.owner;
	};

	Contact.prototype.setOwner = function(owner) {
		this.metadata.owner = owner;
	};

	Contact.prototype.getPermissions = function() {
		return this.metadata.permissions;
	};

	Contact.prototype.hasPermission = function(permission) {
		//console.log('hasPermission', this.getPermissions(), permission, this.getPermissions() & permission);
		return (this.getPermissions() & permission);
	};

	Contact.prototype.getParent = function() {
		return this.metadata.parent;
	};

	Contact.prototype.setParent = function(parent) {
		this.metadata.parent = parent;
	};

	Contact.prototype.getBackend = function() {
		return this.metadata.backend;
	};

	Contact.prototype.setBackend = function(backend) {
		this.metadata.backend = backend;
	};

	Contact.prototype.hasPhoto = function() {
		return (this.getId() !== 'new' && this.data && this.data.photo) || false;
	};

	Contact.prototype.isOpen = function() {
		return this.$fullelem !== null;
	};

	Contact.prototype.reload = function(data) {
		console.log('Contact.reload', data);
		this.id = data.metadata.id;
		this.metadata = data.metadata;
		this.data = data.data;
		/*if(this.$fullelem) {
			this.$fullelem.replaceWith(this.renderContact(this.groupprops));
		}*/
	};

	Contact.prototype.merge = function(mergees) {
		console.log('Contact.merge, mergees', mergees);
		if(!mergees instanceof Array && !mergees instanceof Contact) {
			throw new TypeError('BadArgument: Contact.merge() only takes Contacts');
		} else {
			if(mergees instanceof Contact) {
				mergees = [mergees];
			}
		}

		// For multi_properties
		var addIfNotExists = function(name, newproperty) {
			// If the property isn't set at all just add it and return.
			if(!self.data[name]) {
				self.data[name] = [newproperty];
				return;
			}
			var found = false;
			$.each(self.data[name], function(idx, property) {
				if(name === 'ADR') {
					// Do a simple string comparison
					if(property.value.join(';').toLowerCase() === newproperty.value.join(';').toLowerCase()) {
						found = true;
						return false; // break loop
					}
				} else {
					if(property.value.toLowerCase() === newproperty.value.toLowerCase()) {
						found = true;
						return false; // break loop
					}
				}
			});
			if(found) {
				return;
			}
			// Not found, so adding it.
			self.data[name].push(newproperty);
		};

		var self = this;
		$.each(mergees, function(idx, mergee) {
			console.log('Contact.merge, mergee', mergee);
			if(!mergee instanceof Contact) {
				throw new TypeError('BadArgument: Contact.merge() only takes Contacts');
			}
			if(mergee === self) {
				throw new Error('BadArgument: Why should I merge with myself?');
			}
			$.each(mergee.data, function(name, properties) {
				if(self.multi_properties.indexOf(name) === -1) {
					if(self.data[name] && self.data[name].length > 0) {
						// If the property exists don't touch it.
						return true; // continue
					} else {
						// Otherwise add it.
						self.data[name] = properties;
					}
				} else {
					$.each(properties, function(idx, property) {
						addIfNotExists(name, property);
					});
				}
			});
			console.log('Merged', self.data);
		});
		return true;
	};

	Contact.prototype.showActions = function(act) {
		// non-destructive merge.
		var $actions = $.merge($.merge([], this.$footer.children()), this.$header.children());
		$.each($actions, function(idx, action) {
			$(action).hide();
			$.each(act, function(i, a) {
				if($(action).hasClass(a)) {
					$(action).show();
					return false; // break
				}
			});
		});
	};

	Contact.prototype.setAsSaving = function(obj, state) {
		if(!obj) {
			return;
		}
		$(obj).prop('disabled', state);
		$(obj).toggleClass('loading', state);
		/*if(state) {
			$(obj).addClass('loading');
		} else {
			$(obj).removeClass('loading');
		}*/
	};

	Contact.prototype.handleURL = function(obj) {
		if(!obj) {
			return;
		}
		var $container = this.propertyContainerFor(obj);
		$(document).trigger('request.openurl', {
			type: $container.data('element'),
			url: this.valueFor(obj)
		});
	};

	/**
	 * Update group name internally. No saving as this is done by groups backend.
	 */
	Contact.prototype.renameGroup = function(from, to) {
		if(!this.data.CATEGORIES.length) {
			console.warn(this.getDisplayName(), 'had no groups!?!');
			return;
		}
		var groups = this.data.CATEGORIES[0].value;
		var self = this;
		$.each(groups, function(idx, group) {
			if(from.toLowerCase() === group.toLowerCase()) {
				console.log('Updating group name for', self.getDisplayName(), group, to);
				self.data.CATEGORIES[0].value[idx] = to;
				return false; // break
			}
		});
		$(document).trigger('status.contact.updated', {
			property: 'CATEGORIES',
			contact: this
		});
	};

	Contact.prototype.pushToUndo = function(params) {
		// Check if the same property has been changed before
		// and update it's checksum if so.
		if(typeof params.oldchecksum !== 'undefined') {
			$.each(this.undoQueue, function(idx, item) {
				if(item.checksum === params.oldchecksum) {
					item.checksum = params.newchecksum;
					if(params.action === 'delete') {
						item.action = 'delete';
					}
					return false; // Break loop
				}
			});
		}
		this.undoQueue.push({
			action:params.action, 
			name: params.name,
			checksum: params.newchecksum,
			newvalue: params.newvalue,
			oldvalue: params.oldvalue
		});
		//console.log('undoQueue', this.undoQueue);
	};
	
	Contact.prototype.addProperty = function($option, name) {
		console.log('Contact.addProperty', name);
		var $elem, $list;
		switch(name) {
			case 'NICKNAME':
			case 'TITLE':
			case 'ORG':
			case 'BDAY':
			case 'NOTE':
				$elem = this.$fullelem.find('[data-element="' + name.toLowerCase() + '"]');
				$elem.addClass('new').show();
				$list = this.$fullelem.find('ul.' + name.toLowerCase());
				$list.show();
				$elem.find('input:not(:checkbox),textarea').first().focus();
				$option.prop('disabled', true);
				break;
			case 'TEL':
			case 'URL':
			case 'EMAIL':
				$elem = this.renderStandardProperty(name.toLowerCase());
				$list = this.$fullelem.find('ul.' + name.toLowerCase());
				$list.show();
				$list.append($elem);
				$elem.find('input.value').addClass('new');
				$elem.find('input:not(:checkbox)').first().focus();
				break;
			case 'ADR':
				$elem = this.renderAddressProperty();
				$list = this.$fullelem.find('ul.' + name.toLowerCase());
				$list.show();
				$list.append($elem);
				$elem.find('.display').trigger('click');
				$elem.find('input.value').addClass('new');
				$elem.find('input:not(:checkbox)').first().focus();
				break;
			case 'IMPP':
				$elem = this.renderIMProperty();
				$list = this.$fullelem.find('ul.' + name.toLowerCase());
				$list.show();
				$list.append($elem);
				$elem.find('input.value').addClass('new');
				$elem.find('input:not(:checkbox)').first().focus();
				break;
		}

		if($elem) {
			// If there's already a property of this type enable setting as preferred.
			if(this.multi_properties.indexOf(name) !== -1 && this.data[name] && this.data[name].length > 0) {
				var selector = 'li[data-element="' + name.toLowerCase() + '"]';
				$.each(this.$fullelem.find(selector), function(idx, elem) {
					$(elem).find('input.parameter[value="PREF"]').show();
				});
			} else if(this.multi_properties.indexOf(name) !== -1) {
				$elem.find('input.parameter[value="PREF"]').hide();
			}
			$elem.find('select.type[name="parameters[TYPE][]"], select.type[name="parameters[X-SERVICE-TYPE]"]')
				.combobox({
					singleclick: true,
					classes: ['propertytype', 'float', 'label'],
				});
		}
	};

	Contact.prototype.deleteProperty = function(params) {
		var obj = params.obj;
		if(!this.enabled) {
			return;
		}
		var element = this.propertyTypeFor(obj);
		var $container = this.propertyContainerFor(obj);
		console.log('Contact.deleteProperty, element', element, $container);
		params.name = element;
		params.value = null;

		if(this.multi_properties.indexOf(element) !== -1) {
			params.checksum = this.checksumFor(obj);
			if(params.checksum === 'new' && $.trim(this.valueFor(obj)) === '') {
				// If there's only one property of this type enable setting as preferred.
				if((undefined !== this.data[element] && this.data[element].length) && (this.data[element].length === 1)) {
					var selector = 'li[data-element="' + element.toLowerCase() + '"]';
					this.$fullelem.find(selector).find('input.parameter[value="PREF"]').hide();
				}
				// Hide propertygroup if there are no properties in it
				if(!(undefined !== this.data[element] && this.data[element].length)) {
					$(obj).parent().parent().parent().hide();
				}
				else if(this.data[element].length === 0) {
					$(obj).parent().parent().parent().hide();
				}
				$container.remove();
				return;
			}
		}
		this.setAsSaving(obj, true);
		var self = this;
		$.when(this.storage.patchContact(this.metadata.backend, this.metadata.parent, this.id, params))
			.then(function(response) {
			if(!response.error) {
				if(self.multi_properties.indexOf(element) !== -1) {
					// First find out if an existing element by looking for checksum
					var checksum = self.checksumFor(obj);
					self.pushToUndo({
						action:'delete', 
						name: element,
						oldchecksum: self.checksumFor(obj),
						newvalue: self.valueFor(obj)
					});
					if(checksum) {
						for(var i in self.data[element]) {
							if(self.data[element][i].checksum === checksum) {
								// Found it
								self.data[element].splice(self.data[element].indexOf(self.data[element][i]), 1);
								break;
							}
						}
					}
					// If there's only one property of this type enable setting as preferred.
					if((undefined !== self.data[element] && self.data[element].length) && (self.data[element].length === 1)) {
						var selector = 'li[data-element="' + element.toLowerCase() + '"]';
						self.$fullelem.find(selector).find('input.parameter[value="PREF"]').hide();
					}
					// Hide propertygroup if there are no properties in it
					if(!(undefined !== self.data[element] && self.data[element].length)) {
						$(obj).parent().parent().parent().hide();
					}
					else if(self.data[element].length === 0) {
						$(obj).parent().parent().parent().hide();
					}
					$container.remove();
				} else {
					self.pushToUndo({
						action:'delete', 
						name: element,
						newvalue: $container.find('input.value').val()
					});
					self.setAsSaving(obj, false);
					if(element === 'PHOTO') {
						self.data.photo = false;
						self.data.thumbnail = null;
					} else {
						self.$fullelem.find('[data-element="' + element.toLowerCase() + '"]').hide();
						$container.find('input.value').val('');
						self.$addMenu.find('option[value="' + element.toUpperCase() + '"]').prop('disabled', false);
					}
				}
				$(document).trigger('status.contact.updated', {
					property: element,
					contact: self
				});
				return true;
			} else {
				$(document).trigger('status.contacts.error', response);
				self.setAsSaving(obj, false);
				return false;
			}
		})
		.fail(function(response) {
			console.log(response.message);
			$(document).trigger('status.contacts.error', response);
		});
	};

	/**
	 * @brief Save all properties. Used for merging contacts.
	 * If this is a new contact it will first be saved to the datastore and a
	 * new datastructure will be added to the object.
	 */
	Contact.prototype.saveAll = function(cb) {
		console.log('Contact.saveAll');
		var self = this;
		if(!this.id) {
			this.add({isnew:true}, function(response) {
				if(response.error) {
					console.warn('No response object');
					return false;
				}
				self.saveAll();
			});
			return;
		}

		this.setAsSaving(this.$fullelem, true);

		$.when(this.storage.saveAllProperties(this.metadata.backend, this.metadata.parent, this.id, {data:this.data}))
			.then(function(response) {
			if(!response.error) {
				self.data = response.data.data;
				self.metadata = response.data.metadata;
				if(typeof cb === 'function') {
					cb({error:false});
				}
			} else {
				$(document).trigger('status.contacts.error', {
					message: response.message
				});
				if(typeof cb === 'function') {
					cb({error:true, message:response.message});
				}
			}
			self.setAsSaving(self.$fullelem, false);
		});
	};

	/**
	 * @brief Act on change of a property.
	 * If this is a new contact it will first be saved to the datastore and a
	 * new datastructure will be added to the object.
	 * If the obj argument is not provided 'name' and 'value' MUST be provided
	 * and this is only allowed for single elements like N, FN, CATEGORIES.
	 * @param obj. The form form field that has changed.
	 * @param name. The optional name of the element.
	 * @param value. The optional value.
	 */
	Contact.prototype.saveProperty = function(params) {
		console.log('Contact.saveProperty', params);
		var self = this;

		if(!this.id) {
			this.add({isnew:true}, function(response) {
				if(!response || response.status === 'error') {
					console.warn('No response object');
					return false;
				}
				self.saveProperty(params);
				self.showActions(['close', 'add', 'export', 'delete']);
			});
			return;
		}
		var obj = null;
		var element = null;
		var args = [];

		if(params.obj) {
			obj = params.obj;
			args = this.argumentsFor(obj);
			//args['parameters'] = $.param(this.parametersFor(obj));
			element = this.propertyTypeFor(obj);
		} else {
			args = params;
			element = params.name;
		}

		if(!args) {
			console.log('No arguments. returning');
			return false;
		}
		console.log('args', args);

		this.setAsSaving(obj, true);
		$.when(this.storage.patchContact(this.metadata.backend, this.metadata.parent, this.id, args))
			.then(function(response) {
			if(!response.error) {
				if(!self.data[element]) {
					self.data[element] = [];
				}
				if(self.multi_properties.indexOf(element) !== -1) {
					// First find out if an existing element by looking for checksum
					var checksum = self.checksumFor(obj);
					var value = self.valueFor(obj);
					var parameters = self.parametersFor(obj);
					if(parameters.TYPE && parameters.TYPE.indexOf('PREF') !== -1) {
						parameters.PREF = 1;
						parameters.TYPE.splice(parameters.TYPE.indexOf('PREF', 1));
					}
					if(checksum && checksum !== 'new') {
						self.pushToUndo({
							action:'save', 
							name: element,
							newchecksum: response.data.checksum,
							oldchecksum: checksum,
							newvalue: value,
							oldvalue: obj.defaultValue
						});
						$.each(self.data[element], function(i, el) {
							if(el.checksum === checksum) {
								self.data[element][i] = {
									name: element,
									value: value,
									parameters: parameters,
									checksum: response.data.checksum
								};
								return false;
							}
						});
					} else {
						$(obj).removeClass('new');
						self.pushToUndo({
							action:'add', 
							name: element,
							newchecksum: response.data.checksum,
							newvalue: value,
						});
						self.data[element].push({
							name: element,
							value: value,
							parameters: parameters,
							checksum: response.data.checksum,
						});
					}
					self.propertyContainerFor(obj).data('checksum', response.data.checksum);
				} else {
					// Save value and parameters internally
					var value = obj ? self.valueFor(obj) : params.value;
					self.pushToUndo({
						action: ((obj && obj.defaultValue) || self.data[element].length) ? 'save' : 'add', // FIXME
						name: element,
						newvalue: value,
					});
					switch(element) {
						case 'CATEGORIES':
							// We deal with this in addToGroup()
							break;
						case 'BDAY':
							// reverse order again.
							value = $.datepicker.formatDate('yy-mm-dd', $.datepicker.parseDate(datepickerFormatDate, value));
							self.data[element][0] = {
								name: element,
								value: value,
								parameters: self.parametersFor(obj),
								checksum: response.data.checksum
							};
							break;
						case 'FN':
							if(!self.data.FN || !self.data.FN.length) {
								self.data.FN = [{name:'FN', value:'', parameters:[]}];
							}
							self.data.FN[0].value = value;
							// Used for sorting list elements
							self.displayNames.fn = value;
							var nempty = true;
							if(!self.data.N) {
								// TODO: Maybe add a method for constructing new elements?
								self.data.N = [{name:'N',value:['', '', '', '', ''],parameters:[]}];
							}
							$.each(self.data.N[0].value, function(idx, val) {
								if(val) {
									nempty = false;
									return false;
								}
							});
							if(nempty) {
								self.data.N[0].value = ['', '', '', '', ''];
								var nvalue = value.split(' ');
								// Very basic western style parsing. I'm not gonna implement
								// https://github.com/android/platform_packages_providers_contactsprovider/blob/master/src/com/android/providers/contacts/NameSplitter.java ;)
								self.data.N[0].value[0] = nvalue.length > 2 && nvalue.slice(nvalue.length-1).toString() || nvalue[1] || '';
								self.data.N[0].value[1] = nvalue[0] || '';
								self.data.N[0].value[2] = nvalue.length > 2 && nvalue.slice(1, nvalue.length-1).join(' ') || '';
								setTimeout(function() {
									self.saveProperty({name:'N', value:self.data.N[0].value.join(';')});
								}, 500);
							}
							// If contacts doesn't have a photo load new avatar
							if(!self.hasPhoto() && self.sortOrder === 'fn') {
								self.loadAvatar();
							}
							break;
						case 'N':
							if(!utils.isArray(value)) {
								// Then it is auto-generated from FN.
								value = value.split(';');

								$.each(value, function(idx, val) {
									self.$fullelem.find('#n_' + idx).val(val).get(0).defaultValue = val;
								});
							}

							// Used for sorting list elements
							self.displayNames.fl = value.slice(0, 2).reverse().join(' ');
							self.displayNames.lf = value.slice(0, 2).join(', ').trim();

							var $fullname = self.$fullelem.find('.fullname');
							var update_fn = false;
							if(!self.data.FN) {
								self.data.FN = [{name:'FN', value:'', parameters:[]}];
							}
							/* If FN is empty fill it with the values from N.
							 * As N consists of several fields which each trigger a change/save
							 * also check if the contents of FN equals parts of N and fill
							 * out the rest.
							 */
							if(self.data.FN[0].value === '') {
								self.data.FN[0].value = value[1] + ' ' + value[0];
								$fullname.val(self.data.FN[0].value);
								update_fn = true;
							} else if($fullname.val() === value[1] + ' ') {
								self.data.FN[0].value = value[1] + ' ' + value[0];
								$fullname.val(self.data.FN[0].value);
								update_fn = true;
							} else if($fullname.val() === ' ' + value[0]) {
								self.data.FN[0].value = value[1] + ' ' + value[0];
								$fullname.val(self.data.FN[0].value);
								update_fn = true;
							}
							if(update_fn) {
								setTimeout(function() {
									self.saveProperty({name:'FN', value:self.data.FN[0].value});
								}, 1000);
							}
							if(!self.hasPhoto() && self.sortOrder !== 'fn') {
								self.loadAvatar();
							}
						case 'NICKNAME':
							/* falls through */
						case 'ORG':
							// Auto-fill FN if empty
							if(!self.data.FN) {
								self.data.FN = [{name:'FN', value:value, parameters:[]}];
								self.$fullelem.find('.fullname').val(value).trigger('change');
							}
						case 'TITLE':
							/* falls through */
						case 'NOTE':
							self.data[element][0] = {
								name: element,
								value: value,
								parameters: self.parametersFor(obj),
								checksum: response.data.checksum
							};
							break;
						default:
							break;
					}
				}
				self.setAsSaving(obj, false);
				$(document).trigger('status.contact.updated', {
					property: element,
					contact: self
				});
				return true;
			} else {
				$(document).trigger('status.contacts.error', response);
				self.setAsSaving(obj, false);
				return false;
			}
		});
	};

	/**
	 * Hide contact list element.
	 */
	Contact.prototype.hide = function() {
		this.getListItemElement().hide();
	};

	/**
	 * Show contact list element.
	 */
	Contact.prototype.show = function() {
		this.getListItemElement().show();
	};

	/**
	 * Remove any open contact from the DOM.
	 */
	Contact.prototype.close = function(showListElement) {
		$(document).unbind('status.contact.photoupdated');
		console.log('Contact.close', this);
		if(this.$fullelem) {
			this.$fullelem.hide().remove();
			if(showListElement) {
				this.getListItemElement().show();
			}
			this.$fullelem = null;
			return true;
		} else {
			return false;
		}
	};

	/**
	 * Remove any open contact from the DOM and detach it's list
	 * element from the DOM.
	 * @returns The contact object.
	 */
	Contact.prototype.detach = function() {
		if(this.$fullelem) {
			this.$fullelem.remove();
		}
		if(this.$listelem) {
			this.$listelem.detach();
			return this;
		}
	};

	/**
	 * Set a contacts list element as (un)checked
	 * @returns The contact object.
	 */
	Contact.prototype.setChecked = function(checked) {
		if(this.$listelem) {
			this.$listelem.find('input:checkbox').prop('checked', checked);
			return this;
		}
	};

	/**
	 * Set a contact to en/disabled depending on its permissions.
	 * @param boolean enabled
	 */
	Contact.prototype.setEnabled = function(enabled) {
		if(enabled) {
			this.$fullelem.find('#addproperty').show();
		} else {
			this.$fullelem.find('#addproperty,.action.delete,.action.edit').hide();
		}
		this.enabled = enabled;
		this.$fullelem.find('.value,.action,.parameter').each(function () {
			$(this).prop('disabled', !enabled);
		});
		$(document).trigger('status.contact.enabled', enabled);
	};

	/**
	 * Add a contact to data store.
	 * @params params. An object which can contain the optional properties:
	 *		aid: The id of the addressbook to add the contact to. Per default it will be added to the first.
	 *		fn: The formatted name of the contact.
	 * @param cb Optional callback function which
	 * @returns The callback gets an object as argument with a variable 'status' of either 'success'
	 * or 'error'. On success the 'data' property of that object contains the contact id as 'id', the
	 * addressbook id as 'aid' and the contact data structure as 'details'.
	 */
	Contact.prototype.add = function(params, cb) {
		var self = this;
		$.when(this.storage.addContact(this.metadata.backend, this.metadata.parent))
			.then(function(response) {
			if(!response.error) {
				self.id = String(response.metadata.id);
				self.metadata = response.metadata;
				self.data = response.data;
				console.log('Contact.add, groupprops', self.groupprops);
				if(self.groupprops && self.groupprops.groups.length > 0) {
					self._buildGroupSelect(self.groupprops.groups);
					self.$groupSelect.multiselect('enable');
				}
				// Add contact to current group
				if(self.groupprops
					&& ['all', 'fav', 'uncategorized'].indexOf(self.groupprops.currentgroup.id) === -1
				) {
					if(!self.data.CATEGORIES) {
						self.addToGroup(self.groupprops.currentgroup.name);
						$(document).trigger('request.contact.addtogroup', {
							id: self.id,
							groupid: self.groupprops.currentgroup.id
						});
						self.$groupSelect.find('option[value="' + self.groupprops.currentgroup.id + '"]')
							.attr('selected', 'selected');
						self.$groupSelect.multiselect('refresh');
					}
				}
				$(document).trigger('status.contact.added', {
					id: self.id,
					contact: self
				});
			} else {
				$(document).trigger('status.contacts.error', response);
				return false;
			}
			if(typeof cb === 'function') {
				cb(response);
			}
		});
	};
	/**
	 * Delete contact from data store and remove it from the DOM
	 * @param cb Optional callback function which
	 * @returns An object with a variable 'status' of either success
	 *	or 'error'
	 */
	Contact.prototype.destroy = function(cb) {
		var self = this;
		$.when(this.storage.deleteContact(
			this.metadata.backend,
			this.metadata.parent,
			this.id)
		).then(function(response) {
			if(!response.error) {
				if(self.$listelem) {
					self.$listelem.remove();
				}
				if(self.$fullelem) {
					self.$fullelem.remove();
				}
			}
			if(typeof cb === 'function') {
				if(response.error) {
					cb(response);
				} else {
					cb({id:self.id});
				}
			}
		}).fail(function(response) {
			if(typeof cb === 'function') {
				cb(response);
			}
		});
	};

	Contact.prototype.argumentsFor = function(obj) {
		console.log('Contact.argumentsFor', $(obj));
		var args = {};
		var ptype = this.propertyTypeFor(obj);
		args.name = ptype;

		if(this.multi_properties.indexOf(ptype) !== -1) {
			args.checksum = this.checksumFor(obj);
		}

		if($(obj).hasClass('propertycontainer')) {
			if($(obj).is('select[data-element="categories"]')) {
				args.value = [];
				$.each($(obj).find(':selected'), function(idx, e) {
					args.value.push($(e).text());
				});
			} else {
				args.value = $(obj).val();
			}
		} else {
			var $elements = this.propertyContainerFor(obj)
				.find('input.value,select.value,textarea.value');
			if($elements.length > 1) {
				args.value = [];
				$.each($elements, function(idx, e) {
					args.value[parseInt($(e).attr('name').substr(6,1))] = $(e).val();
					//args['value'].push($(e).val());
				});
			} else {
				var value = $elements.val();
				switch(args.name) {
					case 'BDAY':
						try {
							args.value = $.datepicker.formatDate('yy-mm-dd', $.datepicker.parseDate(datepickerFormatDate, value));
						} catch(e) {
							$(document).trigger(
								'status.contacts.error',
								{message:t('contacts', 'Error parsing date: {date}', {date:value})}
							);
							return false;
						}
						break;
					default:
						args.value = value;
						break;
				}
			}
		}
		args.parameters = this.parametersFor(obj);
		console.log('Contact.argumentsFor', args);
		return args;
	};

	Contact.prototype.queryStringFor = function(obj) {
		var q = 'id=' + this.id;
		var ptype = this.propertyTypeFor(obj);
		q += '&name=' + ptype;

		if(this.multi_properties.indexOf(ptype) !== -1) {
			q += '&checksum=' + this.checksumFor(obj);
		}

		if($(obj).hasClass('propertycontainer')) {
			if($(obj).is('select[data-element="categories"]')) {
				$.each($(obj).find(':selected'), function(idx, e) {
					q += '&value=' + encodeURIComponent($(e).text());
				});
			} else {
				q += '&value=' + encodeURIComponent($(obj).val());
			}
		} else {
			var $elements = this.propertyContainerFor(obj)
				.find('input.value,select.value,textarea.value,.parameter');
			if($elements.length > 1) {
				q += '&' + $elements.serialize();
			} else {
				q += '&value=' + encodeURIComponent($elements.val());
			}
		}
		return q;
	};

	Contact.prototype.propertyContainerFor = function(obj) {
		return $(obj).hasClass('propertycontainer')
			? $(obj)
			: $(obj).parents('.propertycontainer').first();
	};

	Contact.prototype.checksumFor = function(obj) {
		return this.propertyContainerFor(obj).data('checksum');
	};

	Contact.prototype.valueFor = function(obj) {
		var $container = this.propertyContainerFor(obj);
		console.assert($container.length > 0, 'Couldn\'t find container for ' + $(obj));
		return $container.is('input.value')
			? $container.val()
			: (function() {
				var $elem = $container.find('textarea.value,input.value:not(:checkbox)');
				console.assert($elem.length > 0, 'Couldn\'t find value for ' + $container.data('element'));
				if($elem.length === 1) {
					return $elem.val();
				} else if($elem.length > 1) {
					var retval = [];
					$.each($elem, function(idx, e) {
						retval[parseInt($(e).attr('name').substr(6,1))] = $(e).val();
					});
					return retval;
				}
			})();
	};

	Contact.prototype.parametersFor = function(obj, asText) {
		var parameters = {};
		$.each(this.propertyContainerFor(obj)
			.find('select.parameter,input:checkbox:checked.parameter'), function(i, elem) {
			var $elem = $(elem);
			var paramname = $elem.data('parameter');
			if(!parameters[paramname]) {
				parameters[paramname] = [];
			}
			if($elem.is(':checkbox')) {
				if(asText) {
					parameters[paramname].push($elem.attr('title'));
				} else {
					parameters[paramname].push($elem.attr('value'));
				}
			} else if($elem.is('select')) {
				$.each($elem.find(':selected'), function(idx, e) {
					if(asText) {
						parameters[paramname].push($(e).text());
					} else {
						parameters[paramname].push($(e).val());
					}
				});
			}
		});
		return parameters;
	};

	Contact.prototype.propertyTypeFor = function(obj) {
		var ptype = this.propertyContainerFor(obj).data('element');
		return ptype ? ptype.toUpperCase() : null;
	};

	/**
	 * Render an element item to be shown during drag.
	 * @return A jquery object
	 */
	Contact.prototype.renderDragItem = function() {
		if(typeof this.$dragelem === 'undefined') {
			this.$dragelem = this.$dragTemplate.octemplate({
				id: this.id,
				name: this.getPreferredValue('FN', '')
			});
		}
		this.setThumbnail(this.$dragelem);
		return this.$dragelem;
	};

	/**
	 * Render the list item
	 * @return A jquery object to be inserted in the DOM
	 */
	Contact.prototype.renderListItem = function(isnew) {
		this.displayNames.fn = this.getPreferredValue('FN')
			|| this.getPreferredValue('ORG', []).pop()
			|| this.getPreferredValue('EMAIL')
			|| this.getPreferredValue('TEL');

		this.displayNames.fl = this.getPreferredValue('N', [this.displayNames.fn])
			.slice(0, 2).reverse().join(' ');

		this.displayNames.lf = this.getPreferredValue('N', [this.displayNames.fn])
			.slice(0, 2).join(', ').trim();
		// Fix misplaced comma if either first or last name is missing
		if(this.displayNames.lf[0] === ',') {
			this.displayNames.lf = this.displayNames.lf.substr(1);
		}
		if(this.displayNames.lf[this.displayNames.lf.length-1] === ',') {
			this.displayNames.lf = this.displayNames.lf.substr(0, this.displayNames.lf.length-1);
		}

		this.$listelem = this.$listTemplate.octemplate({
			id: this.id,
			parent: this.metadata.parent,
			backend: this.metadata.backend,
			name: this.getDisplayName(),
			email: this.getPreferredValue('EMAIL', ''),
			tel: this.getPreferredValue('TEL', ''),
			adr: this.getPreferredValue('ADR', []).clean('').join(', '),
			categories: this.getPreferredValue('CATEGORIES', [])
				.clean('').join(' / ')
		});
		if (this.getOwner() !== OC.currentUser
			&& !(this.metadata.permissions & OC.PERMISSION_UPDATE
			|| this.metadata.permissions & OC.PERMISSION_DELETE)
		) {
			this.$listelem.find('input:checkbox').prop('disabled', true).css('opacity', '0');
		} else {
			var self = this;
			this.$listelem.find('td.name')
				.draggable({
					cursor: 'move',
					distance: 10,
					revert: 'invalid',
					helper: function(/*event, ui*/) {
						return self.renderDragItem().appendTo('body');
					},
					opacity: 1,
					scope: 'contacts'
				});
		}
		if(isnew) {
			this.setThumbnail();
		}
		this.$listelem.data('obj', this);
		return this.$listelem;
	};

	Contact.prototype._buildGroupSelect = function(availableGroups) {
		var self = this;
		this.$fullelem.find('.groupscontainer').show();
		//this.$groupSelect.find('option').remove();
		$.each(availableGroups, function(idx, group) {
			var $option = $('<option value="' + group.id + '">' + escapeHTML(group.name) + '</option>');
			if(self.inGroup(group.name)) {
				$option.attr('selected', 'selected');
			}
			self.$groupSelect.append($option);
		});
		self.$groupSelect.multiselect({
			header: false,
			selectedList: 3,
			noneSelectedText: self.$groupSelect.attr('title'),
			selectedText: t('contacts', '# groups'),
			minWidth: 300
		});
		self.$groupSelect.bind('multiselectclick', function(event, ui) {
			var action = ui.checked ? 'addtogroup' : 'removefromgroup';
			console.assert(typeof self.id === 'string', 'ID is not a string');
			$(document).trigger('request.contact.' + action, {
				id: self.id,
				groupid: parseInt(ui.value)
			});
			if(ui.checked) {
				self.addToGroup(ui.text);
			} else {
				self.removeFromGroup(ui.text);
			}
		});
		if(!self.id || !self.hasPermission(OC.PERMISSION_UPDATE)) {
			self.$groupSelect.multiselect('disable');
		}
	};

	Contact.prototype._buildAddressBookSelect = function(availableAddressBooks) {
		var self = this;
		console.log('address books', availableAddressBooks.length, availableAddressBooks);
		$.each(availableAddressBooks, function(idx, addressBook) {
			//console.log('addressBook', idx, addressBook);
			var $option = $('<option />')
				.val(addressBook.getId())
				.text(addressBook.getDisplayName() + '(' + addressBook.getBackend() + ')')
				.data('backend', addressBook.getBackend())
				.data('owner', addressBook.getOwner());
			if(self.metadata.parent === addressBook.getId()
				&& self.metadata.backend === addressBook.getBackend()) {
				$option.attr('selected', 'selected');
			}
			self.$addressBookSelect.append($option);
		});
		self.$addressBookSelect.multiselect({
			header: false,
			multiple: false,
			selectedList: 3,
			noneSelectedText: self.$addressBookSelect.attr('title'),
			minWidth: 300
		});
		self.$addressBookSelect.on('multiselectclick', function(event, ui) {
			console.log('AddressBook select', ui);
			self.$addressBookSelect.val(ui.value);
			var opt = self.$addressBookSelect.find(':selected');
			if(self.id) {
				console.log('AddressBook', opt);
				$(document).trigger('request.contact.move', {
					contact: self,
					from: {id:self.getParent(), backend:self.getBackend()},
					target: {id:opt.val(), backend:opt.data('backend')}
				});
			} else {
				self.setBackend(opt.data('backend'));
				self.setParent(opt.val());
				self.setOwner(opt.data('owner'));
			}
		});
	};

	/**
	 * Render the full contact
	 * @return A jquery object to be inserted in the DOM
	 */
	Contact.prototype.renderContact = function(groupprops) {
		var self = this;
		this.groupprops = groupprops;
		
		var values;
		if(this.data) {
			var n = this.getPreferredValue('N', ['', '', '', '', '']),
				bday = this.getPreferredValue('BDAY', '');
			if(bday.length >= 10) {
				try {
					bday = $.datepicker.parseDate('yy-mm-dd', bday.substring(0, 10));
					bday = $.datepicker.formatDate(datepickerFormatDate, bday);
				} catch (e) {
					var message = t('contacts', 'Error parsing birthday {bday}', {bday:bday});
					console.warn('Error parsing birthday', bday, e);
					bday = '';
					$(document).trigger('status.contacts.error', {
						status: 'error',
						message: message
					});
				}
			}
			values = {
				id: this.id,
				favorite:groupprops.favorite ? 'icon-starred' : 'icon-star',
				name: this.getPreferredValue('FN', ''),
				n0: n[0]||'', n1: n[1]||'', n2: n[2]||'', n3: n[3]||'', n4: n[4]||'',
				nickname: this.getPreferredValue('NICKNAME', ''),
				title: this.getPreferredValue('TITLE', ''),
				org: this.getPreferredValue('ORG', []).clean('').join(', '), // TODO Add parts if more than one.
				bday: bday,
				note: this.getPreferredValue('NOTE', '')
			};
		} else {
			values = {id:'', favorite:'', name:'', nickname:'', title:'', org:'', bday:'', note:'', n0:'', n1:'', n2:'', n3:'', n4:''};
		}
		this.$fullelem = this.$fullTemplate.octemplate(values).data('contactobject', this);

		this.$header = this.$fullelem.find('header');
		this.$footer = this.$fullelem.find('footer');
		this.$groupSelect = this.$fullelem.find('#contactgroups');
		this.$addressBookSelect = this.$fullelem.find('#contactaddressbooks');

		this.$fullelem.find('.tooltipped.rightwards.onfocus').tipsy({trigger: 'focus', gravity: 'w'});
		this.$fullelem.on('submit', function() {
			return false;
		});
		
		if(this.getOwner() === OC.currentUser && groupprops.groups.length > 0 && this.getBackend() === 'local') {
			this._buildGroupSelect(groupprops.groups);
		} else {
			this.$fullelem.find('.groupscontainer').hide();
		}
		
		var writeableAddressBooks = this.parent.addressBooks.selectByPermission(OC.PERMISSION_CREATE);
		if(writeableAddressBooks.length > 1 && this.hasPermission(OC.PERMISSION_DELETE)) {
			this._buildAddressBookSelect(writeableAddressBooks);
		} else {
			this.$fullelem.find('.addressbookcontainer').hide();
		}

		this.$addMenu = this.$fullelem.find('#addproperty');
		this.$addMenu.on('change', function(/*event*/) {
			var $opt = $(this).find('option:selected');
			self.addProperty($opt, $(this).val());
			$(this).val('');
		});
		var $fullname = this.$fullelem.find('.fullname');
		this.$fullelem.find('.singleproperties').on('mouseenter', function() {
			$fullname.next('.edit').css('opacity', '1');
		}).on('mouseleave', function() {
			$fullname.next('.edit').css('opacity', '0');
		});
		$fullname.next('.edit').on('click keydown', function(event) {
			//console.log('edit name', event);
			$('.tipsy').remove();
			if(wrongKey(event)) {
				return;
			}
			$(this).css('opacity', '0');
			var $editor = $(this).next('.n.editor').first();
			var bodyListener = function(e) {
				if($editor.find($(e.target)).length === 0) {
					$editor.toggle('blind');
					$('body').unbind('click', bodyListener);
				}
			};
			$editor.toggle('blind', function() {
				$('body').bind('click', bodyListener);
			});
		});

		this.$fullelem.on('click keydown', '.delete', function(event) {
			$('.tipsy').remove();
			if(wrongKey(event)) {
				return;
			}
			self.deleteProperty({obj:event.target});
		});

		this.$fullelem.on('click keydown', '.globe,.mail,.favorite', function(event) {
			$('.tipsy').remove();
			if(wrongKey(event)) {
				return;
			}
			self.handleURL(event.target);
		});

		var buttonHandler =  function(event) {
			$('.tipsy').remove();
			if(wrongKey(event)) {
				return;
			}
			if($(this).is('.close') || $(this).is('.cancel')) {
				$(document).trigger('request.contact.close', {
					id: self.id
				});
			} else if($(this).is('.export')) {
				$(document).trigger('request.contact.export', self.metaData());
			} else if($(this).is('.delete')) {
				$(document).trigger('request.contact.delete', self.metaData());
			}
			return false;
		};
		this.$header.on('click keydown', 'button, a', buttonHandler);
		this.$footer.on('click keydown', 'button, a', buttonHandler);
		
		this.$fullelem.on('keypress', '.value,.parameter', function(event) {
			if(event.keyCode === 13 && $(this).is('input')) {
				$(this).trigger('change');
				// Prevent a second save on blur.
				this.previousValue = this.defaultValue || '';
				this.defaultValue = this.value;
				return false;
			} else if(event.keyCode === 27) {
				$(document).trigger('request.contact.close', {
					id: self.id
				});
			}
		});

		this.$fullelem.on('change', '.value,.parameter', function(event) {
			if($(this).hasClass('value') && this.value === this.defaultValue) {
				return;
			}
			function isMultiByte(str) {
				return /[\uD800-\uDFFF]/.test(str);
			}

			if (isMultiByte(this.value) && self.getBackend()) {
				$(document).trigger('status.contacts.error',
				{error:true, message: t('contacts', 'The backend does not support multi-byte characters.')});
				if(this.defaultValue) {
					this.value = this.defaultValue;
				}
				return;
			}
			//console.log('change', this.defaultValue, this.value);
			this.defaultValue = this.value;
			self.saveProperty({obj:event.target});
		});

		var $bdayinput = this.$fullelem.find('[data-element="bday"]').find('input');
		$bdayinput.datepicker({
			dateFormat : datepickerFormatDate,
			changeMonth: true,
			changeYear: true,
			minDate : new Date(1900,1,1),
			maxDate : new Date()
		});
		$bdayinput.attr('placeholder', $.datepicker.formatDate(datepickerFormatDate, new Date()));

		this.$fullelem.find('.favorite').on('click', function () {
			var state = $(this).hasClass('icon-starred');
			if(!self.data) {
				return;
			}
			if(state) {
				$(this).switchClass('icon-starred', 'icon-star');
			} else {
				$(this).switchClass('icon-star', 'icon-starred');
			}
			$(document).trigger('request.contact.setasfavorite', {
				id: self.id,
				state: !state
			});
		}).tipsy();
		this.loadAvatar();
		if(!this.data) {
			// A new contact
			this.setEnabled(true);
			this.showActions(['cancel']);
			// Show some default properties
			$.each(['email', 'tel'], function(idx, name) {
				var $list = self.$fullelem.find('ul.' + name);
				$list.removeClass('hidden');
				var $property = self.renderStandardProperty(name);
				$property.find('select[name="parameters[TYPE][]"]')
					.combobox({
					singleclick: true,
					classes: ['propertytype', 'float', 'label']
				});
				$list.append($property);
			});
			var $list = self.$fullelem.find('ul.adr');
			$list.removeClass('hidden');
			var $property = self.renderAddressProperty(name);
			$property.find('select[name="parameters[TYPE][]"]')
				.combobox({
				singleclick: true,
				classes: ['propertytype', 'float', 'label']
			});
			$list.append($property);

			// Hide some of the values
			$.each(['bday', 'nickname', 'title'], function(idx, name) {
				self.$fullelem.find('[data-element="' + name + '"]').hide();
			});

			return this.$fullelem;
		}
		// Loop thru all single occurrence values. If not set hide the
		// element, if set disable the add menu entry.
		$.each(values, function(name, value) {
			if(typeof value === 'undefined') {
				return true; //continue
			}
			value = value.toString();
			if(self.multi_properties.indexOf(value.toUpperCase()) === -1) {
				if(!value.length) {
					self.$fullelem.find('[data-element="' + name + '"]').hide();
				} else {
					self.$addMenu.find('option[value="' + name.toUpperCase() + '"]').prop('disabled', true);
				}
			}
		});
		$.each(this.multi_properties, function(idx, name) {
			if(self.data[name]) {
				var $list = self.$fullelem.find('ul.' + name.toLowerCase());
				$list.removeClass('hidden');
				for(var p in self.data[name]) {
					if(typeof self.data[name][p] === 'object') {
						var property = self.data[name][p];
						//console.log(name, p, property);
						var $property = null;
						switch(name) {
							case 'TEL':
							case 'URL':
							case 'EMAIL':
								$property = self.renderStandardProperty(name.toLowerCase(), property);
								if(self.data[name].length === 1) {
									$property.find('input:checkbox[value="PREF"]').hide();
								}
								break;
							case 'ADR':
								$property = self.renderAddressProperty(idx, property);
								break;
							case 'IMPP':
								$property = self.renderIMProperty(property);
								if(self.data[name].length === 1) {
									$property.find('input:checkbox[value="PREF"]').hide();
								}
								break;
						}
						if(!$property) {
							continue;
						}
						//console.log('$property', $property);
						var meta = [];
						if(property.label) {
							if(!property.parameters.TYPE) {
								property.parameters.TYPE = [];
							}
							property.parameters.TYPE.push(property.label);
							meta.push(property.label);
						}
						for(var param in property.parameters) {
							if(property.parameters.hasOwnProperty(param)) {
								//console.log('param', param);
								if(param.toUpperCase() === 'PREF') {
									var $cb = $property.find('input[type="checkbox"]');
									$cb.attr('checked', 'checked');
									meta.push($cb.attr('title'));
								}
								else if(param.toUpperCase() === 'TYPE') {
									for(var etype in property.parameters[param]) {
										if(property.parameters[param].hasOwnProperty(etype)) {
											var found = false;
											var et = property.parameters[param][etype];
											if(typeof et !== 'string') {
												continue;
											}
											$property.find('select.type option').each(function() {
												if($(this).val().toUpperCase() === et.toUpperCase()) {
													$(this).attr('selected', 'selected');
													meta.push($(this).text());
													found = true;
												}
											});
											if(!found) {
												$property.find('select.type option:last-child').after('<option value="'+et+'" selected="selected">'+et+'</option>');
											}
										}
									}
								}
								else if(param.toUpperCase() === 'X-SERVICE-TYPE') {
									//console.log('setting', $property.find('select.impp'), 'to', property.parameters[param].toLowerCase());
									$property.find('select.rtl').val(property.parameters[param].toLowerCase());
								}
							}
						}
						var $meta = $property.find('.meta');
						if($meta.length) {
							$meta.html(meta.join('/'));
						}
						if(self.metadata.owner === OC.currentUser
								|| self.metadata.permissions & OC.PERMISSION_UPDATE
								|| self.metadata.permissions & OC.PERMISSION_DELETE) {
							$property.find('select.type[name="parameters[TYPE][]"], select.type[name="parameters[X-SERVICE-TYPE]"]')
								.combobox({
									singleclick: true,
									classes: ['propertytype', 'float', 'label']
								});
						}
						$list.append($property);
					}
				}
			}
		});
		var actions = ['close', 'export'];
		if(this.hasPermission(OC.PERMISSION_DELETE)) {
			actions.push('delete');
		}
		if(this.hasPermission(OC.PERMISSION_UPDATE)) {
			actions.push('add');
			this.setEnabled(true);
		} else {
			this.setEnabled(false);
		}
		this.showActions(actions);

		return this.$fullelem;
	};

	Contact.prototype.isEditable = function() {
		return ((this.metadata.owner === OC.currentUser)
			|| (this.metadata.permissions & OC.PERMISSION_UPDATE
				|| this.metadata.permissions & OC.PERMISSION_DELETE));
	};

	/**
	 * Render a simple property. Used for EMAIL and TEL.
	 * @return A jquery object to be injected in the DOM
	 */
	Contact.prototype.renderStandardProperty = function(name, property) {
		if(!this.detailTemplates[name]) {
			console.error('No template for', name);
			return;
		}
		var values = property
			? { value: property.value, checksum: property.checksum }
			: { value: '', checksum: 'new' };
		return this.detailTemplates[name].octemplate(values);
	};

	/**
	 * Render an ADR (address) property.
	 * @return A jquery object to be injected in the DOM
	 */
	Contact.prototype.renderAddressProperty = function(idx, property) {
		if(!this.detailTemplates.adr) {
			console.warn('No template for adr', this.detailTemplates);
			return;
		}
		if(typeof idx === 'undefined') {
			if(this.data && this.data.ADR && this.data.ADR.length > 0) {
				idx = this.data.ADR.length - 1;
			} else {
				idx = 0;
			}
		}
		var values = property ? {
				value: property.value.clean('').join(', '),
				checksum: property.checksum,
				adr0: property.value[0] || '',
				adr1: property.value[1] || '',
				adr2: property.value[2] || '',
				adr3: property.value[3] || '',
				adr4: property.value[4] || '',
				adr5: property.value[5] || '',
				adr6: property.value[6] || '',
				idx: idx
			}
			: {value:'', checksum:'new', adr0:'', adr1:'', adr2:'', adr3:'', adr4:'', adr5:'', adr6:'', idx: idx};
		var $elem = this.detailTemplates.adr.octemplate(values);
		var self = this;
		$elem.find('.tooltipped.downwards:not(.onfocus)').tipsy({gravity: 'n'});
		$elem.find('.tooltipped.rightwards.onfocus').tipsy({trigger: 'focus', gravity: 'w'});
		$elem.find('.display').on('click', function() {
			$(this).next('.listactions').hide();
			var $editor = $(this).siblings('.adr.editor').first();
			var $viewer = $(this);
			var bodyListener = function(e) {
				if($editor.find($(e.target)).length === 0) {
					$editor.toggle('blind');
					$viewer.slideDown(550, function() {
						var input = $editor.find('input').first();
						var params = self.parametersFor(input, true);
						$(this).find('.meta').html(params.TYPE.join('/'));
						$(this).find('.adr').text(self.valueFor($editor.find('input').first()).clean('').join(', '));
						$(this).next('.listactions').css('display', 'inline-block');
						$('body').unbind('click', bodyListener);
					});
				}
			};
			$viewer.slideUp(100);
			$editor.toggle('blind', function() {
				$('body').bind('click', bodyListener);
			});
		});
		return $elem;
	};

	/**
	 * Render an IMPP (Instant Messaging) property.
	 * @return A jquery object to be injected in the DOM
	 */
	Contact.prototype.renderIMProperty = function(property) {
		if(!this.detailTemplates.impp) {
			console.warn('No template for impp', this.detailTemplates);
			return;
		}
		var values = property ? {
			value: property.value,
			checksum: property.checksum
		} : {value: '', checksum: 'new'};
		return this.detailTemplates.impp.octemplate(values);
	};

	/**
	 * Set a thumbnail for the contact if a PHOTO property exists
	 */
	Contact.prototype.setThumbnail = function($elem, refresh) {
		if(!this.data.thumbnail && !refresh) {
			this.getListItemElement().find('.avatar').css('height', '32px');
			var name = String(this.getDisplayName()).replace(' ', '').replace(',', '');
			this.getListItemElement().find('.avatar').imageplaceholder(name || '#');
			return;
		}
		if(!$elem) {
			$elem = this.getListItemElement().find('td.name');
		}
		if(!$elem.hasClass('thumbnail') && !refresh) {
			return;
		}
		if(this.data.thumbnail) {
			$elem.removeClass('thumbnail').find('.avatar').remove();
			$elem.css('background-image', 'url(data:image/png;base64,' + this.data.thumbnail + ')');
		} else {
			$elem.addClass('thumbnail');
			$elem.removeAttr('style');
		}
	};

	/**
	 * Render the PHOTO property or a generated avatar.
	 */
	Contact.prototype.loadAvatar = function() {
		var self = this;
		var id = this.id || 'new',
			backend = this.metadata.backend,
			parent = this.metadata.parent;

		var $phototools = this.$fullelem.find('#phototools');
		var $photowrapper = this.$fullelem.find('#photowrapper');

		var finishLoad = function(image) {
			console.log('finishLoad', self.getDisplayName(), image.width, image.height);
			$(image).addClass('contactphoto');
			$photowrapper.removeClass('loading');
			$photowrapper.css({width: image.width + 10, height: image.height + 10});
			$(image).insertAfter($phototools).fadeIn();
		};

		var addAvatar = function() {
			console.log('adding avatar for', self.getDisplayName());
			$photowrapper.find('.contactphoto').remove();
			var name = String(self.getDisplayName()).replace(' ', '').replace(',', '');
			console.log('height', $photowrapper.height());
			$('<div />').appendTo($photowrapper)
				.css({width: '170', height: '170'})
				.addClass('contactphoto')
				.imageplaceholder(name || '#');
		};

		$photowrapper.addClass('loading');
		if(!this.hasPhoto()) {
			$photowrapper.removeClass('loading');
			addAvatar();
		} else {
			$.when(this.storage.getContactPhoto(backend, parent, id))
				.then(function(image) {
					$photowrapper.find('.contactphoto').remove();
					finishLoad(image);
				})
				.fail(function() {
					console.log('Error getting photo.');
					$photowrapper.find('.contactphoto').remove();
					addAvatar();
				});
		}

		if(this.isEditable()) {
			$photowrapper.on('mouseenter', function(event) {
				if($(event.target).is('.favorite') || !self.data) {
					return;
				}
				$phototools.slideDown(200);
			}).on('mouseleave', function() {
				$phototools.slideUp(200);
			});
			$phototools.hover( function () {
				$(this).removeClass('transparent');
			}, function () {
				$(this).addClass('transparent');
			});
			$phototools.find('li a').tipsy();

			$phototools.find('.action').off('click');
			$phototools.find('.edit').on('click', function() {
				$(document).trigger('request.edit.contactphoto', self.metaData());
			});
			$phototools.find('.cloud').on('click', function() {
				$(document).trigger('request.select.contactphoto.fromcloud', self.metaData());
			});
			$phototools.find('.upload').on('click', function() {
				$(document).trigger('request.select.contactphoto.fromlocal', self);
			});
			if(this.hasPhoto()) {
				$phototools.find('.delete').show();
				$phototools.find('.edit').show();
			} else {
				$phototools.find('.delete').hide();
				$phototools.find('.edit').hide();
			}
			$(document).bind('status.contact.photoupdated', function(e, data) {
				console.log('status.contact.photoupdated', data);
				if(!self.hasPhoto()) {
					self.data.PHOTO = [];
				}
				if(data.thumbnail) {
					self.data.thumbnail = data.thumbnail;
					self.data.photo = true;
				} else {
					self.data.thumbnail = null;
					self.data.photo = false;
				}
				self.loadAvatar(true);
				self.setThumbnail(null, true);
			});
		}
	};

	/**
	 * Get the jquery element associated with this object
	 */
	Contact.prototype.getListItemElement = function() {
		if(!this.$listelem) {
			this.renderListItem();
		}
		return this.$listelem;
	};

	/**
	 * Get the preferred value for a property.
	 * If a preferred value is not found the first one will be returned.
	 * @param string name The name of the property like EMAIL, TEL or ADR.
	 * @param def A default value to return if nothing is found.
	 */
	Contact.prototype.getPreferredValue = function(name, def) {
		var pref = def, found = false;
		if(this.data && this.data[name]) {
			var props = this.data[name];
			//console.log('props', props);
			$.each(props, function( i, prop ) {
				//console.log('prop:', i, prop);
				if(i === 0) { // Choose first to start with
					pref = prop.value;
				}
				for(var param in prop.parameters) {
					if(param.toUpperCase() === 'PREF') {
						found = true; //
						break;
					}
				}
				if(found) {
					return false; // break out of loop
				}
			});
		}
		if(name === 'N' && pref.join('').trim() === '') {
			return def;
		}
		return pref;
	};

	/**
	 * Returns an array with the names of the groups the contact is in
	 *
	 * @return Array
	 */
	Contact.prototype.groups = function() {
		return this.getPreferredValue('CATEGORIES', []).clean('');
	};


	/**
	 * Returns true/false depending on the contact being in the
	 * specified group.
	 * @param String name The group name (not case-sensitive)
	 * @return Boolean
	 */
	Contact.prototype.inGroup = function(name) {
		console.log('inGroup', name);
		var categories = this.getPreferredValue('CATEGORIES', []);
		var found = false;

		$.each(categories, function(idx, category) {
			if(name.toLowerCase() === $.trim(category).toLowerCase()) {
				found = true;
				return false;
			}
		});

		return found;
	};

	/**
	 * Add this contact to a group
	 * @param String name The group name
	 */
	Contact.prototype.addToGroup = function(name) {
		console.log('addToGroup', name);
		if(!this.data.CATEGORIES) {
			this.data.CATEGORIES = [{value:[name]}];
		} else {
			if(this.inGroup(name)) {
				return;
			}
			this.data.CATEGORIES[0].value.push(name);
			if(this.$listelem) {
				this.$listelem.find('td.categories')
					.text(this.getPreferredValue('CATEGORIES', []).clean('').join(' / '));
			}
		}
	};

	/**
	 * Remove this contact from a group
	 * @param String name The group name
	 */
	Contact.prototype.removeFromGroup = function(name) {
		name = name.trim();
		if(!this.data.CATEGORIES) {
			console.warn('removeFromGroup. No groups found');
			return;
		} else {
			var found = false;
			var categories = [];
			$.each(this.data.CATEGORIES[0].value, function(idx, category) {
				category = category.trim();
				if(name.toLowerCase() === category.toLowerCase()) {
					found = true;
				} else {
					categories.push(category);
				}
			});
			if(!found) {
				return;
			}
			this.data.CATEGORIES[0].value = categories;
			if(this.$listelem) {
				this.$listelem.find('td.categories')
					.text(categories.join(' / '));
			}
		}
	};

	Contact.prototype.setCurrent = function(on) {
		if(on) {
			this.$listelem.addClass('active');
		} else {
			this.$listelem.removeClass('active');
		}
		$(document).trigger('status.contact.currentlistitem', {
			id: this.id,
			pos: Math.round(this.$listelem.position().top),
			height: Math.round(this.$listelem.height())
		});
	};

	Contact.prototype.setSelected = function(state) {
		//console.log('Selecting', this.getId(), state);
		var $elem = this.getListItemElement();
		var $input = $elem.find('input:checkbox');
		$input.prop('checked', state).trigger('change');
	};

	Contact.prototype.next = function() {
		// This used to work..?
		//var $next = this.$listelem.next('tr:visible');
		var $next = this.$listelem.nextAll('tr').filter(':visible').first();
		if($next.length > 0) {
			this.$listelem.removeClass('active');
			$next.addClass('active');
			$(document).trigger('status.contact.currentlistitem', {
				id: String($next.data('id')),
				pos: Math.round($next.position().top),
				height: Math.round($next.height())
			});
		}
	};

	Contact.prototype.prev = function() {
		//var $prev = this.$listelem.prev('tr:visible');
		var $prev = this.$listelem.prevAll('tr').filter(':visible').first();
		if($prev.length > 0) {
			this.$listelem.removeClass('active');
			$prev.addClass('active');
			$(document).trigger('status.contact.currentlistitem', {
				id: String($prev.data('id')),
				pos: Math.round($prev.position().top),
				height: Math.round($prev.height())
			});
		}
	};

	var ContactList = function(
			storage,
			addressBooks,
			contactlist,
			contactlistitemtemplate,
			contactdragitemtemplate,
			contactfulltemplate,
			contactdetailtemplates
		) {
		//console.log('ContactList', contactlist, contactlistitemtemplate, contactfulltemplate, contactdetailtemplates);
		var self = this;
		this.length = 0;
		this.contacts = {};
		this.addressBooks = addressBooks;
		this.deletionQueue = [];
		this.storage = storage;
		this.$contactList = contactlist;
		this.$contactDragItemTemplate = contactdragitemtemplate;
		this.$contactListItemTemplate = contactlistitemtemplate;
		this.$contactFullTemplate = contactfulltemplate;
		this.contactDetailTemplates = contactdetailtemplates;
		this.$contactList.scrollTop(0);
		//this.getAddressBooks();
		$(document).bind('status.contact.added', function(e, data) {
			self.length += 1;
			self.contacts[String(data.id)] = data.contact;
			//self.insertContact(data.contact.renderListItem(true));
		});
		$(document).bind('status.contact.moved', function(e, data) {
			var contact = data.contact;
			contact.close();
			contact.reload(data.data);
			self.contacts[contact.getId()] = contact;
			$(document).trigger('request.contact.open', {
				id: contact.getId()
			});
			console.log('status.contact.moved', data);
		});
		$(document).bind('request.contact.close', function(/*e, data*/) {
			self.currentContact = null;
		});
		$(document).bind('status.contact.updated', function(e, data) {
			if(['FN', 'EMAIL', 'TEL', 'ADR', 'CATEGORIES'].indexOf(data.property) !== -1) {
				data.contact.getListItemElement().remove();
				self.insertContact(data.contact.renderListItem(true));
			} else if(data.property === 'PHOTO') {
				$(document).trigger('status.contact.photoupdated', {
					id: data.contact.getId()
				});
			}
		});
		$(document).bind('status.addressbook.removed', function(e, data) {
			var addressBook = data.addressbook;
			self.purgeFromAddressbook(addressBook);
			$(document).trigger('request.groups.reload');
			$(document).trigger('status.contacts.deleted', {
				numcontacts: self.length
			});
		});
		$(document).bind('status.addressbook.imported', function(e, data) {
			console.log('status.addressbook.imported', data);
			var addressBook = data.addressbook;
			self.purgeFromAddressbook(addressBook);
			$.when(self.loadContacts(addressBook.getBackend(), addressBook.getId(), true))
			.then(function() {
				self.setSortOrder();
				$(document).trigger('request.groups.reload');
			});
		});
		$(document).bind('status.addressbook.activated', function(e, data) {
			console.log('status.addressbook.activated', data);
			var addressBook = data.addressbook;
			if(!data.state) {
				self.purgeFromAddressbook(addressBook);
				$(document).trigger('status.contacts.deleted', {
					numcontacts: self.length
				});
			} else {
				$.when(self.loadContacts(addressBook.getBackend(), addressBook.getId(), true))
				.then(function() {
					self.setSortOrder();
					$(document).trigger('request.groups.reload');
				});
			}
		});
	};

	/**
	 * Get the number of contacts in the list
	 * @return integer
	 */
	ContactList.prototype.count = function() {
		return Object.keys(this.contacts.contacts).length;
	};

	/**
	* Remove contacts from the internal list and the DOM
	*
	* @param AddressBook addressBook
	*/
	ContactList.prototype.purgeFromAddressbook = function(addressBook) {
		var self = this;
		$.each(this.contacts, function(idx, contact) {
			if(contact.getBackend() === addressBook.getBackend()
				&& contact.getParent() === addressBook.getId()) {
				//console.log('Removing', contact);
				delete self.contacts[contact.getId()];
				//var c = self.contacts.splice(self.contacts.indexOf(contact.getId()), 1);
				//console.log('Removed', c);
				contact.detach();
				contact = null;
				self.length -= 1;
			}
		});
		$(document).trigger('status.contacts.count', {
			count: self.length
		});
	};

	/**
	* Show/hide contacts belonging to an addressbook.
	* @param int aid. Addressbook id.
	* @param boolean show. Whether to show or hide.
	* @param boolean hideothers. Used when showing shared addressbook as a group.
	*/
	ContactList.prototype.showFromAddressbook = function(aid, show, hideothers) {
		console.log('ContactList.showFromAddressbook', aid, show);
		aid = String(aid);
		for(var contact in this.contacts) {
			if(this.contacts[contact].getParent() === aid) {
				this.contacts[contact].getListItemElement().toggle(show);
			} else if(hideothers) {
				this.contacts[contact].getListItemElement().hide();
			}
		}
		this.setSortOrder();
	};

	/**
	* Show only uncategorized contacts.
	* @param int aid. Addressbook id.
	* @param boolean show. Whether to show or hide.
	* @param boolean hideothers. Used when showing shared addressbook as a group.
	*/
	ContactList.prototype.showUncategorized = function() {
		console.log('ContactList.showUncategorized');
		for(var contact in this.contacts) {
			if(this.contacts[contact].getPreferredValue('CATEGORIES', []).clean('').length === 0) {
				this.contacts[contact].getListItemElement().show();
				this.contacts[contact].setThumbnail();
			} else {
				this.contacts[contact].getListItemElement().hide();
			}
		}
		this.setSortOrder();
	};

	/**
	* Show/hide contacts belonging to shared addressbooks.
	* @param Boolean show. Whether to show or hide.
	*/
	ContactList.prototype.showSharedAddressbooks = function(show) {
		console.log('ContactList.showSharedAddressbooks', show);
		for(var contact in this.contacts) {
			if(this.contacts[contact].metadata.owner !== OC.currentUser) {
				if(show) {
					this.contacts[contact].getListItemElement().show();
				} else {
					this.contacts[contact].getListItemElement().hide();
				}
			}
		}
		this.setSortOrder();
	};

	/**
	* Show contacts in list
	* @param String[] contacts. A list of contact ids.
	*/
	ContactList.prototype.showContacts = function(contacts) {
		console.log('showContacts', contacts);
		var self = this;
		if(contacts.length === 0) {
			// ~5 times faster
			$('tr:visible.contact').hide();
			return;
		}
		if(contacts === 'all') {
			// ~2 times faster
			var $elems = $('tr.contact:not(:visible)');
			$elems.show();
			$.each($elems, function(idx, elem) {
				try {
					var id = $(elem).data('id');
					self.contacts[id].setThumbnail();
				} catch(e) {
					console.warn('Failed getting id from', $elem, e);
				}
			});
			this.setSortOrder();
			return;
		}
		console.time('show');
		$('tr.contact').filter(':visible').hide();
		$.each(contacts, function(idx, id) {
			var contact = self.findById(id);
			if(contact === null) {
				return true; // continue
			}
			contact.getListItemElement().show();
			contact.setThumbnail();
		});
		console.timeEnd('show');

		// Amazingly this is slightly faster
		//console.time('show');
		for(var id in this.contacts) {
			if(this.contacts.hasOwnProperty(id)) {
				var contact = this.findById(id);
				if(contact === null) {
					continue;
				}
				if(contacts.indexOf(String(id)) === -1) {
					contact.getListItemElement().hide();
				} else {
					contact.getListItemElement().show();
					contact.setThumbnail();
				}
			}
		}
		//console.timeEnd('show');*/

		this.setSortOrder();
	};

	ContactList.prototype.contactPos = function(id) {
		var contact = this.findById(id);
		if(!contact) {
			return 0;
		}
		
		var $elem = contact.getListItemElement();
		var pos = Math.round($elem.offset().top - (this.$contactList.offset().top + this.$contactList.scrollTop()));
		console.log('contactPos', pos);
		return pos;
	};

	ContactList.prototype.hideContact = function(id) {
		var contact = this.findById(id);
		if(contact === null) {
			return false;
		}
		contact.hide();
	};

	ContactList.prototype.closeContact = function(id) {
		var contact = this.findById(id);
		if(contact === null) {
			return false;
		}
		contact.close();
	};

	/**
	* Returns a Contact object by searching for its id
	* @param id the id of the node
	* @return the Contact object or undefined if not found.
	* FIXME: If continious loading is reintroduced this will have
	* to load the requested contact if not in list.
	*/
	ContactList.prototype.findById = function(id) {
		if(!id) {
			console.warn('ContactList.findById: id missing');
			return false;
		}
		id = String(id);
		if(typeof this.contacts[id] === 'undefined') {
			console.warn('Could not find contact with id', id);
			//console.trace();
			return null;
		}
		return this.contacts[String(id)];
	};

	/**
	* TODO: Instead of having a timeout the contacts should be moved to a "Trash" backend/address book
	* https://github.com/owncloud/contacts/issues/107
	* @param Object|Object[] data An object or array of objects containing contact identification
	* {
	* 	contactid: '1234',
	* 	addressbookid: '4321',
	* 	backend: 'local'
	* }
	*/
	ContactList.prototype.delayedDelete = function(data) {
		console.log('delayedDelete, data:', typeof data, data);
		var self = this;
		if(!utils.isArray(data)) {
			this.currentContact = null;
			//self.$contactList.show();
			if(data instanceof Contact) {
				this.deletionQueue.push(data);
			} else {
				var contact = this.findById(data.contactId);
				if(contact instanceof Contact) {
					this.deletionQueue.push(contact);
				}
			}
		} else if(utils.isArray(data)) {
			$.each(data, function(idx, contact) {
				//console.log('delayedDelete, meta:', contact);
				if(contact instanceof Contact) {
					self.deletionQueue.push(contact);
				}
			});
			//$.extend(this.deletionQueue, data);
		} else {
			throw { name: 'WrongParameterType', message: 'ContactList.delayedDelete only accept objects or arrays.'};
		}
		//console.log('delayedDelete, deletionQueue', this.deletionQueue);
		$.each(this.deletionQueue, function(idx, contact) {
			//console.log('delayedDelete', contact);
			contact && contact.detach().setChecked(false);
		});
		//console.log('deletionQueue', this.deletionQueue);
		if(!window.onbeforeunload) {
			window.onbeforeunload = function(e) {
				e = e || window.event;
				var warn = t('contacts', 'Some contacts are marked for deletion, but not deleted yet. Please wait for them to be deleted.');
				if (e) {
					e.returnValue = String(warn);
				}
				return warn;
			};
		}
		if(this.$contactList.find('tr:visible').length === 0) {
			$(document).trigger('status.visiblecontacts');
		}
		OC.notify({
			message:t('contacts','Click to undo deletion of {num} contacts', {num: self.deletionQueue.length}),
			//timeout:5,
			timeouthandler:function() {
				//console.log('timeout');
				self.deleteContacts();
			},
			clickhandler:function() {
				//console.log('clickhandler');
				//OC.notify({cancel:true});
				OC.notify({cancel:true, message:t('contacts', 'Cancelled deletion of {num} contacts', {num: self.deletionQueue.length})});
				$.each(self.deletionQueue, function(idx, contact) {
					self.insertContact(contact.getListItemElement());
				});
				self.deletionQueue = [];
				window.onbeforeunload = null;
			}
		});
	};

	/**
	* Delete contacts in the queue
	* TODO: Batch delete contacts instead of sending multiple requests.
	*/
	ContactList.prototype.deleteContacts = function() {
		var self = this,
			contact,
			contactMap = {};
		console.log('ContactList.deleteContacts, deletionQueue', this.deletionQueue);

		if(this.deletionQueue.length === 1) {
			contact = this.deletionQueue.shift();
			// Let contact remove itself.
			var id = contact.getId();
			contact.destroy(function(response) {
				console.log('deleteContact', response, self.length);
				if(!response.error) {
					delete self.contacts[id];
					$(document).trigger('status.contact.deleted', {
						id: id
					});
					self.length -= 1;
					if(self.length === 0) {
						$(document).trigger('status.nomorecontacts');
					}
				} else {
					self.insertContact(contact.getListItemElement());
					OC.notify({message:response.message});
				}
			});
		} else {

			// Make a map of backends, address books and contacts for easier processing.
			do {
				contact = this.deletionQueue.shift();
				if(!contactMap[contact.getBackend()]) {
					contactMap[contact.getBackend()] = {};
				}
				if(!contactMap[contact.getBackend()][contact.getParent()]) {
					contactMap[contact.getBackend()][contact.getParent()] = [];
				}
				contactMap[contact.getBackend()][contact.getParent()].push(contact.getId());
			} while(this.deletionQueue.length > 0);
			console.log('map', contactMap);

			// Call each backend/addressBook to delete contacts.
			$.each(contactMap, function(backend, addressBooks) {
				console.log(backend, addressBooks);
				$.each(addressBooks, function(addressBook, contacts) {
					console.log(addressBook, contacts);
					var ab = self.addressBooks.find({backend:backend, id:addressBook});
					ab.deleteContacts(contacts, function(response) {
						console.log('response', response);
						if(!response.error) {
							// We get a result set back, so process all of them.
							$.each(response.data.result, function(idx, result) {
								console.log('deleting', idx, result.id);
								if(result.status === 'success') {
									delete self.contacts[result.id];
									$(document).trigger('status.contact.deleted', {
										id: result.id
									});
									self.length -= 1;
									if(self.length === 0) {
										$(document).trigger('status.nomorecontacts');
									}
								} else {
									// Error deleting, so re-insert element.
									// TODO: Collect errors and display them when done.
									self.insertContact(self.contacts[result.id].getListItemElement());
								}
							});
						} else {
							console.warn(response);
						}
					});
				});
			});
		}

		window.onbeforeunload = null;
		return;

	};

	/**
	 * Insert a rendered contact list item into the list
	 * @param contact jQuery object.
	 */
	ContactList.prototype.insertContact = function($contact) {
		$contact.find('td.name').draggable({
			distance: 10,
			revert: 'invalid',
			//containment: '#content',
			helper: function (/*event, ui*/) {
				return $(this).clone().appendTo('body').css('zIndex', 5).show();
			},
			opacity: 0.8,
			scope: 'contacts'
		});
		var name = $contact.find('.nametext').text().toLowerCase();
		var added = false;
		this.$contactList.find('tr').each(function() {
			if ($(this).find('.nametext').text().toLowerCase().localeCompare(name) > 0) {
				$(this).before($contact);
				added = true;
				return false;
			}
		});
		if(!added) {
			this.$contactList.append($contact);
		}
		if($contact.data('obj').isOpen()) {
			$contact.hide();
		} else {
			$contact.show();
		}
		return $contact;
	};

	/**
	* Add contact
	* @param object props
	*/
	ContactList.prototype.addContact = function(props) {
		// Get first address book
		var addressBooks = this.addressBooks.selectByPermission(OC.PERMISSION_UPDATE);
		var addressBook = addressBooks[0];
		var metadata = {
			parent: addressBook.getId(),
			backend: addressBook.getBackend(),
			permissions: addressBook.getPermissions(),
			owner: addressBook.getOwner()
		};
		var contact = new Contact(
			this,
			null,
			metadata,
			null,
			this.$contactListItemTemplate,
			this.$contactDragItemTemplate,
			this.$contactFullTemplate,
			this.contactDetailTemplates
		);
		if(this.currentContact) {
			this.contacts[this.currentContact].close();
		}
		return contact.renderContact(props);
	};

	/**
	 * Get contacts selected in list
	 *
	 * @returns array of contact objects.
	 */
	ContactList.prototype.getSelectedContacts = function() {
		var contacts = [];

		var self = this;
		$.each(this.$contactList.find('tbody > tr > td > input:checkbox:visible:checked'), function(idx, checkbox) {
			var id = String($(checkbox).val());
			var contact = self.contacts[id];
			if(contact) {
				contacts.push(contact);
			}
		});
		return contacts;
	};

	ContactList.prototype.setCurrent = function(id, deselect_other) {
		console.log('ContactList.setCurrent', id);
		if(!id) {
			return;
		}
		var self = this;
		if(deselect_other === true) {
			$.each(this.contacts, function(contact) {
				self.contacts[contact].setCurrent(false);
			});
		}
		this.contacts[String(id)].setCurrent(true);
	};

	/**
	 * (De)-select a contact
	 *
	 * @param string id
	 * @param bool state
	 * @param bool reverseOthers
	 */
	ContactList.prototype.setSelected = function(id, state, reverseOthers) {
		console.log('ContactList.setSelected', id);
		if(!id) {
			return;
		}
		var self = this;
		if(reverseOthers === true) {
			var $rows = this.$contactList.find('tr:visible.contact');
			$.each($rows, function(idx, row) {
				self.contacts[$(row).data('id')].setSelected(!state);
			});
		}
		this.contacts[String(id)].setSelected(state);
	};

	/**
	 * Select a range of contacts by their id.
	 *
	 * @param string from
	 * @param string to
	 */
	ContactList.prototype.selectRange = function(from, to) {
		var self = this;
		var $rows = this.$contactList.find('tr:visible.contact');
		var index1 = $rows.index(this.contacts[String(from)].getListItemElement());
		var index2 = $rows.index(this.contacts[String(to)].getListItemElement());
		from = Math.min(index1, index2);
		to = Math.max(index1, index2)+1;
		$rows = $rows.slice(from, to);
		$.each($rows, function(idx, row) {
			self.contacts[$(row).data('id')].setSelected(true);
		});
	};

	ContactList.prototype.setSortOrder = function(order) {
		order = order || contacts_sortby;
		//console.time('set name');
		var $rows = this.$contactList.find('tr:visible.contact');
		var self = this;
		$.each($rows, function(idx, row) {
			self.contacts[$(row).data('id')].setDisplayMethod(order);
		});
		//console.timeEnd('set name');
		if($rows.length > 1) {
			//console.time('sort');
			var rows = $rows.get();
			if(rows[0].firstElementChild && rows[0].firstElementChild.textContent) {
				rows.sort(function(a, b) {
					// 10 (TEN!) times faster than using jQuery!
					return a.firstElementChild.lastElementChild.textContent.trim().toUpperCase()
						.localeCompare(b.firstElementChild.lastElementChild.textContent.trim().toUpperCase());
				});
			} else {
				// IE8 doesn't support firstElementChild or textContent
				rows.sort(function(a, b) {
					return $(a).find('.nametext').text().toUpperCase()
						.localeCompare($(b).find('td.name').text().toUpperCase());
				});
			}
			this.$contactList.prepend(rows);
			//console.timeEnd('sort');
		}
	};

	ContactList.prototype.insertContacts = function(contacts) {
		var self = this, items = [];
		$.each(contacts, function(c, contact) {
			var id = String(contact.metadata.id);
			self.contacts[id]
				= new Contact(
					self,
					id,
					contact.metadata,
					contact.data,
					self.$contactListItemTemplate,
					self.$contactDragItemTemplate,
					self.$contactFullTemplate,
					self.contactDetailTemplates
				);
			self.length +=1;
			var $item = self.contacts[id].renderListItem();
			if(!$item) {
				console.warn('Contact', contact, 'could not be rendered!');
				return true; // continue
			}
			items.push($item.get(0));
		});
		if(items.length > 0) {
			self.$contactList.append(items);
		}
		$(document).trigger('status.contacts.count', {
			count: self.length
		});
	};

	/**
	* Load contacts
	* @param string backend Name of the backend ('local', 'ldap' etc.)
	* @param string addressBookId
	*/
	ContactList.prototype.loadContacts = function(backend, addressBookId, isActive) {
		if(!isActive) {
			return;
		}
		var self = this;

		return $.when(self.storage.getContacts(backend, addressBookId, false))
			.then(function(response) {
			console.log('ContactList.loadContacts - fetching', response);
			if(!response.error) {
				if(response.data) {
					self.insertContacts(response.data.contacts);
				}
			} else {
				console.warn('ContactList.loadContacts - no data!!');
			}
		})
		.fail(function(response) {
			console.warn('Request Failed:', response.message);
			$(document).trigger('status.contacts.error', response);
		});

	};

	OC.Contacts.ContactList = ContactList;

})(window, jQuery, OC);
