/**
 * Google maps AdvMarker namespace.
 */
var AdvMarker = {};

/**
 * @param {GMap2} map The map that the markers should be added to.
 * @param {Object} options A set of options
 * @param [options.gridSize=20] The level at when clusters should start
 *            grouping.
 * @param clusterPopupCallback The callback when the custer is clicked.
 */
AdvMarker.Manager = function( map, options )
{
	var _map = map;
	var _typeGroup = {};
	var _allMarkerArray = [];
	var _allClusterArray = [];
	var _hiddenMarkers = [];
	var _currentLevelCluster = [];

	var _options = {
		gridSize : 25,
		clusterPopupCallback : options.clusterPopupCallback
	};

	GEvent.addListener(_map, "moveend", function()
	{
		resetViewport();
	});

	/**
	 * Clear all clusters/markers.
	 * <p>
	 * Note: types that are registered are not cleared
	 * </p>
	 */
	this.clear = function()
	{
		for( var int = 0; int < _allClusterArray.length; int++ ) {
			var cluster = _allClusterArray[int];
			cluster.clear();
		}

		_allMarkerArray = [];
		_allClusterArray = [];
		_hiddenMarkers = [];
		_currentLevelCluster = [];

		for( var typeName in _typeGroup ) {
			var typeObj = _typeGroup[typeName];
			typeObj.clear();
		}

		_map.clearOverlays();
	};

	/**
	 * Get a list of all the markers.
	 * <p>
	 * Note: this will return all registered markers even if they are hidden
	 * </p>
	 * 
	 * @return {Array of AdvMarker.Marker}
	 */
	this.getMarkerArray = function()
	{
		return _allMarkerArray;
	};

	/**
	 * Add a new type for markers to be grouped on.
	 * 
	 * @param {string} name
	 * @param {GIcon} icon
	 */
	this.addType = function( name, icon )
	{
		_typeGroup[name] = new AdvMarker.Type(name, icon);
	};

	/**
	 * Add a new marker to be managed.
	 * 
	 * @param {GLatLng} point
	 * @param {string} typeName Must be previously registered with
	 *            <code>addType</code>
	 * @return {AdvMarker.Marker} the created marker
	 */
	this.addMarker = function( point, typeName )
	{
		var typeName = typeName;
		var gMarker = new GMarker(point);
		var marker = new AdvMarker.Marker(gMarker, _typeGroup[typeName]);

		_typeGroup[typeName].addMarker(marker);
		_allMarkerArray.push(marker);

		addToCluster(marker);

		return marker;
	};

	/**
	 * Toggle the visibility of a marker type.
	 * 
	 * @param {string} typeName Must be previously registered with
	 *            <code>addType</code>
	 */
	this.toggleType = function( typeName )
	{
		if( _typeGroup[typeName].isHidden() ) {
			_typeGroup[typeName].show();
		}
		else {
			_typeGroup[typeName].hide();
		}
	};

	/**
	 * Add a marker to the closest cluster.
	 * <p>
	 * Note: This will create clusters if there is not a close cluster
	 * </p>
	 * 
	 * @param {AdvMarker.Marker} marker
	 * @param {Array of AdvMarker.Cluster} [clusterList=_currentLevelCluster] A
	 *            list of clusters to find a close match.
	 */
	function addToCluster( marker, clusterList )
	{
		if( clusterList === undefined ) {
			clusterList = _currentLevelCluster;
		}

		// if( !marker.inViewport(_map) ) {
		// _hiddenMarkers.push(marker);
		// return;
		// }
		var markerXY = _map.fromLatLngToDivPixel(marker.getLatLng());
		for( var int = 0; int < clusterList.length; int++ ) {
			var cluster = clusterList[int];

			var clusterXY = _map.fromLatLngToDivPixel(cluster.getLatLng());

			if( markerXY.x >= clusterXY.x - _options.gridSize && markerXY.x <= clusterXY.x + _options.gridSize && markerXY.y >= clusterXY.y - _options.gridSize && markerXY.y <= clusterXY.y + _options.gridSize ) {
				cluster.addMarker(marker);
				return;
			}
		}

		var cluster = new AdvMarker.Cluster(_map, _options.clusterPopupCallback);
		cluster.addMarker(marker);
		_allClusterArray.push(cluster);
		_currentLevelCluster.push(cluster);
	}

	/**
	 * Reset the viewport so markers/clusters are all valid.
	 */
	function resetViewport()
	{
		var clusters = getClustersInViewport();
		_currentLevelCluster = [];

		var tmpMarkers = [];
		for( var i = 0; i < clusters.length; ++i ) {
			var cluster = clusters[i];
			var oldZoom = cluster.getCurrentZoom();
			if( oldZoom === null ) {
				continue;
			}
			var curZoom = _map.getZoom();
			if( curZoom !== oldZoom ) {

				// If the cluster zoom level changed then destroy the cluster
				// and collect its markers.
				var clusterMarkers = cluster.getMarkers();
				for( var j = 0; j < clusterMarkers.length; ++j ) {
					// Add it back into visible clusters
					addToCluster(clusterMarkers[j]);
				}
				cluster.clear();
				_allClusterArray.splice(_allClusterArray.indexOf(cluster), 1);
			}
		}

	}

	/**
	 * Get a list of all clusters in the viewport.
	 * 
	 * @return {Array of AdvMarker.Cluster}
	 */
	function getClustersInViewport()
	{
		var clusters = [];
		for( var i = 0; i < _allClusterArray.length; i++ ) {
			if( _allClusterArray[i].inViewport(_map) ) {
				clusters.push(_allClusterArray[i]);
			}
		}
		return clusters;
	}
};

/**
 * A grouping of markers.
 * 
 * @param map
 * @param popupCallback
 * @return
 */
AdvMarker.Cluster = function( map, popupCallback )
{
	var _clusterMarker;
	var _markerArray = [];
	var _map = map;
	var _visibleMarkers = [];
	var _self = this;
	var _popupCallBack = popupCallback;
	var _zoom = map.getZoom();
	var _clusterType = '';
	var clickEvent;

	/**
	 * Get the latlng position of the cluster marker.
	 * 
	 * @todo Find the center of all markers.
	 * @param {GLatLng}
	 */
	this.getLatLng = function()
	{
		return _markerArray[0].getLatLng();
	};

	/**
	 * Get a list of all markers in the cluster.
	 * 
	 * @return {Array of AdvMarker.Cluster}
	 */
	this.getMarkers = function()
	{
		return _markerArray;
	};

	/**
	 * Add a marker to the cluster
	 * 
	 * @param {AdvMarker.Marker} marker
	 */
	this.addMarker = function( marker )
	{
		marker.setCluster(this);
		_markerArray.push(marker);

		if( _markerArray.length == 1 ) {
			generateClusterMarker();
		}

		addMarkerType(marker);
	};

	/**
	 * Clear everything in the cluster.
	 * <p>
	 * This will remove all overlays/listeners created by Cluster
	 * </p>
	 */
	this.clear = function()
	{
		GEvent.removeListener(clickEvent);
		_map.removeOverlay(_clusterMarker);
	};

	/**
	 * Update the cluster so the icon and visibility are corrected.
	 */
	this.update = function()
	{
		_clusterMarker.hide();

		_visibleMarkers = [];
		_clusterType = '';

		for( var int = 0; int < _markerArray.length; int++ ) {
			var marker = _markerArray[int];
			addMarkerType(marker);
		}

		if( _visibleMarkers.length == 0 ) {
			_clusterMarker.hide();
		}
	};

	/**
	 * Test to see if the passed in marker is a new type.
	 * 
	 * @param {AdvMarker.Marker} marker
	 */
	function addMarkerType( marker )
	{
		if( false == marker.getType().isHidden() ) {

			if( _clusterType == '' ) {
				_clusterType = marker.getType().getName();
				_clusterMarker.setImage(marker.getType().getIcon().image);
			}

			if( _clusterType != marker.getType().getName() ) {
				_clusterType = 'group';
				var imgType = '.png';
				if( $.browser.msie && $.browser.version == 6 )
				{
					imgType = '.gif';
				}
				_clusterMarker.setImage('/images/icons/community_home' + imgType);
			}

			_visibleMarkers.push(marker);
			_clusterMarker.show();
		}
	}

	/**
	 * Generate the GIcon the cluster will use.
	 */
	function generateClusterMarker()
	{
		_clusterMarker = new GMarker(_markerArray[0].getLatLng(), _markerArray[0].getType().getIcon());
		if( _popupCallBack ) {
			clickEvent = GEvent.addListener(_clusterMarker, "click", function()
			{
				popupCallback(_clusterMarker, _map, _visibleMarkers);
			});
		}

		_map.addOverlay(_clusterMarker);

		_clusterMarker.hide();
	}

	/**
	 * Check a marker, whether it is in current map viewport.
	 * 
	 * @return {Boolean}
	 */
	this.inViewport = function()
	{
		var bounds = _map.getBounds();

		// Expand the bounds so that when the scroll the markers don't jump
		// around.
		var sw = bounds.getSouthWest();
		var ne = bounds.getNorthEast();
		var dx = ne.lng() - sw.lng();
		var dy = ne.lat() - sw.lat();
		dx *= 0.1;
		dy *= 0.1;
		bounds = new GLatLngBounds(new GLatLng(sw.lat() - dy, sw.lng() - dx), new GLatLng(ne.lat() + dy, ne.lng() + dx));

		return bounds.containsLatLng(_clusterMarker.getLatLng());
	};

	/**
	 * Get the zoom of the cluster was added in..
	 */
	this.getCurrentZoom = function()
	{
		return _zoom;
	};

	/**
	 * Call the onclick function that was registered.
	 */
	this.openInfoWindow = function()
	{
		popupCallback(_clusterMarker, _map, _visibleMarkers);
	};
};

/**
 * The marker to wrap the gMarker with function/params needed by the
 * AdvMarkerManager.
 * 
 * @param {GMarker} gMarker
 * @param {AdvMarker.Type} type
 */
AdvMarker.Marker = function( gMarker, type )
{
	var _type = type;
	var _marker = gMarker;
	var _cluster;

	/**
	 * Check a marker, whether it is in current map viewport.
	 * 
	 * @return {Boolean}
	 */
	_marker.inViewport = function( map )
	{
		var bounds = map.getBounds();

		// Expand the bounds so that when the scroll the markers don't jump
		// around.
		var sw = bounds.getSouthWest();
		var ne = bounds.getNorthEast();
		var dx = ne.lng() - sw.lng();
		var dy = ne.lat() - sw.lat();
		dx *= 0.1;
		dy *= 0.1;
		bounds = new GLatLngBounds(new GLatLng(sw.lat() - dy, sw.lng() - dx), new GLatLng(ne.lat() + dy, ne.lng() + dx));

		return bounds.containsLatLng(_marker.getLatLng());
	};

	/**
	 * Set the marker cluster.
	 * 
	 * @param {AdvMarker.Cluster} cluster
	 */
	_marker.setCluster = function( cluster )
	{
		_cluster = cluster;
	};

	/**
	 * Get the cluster the marker belongs to.
	 */
	_marker.getCluster = function()
	{
		return _cluster;
	};

	/**
	 * Get the type the marker belongs to.
	 */
	_marker.getType = function()
	{
		return _type;
	};

	return _marker;
};

/**
 * AdvMarker Type.
 * 
 * @param {string} name
 * @param {GIcon} icon
 */
AdvMarker.Type = function( name, icon )
{
	var _name = name;
	var _icon = icon;
	var _hidden = false;
	var _markerList = [];

	/**
	 * Get the type name.
	 * 
	 * @return {string}
	 */
	this.getName = function()
	{
		return name;
	};

	/**
	 * Get the type icon
	 * 
	 * @return {GIcon}
	 */
	this.getIcon = function()
	{
		return icon;
	};

	/**
	 * A marker to the type.
	 * 
	 * @param {AdvMarker.marker} marker
	 */
	this.addMarker = function( marker )
	{
		_markerList.push(marker);
	};

	/**
	 * Check to see if the type is hidden.
	 */
	this.isHidden = function()
	{
		return _hidden;
	};

	/**
	 * Show the type.
	 * <p>
	 * This will trigger the clusters associated to the markers to be updated
	 * </p>
	 */
	this.show = function()
	{
		_hidden = false;
		updateMarkerCluster();
	};

	/**
	 * Hide the type.
	 * <p>
	 * This will trigger the clusters associated to the markers to be updated
	 * </p>
	 */
	this.hide = function()
	{
		_hidden = true;
		updateMarkerCluster();
	};

	/**
	 * Clear the all markers from the type.
	 */
	this.clear = function()
	{
		_markerList = [];
	};

	/**
	 * Trigger an update for the clusters for all contained markers.
	 */
	function updateMarkerCluster()
	{
		for( var int = 0; int < _markerList.length; int++ ) {
			var marker = _markerList[int];
			marker.getCluster().update();
		}
	}
};

// Prototype indexOf functionality
if( !Array.indexOf ) {
	Array.prototype.indexOf = function( obj )
	{
		for( var i = 0; i < this.length; i++ ) {
			if( this[i] == obj ) {
				return i;
			}
		}
		return -1;
	}
}

