define(['domReady', 'jquery', 'cla_select2'], function (domReady, $, select2) {
	/**
	 * Create a new Contact Picker.
	 *
	 * @param {string}  id     - Element ID of <select> element
	 * @param {options} object - Options that control the behaviour of the picker
	 */
	return function (id, options) {
		this.id = id || 'contact_picker';

		this.options = $.extend({
			createContacts: false,
			clearSelection: false,
			exclude: null
		}, options);
		
		var _this = this;
		
		domReady(function () {
			var picker = $('#' + id).select2({
				width: '100%',
				placeholder: lmsg('people.contact_picker.placeholder'),
				tags: true,
				ajax: {
					url: '/api/people/contacts',
					data: function (parameters) {
						return prepareContactApiRequest(picker, parameters);
					},
					processResults: processContactApiResults,
					delay: 250
				},
				createTag: createNewContactResult,
				templateResult: formatContactResult
			});
			
			// Create contacts as they are selected
			picker.on('select2:select', function (event) {
				var contact = event.params.data;
				
				// Simply fire an event if the user selected an existing contact
				if (!_this.options.createContacts || !contact.isNew) {
					picker.trigger('contact_picker:select', {
						id: contact.id,
						name: contact.text
					});
					
					return;
				}

				// Validate the input
				if (!contact.text) {
					cla.showMessage(lmsg('people.contact_picker.error.empty'), null, true);
					
					return;
				}
				
				// Create the contact
				$.ajax({
					method: 'post',
					url: '/api/people/contacts',
					data: {
						name: contact.text
					},
					dataType: 'json',
					success: function (data) {
						return handleContactApiResponse(picker, data);
					},
					error: function (xhr, status, error) {
						cla.showMessage(xhr.responseJSON ? xhr.responseJSON.message : error, null, true);
					}
				});
			});
			
			// Clear the selection every time an option is chosen
			if (_this.options.clearSelection) {
				picker.on('select2:select', function () {
					$(this).val(null).trigger('change');
				});
			}
		});
		
		/**
		 * Prepare the data used to request for contacts.
		 *
		 * @param {jQuery} picker
		 * @param {object} parameters
		 * @return {object}
		 */
		function prepareContactApiRequest(picker, parameters) {
			var data = {
				keywords: parameters.term,
				offset: parameters.page ? (parameters.page - 1) * 20 : 0
			};
			
			// Update options from externally set data on the picker
			$.extend(_this.options, picker.data());
			
			// Include exclusion IDs with the request data
			data.exclude = _this.options.exclude;
			
			// Acquire exclusion IDs from a function if one is given
			if (typeof _this.options.exclude === 'function')
				data.exclude = _this.options.exclude();
			
			// Ensure that we're working with an array
			if (!Array.isArray(data.exclude))
				data.exclude = [data.exclude];
			
			// Include the selections of the picker element
			data.exclude.push.apply(data.exclude, picker.val());
			
			// Join the IDs together as a single string to avoid
			// massive query strings
			data.exclude = data.exclude.join(',');
			
			return data;
		}
		
		/**
		 * Handle a successful response from the contact API.
		 *
		 * @param {jQuery} picker
		 * @param {object} data
		 */
		function handleContactApiResponse(picker, data) {
			// Handle unexpected response data
			if (!data.contact || typeof data.contact !== 'object') {
				cla.showMessage(lmsg('people.contact_picker.error.unexpected_response'), null, true);
				
				return;
			}
			
			// Format the contact
			var formattedContact = formatContact(data.contact);
			
			// Append the contact as a selected option if appropriate
			if (!_this.options.clearSelection) {
				picker.append(new Option(formattedContact, data.contact.id, false, true)).trigger('change');
			}
			
			// Fire the contact selection event for external use
			picker.trigger('contact_picker:select', {
				id: data.contact.id,
				name: formattedContact
			});
			
			cla.showMessage(lmsg('people.editcontactsubmit.addsuccess'));
		}
		
		/**
		 * Process contact API results into the Select2 format.
		 *
		 * @param {object} data
		 * @param {object} parameters
		 * @returns {object}
		 */
		function processContactApiResults(data, parameters) {
			// Bail if we have no contacts to iterate
			if (!data.contacts) {
				return {
					results: []
				};
			}
			
			var i, contact, results = [];
			
			for (i = 0; i < data.contacts.length; i++) {
				contact = data.contacts[i];
				
				// Skip contacts without an ID
				if (!contact.id) {
					continue;
				}
				
				// Create the result
				results.push({
					'id': contact.id,
					'text': formatContact(contact)
				});
			}
			
			return {
				results: results,
				pagination: {
					more: (parameters.page * 20) < data.total
				}
			}
		}
		
		/**
		 * Create a new search result from user input.
		 *
		 * @param {object} parameters
		 */
		function createNewContactResult(parameters) {
			var term = $.trim(parameters.term);
			
			// Skip contact creation for empty input
			if (!term) {
				return null;
			}
			
			// Loosely match an email address, optionally preceded by a name
			var pattern = /(.*?)[<(]*(\S+@\S+\.[^>)\s]+)[>)]*/;
			var matches = pattern.exec(term);
			
			// Skip contact creation for invalid input
			if (!matches) {
				return null;
			}
			
			// Format the result
			var text = [matches[1], '(' + matches[2] + ')'].join('');
			
			// Build the "create new contact" result object
			return {
				id: text,
				text: text,
				prepend: ['<strong>', lmsg('people.contact_picker.add'), ':', '</strong>'].join(''),
				isNew: true
			};
		}
		
		/**
		 * Format a contact search result.
		 *
		 * @param {object} contact
		 * @return {string}
		 */
		function formatContactResult(contact) {
			if (contact.isNew) {
				var text = $('<span>' + contact.text + '</span>').text();
				
				return $('<span>' + [contact.prepend || '', text, contact.append || ''].join(' ') + '</span>');
			}

			return contact.text;
		}
		
		/**
		 * Format a contact object from an API response.
		 *
		 * @param {object} contact
		 * @return {string}
		 */
		function formatContact(contact) {
			// Build a list of non-empty details
			var details = [];
			
			if (contact.firstname)
				details.push(contact.firstname);
			
			if (contact.surname)
				details.push(contact.surname);
			
			if (contact.emailad)
				details.push('(' + contact.emailad + ')');
			
			// Join them together
			return details.join(' ');
		}
	}
});
