(function( $ ) {
	$.gmap = function() { return this.init.apply( this, arguments ); };
	
	$.gmap.defaults = {
		controlSize: 'small', // 'default', 'large', 'medium', 'small' or 'none'
		enableMapTypeSwitch: true,
		center: [ 52.2, 5.15 ],
		zoom: 10,
		mapType: G_NORMAL_MAP // G_NORMAL_MAP, G_SATELLITE_MAP, G_HYBRID_MAP
	};
	
	/**
	 * Icons collection; users can add icons themselves, which can be referenced by name for use in the 'addMarker' function
	 */
	$.gmap.icons = {
		'default': new GIcon( G_DEFAULT_ICON )
	};
	
	$.gmap.iconDefaults = {
		image: "",
		shadow: "",
		size: null,
		shadowSize: null,
		anchor: null,
		infoWindowAnchor: null
	};
	
	// Default options for a point to be created
	$.gmap.markerDefaults = {
		defaultIcon: $.gmap.icons[ 'default' ],	// Default icon for new markers. If both 'icon' and 'defaultIcon' are not set, 'G_DEFAULT_ICON' is used.
		latLng: [],			// Point lat & lng
		info: null,			// Point HTML for infoWindow
		title: null,		// Marker title
		isDraggable: false,	// Point is draggable?
		icon: null			// Optional Icon to pass in
	};
	
	// Defaults for a Polyline
	$.gmap.polylineDefaults = {
		polyline: null,			// A ready-to-go polyline. Other options are ignored if 'polyline' is set
		points: [],				// An array vof GLatLng objects
		strokeColor: '#00adef',	// Colour of the line
		strokeWidth: 8,			// Width of the line
		strokeOpacity: 0.6,		// Opacity of the line
		geodesic: false,		// Is line Geodesic (i.e. bends to the curve of the earth)?
		clickable: true			// Is line clickable?
	};
	
	// Defaults for a Polygon
	$.gmap.polygonDefaults = {
		polygon: null,			// A ready-to-go polygon. Other options are ignored if 'polygon' is set
		points: [],				// An array vof GLatLng objects
		strokeColor: '#00adef',	// Colour of the line
		strokeWidth: 8,			// Width of the line
		strokeOpacity: 0.7,		// Opacity of the line
		fillColor: '#00adef',	// Fill color
	 	fillOpacity: 0.4,		// Fill opacity
	 	clickable: true,
		// Circle defaults
		center: [],
		radius: 1000,
		sides: 18
	};
	
	$.gmap.centerDefaults = {
		method: 'normal', // 'normal' or 'pan'
		latLng: [],
		mapType: null, // null, G_NORMAL_MAP, G_SATELLITE_MAP, G_HYBRID_MAP
		zoom: null
	};
	
	$.gmap.prototype = {
		map: null,
		
		init: function( element, options ) {
			if ( !$.gmap.isBrowserCompatible() ) {
				return false;
			}
			
			var options = $.extend( {}, $.gmap.defaults, options );
			this.map = new GMap2( element );
			
			switch(options.controlSize) {
				case 'none':
					break;
				case 'small':
					this.map.addControl( new GSmallZoomControl() );
					break;
				case 'medium':
					this.map.addControl( new GSmallMapControl() );
					break;
				case 'large':
					this.map.addControl( new GLargeMapControl() );
					this.map.addControl( new GScaleControl() );
					break;
				case 'default':
				default:
					this.map.setUIToDefault();
			}
			
			if ( options.enableMapTypeSwitch ) {
				this.map.addControl( new GMapTypeControl() );
			}
			
			this.map.enableScrollWheelZoom();
			this.map.enableContinuousZoom();
			this.map.setMapType( options.mapType );
			this.map.setCenter( new GLatLng( options.center[0], options.center[1] ), options.zoom );
			
			// Clean up on unload
			$( window ).unload( function() { $.gmap.destroy() } );
			
			return this;
		},
		
		getCenter: function( callback ) {
			if (typeof callback == 'function') return callback( center );
		},
		
		/**
		 * Set center, optionally change zoom & mapType
		 *
		 * @param options - Options object for this method.
		 *		Required properties:
		 *			- latLng {array}: array containing latitude/longitude coordinates
		 *		Optional properties:
		 *			- method {string} 'pan' or 'normal'; default is 'normal'.
		 *			- zoom {mixed}: a zoom level (number), or 'max'/'min' for maximum/minimum zoom level
		 *			- mapType {string}
		 */
		setCenter: function( options ) {
			var options = $.extend( {}, $.gmap.centerDefaults, options );
			
			if ( options.zoom ) {
				this.setZoom( options.zoom );
			}
			if ( options.latLng && options.latLng.length >= 2 ) {
				var point = new GLatLng( options.latLng[0], options.latLng[1] );
				options.method === 'pan' ? this.map.panTo( point ) : this.map.setCenter( point );
			}
			if ( options.mapType ) {
				this.map.setMapType( options.mapType );
			}
			
			return this;
		},
		
		getZoom: function( callback ) {
			if (typeof callback == 'function') return callback( this.map.getZoom() );
		},
		
		/**
		 * Set zoom level.
		 *
		 * @param zoom {mixed}: a zoom level (number), or 'max'/'min' for maximum/minimum zoom level
		 */
		setZoom: function( zoom ) {
			var numZoom = 30; // max zoom
			
			if ( typeof zoom == 'string'  ) {
				if ( zoom === 'max' )
					zoom = numZoom;
				else if ( zoom === 'min' )
					zoom = 0;
			}
			else {
				zoom = Math.min( Math.max( zoom, 0 ), numZoom );
			}
			
			this.map.setZoom( zoom );
		},
		
		zoomIn: function() {
			this.map.zoomIn();
		},
		
		zoomOut: function() {
			this.map.zoomOut();
		},
		
		clearOverlay: function( overlay ) {
			// In GMaps, a marker/polyline/polygon is an instanceof GOverlay
			if ( overlay instanceof GOverlay ) {
				this.map.removeOverlay( overlay );
			}
			
			return this;
		},
		
		clearOverlays: function() {
			this.map.clearOverlays();
			
			return this;
		},

        addOverlay: function( overlay ) {
            if ( overlay instanceof GGeoXml ) {
                this.map.addOverlay( overlay );
            }
        },
		
		resized: function() {
			this.map.checkResize();
		},
		
		/**
		 * Create a marker and add it as a point to the map.
		 *
		 * @param options - Options object for this method.
		 *		Required properties:
		 *			- latLng {array}: array containing latitude/longitude coordinates
		 *		Optional properties:
		 *			- icon {mixed} GIcon
		 *			- info {string} : content of popup window.
		 *			- title {string}: html title
		 */
		addMarker: function( options, callback ) {
			var options = $.extend( {}, $.gmap.markerDefaults, options );
			var dit = this;
			var markerOptions = {
				icon: ( options.icon instanceof GIcon ? options.icon : false
					|| ( options.icon instanceof String && $.gmap.icons[ options.icon ] ) ? $.gmap.icons[ options.icon ] : false
					|| options.defaultIcon instanceof GIcon ? options.defaultIcon : false
					|| new GIcon( G_DEFAULT_ICON )
				)
			};
			
			if ( options.isDraggable ) {
				$.extend( markerOptions, { draggable: options.isDraggable, dragCrossMove: true } );
			}
			
			if ( options.title ) {
				markerOptions.title = options.title;
			}
			
			// Create marker, optional parameter to make it draggable
			var marker = new GMarker( new GLatLng( options.latLng[0],options.latLng[1] ), markerOptions );
			
			// If it has HTML to pass in, bindInfoWindowHtml
			if ( options.info ) {
				marker.bindInfoWindowHtml( options.info );
			}
			
			if ( options.isDraggable )
				GEvent.addListener( marker, 'dragend', function( latlng ){
					var point = this.getLatLng();
					$( document ).trigger( 'gmap.markerDragend', [ this, [ point.lat(), point.lng() ] ] );
				});
			
			// When a mousedown event fires on a marker: fire the jQuery event gmap.markerClick.
			GEvent.addListener( marker, 'mousedown', function(){
				var point = this.getLatLng();
				$( document ).trigger( 'gmap.markerSelected', [ this, point ] );
			});
			
			// change marker appearance when hovering
			if ( markerOptions.icon.hover ) {
				GEvent.addListener( marker, 'mouseover', function() {
					dit.setMarkerImage( { image: markerOptions.icon.hover } ); // change graphic
					//marker.topMarkerZIndex(); // bring marker to top
				}); 
				
				GEvent.addListener( marker, 'mouseout', function() {
					dit.setMarkerImage( { image: markerOptions.icon.image } );
					//marker.restoreMarkerZIndex();
				});
			}
			
			// Direct rendering to map
			this.map.addOverlay( marker );
			
			if (typeof callback == 'function') {
				callback( marker );
			}
			
			return this;
		},
		
		openInfoWindow: function( marker ) {
			if ( marker instanceof GMarker ) {
				GEvent.trigger( marker, 'click' );
			}
		},
		
		/**
		 * Move an existing marker
		 *
		 * @param options - Options object for this method.
		 *		Required properties:
		 *			- marker {google.maps.Marker}: marker to move
		 *			- latLng {array}: array containing latitude/longitude coordinates
		 *		Optional properties:
		 *			- info {string} : content of popup window.
		 */
		moveMarker: function( options ) {
			options.marker.setLatLng( new GLatLng( options.latLng[0],options.latLng[1] ) );
			
			if ( options.info ) {
				marker.bindInfoWindowHtml( options.info );
			}
			
			return this;
		},
		
		/**
		 *	Create a circle-shaped polygon and render on the map.
		 *
		 * @param options - Options object for this method.
		 *		Required properties:
		 *			- latLng {Array} A pair of coordinates that indicate the circle's center
		 *		Optional properties:
		 *			- radius {float}: Radius of the circle in meters
		 *			- sides {int}: Number of sides on the circle
		 *			- strokeColor {string}:  Hex value of a color (hash mark, followed by six hex chars. Example: #ab40ff)
		 *			- strokeWidth {int}: width in pixels
		 *			- strokeOpacity {float}: opacity between 0 and 1
		 *			- fillColor {string}: Hex value of a color (hash mark, followed by six hex chars. Example: #ab40ff)
		 *			- fillOpacity {float}: opacity between 0 and 1
		 */
		addCircle: function( options, callback ) {
			var options = $.extend({}, $.gmap.polygonDefaults, options);
			
			// Convert from meters to kilometers
			options.radius /= 1000;
			
			var center = options.latLng;
			if ( center instanceof Array ) {
				center = new GLatLng( center[ 0 ], center[ 1 ] );
			}
			
			// Calculate the ratio of km/degree at this particular lat and long
			var latConv = center.distanceFrom(new GLatLng( center.lat() + 0.1, center.lng() )) / 100;
			var lngConv = center.distanceFrom(new GLatLng( center.lat(), center.lng() + 0.1 )) / 100;
				
			var angle = Math.PI * ((1/options.sides) - (1/2));
			var rotatedAngle, x, y;
			var points = [];
			
			var i = options.sides;
			while( i-- ) {
				rotatedAngle = angle + ( i * 2 * Math.PI / options.sides );
				x = center.lng() + ( ( options.radius / lngConv ) * Math.cos( rotatedAngle ));
				y = center.lat() + ( ( options.radius / latConv ) * Math.sin( rotatedAngle ));
				points.push( new GLatLng( y, x ) );
			}
			points.push( points[ 0 ] );
			
			var circle = new GPolygon(points, options.strokeColor, options.strokeWeight,
						options.strokeOpacity, options.fillColor, options.fillOpacity);
			
			this.map.addOverlay( circle );
			
			if (typeof callback == 'function') {
				callback( circle );
			}
			
			return this;
		},
		
		/**
		 *	Create a polyline and render on the map.
		 *
		 *	@param options - Options object for this method.
		 *		Required properties:
		 *		- polyline: if provided and an instance of OpenLayers.Feature.Vector, drawn immediately, ignoring other options.
		 *		- points {Array}: An Array of either coordinates (array of lat,long) or of OpenLayers.Geometry.Point objects.
		 *		Optional properties:
		 *		- strokeColor {string}: Hex value of a color (hash mark, followed by six hex chars. Example: #ab40ff)
		 *		- strokeWidth {int}: width in pixels
		 *		- strokeOpacity {float}: opacity between 0 and 1
		 */
		addPolyline: function( options, callback ) {
			var options = $.extend({}, $.gmap.polylineDefaults, options);
			var polyline = null;
			
			// If we're offered a ready-to-go polyline, draw it
			if ( options.polyline && options.polyline instanceof GPolyline ) {
				polyline = options.polyline;
			}
			else {
				// if polylinePoints is an array of arrays, create an array of GLatLng
				if ( options.points.length > 0 && options.points[ 0 ] instanceof Array ) {
					options.polylineArray = options.points; // Create new array to avoid modifying the original
					options.points = [];
					
					for ( var i = 0; i < options.polylineArray.length; i++ ) {
						options.points[ i ] = new GLatLng( options.polylineArray[ i ][ 0 ], options.polylineArray[ i ][ 1 ] );
					}
				}
				
				polyline = new GPolyline( options.points, options.strokeColor, options.strokeWidth, options.strokeOpacity );
			}
			
			this.map.addOverlay( polyline );
			
			if (typeof callback == 'function') {
				callback( polyline );
			}
			
			return this;
		},
		
		/**
		 * Set the foreground image for the icon used for a Marker
		 */
		setMarkerImage: function( options ) {
			if ( options.marker && options.image && options.marker instanceof GMarker )
				options.marker.setImage( options.image );
		}
	};
	
	$.gmap.isBrowserCompatible = function() {
		var compatible = GBrowserIsCompatible();
		
		if ( !compatible ) {
			console.debug( 'Browser is not compatible with Google Maps' );
		}
		
		return compatible;
	};
	
	$.gmap.destroy = function() {
		GUnload();
	};
	
	/**
	 * Create an icon to return to addMarker. Can be reused!
	 *
	 * Options:
	 *		- image:		String, path/url to image
	 *		- size:			Array, width/height
	 *		- shadow: 		String, path/url to image
	 *		- shadowSize:	Array, width/height
	 *
	 */
	$.gmap.createIcon = function( options ) {
		var options = $.extend({}, $.gmap.iconDefaults, options);
		
		// If required icon properties are present, create an icon from scratch. Otherwise, copy the default.
		// Advantage to the former: mouseover area will be correct.
		if ( options.image && options.size )
			var icon = new GIcon( false, options.image );
		else
			var icon = new GIcon( G_DEFAULT_ICON );
			
		if ( options.hoverImage )
			icon.hover = options.hoverImage;
		
		if ( options.shadow )
			icon.shadow = options.shadow;
		
		if ( options.size ) {
			if ( options.size instanceof GSize )
				icon.iconSize = options.size;
			else 
				try { icon.iconSize =  new GSize( options.size[ 0 ], options.size[ 1 ] ) } catch( e ) {};
		}
		
		if ( options.shadowSize ) {
			if ( options.shadowSize instanceof GSize )
				icon.shadowSize = options.shadowSize;
			else 
				try { icon.shadowSize =  new GSize( options.shadowSize[ 0 ], options.shadowSize[ 1 ] ) } catch( e ) {};
		}
		
		if ( options.anchor ) {
			if ( options.anchor instanceof GPoint )
				icon.iconAnchor = options.anchor;
			else
				try { icon.iconAnchor =  new GPoint( options.anchor[ 0 ], options.anchor[ 1 ] ) } catch( e ) {};
		}
		else {
			icon.iconAnchor = new GPoint( icon.iconSize.width / 2, icon.iconSize.height / 2 );
		}
		
		if ( options.infoWindowAnchor ) {
			if ( options.infoWindowAnchor instanceof GPoint )
				icon.infoWindowAnchor = options.infoWindowAnchor;
			else
				try { icon.infoWindowAnchor =  new GPoint( options.infoWindowAnchor[ 0 ], options.infoWindowAnchor[ 1 ] ) } catch( e ) {};
		}
		else {
			icon.infoWindowAnchor = new GPoint( icon.iconSize.width / 2, icon.iconSize.height / 4 );
		}
		
		return icon;
	};
	
	$.fn.gmap = function( options ) {
		var isMethodCall = (typeof options == 'string');
		var args = Array.prototype.slice.call( arguments, 1 );
		
		return this.each(function() {
			var instance = $.data( this, '$.gmap' );
			
			// constructor
			!instance && !isMethodCall && $.data( this, '$.gmap', new $.gmap( this, options ) );
			
			// regular method
			instance && isMethodCall && $.isFunction( instance[options] ) && instance[options].apply( instance, args );
		});
	};
})( jQuery );
