export var MarkerCluster = L.MarkerCluster = L.Marker.extend({

	options: L.Icon.prototype.options,



	initialize: function (group, zoom, a, b) {



		L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0),

            { icon: this, pane: group.options.clusterPane });



		this._group = group;

		this._zoom = zoom;



		this._markers = [];

		this._childClusters = [];

		this._childCount = 0;

		this._iconNeedsUpdate = true;

		this._boundsNeedUpdate = true;



		this._bounds = new L.LatLngBounds();



		if (a) {

			this._addChild(a);

		}

		if (b) {

			this._addChild(b);

		}

	},



	//Recursively retrieve all child markers of this cluster

	getAllChildMarkers: function (storageArray, ignoreDraggedMarker) {

		storageArray = storageArray || [];



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

			this._childClusters[i].getAllChildMarkers(storageArray);

		}



		for (var j = this._markers.length - 1; j >= 0; j--) {

			if (ignoreDraggedMarker && this._markers[j].__dragStart) {

				continue;

			}

			storageArray.push(this._markers[j]);

		}



		return storageArray;

	},



	//Returns the count of how many child markers we have

	getChildCount: function () {

		return this._childCount;

	},



	//Zoom to the minimum of showing all of the child markers, or the extents of this cluster

	zoomToBounds: function (fitBoundsOptions) {

		var childClusters = this._childClusters.slice(),

			map = this._group._map,

			boundsZoom = map.getBoundsZoom(this._bounds),

			zoom = this._zoom + 1,

			mapZoom = map.getZoom(),

			i;



		//calculate how far we need to zoom down to see all of the markers

		while (childClusters.length > 0 && boundsZoom > zoom) {

			zoom++;

			var newClusters = [];

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

				newClusters = newClusters.concat(childClusters[i]._childClusters);

			}

			childClusters = newClusters;

		}



		if (boundsZoom > zoom) {

			this._group._map.setView(this._latlng, zoom);

		} else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead

			this._group._map.setView(this._latlng, mapZoom + 1);

		} else {

			this._group._map.fitBounds(this._bounds, fitBoundsOptions);

		}

	},



	getBounds: function () {

		var bounds = new L.LatLngBounds();

		bounds.extend(this._bounds);

		return bounds;

	},



	_updateIcon: function () {

		this._iconNeedsUpdate = true;

		if (this._icon) {

			this.setIcon(this);

		}

	},



	//Cludge for Icon, we pretend to be an icon for performance

	createIcon: function () {

		if (this._iconNeedsUpdate) {

			this._iconObj = this._group.options.iconCreateFunction(this);

			this._iconNeedsUpdate = false;

		}

		return this._iconObj.createIcon();

	},

	createShadow: function () {

		return this._iconObj.createShadow();

	},





	_addChild: function (new1, isNotificationFromChild) {



		this._iconNeedsUpdate = true;



		this._boundsNeedUpdate = true;

		this._setClusterCenter(new1);



		if (new1 instanceof L.MarkerCluster) {

			if (!isNotificationFromChild) {

				this._childClusters.push(new1);

				new1.__parent = this;

			}

			this._childCount += new1._childCount;

		} else {

			if (!isNotificationFromChild) {

				this._markers.push(new1);

			}

			this._childCount++;

		}



		if (this.__parent) {

			this.__parent._addChild(new1, true);

		}

	},



	/**

	 * Makes sure the cluster center is set. If not, uses the child center if it is a cluster, or the marker position.

	 * @param child L.MarkerCluster|L.Marker that will be used as cluster center if not defined yet.

	 * @private

	 */

	_setClusterCenter: function (child) {

		if (!this._cLatLng) {

			// when clustering, take position of the first point as the cluster center

			this._cLatLng = child._cLatLng || child._latlng;

		}

	},



	/**

	 * Assigns impossible bounding values so that the next extend entirely determines the new bounds.

	 * This method avoids having to trash the previous L.LatLngBounds object and to create a new one, which is much slower for this class.

	 * As long as the bounds are not extended, most other methods would probably fail, as they would with bounds initialized but not extended.

	 * @private

	 */

	_resetBounds: function () {

		var bounds = this._bounds;



		if (bounds._southWest) {

			bounds._southWest.lat = Infinity;

			bounds._southWest.lng = Infinity;

		}

		if (bounds._northEast) {

			bounds._northEast.lat = -Infinity;

			bounds._northEast.lng = -Infinity;

		}

	},



	_recalculateBounds: function () {

		var markers = this._markers,

		    childClusters = this._childClusters,

		    latSum = 0,

		    lngSum = 0,

		    totalCount = this._childCount,

		    i, child, childLatLng, childCount;



		// Case where all markers are removed from the map and we are left with just an empty _topClusterLevel.

		if (totalCount === 0) {

			return;

		}



		// Reset rather than creating a new object, for performance.

		this._resetBounds();



		// Child markers.

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

			childLatLng = markers[i]._latlng;



			this._bounds.extend(childLatLng);



			latSum += childLatLng.lat;

			lngSum += childLatLng.lng;

		}



		// Child clusters.

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

			child = childClusters[i];



			// Re-compute child bounds and weighted position first if necessary.

			if (child._boundsNeedUpdate) {

				child._recalculateBounds();

			}



			this._bounds.extend(child._bounds);



			childLatLng = child._wLatLng;

			childCount = child._childCount;



			latSum += childLatLng.lat * childCount;

			lngSum += childLatLng.lng * childCount;

		}



		this._latlng = this._wLatLng = new L.LatLng(latSum / totalCount, lngSum / totalCount);



		// Reset dirty flag.

		this._boundsNeedUpdate = false;

	},



	//Set our markers position as given and add it to the map

	_addToMap: function (startPos) {

		if (startPos) {

			this._backupLatlng = this._latlng;

			this.setLatLng(startPos);

		}

		this._group._featureGroup.addLayer(this);

	},



	_recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) {

		this._recursively(bounds, this._group._map.getMinZoom(), maxZoom - 1,

			function (c) {

				var markers = c._markers,

					i, m;

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

					m = markers[i];



					//Only do it if the icon is still on the map

					if (m._icon) {

						m._setPos(center);

						m.clusterHide();

					}

				}

			},

			function (c) {

				var childClusters = c._childClusters,

					j, cm;

				for (j = childClusters.length - 1; j >= 0; j--) {

					cm = childClusters[j];

					if (cm._icon) {

						cm._setPos(center);

						cm.clusterHide();

					}

				}

			}

		);

	},



	_recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, mapMinZoom, previousZoomLevel, newZoomLevel) {

		this._recursively(bounds, newZoomLevel, mapMinZoom,

			function (c) {

				c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel);



				//TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be.

				//As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate

				if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) {

					c.clusterShow();

					c._recursivelyRemoveChildrenFromMap(bounds, mapMinZoom, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds

				} else {

					c.clusterHide();

				}



				c._addToMap();

			}

		);

	},



	_recursivelyBecomeVisible: function (bounds, zoomLevel) {

		this._recursively(bounds, this._group._map.getMinZoom(), zoomLevel, null, function (c) {

			c.clusterShow();

		});

	},



	_recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) {

		this._recursively(bounds, this._group._map.getMinZoom() - 1, zoomLevel,

			function (c) {

				if (zoomLevel === c._zoom) {

					return;

				}



				//Add our child markers at startPos (so they can be animated out)

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

					var nm = c._markers[i];



					if (!bounds.contains(nm._latlng)) {

						continue;

					}



					if (startPos) {

						nm._backupLatlng = nm.getLatLng();



						nm.setLatLng(startPos);

						if (nm.clusterHide) {

							nm.clusterHide();

						}

					}



					c._group._featureGroup.addLayer(nm);

				}

			},

			function (c) {

				c._addToMap(startPos);

			}

		);

	},



	_recursivelyRestoreChildPositions: function (zoomLevel) {

		//Fix positions of child markers

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

			var nm = this._markers[i];

			if (nm._backupLatlng) {

				nm.setLatLng(nm._backupLatlng);

				delete nm._backupLatlng;

			}

		}



		if (zoomLevel - 1 === this._zoom) {

			//Reposition child clusters

			for (var j = this._childClusters.length - 1; j >= 0; j--) {

				this._childClusters[j]._restorePosition();

			}

		} else {

			for (var k = this._childClusters.length - 1; k >= 0; k--) {

				this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel);

			}

		}

	},



	_restorePosition: function () {

		if (this._backupLatlng) {

			this.setLatLng(this._backupLatlng);

			delete this._backupLatlng;

		}

	},



	//exceptBounds: If set, don't remove any markers/clusters in it

	_recursivelyRemoveChildrenFromMap: function (previousBounds, mapMinZoom, zoomLevel, exceptBounds) {

		var m, i;

		this._recursively(previousBounds, mapMinZoom - 1, zoomLevel - 1,

			function (c) {

				//Remove markers at every level

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

					m = c._markers[i];

					if (!exceptBounds || !exceptBounds.contains(m._latlng)) {

						c._group._featureGroup.removeLayer(m);

						if (m.clusterShow) {

							m.clusterShow();

						}

					}

				}

			},

			function (c) {

				//Remove child clusters at just the bottom level

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

					m = c._childClusters[i];

					if (!exceptBounds || !exceptBounds.contains(m._latlng)) {

						c._group._featureGroup.removeLayer(m);

						if (m.clusterShow) {

							m.clusterShow();

						}

					}

				}

			}

		);

	},



	//Run the given functions recursively to this and child clusters

	// boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to

	// zoomLevelToStart: zoom level to start running functions (inclusive)

	// zoomLevelToStop: zoom level to stop running functions (inclusive)

	// runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level

	// runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level

	_recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) {

		var childClusters = this._childClusters,

		    zoom = this._zoom,

		    i, c;



		if (zoomLevelToStart <= zoom) {

			if (runAtEveryLevel) {

				runAtEveryLevel(this);

			}

			if (runAtBottomLevel && zoom === zoomLevelToStop) {

				runAtBottomLevel(this);

			}

		}



		if (zoom < zoomLevelToStart || zoom < zoomLevelToStop) {

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

				c = childClusters[i];

				if (c._boundsNeedUpdate) {

					c._recalculateBounds();

				}

				if (boundsToApplyTo.intersects(c._bounds)) {

					c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel);

				}

			}

		}

	},



	//Returns true if we are the parent of only one cluster and that cluster is the same as us

	_isSingleParent: function () {

		//Don't need to check this._markers as the rest won't work if there are any

		return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount;

	}

});



