(function (factory) {

	// Packaging/modules magic dance

	var L;

	if (typeof define === 'function' && define.amd) {

		// AMD

		define(['leaflet'], factory);

	} else if (typeof module !== 'undefined') {

		// Node/CommonJS

		L = require('leaflet');

		module.exports = factory(L);

	} else {

		// Browser globals

		if (typeof window.L === 'undefined')

			throw 'Leaflet must be loaded first';

		factory(window.L);

	}

}(function (L) {

	'use strict';

	L.Control.Geocoder = L.Control.extend({

		options: {

			showResultIcons: false,

			collapsed: true,

			expand: 'click',

			position: 'topright',

			placeholder: 'Search...',

			errorMessage: 'Nothing found.'

		},



		_callbackId: 0,



		initialize: function (options) {

			L.Util.setOptions(this, options);

			if (!this.options.geocoder) {

				this.options.geocoder = new L.Control.Geocoder.Nominatim();

			}

		},



		onAdd: function (map) {

			var className = 'leaflet-control-geocoder',

			    container = L.DomUtil.create('div', className),

				icon = L.DomUtil.create('div', 'leaflet-control-geocoder-icon', container),

			    form = this._form = L.DomUtil.create('form', className + '-form', container),

			    input;



			this._map = map;

			this._container = container;

			input = this._input = L.DomUtil.create('input');

			input.type = 'text';

			input.placeholder = this.options.placeholder;



			L.DomEvent.addListener(input, 'keydown', this._keydown, this);

			//L.DomEvent.addListener(input, 'onpaste', this._clearResults, this);

			//L.DomEvent.addListener(input, 'oninput', this._clearResults, this);



			this._errorElement = document.createElement('div');

			this._errorElement.className = className + '-form-no-error';

			this._errorElement.innerHTML = this.options.errorMessage;



			this._alts = L.DomUtil.create('ul', className + '-alternatives leaflet-control-geocoder-alternatives-minimized');



			form.appendChild(input);

			form.appendChild(this._errorElement);

			container.appendChild(this._alts);



			L.DomEvent.addListener(form, 'submit', this._geocode, this);



			if (this.options.collapsed) {

				if (this.options.expand === 'click') {

					L.DomEvent.addListener(icon, 'click', function(e) {

						// TODO: touch

						if (e.button === 0 && e.detail === 1) {

							this._toggle();

						}

					}, this);

				} else {

					L.DomEvent.addListener(icon, 'mouseover', this._expand, this);

					L.DomEvent.addListener(icon, 'mouseout', this._collapse, this);

					this._map.on('movestart', this._collapse, this);

				}

			} else {

				this._expand();

			}



			L.DomEvent.disableClickPropagation(container);



			return container;

		},



		_geocodeResult: function (results) {

			L.DomUtil.removeClass(this._container, 'leaflet-control-geocoder-throbber');

			if (results.length === 1) {

				this._geocodeResultSelected(results[0]);

			} else if (results.length > 0) {

				this._alts.innerHTML = '';

				this._results = results;

				L.DomUtil.removeClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized');

				for (var i = 0; i < results.length; i++) {

					this._alts.appendChild(this._createAlt(results[i], i));

				}

			} else {

				L.DomUtil.addClass(this._errorElement, 'leaflet-control-geocoder-error');

			}

		},



		markGeocode: function(result) {

			this._map.fitBounds(result.bbox);



			if (this._geocodeMarker) {

				this._map.removeLayer(this._geocodeMarker);

			}



			this._geocodeMarker = new L.Marker(result.center)

				.bindPopup(result.html || result.name)

				.addTo(this._map)

				.openPopup();



			return this;

		},



		_geocode: function(event) {

			L.DomEvent.preventDefault(event);



			L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-throbber');

			this._clearResults();

			this.options.geocoder.geocode(this._input.value, this._geocodeResult, this);



			return false;

		},



		_geocodeResultSelected: function(result) {

			if (this.options.collapsed) {

				this._collapse();

			} else {

				this._clearResults();

			}

			this.markGeocode(result);

		},



		_toggle: function() {

			if (this._container.className.indexOf('leaflet-control-geocoder-expanded') >= 0) {

				this._collapse();

			} else {

				this._expand();

			}

		},



		_expand: function () {

			L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-expanded');

			this._input.select();

		},



		_collapse: function () {

			this._container.className = this._container.className.replace(' leaflet-control-geocoder-expanded', '');

			L.DomUtil.addClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized');

			L.DomUtil.removeClass(this._errorElement, 'leaflet-control-geocoder-error');

		},



		_clearResults: function () {

			L.DomUtil.addClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized');

			this._selection = null;

			L.DomUtil.removeClass(this._errorElement, 'leaflet-control-geocoder-error');

		},



		_createAlt: function(result, index) {

			var li = document.createElement('li'),

			    a = L.DomUtil.create('a', '', li),

			    icon = this.options.showResultIcons && result.icon ? L.DomUtil.create('img', '', a) : null,

			    text = result.html ? undefined : document.createTextNode(result.name);



			if (icon) {

				icon.src = result.icon;

			}



			a.href = '#';

			a.setAttribute('data-result-index', index);



			if (result.html) {

				a.innerHTML = result.html;

			} else {

				a.appendChild(text);

			}



			L.DomEvent.addListener(li, 'click', function clickHandler(e) {

				L.DomEvent.preventDefault(e);

				this._geocodeResultSelected(result);

			}, this);



			return li;

		},



		_keydown: function(e) {

			var _this = this,

				select = function select(dir) {

					if (_this._selection) {

						L.DomUtil.removeClass(_this._selection.firstChild, 'leaflet-control-geocoder-selected');

						_this._selection = _this._selection[dir > 0 ? 'nextSibling' : 'previousSibling'];

					}

					if (!_this._selection) {

						_this._selection = _this._alts[dir > 0 ? 'firstChild' : 'lastChild'];

					}



					if (_this._selection) {

						L.DomUtil.addClass(_this._selection.firstChild, 'leaflet-control-geocoder-selected');

					}

				};



			switch (e.keyCode) {

			// Escape

			case 27:

				this._collapse();

				break;

			// Up

			case 38:

				select(-1);

				L.DomEvent.preventDefault(e);

				break;

			// Up

			case 40:

				select(1);

				L.DomEvent.preventDefault(e);

				break;

			// Enter

			case 13:

				if (this._selection) {

					var index = parseInt(this._selection.firstChild.getAttribute('data-result-index'), 10);

					this._geocodeResultSelected(this._results[index]);

					this._clearResults();

					L.DomEvent.preventDefault(e);

				}

			}

			return true;

		}

	});



	L.Control.geocoder = function(id, options) {

		return new L.Control.Geocoder(id, options);

	};



	L.Control.Geocoder.callbackId = 0;

	L.Control.Geocoder.jsonp = function(url, params, callback, context, jsonpParam) {

		var callbackId = '_l_geocoder_' + (L.Control.Geocoder.callbackId++);

		params[jsonpParam || 'callback'] = callbackId;

		window[callbackId] = L.Util.bind(callback, context);

		var script = document.createElement('script');

		script.type = 'text/javascript';

		script.src = url + L.Util.getParamString(params);

		script.id = callbackId;

		document.getElementsByTagName('head')[0].appendChild(script);

	};

	L.Control.Geocoder.getJSON = function(url, params, callback) {

		var xmlHttp = new XMLHttpRequest();

		xmlHttp.open( "GET", url + L.Util.getParamString(params), true);

		xmlHttp.send(null);

		xmlHttp.onreadystatechange = function () {

			if (xmlHttp.readyState != 4) return;

			if (xmlHttp.status != 200 && req.status != 304) return;

			callback(JSON.parse(xmlHttp.response));

		};

	};



	L.Control.Geocoder.template = function (str, data, htmlEscape) {

		return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) {

			var value = data[key];

			if (value === undefined) {

				value = '';

			} else if (typeof value === 'function') {

				value = value(data);

			}

			return L.Control.Geocoder.htmlEscape(value);

		});

	};



	// Adapted from handlebars.js

	// https://github.com/wycats/handlebars.js/

	L.Control.Geocoder.htmlEscape = (function() {

		var badChars = /[&<>"'`]/g;

		var possible = /[&<>"'`]/;

		var escape = {

		  '&': '&amp;',

		  '<': '&lt;',

		  '>': '&gt;',

		  '"': '&quot;',

		  '\'': '&#x27;',

		  '`': '&#x60;'

		};



		function escapeChar(chr) {

		  return escape[chr];

		}



		return function(string) {

			if (string == null) {

				return '';

			} else if (!string) {

				return string + '';

			}



			// Force a string conversion as this will be done by the append regardless and

			// the regex test will do this transparently behind the scenes, causing issues if

			// an object's to string has escaped characters in it.

			string = '' + string;



			if (!possible.test(string)) {

				return string;

			}

			return string.replace(badChars, escapeChar);

		};

	})();



	L.Control.Geocoder.Nominatim = L.Class.extend({

		options: {

			serviceUrl: '//nominatim.openstreetmap.org/',

			geocodingQueryParams: {},

			reverseQueryParams: {},

			htmlTemplate: function(r) {

				var a = r.address,

					parts = [];

				if (a.road || a.building) {

					parts.push('{building} {road} {house_number}');

				}



				if (a.city || a.town || a.village) {

					parts.push('<span class="' + (parts.length > 0 ? 'leaflet-control-geocoder-address-detail' : '') +

						'">{postcode} {city}{town}{village}</span>');

				}



				if (a.state || a.country) {

					parts.push('<span class="' + (parts.length > 0 ? 'leaflet-control-geocoder-address-context' : '') +

						'">{state} {country}</span>');

				}



				return L.Control.Geocoder.template(parts.join('<br/>'), a, true);

			}

		},



		initialize: function(options) {

			L.Util.setOptions(this, options);

		},



		geocode: function(query, cb, context) {

			L.Control.Geocoder.jsonp(this.options.serviceUrl + 'search/', L.extend({

				q: query,

				limit: 5,

				format: 'json',

				addressdetails: 1

			}, this.options.geocodingQueryParams),

			function(data) {

				var results = [];

				for (var i = data.length - 1; i >= 0; i--) {

					var bbox = data[i].boundingbox;

					for (var j = 0; j < 4; j++) bbox[j] = parseFloat(bbox[j]);

					results[i] = {

						icon: data[i].icon,

						name: data[i].display_name,

						html: this.options.htmlTemplate ?

							this.options.htmlTemplate(data[i])

							: undefined,

						bbox: L.latLngBounds([bbox[0], bbox[2]], [bbox[1], bbox[3]]),

						center: L.latLng(data[i].lat, data[i].lon),

						properties: data[i]

					};

				}

				cb.call(context, results);

			}, this, 'json_callback');

		},



		reverse: function(location, scale, cb, context) {

			L.Control.Geocoder.jsonp(this.options.serviceUrl + 'reverse/', L.extend({

				lat: location.lat,

				lon: location.lng,

				zoom: Math.round(Math.log(scale / 256) / Math.log(2)),

				addressdetails: 1,

				format: 'json'

			}, this.options.reverseQueryParams), function(data) {

				var result = [],

				    loc;



				if (data && data.lat && data.lon) {

					loc = L.latLng(data.lat, data.lon);

					result.push({

						name: data.display_name,

						html: this.options.htmlTemplate ?

							this.options.htmlTemplate(data)

							: undefined,

						center: loc,

						bounds: L.latLngBounds(loc, loc),

						properties: data

					});

				}



				cb.call(context, result);

			}, this, 'json_callback');

		}

	});



	L.Control.Geocoder.nominatim = function(options) {

		return new L.Control.Geocoder.Nominatim(options);

	};



	L.Control.Geocoder.Bing = L.Class.extend({

		initialize: function(key) {

			this.key = key;

		},



		geocode : function (query, cb, context) {

			L.Control.Geocoder.jsonp('//dev.virtualearth.net/REST/v1/Locations', {

				query: query,

				key : this.key

			}, function(data) {

				var results = [];

				for (var i = data.resourceSets[0].resources.length - 1; i >= 0; i--) {

					var resource = data.resourceSets[0].resources[i],

						bbox = resource.bbox;

					results[i] = {

						name: resource.name,

						bbox: L.latLngBounds([bbox[0], bbox[1]], [bbox[2], bbox[3]]),

						center: L.latLng(resource.point.coordinates)

					};

				}

				cb.call(context, results);

			}, this, 'jsonp');

		},



		reverse: function(location, scale, cb, context) {

			L.Control.Geocoder.jsonp('//dev.virtualearth.net/REST/v1/Locations/' + location.lat + ',' + location.lng, {

				key : this.key

			}, function(data) {

				var results = [];

				for (var i = data.resourceSets[0].resources.length - 1; i >= 0; i--) {

					var resource = data.resourceSets[0].resources[i],

						bbox = resource.bbox;

					results[i] = {

						name: resource.name,

						bbox: L.latLngBounds([bbox[0], bbox[1]], [bbox[2], bbox[3]]),

						center: L.latLng(resource.point.coordinates)

					};

				}

				cb.call(context, results);

			}, this, 'jsonp');

		}

	});



	L.Control.Geocoder.bing = function(key) {

		return new L.Control.Geocoder.Bing(key);

	};



	L.Control.Geocoder.RaveGeo = L.Class.extend({

		options: {

			querySuffix: '',

			deepSearch: true,

			wordBased: false

		},



		jsonp: function(params, callback, context) {

			var callbackId = '_l_geocoder_' + (L.Control.Geocoder.callbackId++),

				paramParts = [];

			params.prepend = callbackId + '(';

			params.append = ')';

			for (var p in params) {

				paramParts.push(p + '=' + escape(params[p]));

			}



			window[callbackId] = L.Util.bind(callback, context);

			var script = document.createElement('script');

			script.type = 'text/javascript';

			script.src = this._serviceUrl + '?' + paramParts.join('&');

			script.id = callbackId;

			document.getElementsByTagName('head')[0].appendChild(script);

		},



		initialize: function(serviceUrl, scheme, options) {

			L.Util.setOptions(this, options);



			this._serviceUrl = serviceUrl;

			this._scheme = scheme;

		},



		geocode: function(query, cb, context) {

			L.Control.Geocoder.jsonp(this._serviceUrl, {

				address: query + this.options.querySuffix,

				scheme: this._scheme,

				outputFormat: 'jsonp',

				deepSearch: this.options.deepSearch,

				wordBased: this.options.wordBased

			}, function(data) {

				var results = [];

				for (var i = data.length - 1; i >= 0; i--) {

					var r = data[i],

						c = L.latLng(r.y, r.x);

					results[i] = {

						name: r.address,

						bbox: L.latLngBounds([c]),

						center: c

					};

				}

				cb.call(context, results);

			}, this);

		}

	});



	L.Control.Geocoder.raveGeo = function(serviceUrl, scheme, options) {

		return new L.Control.Geocoder.RaveGeo(serviceUrl, scheme, options);

	};



	L.Control.Geocoder.MapQuest = L.Class.extend({

		initialize: function(key) {

			// MapQuest seems to provide URI encoded API keys,

			// so to avoid encoding them twice, we decode them here

			this._key = decodeURIComponent(key);

		},



		_formatName: function() {

			var r = [],

				i;

			for (i = 0; i < arguments.length; i++) {

				if (arguments[i]) {

					r.push(arguments[i]);

				}

			}



			return r.join(', ');

		},



		geocode: function(query, cb, context) {

			L.Control.Geocoder.jsonp('//www.mapquestapi.com/geocoding/v1/address', {

				key: this._key,

				location: query,

				limit: 5,

				outFormat: 'json'

			}, function(data) {

				var results = [],

					loc,

					latLng;

				if (data.results && data.results[0].locations) {

					for (var i = data.results[0].locations.length - 1; i >= 0; i--) {

						loc = data.results[0].locations[i];

						latLng = L.latLng(loc.latLng);

						results[i] = {

							name: this._formatName(loc.street, loc.adminArea4, loc.adminArea3, loc.adminArea1),

							bbox: L.latLngBounds(latLng, latLng),

							center: latLng

						};

					}

				}



				cb.call(context, results);

			}, this);

		},



		reverse: function(location, scale, cb, context) {

			L.Control.Geocoder.jsonp('//www.mapquestapi.com/geocoding/v1/reverse', {

				key: this._key,

				location: location.lat + ',' + location.lng,

				outputFormat: 'json'

			}, function(data) {

				var results = [],

					loc,

					latLng;

				if (data.results && data.results[0].locations) {

					for (var i = data.results[0].locations.length - 1; i >= 0; i--) {

						loc = data.results[0].locations[i];

						latLng = L.latLng(loc.latLng);

						results[i] = {

							name: this._formatName(loc.street, loc.adminArea4, loc.adminArea3, loc.adminArea1),

							bbox: L.latLngBounds(latLng, latLng),

							center: latLng

						};

					}

				}



				cb.call(context, results);

			}, this);

		}

	});



	L.Control.Geocoder.mapQuest = function(key) {

		return new L.Control.Geocoder.MapQuest(key);

	};



	L.Control.Geocoder.Mapbox = L.Class.extend({

		options: {

			service_url: 'https://api.tiles.mapbox.com/v4/geocode/mapbox.places-v1/'

		},



		initialize: function(access_token) {

			this._access_token = access_token;

		},



		geocode: function(query, cb, context) {

			L.Control.Geocoder.getJSON(this.options.service_url + encodeURIComponent(query) + '.json', {

				access_token: this._access_token,

			}, function(data) {

				var results = [],

				loc,

				latLng,

				latLngBounds;

				if (data.features && data.features.length) {

					for (var i = 0; i <= data.features.length - 1; i++) {

						loc = data.features[i];

						latLng = L.latLng(loc.center.reverse());

						if(loc.hasOwnProperty('bbox'))

							{

								latLngBounds = L.latLngBounds(L.latLng(loc.bbox.slice(0, 2).reverse()), L.latLng(loc.bbox.slice(2, 4).reverse()));

							}

							else

							{

								latLngBounds = L.latLngBounds(latLng, latLng);

							}

							results[i] = {

								name: loc.place_name,

								bbox: latLngBounds,

								center: latLng

							};

						}

					}



					cb.call(context, results);

			});

		},



		reverse: function(location, scale, cb, context) {

			L.Control.Geocoder.getJSON(this.options.service_url + encodeURIComponent(location.lng) + ',' + encodeURIComponent(location.lat) + '.json', {

				access_token: this._access_token,

			}, function(data) {

				var results = [],

				loc,

				latLng,

				latLngBounds;

				if (data.features && data.features.length) {

					for (var i = 0; i <= data.features.length - 1; i++) {

						loc = data.features[i];

						latLng = L.latLng(loc.center.reverse());

						if(loc.hasOwnProperty('bbox'))

						{

							latLngBounds = L.latLngBounds(L.latLng(loc.bbox.slice(0, 2).reverse()), L.latLng(loc.bbox.slice(2, 4).reverse()));

						}

						else

						{

							latLngBounds = L.latLngBounds(latLng, latLng);

						}

						results[i] = {

							name: loc.place_name,

							bbox: latLngBounds,

							center: latLng

						};

					}

				}



				cb.call(context, results);

			});

		}

	});



	L.Control.Geocoder.mapbox = function(access_token) {

			return new L.Control.Geocoder.Mapbox(access_token);

	};



	L.Control.Geocoder.Google = L.Class.extend({

		options: {

			service_url: 'https://maps.googleapis.com/maps/api/geocode/json'

		},



		initialize: function(key) {

				this._key = key;

		},



		geocode: function(query, cb, context) {

			var params = {

				address: query,

			};

			if(this._key && this._key.length)

			{

				params['key'] = this._key

			}



			L.Control.Geocoder.getJSON(this.options.service_url, params, function(data) {

					var results = [],

							loc,

							latLng,

							latLngBounds;

					if (data.results && data.results.length) {

						for (var i = 0; i <= data.results.length - 1; i++) {

							loc = data.results[i];

							latLng = L.latLng(loc.geometry.location);

							latLngBounds = L.latLngBounds(L.latLng(loc.geometry.viewport.northeast), L.latLng(loc.geometry.viewport.southwest));

							results[i] = {

									name: loc.formatted_address,

									bbox: latLngBounds,

									center: latLng

							};

						}

					}



					cb.call(context, results);

			});

		},



		reverse: function(location, scale, cb, context) {

			var params = {

				latlng: encodeURIComponent(location.lat) + ',' + encodeURIComponent(location.lng)

			};

			if(this._key && this._key.length)

			{

				params['key'] = this._key

			}

			L.Control.Geocoder.getJSON(this.options.service_url, params, function(data) {

				var results = [],

						loc,

						latLng,

						latLngBounds;

				if (data.results && data.results.length) {

					for (var i = 0; i <= data.results.length - 1; i++) {

						loc = data.results[i];

						latLng = L.latLng(loc.geometry.location);

						latLngBounds = L.latLngBounds(L.latLng(loc.geometry.viewport.northeast), L.latLng(loc.geometry.viewport.southwest));

						results[i] = {

							name: loc.formatted_address,

							bbox: latLngBounds,

							center: latLng

						};

					}

				}



				cb.call(context, results);

			});

		}

	});



	L.Control.Geocoder.google = function(key) {

		return new L.Control.Geocoder.Google(key);

	};

	return L.Control.Geocoder;

}));

