(function() {

	'use strict';



	var L = require('leaflet');



	var Itinerary = require('./itinerary');

	var Line = require('./line');

	var Plan = require('./plan');

	var OSRMv1 = require('./osrm-v1');



	module.exports = Itinerary.extend({

		options: {

			fitSelectedRoutes: 'smart',

			routeLine: function(route, options) { return new Line(route, options); },

			autoRoute: true,

			routeWhileDragging: false,

			routeDragInterval: 500,

			waypointMode: 'connect',

			showAlternatives: false,

			defaultErrorHandler: function(e) {

				console.error('Routing error:', e.error);

			}

		},



		initialize: function(options) {

			L.Util.setOptions(this, options);



			this._router = this.options.router || new OSRMv1(options);

			this._plan = this.options.plan || new Plan(this.options.waypoints, options);

			this._requestCount = 0;



			Itinerary.prototype.initialize.call(this, options);



			this.on('routeselected', this._routeSelected, this);

			if (this.options.defaultErrorHandler) {

				this.on('routingerror', this.options.defaultErrorHandler);

			}

			this._plan.on('waypointschanged', this._onWaypointsChanged, this);

			if (options.routeWhileDragging) {

				this._setupRouteDragging();

			}

		},



		_onZoomEnd: function() {

			if (!this._selectedRoute ||

				!this._router.requiresMoreDetail) {

				return;

			}



			var map = this._map;

			if (this._router.requiresMoreDetail(this._selectedRoute,

					map.getZoom(), map.getBounds())) {

				this.route({

					callback: L.bind(function(err, routes) {

						var i;

						if (!err) {

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

								this._routes[i].properties = routes[i].properties;

							}

							this._updateLineCallback(err, routes);

						}



					}, this),

					simplifyGeometry: false,

					geometryOnly: true

				});

			}

		},



		onAdd: function(map) {

			if (this.options.autoRoute) {

				this.route();

			}



			var container = Itinerary.prototype.onAdd.call(this, map);



			this._map = map;

			this._map.addLayer(this._plan);



			this._map.on('zoomend', this._onZoomEnd, this);



			if (this._plan.options.geocoder) {

				container.insertBefore(this._plan.createGeocoders(), container.firstChild);

			}



			return container;

		},



		onRemove: function(map) {

			map.off('zoomend', this._onZoomEnd, this);

			if (this._line) {

				map.removeLayer(this._line);

			}

			map.removeLayer(this._plan);

			if (this._alternatives && this._alternatives.length > 0) {

				for (var i = 0, len = this._alternatives.length; i < len; i++) {

					map.removeLayer(this._alternatives[i]);

				}

			}

			return Itinerary.prototype.onRemove.call(this, map);

		},



		getWaypoints: function() {

			return this._plan.getWaypoints();

		},



		setWaypoints: function(waypoints) {

			this._plan.setWaypoints(waypoints);

			return this;

		},



		spliceWaypoints: function() {

			var removed = this._plan.spliceWaypoints.apply(this._plan, arguments);

			return removed;

		},



		getPlan: function() {

			return this._plan;

		},



		getRouter: function() {

			return this._router;

		},



		_routeSelected: function(e) {

			var route = this._selectedRoute = e.route,

				alternatives = this.options.showAlternatives && e.alternatives,

				fitMode = this.options.fitSelectedRoutes,

				fitBounds =

					(fitMode === 'smart' && !this._waypointsVisible()) ||

					(fitMode !== 'smart' && fitMode);



			this._updateLines({route: route, alternatives: alternatives});



			if (fitBounds) {

				this._map.fitBounds(this._line.getBounds());

			}



			if (this.options.waypointMode === 'snap') {

				this._plan.off('waypointschanged', this._onWaypointsChanged, this);

				this.setWaypoints(route.waypoints);

				this._plan.on('waypointschanged', this._onWaypointsChanged, this);

			}

		},



		_waypointsVisible: function() {

			var wps = this.getWaypoints(),

				mapSize,

				bounds,

				boundsSize,

				i,

				p;



			try {

				mapSize = this._map.getSize();



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

					p = this._map.latLngToLayerPoint(wps[i].latLng);



					if (bounds) {

						bounds.extend(p);

					} else {

						bounds = L.bounds([p]);

					}

				}



				boundsSize = bounds.getSize();

				return (boundsSize.x > mapSize.x / 5 ||

					boundsSize.y > mapSize.y / 5) && this._waypointsInViewport();



			} catch (e) {

				return false;

			}

		},



		_waypointsInViewport: function() {

			var wps = this.getWaypoints(),

				mapBounds,

				i;



			try {

				mapBounds = this._map.getBounds();

			} catch (e) {

				return false;

			}



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

				if (mapBounds.contains(wps[i].latLng)) {

					return true;

				}

			}



			return false;

		},



		_updateLines: function(routes) {

			var addWaypoints = this.options.addWaypoints !== undefined ?

				this.options.addWaypoints : true;

			this._clearLines();



			// add alternatives first so they lie below the main route

			this._alternatives = [];

			if (routes.alternatives) routes.alternatives.forEach(function(alt, i) {

				this._alternatives[i] = this.options.routeLine(alt,

					L.extend({

						isAlternative: true

					}, this.options.altLineOptions || this.options.lineOptions));

				this._alternatives[i].addTo(this._map);

				this._hookAltEvents(this._alternatives[i]);

			}, this);



			this._line = this.options.routeLine(routes.route,

				L.extend({

					addWaypoints: addWaypoints,

					extendToWaypoints: this.options.waypointMode === 'connect'

				}, this.options.lineOptions));

			this._line.addTo(this._map);

			this._hookEvents(this._line);

		},



		_hookEvents: function(l) {

			l.on('linetouched', function(e) {

				this._plan.dragNewWaypoint(e);

			}, this);

		},



		_hookAltEvents: function(l) {

			l.on('linetouched', function(e) {

				var alts = this._routes.slice();

				var selected = alts.splice(e.target._route.routesIndex, 1)[0];

				this.fire('routeselected', {route: selected, alternatives: alts});

			}, this);

		},



		_onWaypointsChanged: function(e) {

			if (this.options.autoRoute) {

				this.route({});

			}

			if (!this._plan.isReady()) {

				this._clearLines();

				this._clearAlts();

			}

			this.fire('waypointschanged', {waypoints: e.waypoints});

		},



		_setupRouteDragging: function() {

			var timer = 0,

				waypoints;



			this._plan.on('waypointdrag', L.bind(function(e) {

				waypoints = e.waypoints;



				if (!timer) {

					timer = setTimeout(L.bind(function() {

						this.route({

							waypoints: waypoints,

							geometryOnly: true,

							callback: L.bind(this._updateLineCallback, this)

						});

						timer = undefined;

					}, this), this.options.routeDragInterval);

				}

			}, this));

			this._plan.on('waypointdragend', function() {

				if (timer) {

					clearTimeout(timer);

					timer = undefined;

				}

				this.route();

			}, this);

		},



		_updateLineCallback: function(err, routes) {

			if (!err) {

				routes = routes.slice();

				var selected = routes.splice(this._selectedRoute.routesIndex, 1)[0];

				this._updateLines({

					route: selected,

					alternatives: this.options.showAlternatives ? routes : []

				});

			} else if (err.type !== 'abort') {

				this._clearLines();

			}

		},



		route: function(options) {

			var ts = ++this._requestCount,

				wps;



			if (this._pendingRequest && this._pendingRequest.abort) {

				this._pendingRequest.abort();

				this._pendingRequest = null;

			}



			options = options || {};



			if (this._plan.isReady()) {

				if (this.options.useZoomParameter) {

					options.z = this._map && this._map.getZoom();

				}



				wps = options && options.waypoints || this._plan.getWaypoints();

				this.fire('routingstart', {waypoints: wps});

				this._pendingRequest = this._router.route(wps, function(err, routes) {

					this._pendingRequest = null;



					if (options.callback) {

						return options.callback.call(this, err, routes);

					}



					// Prevent race among multiple requests,

					// by checking the current request's count

					// against the last request's; ignore result if

					// this isn't the last request.

					if (ts === this._requestCount) {

						this._clearLines();

						this._clearAlts();

						if (err && err.type !== 'abort') {

							this.fire('routingerror', {error: err});

							return;

						}



						routes.forEach(function(route, i) { route.routesIndex = i; });



						if (!options.geometryOnly) {

							this.fire('routesfound', {waypoints: wps, routes: routes});

							this.setAlternatives(routes);

						} else {

							var selectedRoute = routes.splice(0,1)[0];

							this._routeSelected({route: selectedRoute, alternatives: routes});

						}

					}

				}, this, options);

			}

		},



		_clearLines: function() {

			if (this._line) {

				this._map.removeLayer(this._line);

				delete this._line;

			}

			if (this._alternatives && this._alternatives.length) {

				for (var i in this._alternatives) {

					this._map.removeLayer(this._alternatives[i]);

				}

				this._alternatives = [];

			}

		}

	});

})();

