(function($){
	$.fn.Postcoder = function(options){
		var self = this;
		var address;
		var addressLines = [];
		
		var STATUS = {
			ERROR: 0,
			WARNING: 1
		};
		
		var defaults = {
			searchType: undefined,
			searchURL: undefined,
			searchButton: undefined,
			outputMap: undefined,
			debug: false,
			displayUserHint: true,
			timeout: 5000,
			uppercaseTown: true,
			
			onInit: function(){},
			onClickSearch: function(){},
			onClickAddress: function(){},
			onReturnData: function(){},
			onPopulateResults: function(){},
			onPopulateForm: function(){}
		};
		
		var settings = $.extend({}, defaults, options);
		
		var init = function(){
			startDebug.call(self, "init");
			
			displayDebug.call(self, "Postcoder->init();");
			displayDebug.call(self, "Initial config:", settings);
			
			if(settings.onInit.constructor == Function){
				displayDebug.call(self, "trigger function", "onInit();");
				settings.onInit.call(self);
			}
			
			if(validateConfig.call(self)){
				$("#" + settings.searchButton).click(onClickSearch);
				
				self.keyup(function(e) {
					if(e.keyCode == 13) {
						onClickSearch.call(self);
					}
				});
			}
			
			endDebug.call(self);
		};
		
		var onClickSearch = function(){
			startDebug.call(self, "onClickSearch");
			
			displayDebug.call(self, "Postcoder->onClickSearch();");
			displayDebug.call(self, "searchValue", self.val());
			
			if(settings.onClickSearch.constructor == Function){
				displayDebug.call(self, "trigger function", "onClickSearch();");
				settings.onClickSearch.call(self);
			}
			
			clearFields.call(self);
			
			if(self.val() != ""){
				proxyRequest.call(self);
			} else {
				displayError.call(self, STATUS.WARNING, -5, "Please enter a valid UK postcode.", null);
				return false;
			}
		};
		
		var onClickAddress = function(e){
			startDebug.call(self, "onClickAddress");
			
			displayDebug.call(self, "Postcoder->onClickAddress();");

			if(settings.onClickAddress.constructor == Function){
				displayDebug.call(self, "trigger function", "onClickAddress();");
				settings.onClickAddress.call(self);
			}

			var el = e.target.id.replace("wsi", "").split("-");

			applyTemplate.call(self, addressLines[el[0]], (addressLines[el[0]]["premise"] ? addressLines[el[0]]["premise"][el[1]] : undefined));
		
			populateResults.call(self);
			
			populateForm.call(self);
			
			hideModalbox.call(self, e);
			
			e.stopPropagation();
			
			endDebug.call(self);
			
			$('#addressSelect').html('');
			$('#addressSelect').slideUp('medium', function(){
				$(this).children().remove();
			});
		};
		
		var validateConfig = function(){
			displayDebug.call(self, "Postcoder->validateConfig();");
			
			if($.inArray(settings.searchType, ["PREMISE_LIST", "MATCH_ADDRESS", "THRFARE_ADDRESS", "GRIDS", "GEODATA"]) == -1){
				displayError.call(self, STATUS.ERROR, 5000, "The service you have requested does not exist.", "Must be one of 'PREMISE_LIST', 'MATCH_ADDRESS', 'THRFARE_ADDRESS', 'GRIDS', 'GEODATA'");
				return false;
			}
			
			return true;
		};
		
		var proxyRequest = function(){
			displayDebug.call(self, "Postcoder->proxyRequest();");
			
			$.ajax({
			  	url: settings.searchURL,
				type: "POST",
				data: {
					searchValue: self.val(), 
					searchType: settings.searchType
				},
				dataType: "json",
			  	success: onRequestSuccess,
				error: onRequestFailure,
				complete: function(){
					endDebug.call(self);
				},
				timeout: settings.timeout
			});
		};
		
		var onRequestSuccess = function(data, status, xhr){
			displayDebug.call(self, "Postcoder->onRequestSuccess();");
			
			if(settings.onReturnData.constructor == Function){
				displayDebug.call(self, "trigger function", "onReturnData();");
				settings.onReturnData.call(self);
			}

			if(data && data.constructor == Object){	
				if(data.success){
					if(data.numberOfResults > 0){
						switch(settings.searchType){
							case "GEODATA":
								addressLines = {
									geoCoords: data.geoCoords,
									localAuthority: data.localAuthority,
									nhs: data.nhs
								};
								
								populateForm.call(self);
								
								break;

							case "GRIDS":
								addressLines = {
									geoCoords: data.geoCoords
								};
								
								populateForm.call(self);

								break;
								
							case "THRFARE_ADDRESS":
								addressLines = data.address;
							
								if(addressLines.constructor == Object){
									applyTemplate.call(self, addressLines, undefined);

									populateResults.call(self);

									populateForm.call(self);
								} else {
									displaySelection.call(this);
								}

								break;
									
							default:
								addressLines = data.address;
								
								if(addressLines.constructor == Object && addressLines.premise.constructor == Object){
									applyTemplate.call(self, addressLines, addressLines.premise);

									populateResults.call(self);

									populateForm.call(self);
								} else {
									displaySelection.call(this);
								}
						}
					} else {
						displayError.call(self, STATUS.WARNING, data.retcode, data.userHint, null);
					}
				} else {
					displayError.call(self, STATUS.ERROR, data.error.code, data.error.message, data.error.detail);
				}
			} else {
				displayError.call(self, STATUS.ERROR, 5100, "No data was received from request", null);
			}
		};
		
		var onRequestFailure = function(xhr, status, error){
			displayDebug.call(self, "Postcoder->onRequestFailure();");
			
			displayError.call(self, STATUS.ERROR, 5101, "There was a problem with the request", error);
		};
		
		var displaySelection = function(){
			displayDebug.call(self, "Postcoder->displaySelection();");
			
			var outerList = $("<ul></ul>");
			
			if(addressLines.constructor == Object){
				addressLines = [addressLines];
			}						

			switch(settings.searchType){
				case "THRFARE_ADDRESS":
					$.each(addressLines, function(i, addressGroup){
						var addressGroupTitle = [];

						$.each(["dependentStreet", "street", "doubleDependentLocality", "dependentLocality", "postTown", "county", "postcode"], function(k, prop){
							if(typeof(addressGroup[prop]) != "undefined" && addressGroup[prop] != ""){
								if(prop == "postTown" && settings.uppercaseTown){
									addressGroup[prop] = addressGroup[prop].toUpperCase();
								}
								addressGroupTitle.push(addressGroup[prop]);
							}
						});						

						$(outerList).append($("<li><a href=\"javascript:;\" id=\"wsi" + i + "\">" + addressGroupTitle.join(", ") + "</a></li>"));

						$(outerList).find("li").click(onClickAddress);
					});
					break;
					
				default:
					$.each(addressLines, function(i, addressGroup){
						var addressGroupTitle = [];

						$.each(["dependentStreet", "street", "doubleDependentLocality", "dependentLocality", "postTown", "county", "postcode"], function(k, prop){
							if(typeof(addressGroup[prop]) != "undefined" && addressGroup[prop] != ""){
								if(prop == "postTown" && settings.uppercaseTown){
									addressGroup[prop] = addressGroup[prop].toUpperCase();
								}
								addressGroupTitle.push(addressGroup[prop]);
							}
						});						

						$(outerList).append($("<li><a href=\"javascript:;\" id=\"wsi" + i + "\">" + addressGroupTitle.join(", ") + "</a></li>"));

						var innerList = $(outerList).children("li").last().append($("<ul></ul>"));

						if(addressGroup.premise.constructor == Object){
							addressGroup.premise = [addressGroup.premise];
						}

						$.each(addressGroup.premise, function(j, premiseLine){
							var addressLineTitle = [];

							if(typeof(premiseLine.organisationName) != "undefined"){
								addressLineTitle.push(premiseLine.organisationName);
							}

							if(typeof(premiseLine.formattedPremise) != "undefined"){
								addressLineTitle.push(premiseLine.formattedPremise);
							}

							if(typeof(addressGroup.street) != "undefined"){
								addressLineTitle.push(addressGroup.street);
							}

							$(innerList).children("ul").last().append($("<li><a href=\"javascript:;\" id=\"wsi" + i + "-" + j + "\">" + addressLineTitle.join(", ") + "</a></li>"));
						});

						$(innerList).find("li").click(onClickAddress);
					});		
			}
			
			$('#addressSelect').html('');
			outerList.appendTo('#addressSelect');
			$('#addressSelect').hide().slideDown();
			
			// showModalbox.call(self, "Please select your address", outerList);
		};
		
		var showModalbox = function(title, content){
			var div = $("<div id=\"wsModal\"><h1>" + title + "</h1></div>");
			$(div).hide().append(content);
			var mbo = $(document.body).prepend($("<div id=\"wsModalOverlay\"></div>"));
			mbo.click(hideModalbox);
			$(document.body).prepend(div);
			$(div).css("left", ($(window).width() - $(div).outerWidth())/2 + "px");
			$(div).slideDown("slow");
		};
		
		var hideModalbox = function(e){
			displayDebug.call(self, "Postcoder->hideSelection();");
			
			$("#wsModal").slideUp("slow", function(){
				$(this).remove();
				$("#wsModalOverlay").remove();
			});
			
			e.stopPropagation();
		};
		
		var applyTemplate = function(addressThoroughfare, addressPremise){
			displayDebug.call(self, "Postcoder->applyTemplate();");
			
			address = {
				organisationName: (addressPremise ? addressPremise.organisationName : undefined),
				organisationDepartment: (addressPremise ? addressPremise.organisationDepartment : undefined),
				subBuildingName: (addressPremise ? addressPremise.subBuildingName : undefined),
				buildingName: (addressPremise ? addressPremise.buildingName : undefined),
				buildingNumber: (addressPremise ? addressPremise.buildingNumber : undefined),
				dependentStreet: addressThoroughfare.dependentStreet,
				street: addressThoroughfare.street,
				doubleDependentLocality: addressThoroughfare.doubleDependentLocality,						
				dependentLocality: addressThoroughfare.dependentLocality,
				postTown: addressThoroughfare.postTown,
				county: addressThoroughfare.county,
				postcode: addressThoroughfare.postcode
			};
		};
		
		var populateResults = function(){
			displayDebug.call(self, "Postcoder->populateResults();");
			
			if(settings.onPopulateResults.constructor == Function){
				displayDebug.call(self, "trigger function", "onPopulateResults();");
				settings.onPopulateResults.call(self);
			}
			
			var numRowData = 0;
			var numRowAvailable = settings.outputMap.addressLines.length;
			
			if(address.buildingNumber && address.street){
				address.street = address.buildingNumber + " " + address.street;
				delete address.buildingNumber;
			}
			
			if(settings.uppercaseTown){
				address.postTown = address.postTown.toUpperCase();
			}
			
			$.each(address, function(index, value){
				if(settings.outputMap[index]){
					$("#" + settings.outputMap[index]).val(value);
					delete address[index];
				}
				
				if($.inArray(index, ["organisationDepartment", "subBuildingName", "buildingName", "buildingNumber", "dependentStreet", "street", "doubleDependentLocality", "dependentLocality"]) >= 0){
					if(typeof(value) != "undefined"){
						numRowData++;
					} else {
						delete address[index];
					}
				}
			});
			
			if(numRowAvailable < numRowData){
				for(var i = numRowData - numRowAvailable; i > 0; i--){
					var smallest = undefined;
					var lastLine = {
						key: undefined,
						count: 0
					};
					
					$.each(address, function(index, value){
						if(typeof (lastLine.key) != "undefined"){
							var lineLength = value.length + lastLine.count;
							
							if(typeof (smallest) == "undefined" || lineLength < smallest.count){
								smallest = {
									key: index,
									count: lineLength,
									prevKey: lastLine.key
								};
							}
						}
						
						lastLine = {
							key: index,
							count: value.length
						};
					});
					
					address[smallest.prevKey] = address[smallest.prevKey] + ", " + address[smallest.key];
					delete address[smallest.key];
				}
			}
			
			return address;
		};
		
		var populateForm = function(){
			displayDebug.call(self, "Postcoder->populateForm();");
			
			if(settings.onPopulateForm.constructor == Function){
				displayDebug.call(self, "trigger function", "onPopulateForm();");
				settings.onPopulateForm.call(self);
			}
			
			switch(settings.searchType){
				case "GEODATA":
					if(settings.outputMap.geoCoords){
						if(settings.outputMap.geoCoords.easting){
							$("#" + settings.outputMap.geoCoords.easting).val(addressLines.geoCoords.easting);
						}
					
						if(settings.outputMap.geoCoords.northing){
							$("#" + settings.outputMap.geoCoords.northing).val(addressLines.geoCoords.northing);
						}
					
						if(settings.outputMap.geoCoords.status){
							$("#" + settings.outputMap.geoCoords.status).val(addressLines.geoCoords.status);
						}
					
						if(settings.outputMap.geoCoords.etrs89){
							if(settings.outputMap.geoCoords.etrs89.latitude){
								$("#" + settings.outputMap.geoCoords.etrs89.latitude).val(addressLines.geoCoords.etrs89.latitude);
							}
						
							if(settings.outputMap.geoCoords.etrs89.longitude){
								$("#" + settings.outputMap.geoCoords.etrs89.longitude).val(addressLines.geoCoords.etrs89.longitude);
							}
						}
			
						if(settings.outputMap.geoCoords.osgb36){
							if(settings.outputMap.geoCoords.osgb36.latitude){
								$("#" + settings.outputMap.geoCoords.osgb36.latitude).val(addressLines.geoCoords.osgb36.latitude);
							}
						
							if(settings.outputMap.geoCoords.osgb36.longitude){
								$("#" + settings.outputMap.geoCoords.osgb36.longitude).val(addressLines.geoCoords.osgb36.longitude);
							}
						}
					}
				
					if(settings.outputMap.localAuthority){
						if(settings.outputMap.localAuthority.authorityName){
							$("#" + settings.outputMap.localAuthority.authorityName).val(addressLines.localAuthority.authorityName);
						}
					
						if(settings.outputMap.localAuthority.ward){
							if(settings.outputMap.localAuthority.ward.code){
								$("#" + settings.outputMap.localAuthority.ward.code).val(addressLines.localAuthority.ward.code);
							}
						
							if(settings.outputMap.localAuthority.ward.name){
								$("#" + settings.outputMap.localAuthority.ward.name).val(addressLines.localAuthority.ward.name);
							}
						}
					}
				
					if(settings.outputMap.nhs){
						if(settings.outputMap.nhs.healthAuthority){
							if(settings.outputMap.nhs.healthAuthority.code){
								$("#" + settings.outputMap.nhs.healthAuthority.code).val(addressLines.nhs.healthAuthority.code);
							}
						
							if(settings.outputMap.nhs.healthAuthority.name){
								$("#" + settings.outputMap.nhs.healthAuthority.name).val(addressLines.nhs.healthAuthority.name);
							}
						}
					
						if(settings.outputMap.nhs.primaryCareTrust){
							if(settings.outputMap.nhs.primaryCareTrust.code){
								$("#" + settings.outputMap.nhs.primaryCareTrust.code).val(addressLines.nhs.primaryCareTrust.code);
							}
						
							if(settings.outputMap.nhs.primaryCareTrust.name){
								$("#" + settings.outputMap.nhs.primaryCareTrust.name).val(addressLines.nhs.primaryCareTrust.name);
							}
						
							if(settings.outputMap.nhs.primaryCareTrust.ha){
								$("#" + settings.outputMap.nhs.primaryCareTrust.ha).val(addressLines.nhs.primaryCareTrust.ha);
							}
						}
					}
					break;
					
				case "GRIDS":
					if(settings.outputMap.geoCoords){
						if(settings.outputMap.geoCoords.easting){
							$("#" + settings.outputMap.geoCoords.easting).val(addressLines.geoCoords.easting);
						}
				
						if(settings.outputMap.geoCoords.northing){
							$("#" + settings.outputMap.geoCoords.northing).val(addressLines.geoCoords.northing);
						}
				
						if(settings.outputMap.geoCoords.status){
							$("#" + settings.outputMap.geoCoords.status).val(addressLines.geoCoords.status);
						}
				
						if(settings.outputMap.geoCoords.etrs89){
							if(settings.outputMap.geoCoords.etrs89.latitude){
								$("#" + settings.outputMap.geoCoords.etrs89.latitude).val(addressLines.geoCoords.etrs89.latitude);
							}
					
							if(settings.outputMap.geoCoords.etrs89.longitude){
								$("#" + settings.outputMap.geoCoords.etrs89.longitude).val(addressLines.geoCoords.etrs89.longitude);
							}
						}
		
						if(settings.outputMap.geoCoords.osgb36){
							if(settings.outputMap.geoCoords.osgb36.latitude){
								$("#" + settings.outputMap.geoCoords.osgb36.latitude).val(addressLines.geoCoords.osgb36.latitude);
							}
					
							if(settings.outputMap.geoCoords.osgb36.longitude){
								$("#" + settings.outputMap.geoCoords.osgb36.longitude).val(addressLines.geoCoords.osgb36.longitude);
							}
						}
					}
					break;
					
				default:
					var i = 0;
					$.each(address, function(index, value){
						$("#" + settings.outputMap.addressLines[i++]).val(value);
					});	
			}
		};
		
		var clearFields = function(){
			displayDebug.call(self, "Postcoder->clearFields();");
			
			address = undefined;
			addressLines = [];

			$("#" + settings.outputMap.postcode).closest('form').find(":text").each(function(){
				$(this).val("");
			});
		};
		
		var displayError = function(status, code, message, detail){
			displayDebug.call(self, "Postcoder->displayError();");

			switch(status){
				case STATUS.ERROR:
					var errorDiv = $('<div class="error"><p>There has been a problem. Please enter your address manually</p></div>');
					$('#addressSelect').html('');
					errorDiv.appendTo('#addressSelect');
					$('#addressSelect').hide().slideDown();
					// showModalbox.call(self, "Warning", "<p>There has been a problem. Please enter your address manually.</p>");
					break;
				case STATUS.WARNING:
					if(settings.displayUserHint){
						// showModalbox.call(self, "Warning", "<p>" + message + "</p>");
					} else {
						// showModalbox.call(self, "Warning", "<p>There has been a problem. Please enter your address manually.</p>");
						message = 'There has been a problem. Please enter your address manually';
					}
					var errorDiv = $('<div class="error"><p>' + message + '</p></div>');
					$('#addressSelect').html('');
					errorDiv.appendTo('#addressSelect');
					$('#addressSelect').hide().slideDown();
					break;
			}
		
			displayDebug.call(self, status, code, message, detail);
		};
		
		var startDebug = function(groupName){
			if(settings.debug && typeof(console) != "undefined"){
				console.group('Postcoder Websoap (#' + self.attr("id") + ") - " + groupName);
			}
		};
		
		var displayDebug = function(){
			if(settings.debug && typeof(console) != "undefined"){
				console.info(arguments);
			}
		};
		
		var endDebug = function(){
			if(settings.debug && typeof(console) != "undefined"){
				console.groupEnd();
			}
		};

		return this.each(function(){	
			init.call(self);
		});
	};
})(jQuery);
