if (Garmin == undefined) var Garmin = {};
if (Garmin.Foto == undefined) Garmin.Foto = {};

/** Abstract base class for on-line photo service APIs.
 *
 * TODO this class should not be Google Map or loader aware. That code should be elsewere.
 *
 * @abstract
 * @constructor 
 * @param {map} GMap2 instance
 */
Garmin.Foto.AbstractPhotoAPI = Class.create();
Garmin.Foto.AbstractPhotoAPI.prototype = {

	initialize: function(){
		this._init.call(arguments);			
		this.photos = null;
		this.eventListener = null;
		this.photosPerPage = 30;
		this.startPhoto = 0; //zero based
		this.totalPhotos = null;
		this.MAX_PHOTOS_PER_PAGE = 100;
		this.overlayHash = new Hash();
		this.queryBounds = null;
		this.infoWindowTemplate = null;
		this.highlightPhotoOnRefresh = null;
	},
	
	/** superclasses should call this._init(map) in constructor
	 * @param {GMap2} map Google Map instance 
	 * @param {String} infoWindowTemplate HTML template for GInfoWindow
	 */
	_init: function(map, infoWindowTemplate){
       		this.map = map;
       		this.infoWindowTemplate = infoWindowTemplate;
 	},
	
	/** call reset before new query is issued. */
	reset: function() {
		this.startPhoto = 0; //zero based
		this.totalPhotos = null;
		this.photosPerPage = 30;
		this.photos = null;
		this.overlayHash = new Hash();
	},
	
	/** Google map setter */
	setMap: function(map) {
		this.map = map;
	},
	 
	/** returns array of Photo objects */
	getPhotos: function() {
		return this.photos;
	},
	
	/** get total photos returned for current query */
	getTotalPhotos: function() {
		return this.totalPhotos;
	},
	
	/** get number of photo pages returned for current query */
	getTotalPages: function() {
		return Math.ceil(this.totalPhotos / this.getPhotosPerPage());
	},
	
	/** current zero based start photo position within total count. */
	getStartPhoto: function() {
		return this.startPhoto;
	},
	
	/** current zero based end photo position within total count. */
	getEndPhoto: function() {
		return (!this.totalPhotos || this.startPhoto + this.photosPerPage < this.totalPhotos) 
			? this.startPhoto + this.photosPerPage -1
			: this.totalPhotos-1;
	},
	
	/** number of photos returned per page */
	getPhotosPerPage: function() {
		return this.photosPerPage;
	},
	
	/** current zero based page number */
	getCurrentPage: function() {
		return Math.max(Math.floor(this.startPhoto / this.photosPerPage), 0);
	},
	
	setPage: function(page) {
		this.startPhoto = page * this.getPhotosPerPage();
		if (this.startPhoto > this.getTotalPhotos()-1)
			this.startPhoto = this.getTotalPhotos()-1;
		if (this.startPhoto < 0)
			this.startPhoto = 0;
		return this.startPhoto;
	},
	
	/** Generate a unique file name: apiService-photoId */
	uniqueFileName: function() {
		throw new Error("AbstractPhotoAPI.uniqueFileName must be overriden by a super class");
	},
	
	/** Construct a REST query from current map position and query options
	 * @param {String} size Size of the photos to return.
	 */
	constructQueryURL: function(photoSize) {
		throw new Error("AbstractPhotoAPI.constructQueryURL must be overriden by a super class");
	},

	/** Calculate percentage of map size apply to query.
	 * @param {int} screenPercentage percentage of width and height to include in query. Default 88
	 */
	caclulateQueryBounds: function(screenPercentage) {
		var percentage = screenPercentage || 88;
		var bounds = this.map.getBounds();
		var southWest = bounds.getSouthWest();
		var northEast = bounds.getNorthEast();
		var lngDelta = (northEast.lng() - southWest.lng()) * ((100-percentage)/100);
		var latDelta = (northEast.lat() - southWest.lat()) * ((100-percentage)/100);
		this.queryBounds = new GLatLngBounds(
			new GLatLng(southWest.lat() + latDelta, southWest.lng() + lngDelta),
			new GLatLng(northEast.lat() - latDelta, northEast.lng() - lngDelta));
		return this.queryBounds;
	},
	
	/** Construct Photo instances from JSON query response.
	 * @param {Object} json Photo-service-specific JSON query results.
	 */ 
	bindPhotos: function(json) {
		throw new Error("AbstractPhotoAPI.bindPhotos must be overriden by a super class");
	},
	
	/** JSON Call-back for photo service.  Photo service-specific details are handled in bindPhotos method.
	 */
	showPhotoIcons: function(json) {
	  	this.map.clearOverlays(); //clear existing markers
		this.overlayHash = new Hash();
		this.photos = this.bindPhotos(json);
		for (var i = 0; i < this.photos.length; i++) {
			var photo = this.photos[i];
			this.createOverlay(photo, false);
		}
		this.map.addOverlay(new Garmin.Foto.FocusRectangle(null, this.queryBounds));
		if (this.highlightPhotoOnRefresh) {
			loader.panToPhoto(this.highlightPhotoOnRefresh);
			this.highlightPhotoOnRefresh = null;
		}
	},

	/** This photo will be centered and highlighted on next map refresh */
	setHighlightPhotoOnMapUpdate: function(photo) {
		this.highlightPhotoOnRefresh = photo;
	},

	/** Construct IconOverlay overlay from photo instance.
	 * @param {Photo} photo
	 * @param {Boolean} makeDraggable create a draggable GIcon overlay
	 */
	createOverlay: function(photo) {
		var marker = new Garmin.Foto.IconOverlay(null, photo);
		this.map.addOverlay(marker);
		this._addListeners(marker);
		this.overlayHash[photo.getPhotoID()] = marker;
		return marker;
	},
	
	/** Construct GMarker overlay from photo instance.
	 * @param {Photo} photo
	 * @param {Boolean} makeDraggable create a draggable GIcon overlay
	 */
	createMoveOverlay: function(photo) {
		var draggable = true;
		var title = draggable ? bundle_photos.photoIconMoveHint : bundle_photos.photoIconHint;
		//var zIndexProcess = function(_marker,b) {
		//	return GOverlay.getZIndex(_marker.getPoint().lat()) + draggable ? 0 : 1000000;
		//};
		var marker = new GMarker( 
			new GLatLng(photo.getLat(), photo.getLng()), 
			{icon: photo.getIcon(), draggable: draggable, title: title}
		); // , dragCrossMove: false, zIndexProcess: zIndexProcess
		marker.photo = photo;
		this._addMoveListeners(marker);
		this.map.addOverlay(marker);
		this.overlayHash[photo.getPhotoID()] = marker;
		if (draggable)
			marker.zIndex = GOverlay.getZIndex(-90.0);
		return marker;
	},

	/** Remove overlay from Google map and overlayHash. */
	removeOverlay: function(photo) {
		var marker = this.overlayHash[photo.getPhotoID()];
		if (marker) {
			this.map.removeOverlay(marker);
			delete this.overlayHash[photo.getPhotoID()];
		}
	},

	/** quickly find GMarker given a photo */
	lookupOverlay: function(photo) {
		if (photo) {
			return this.overlayHash[photo.getPhotoID()];
		}
	},

	/** lookup photo from photoID */
	lookupPhoto: function(photoID) {
		var overlay = this.overlayHash[photoID];
		return overlay ? overlay.photo : null;
	},

	/** set photo to 'current' */
	dimOldMarker: function() {
		var photo = loader.getCurrentPhoto();
		var oldMarker = photo ? this.lookupOverlay(photo) : null;
		if (oldMarker && oldMarker.setSelect) {
			oldMarker.setSelect(false);
			log("old marker: "+oldMarker.photo);
		}
	},

	/** set photo to 'current' */
	highlightMarker: function(marker) {
		loader.setCurrentPhoto(marker.photo);
		if (marker.setSelect) {
			marker.setSelect(true);
			log("new marker: "+marker);
		} else {
			log("new marker with no setSelect method: "+marker);
		}
	},

	/** Adds listeners to GMarker instance */
	_addListeners: function(_marker) {
		//GMarker events: click,dblclick,mouseover,mouseout,drag,dragstart,dragend
		var marker = _marker;
		var map = this.map;
		var loader = this.map.loader; //HACK: loader forms bad circular reference
		var self = this;
		GEvent.addListener(marker, "click", function() {
			self.showInfoWindow(marker);
		});
		GEvent.addListener(marker, "mouseover", function() {
			log("toTopDepth: mouseover marker ="+ marker);
			marker.toTopDepth();
		});
		GEvent.addListener(marker, "mouseout", function() {
			marker.toDefaultDepth();
		});
		GEvent.addListener(marker, "dblclick", function() {
			self.dimOldMarker(marker);
			loader.addPhoto(marker.photo);
			loader.previewPhoto(marker.photo);
			map.closeInfoWindow();
			self.highlightMarker(marker);
		});
	},

	/** Adds listeners to GMarker instance */
	_addMoveListeners: function(_marker) {
		//GMarker events: click,dblclick,mouseover,mouseout,drag,dragstart,dragend
		var moveMarker = _marker;
		var map = this.map;
		var loader = this.map.loader;
		var self = this;
		GEvent.addListener(moveMarker, "dragend", function() {
			moveMarker.disableDragging();
			var photo = moveMarker.photo;
			photo.setLat(moveMarker.getLatLng().lat());
			photo.setLng(moveMarker.getLatLng().lng());
			self.setHighlightPhotoOnMapUpdate(photo);
			loader.updateMap(true);
		});
	},

	showInfoWindow: function(marker) {
		this.dimOldMarker(marker);
		var balloon = this.balloonHTML(marker.photo);
		
		// convert lat lng to pxl
		var markerPos = this.map.fromLatLngToDivPixel(marker.getPoint());			
		
		// transform pxl
		markerPos.x += marker.infoWindowAnchor.x;
		markerPos.y += marker.infoWindowAnchor.y;
		
		// convert pxl to lat lng
		var ballonPos = this.map.fromDivPixelToLatLng(markerPos); 

		this.map.openInfoWindowHtml(ballonPos, balloon);
		this.map.loader.previewPhoto(marker.photo);
		this.highlightMarker(marker);
	},

	/** Build an HTML GInfoWindow by binding properties to template.
	 * This method allows properties to be modified before template evaluation.
	 * @param {Photo} photo
	 */
	balloonHTML: function(photo) {
		var properties = photo.properties();
		properties.merge({
			panelHeight: properties.height + 60,
			title: properties.title.truncate(33, '&#8230;')
		});
		return this.infoWindowTemplate.evaluate(properties);
	}
	
};

