(function() {

	'use strict';



	var L = require('leaflet'),

		corslite = require('@mapbox/corslite'),

		polyline = require('@mapbox/polyline'),

		osrmTextInstructions = require('osrm-text-instructions')('v5');



	// Ignore camelcase naming for this file, since OSRM's API uses

	// underscores.

	/* jshint camelcase: false */



	var Waypoint = require('./waypoint');



	/**

	 * Works against OSRM's new API in version 5.0; this has

	 * the API version v1.

	 */

	module.exports = L.Class.extend({

		options: {

			serviceUrl: 'https://router.project-osrm.org/route/v1',

			profile: 'driving',

			timeout: 30 * 1000,

			routingOptions: {

				alternatives: true,

				steps: true

			},

			polylinePrecision: 5,

			useHints: true,

			suppressDemoServerWarning: false,

			language: 'en'

		},



		initialize: function(options) {

			L.Util.setOptions(this, options);

			this._hints = {

				locations: {}

			};



			if (!this.options.suppressDemoServerWarning &&

				this.options.serviceUrl.indexOf('//router.project-osrm.org') >= 0) {

				console.warn('You are using OSRM\'s demo server. ' +

					'Please note that it is **NOT SUITABLE FOR PRODUCTION USE**.\n' +

					'Refer to the demo server\'s usage policy: ' +

					'https://github.com/Project-OSRM/osrm-backend/wiki/Api-usage-policy\n\n' +

					'To change, set the serviceUrl option.\n\n' +

					'Please do not report issues with this server to neither ' +

					'Leaflet Routing Machine or OSRM - it\'s for\n' +

					'demo only, and will sometimes not be available, or work in ' +

					'unexpected ways.\n\n' +

					'Please set up your own OSRM server, or use a paid service ' +

					'provider for production.');

			}

		},



		route: function(waypoints, callback, context, options) {

			var timedOut = false,

				wps = [],

				url,

				timer,

				wp,

				i,

				xhr;



			options = L.extend({}, this.options.routingOptions, options);

			url = this.buildRouteUrl(waypoints, options);

			if (this.options.requestParameters) {

				url += L.Util.getParamString(this.options.requestParameters, url);

			}



			timer = setTimeout(function() {

				timedOut = true;

				callback.call(context || callback, {

					status: -1,

					message: 'OSRM request timed out.'

				});

			}, this.options.timeout);



			// Create a copy of the waypoints, since they

			// might otherwise be asynchronously modified while

			// the request is being processed.

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

				wp = waypoints[i];

				wps.push(new Waypoint(wp.latLng, wp.name, wp.options));

			}



			return xhr = corslite(url, L.bind(function(err, resp) {

				var data,

					error =  {};



				clearTimeout(timer);

				if (!timedOut) {

					if (!err) {

						try {

							data = JSON.parse(resp.responseText);

							try {

								return this._routeDone(data, wps, options, callback, context);

							} catch (ex) {

								error.status = -3;

								error.message = ex.toString();

							}

						} catch (ex) {

							error.status = -2;

							error.message = 'Error parsing OSRM response: ' + ex.toString();

						}

					} else {

						error.message = 'HTTP request failed: ' + err.type +

							(err.target && err.target.status ? ' HTTP ' + err.target.status + ': ' + err.target.statusText : '');

						error.url = url;

						error.status = -1;

						error.target = err;

					}



					callback.call(context || callback, error);

				} else {

					xhr.abort();

				}

			}, this));

		},



		requiresMoreDetail: function(route, zoom, bounds) {

			if (!route.properties.isSimplified) {

				return false;

			}



			var waypoints = route.inputWaypoints,

				i;

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

				if (!bounds.contains(waypoints[i].latLng)) {

					return true;

				}

			}



			return false;

		},



		_routeDone: function(response, inputWaypoints, options, callback, context) {

			var alts = [],

			    actualWaypoints,

			    i,

			    route;



			context = context || callback;

			if (response.code !== 'Ok') {

				callback.call(context, {

					status: response.code

				});

				return;

			}



			actualWaypoints = this._toWaypoints(inputWaypoints, response.waypoints);



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

				route = this._convertRoute(response.routes[i]);

				route.inputWaypoints = inputWaypoints;

				route.waypoints = actualWaypoints;

				route.properties = {isSimplified: !options || !options.geometryOnly || options.simplifyGeometry};

				alts.push(route);

			}



			this._saveHintData(response.waypoints, inputWaypoints);



			callback.call(context, null, alts);

		},



		_convertRoute: function(responseRoute) {

			var result = {

					name: '',

					coordinates: [],

					instructions: [],

					summary: {

						totalDistance: responseRoute.distance,

						totalTime: responseRoute.duration

					}

				},

				legNames = [],

				waypointIndices = [],

				index = 0,

				legCount = responseRoute.legs.length,

				hasSteps = responseRoute.legs[0].steps.length > 0,

				i,

				j,

				leg,

				step,

				geometry,

				type,

				modifier,

				text,

				stepToText;



			if (this.options.stepToText) {

				stepToText = this.options.stepToText;

			} else {

				stepToText = L.bind(osrmTextInstructions.compile, osrmTextInstructions, this.options.language);

			}



			for (i = 0; i < legCount; i++) {

				leg = responseRoute.legs[i];

				legNames.push(leg.summary && leg.summary.charAt(0).toUpperCase() + leg.summary.substring(1));

				for (j = 0; j < leg.steps.length; j++) {

					step = leg.steps[j];

					geometry = this._decodePolyline(step.geometry);

					result.coordinates.push.apply(result.coordinates, geometry);

					type = this._maneuverToInstructionType(step.maneuver, i === legCount - 1);

					modifier = this._maneuverToModifier(step.maneuver);

					text = stepToText(step, {legCount: legCount, legIndex: i});



					if (type) {

						if ((i == 0 && step.maneuver.type == 'depart') || step.maneuver.type == 'arrive') {

							waypointIndices.push(index);

						}



						result.instructions.push({

							type: type,

							distance: step.distance,

							time: step.duration,

							road: step.name,

							direction: this._bearingToDirection(step.maneuver.bearing_after),

							exit: step.maneuver.exit,

							index: index,

							mode: step.mode,

							modifier: modifier,

							text: text

						});

					}



					index += geometry.length;

				}

			}



			result.name = legNames.join(', ');

			if (!hasSteps) {

				result.coordinates = this._decodePolyline(responseRoute.geometry);

			} else {

				result.waypointIndices = waypointIndices;

			}



			return result;

		},



		_bearingToDirection: function(bearing) {

			var oct = Math.round(bearing / 45) % 8;

			return ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'][oct];

		},



		_maneuverToInstructionType: function(maneuver, lastLeg) {

			switch (maneuver.type) {

			case 'new name':

				return 'Continue';

			case 'depart':

				return 'Head';

			case 'arrive':

				return lastLeg ? 'DestinationReached' : 'WaypointReached';

			case 'roundabout':

			case 'rotary':

				return 'Roundabout';

			case 'merge':

			case 'fork':

			case 'on ramp':

			case 'off ramp':

			case 'end of road':

				return this._camelCase(maneuver.type);

			// These are all reduced to the same instruction in the current model

			//case 'turn':

			//case 'ramp': // deprecated in v5.1

			default:

				return this._camelCase(maneuver.modifier);

			}

		},



		_maneuverToModifier: function(maneuver) {

			var modifier = maneuver.modifier;



			switch (maneuver.type) {

			case 'merge':

			case 'fork':

			case 'on ramp':

			case 'off ramp':

			case 'end of road':

				modifier = this._leftOrRight(modifier);

			}



			return modifier && this._camelCase(modifier);

		},



		_camelCase: function(s) {

			var words = s.split(' '),

				result = '';

			for (var i = 0, l = words.length; i < l; i++) {

				result += words[i].charAt(0).toUpperCase() + words[i].substring(1);

			}



			return result;

		},



		_leftOrRight: function(d) {

			return d.indexOf('left') >= 0 ? 'Left' : 'Right';

		},



		_decodePolyline: function(routeGeometry) {

			var cs = polyline.decode(routeGeometry, this.options.polylinePrecision),

				result = new Array(cs.length),

				i;

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

				result[i] = L.latLng(cs[i]);

			}



			return result;

		},



		_toWaypoints: function(inputWaypoints, vias) {

			var wps = [],

			    i,

			    viaLoc;

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

				viaLoc = vias[i].location;

				wps.push(new Waypoint(L.latLng(viaLoc[1], viaLoc[0]),

				                            inputWaypoints[i].name,

											inputWaypoints[i].options));

			}



			return wps;

		},



		buildRouteUrl: function(waypoints, options) {

			var locs = [],

				hints = [],

				wp,

				latLng,

			    computeInstructions,

			    computeAlternative = true;



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

				wp = waypoints[i];

				latLng = wp.latLng;

				locs.push(latLng.lng + ',' + latLng.lat);

				hints.push(this._hints.locations[this._locationKey(latLng)] || '');

			}



			computeInstructions =

				true;



			return this.options.serviceUrl + '/' + this.options.profile + '/' +

				locs.join(';') + '?' +

				(options.geometryOnly ? (options.simplifyGeometry ? '' : 'overview=full') : 'overview=false') +

				'&alternatives=' + computeAlternative.toString() +

				'&steps=' + computeInstructions.toString() +

				(this.options.useHints ? '&hints=' + hints.join(';') : '') +

				(options.allowUTurns ? '&continue_straight=' + !options.allowUTurns : '');

		},



		_locationKey: function(location) {

			return location.lat + ',' + location.lng;

		},



		_saveHintData: function(actualWaypoints, waypoints) {

			var loc;

			this._hints = {

				locations: {}

			};

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

				loc = waypoints[i].latLng;

				this._hints.locations[this._locationKey(loc)] = actualWaypoints[i].hint;

			}

		},

	});

})();

