// Metroseeq namespace
var mesq = mesq || {};




/**
 * Manages a list of mesq.Items, but doesn't interact
 * with Google API - results from API calls are pushed onto the object
 * externally via mesq.Maps
 */
mesq.List = Class.create();
Object.extend(mesq.List.prototype, {
	
	// object
	container: null,
	listContainer: null,
	map:null, // TODO This reference shouldn't be necessary and handled in map-object 
	items: null,
	cookieJar:null,
	
	// cache
	cached_refreshFunc:null,
	cached_listItem_onSelectFunc:null,
	
	// strings
	strShowingResults: 'Showing #{count} results',
	strNoResults: 'No results found',
	strErrorNoResultsSuggestion: 
		'<p class="error">' +
		'Don\'t see what you\'re looking for?<br /> ' +
		'Try to search by typing in a <i>cuisine</i> ' +
		'in the "What" box' +
		'</p>',
	
	initialize: function(domContainer, map) {
		
		this.container = $(domContainer);
  		this.listContainer = (this.container.down('ul')) ? this.container.down('ul') : new Element('ul', {'class':'results'});
 		this.container.insert(this.listContainer);
 		this.map = map;
 		this.items = new Array();
 		// @see http://prototypejs.org/api/event/stopObserving
 		this.cached_refreshFunc = this.onPinned.bind(this);
 		this.cached_listItem_onSelectFunc = this.listItem_onSelect.bind(this);
 		
 		// cookies
 		this.cookieJar = new CookieJar({
 			expires: '', // store for length of session
 			path:'/'
 		});
 		
 		// event handlers
 		GEvent.addListener(this.map.gmap, "infowindowclose", this.onInfoWindowClose.bind(this));
 		var existingItems = this.container.select('.result');
 		
 		if(existingItems.length) {
 			if(window.app.debugprofile) console.time('mesq.List.initialize(): Setup existing items');
 			// HACK
	 		this.map.gmap.setCenter(new GLatLng(0,0));
	 		var bounds = new GLatLngBounds();
 			existingItems.each(function(el) {
 				var lat = parseFloat(el.down('.latitude').innerHTML);
	 			var lng = parseFloat(el.down('.longitude').innerHTML);
	 			if(lat && lng) {
		 			var latLng = new GLatLng(lat, lng);
		 			bounds.extend(latLng);
	 			}
		 		this.createItemFromNode(el);
	 		}.bind(this));
			 		
			this.refresh();
			
			this.map.gmap.setCenter(bounds.getCenter());
	 		this.map.zoomByBounds(bounds);
	 		this.map.gmap.enableDragging();
	 		
	 		if(window.app.debugprofile) console.timeEnd('mesq.List.initialize(): Setup existing items');
 		}
 		 		
 		Event.observe(this.container.down('.toggle'), 'click', this.toggle.bind(this));
	},
	
	createItemFromNode: function(node) {
		var origID = String(node.down('div').id);
		var id = Number(origID.replace(/Place/, ''));
		if(!id) return false;
		
		if(node.down('.itemDataJson')) {
			// if the json is cached on page render, use it (larger datasets)
			var data = String(node.down('.itemDataJson').innerHTML).unescapeHTML().evalJSON();
			var item = this.addItem(data, node);
		} else {
			// otherwise request json data from server
			new Ajax.Request(
				'place/itemdatajson/' + id,
				{
					onSuccess: this.createItemFromNode_onSuccess.bind(this, node),
					onFailure: window.app.ajaxErrorHandler
				}
			);
		}
		
	},
	
	createItemFromNode_onSuccess: function(node, response) {
		var item = this.addItem(response.responseText.evalJSON(), node);
		
		this.refresh();
	},
	
	/**
	 * Adds a mesq.ListItem to the list if its not already present.
	 * Need to call refresh() to add the appropriate DOM-node
	 * and redraw the list.
	 */
	addItem:function(data, itemContainer, marker) {
		if(window.app.debugprofile) console.time('mesq.List.addItem()');
		
		var itemContainer = itemContainer;
		
		// match items with latlng and title to avoid duplicates
		// duplicates are detected in place/batchAugumentOrCreate on the serverside,
		// so we just have to compare IDs on clientside
		var existingItem = this.items.find(function(el) {
			if(Number(data.ID) == Number(el.getID())) {
				if(window.app.debug) console.debug("List.addItem(): Duplicate detected with ID %s = %s", data.ID, el.getID());
				return true;
			} else {
				return false;
			}
		});
		if(existingItem) {
			// don't add again, but overwrite pinned state (might come from a cookie)
			if(window.app.debug) console.debug("List.addItem(): adding existing to pinned: %o %o %o", existingItem.getID(), existingItem.pinned, data.pinned);
			existingItem.pinned = existingItem.pinned || data.pinned;
			return existingItem;
		}
		
		var latLng = new GLatLng(parseFloat(data.lat), parseFloat(data.lng));
		
		// ensure item is within a specifc distance radius to center point
		var centerPoint = this.map.getCenterPoint();
		if(centerPoint) {
			var distanceKm = latLng.distanceFrom(centerPoint) / 1000;
			if(distanceKm > this.map.maxDistanceKm) return false;
		}
		
		// add new listItem (optionally from existing node, e.g. for seeqlists)
		if(!itemContainer || typeof itemContainer == 'undefined') {
			// TODO performance vs. stability?
			//this.listContainer.insert('<li class="result">' + data.listHTML + '</li>');
			//var itemContainer = this.listContainer.down('li:last');
			//itemContainer.innerHTML = data.listHTML;
			this.listContainer.insert('<li class="result"></li>');
			var itemContainer = this.listContainer.select('li').last();
			itemContainer.update(data.listHTML);
		}

		// adding marker
		var marker = (typeof(marker) != 'undefined') ? marker : this.map.addMarkerFromData(data);

		var item = new mesq.ListItem(data, marker, itemContainer);
		
		// transfer saved pinned state from cookie
		if(data.pinned) item.pinned = true;
		
		// mark as newcomer for later highlighting
		item.newcomer = true;
		this.items.push(item);
		
		// HACK Chaining events to avoid too much scope in DealList module
		Event.observe(item.container, 'ListItem:load', function(e) { this.container.fire('List:load');}.bind(this));
   		
   		if(window.app.debugprofile) console.timeEnd('mesq.List.addItem()');
   		
   		return item;
	},
	
	removeItemByLatLng:function(lat,lng) {
		// TODO testing with real data
		this.items.findAll(function(el) { return (el.lat == lat && el.lng == lng);}).each(function(el) {el.remove()});
	},
	
	getItems: function() {
		return this.items;
	},
	
	refresh:function() {
		if(window.app.debug) console.debug("List.refresh() called");
		if(window.app.debugprofile) console.time('mesq.List.refresh()');
		
		this.clearMessages();
		this.clear();
		
		// needs to be computed before sorting entries
		if(window.app.debugprofile) console.time('mesq.List.refresh(): Resorting items');
		if(window.app.isPrint()) {
			this.sortByMarker();
		} else {
			if(this.map.searchEnabled() && this.map.getCenterPoint()) {
				for(var i=0;i<this.items.length;i++) {
					this.items[i].computeDistanceFromCenter(this.map.getCenterPoint());
				}
				this.sortByDistanceAndPin();
			}
		}
		if(window.app.debugprofile) console.timeEnd('mesq.List.refresh(): Resorting items');
		
		if(window.app.debugprofile) console.time('mesq.List.refresh(): Resorting DOM nodes');
		for(var i=0;i<this.items.length;i++) {
			// don't use insert() for performance reasons
			this.listContainer.appendChild(this.items[i].container);
			
			this.items[i].refresh();
			
			Event.observe(this.items[i].container, 'ListItem:pinned', this.cached_refreshFunc);
			Event.observe(this.items[i].container, 'ListItem:selected', this.cached_listItem_onSelectFunc);
			if(this.items[i].newcomer) {
				//if(!this.map.isFirstSearch) new Effect.Highlight(itemNode);
				//if(!this.map.isFirstSearch) this.items[i].container.show();
				this.items[i].newcomer = false;
			}
		}
		if(window.app.debugprofile) console.timeEnd('mesq.List.refresh(): Resorting DOM nodes');
		
		if(this.items.length) {
			$('ResultError').update(this.strShowingResults.interpolate({'count':this.items.length}));
		} else {
			$('ResultError').update(this.strNoResults);
			if(!this.container.down('li.error')) {
				this.container.down('ul').insert('<li class="error">' + this.strErrorNoResultsSuggestion + '</li>');
			}
		}
		
		this.container.fire('List:load');
		
		if(window.app.debugprofile) console.timeEnd('mesq.List.refresh()');
	},
	
	toggle: function(e) {
		this.container.down('.toggle').toggleClassName('expanded');
		this.container.down('ul').toggle();
		this.container.down('.toggle a').update((this.container.down('ul').visible() ? "collapse" : "expand" ));
		
		if(e) e.stop();
	},
	
	onPinned: function(e) {
		// need to refresh to get proper pinned states
		this.refresh();
		
		// if unset is required, clear out and reset all pins
		if(!e.memo.obj.pinned) this.clearPinnedFromCookie();
		
		// save all pinned places
		this.savePinnedToCookie();
	},
	
	listItem_onSelect: function(e) {
		var listItem = e.memo.obj;
		this.map.selectedItem = (listItem) ? listItem : false;
	},
	
	onInfoWindowClose: function(e) {
		this.map.selectedItem = null;
	},
	
	sortByDistanceAndPin: function() {
		if(window.app.debugprofile) console.time('mesq.List.sortByDistanceAndPin()');
		this.items = this.items.sortBy(function(item) {
			// make sure pinned are sorted on top (= have a higher distance)
			var distance = (item.pinned == true) ? Number(item.data.distanceMiles) : 999999 + Number(item.data.distanceMiles);
			if(window.app.debug) console.debug("List.sortByDistanceAndPin(): %o, distance %s for id: %s,%o: ", item.pinned, distance, item.data.ID, item);
			return distance;
		}.bind(this));
		if(window.app.debugprofile) console.timeEnd('mesq.List.sortByDistanceAndPin()');
	},
	
	sortByMarker: function() {
		this.items = this.items.sortBy(function(item) {
			// make sure pinned are sorted on top (= have a higher distance)
			return item.marker.getIcon().image;
		}.bind(this));
	},
	
	setMessage: function(msg, type) {
		this.clearMessages();
		this.addMessage(msg, type);
	},
	
	addMessage: function(msg, type) {
		this.listContainer.insert(
			'<li class="message #{type}><p>#{msg}</p></li>'.interpolate({type:type.stripTags(),msg:msg.stripTags()})
		);
	},
	
	showMainLoader: function() {
		if(this.container.select('.ajaxLoader')[0]) return false;
		this.clearMessages();
		this.container.insert('<div class="ajaxLoader" style="opacity: 0"><img src="' + AJAX_LOADER_IMG + '" /></div>');
		var loaderDiv = this.container.select('.ajaxLoader')[0];
		if(loaderDiv) new Effect.Appear(loaderDiv);
	},
	
	hideMainLoader: function() {
		var loaderDiv = this.container.select('.ajaxLoader')[0];
		if(loaderDiv) loaderDiv.remove();
	},
	
	getItemByID: function(id) {
		return this.items.find(function(el) {
			return (el.data.ID == id);
		});
	},
	
	/**
	 * @param string url Full URL with http://
	 */
	getItemByURL: function(url) {
		return this.items.find(function(s) {
			return (s.data.FullURL == url);
		});
	},
	
	getPinnedItems: function() {
		return this.items.findAll(function(item) { return (item.pinned); });
	},
	
	savePinnedToCookie: function() {
		var pinnedItems = this.getPinnedItems();
		if(pinnedItems.length) {
			var pinnedItemsInfo = pinnedItems.collect(function(item) {
				return item.getSpec();
			});
			this.cookieJar.put('pinnedItemsInfo', pinnedItemsInfo);
		}
	},
	
	addPinnedFromCookie: function() {
		var pinnedItemsInfo = this.cookieJar.get('pinnedItemsInfo');
		if(!pinnedItemsInfo) return false;
		
		new Ajax.Request(
			'place/batchAugumentOrCreate/?frompinned=1',
			{
				method:'post',
				parameters: {
					jsonResults: pinnedItemsInfo.toJSON()
				},
				onSuccess: this.addPinnedFromCookie_onComplete.bind(this),
				onFailure: function(e) {this.list.setMessage(this.strLoadingFailed,'error');}.bind(this)
			}
		);
	},
	
	addPinnedFromCookie_onComplete: function(response) {
		var items = response.responseText.evalJSON();
		if(!items || !items.length) return false;
		
		for(var i=0; i<items.length; i++) {
			items[i].pinned = true;
			var item = this.addItem(items[i]);
			item.pin();
		}
		
		this.refresh();
	},
	
	clearPinnedFromCookie: function() {
		this.cookieJar.remove('pinnedItemsInfo');
	},
	
	clearMessages: function() {
		this.container.select('p.message').each(function(el) {el.remove();});
	},
	
	clear: function() {
		var items = this.listContainer.childElements();
		if(items.length) items.each(function(el) {
			Event.stopObserving(el, 'ListItem:pinned', this.cached_refreshFunc);
			Event.stopObserving(el, 'ListItem:selected', this.cached_listItem_onSelectFunc);
			el.remove();
		}.bind(this));
		
		this.map.selectedItem = null;
	}
	
});


/**
 * A single result item, used to manage list display,
 * info windows etc.
 * 
 * @todo Refactor into "Place" which holds data,
 * a "PlaceMarker" entity which manages infowindows (has one "Place")
 * and the "ListItem" (has one "Place")
 */
mesq.ListItem = Class.create();
Object.extend(mesq.ListItem.prototype, {
	
	// objects
	data: null,
	marker: null,
	iwContentNode:null,
	iwMaxContentNode:null,
	container:null,
	map:null,
	
	// state
	pinned: false,
	
	// strings
	strMaxTitle: "More Info",
	strLoadingFailed: 'Loading failed',
	strAddedPlace: 'Added successfully',
	strEnterListName: "Please enter a name for your first SEEQList (e.g. 'Awesome seafood')",
	
	// assets
	imgPushpinOn: 'themes/mesq/images/pushpin_on.gif',
	imgPushpinOff: 'themes/mesq/images/pushpin_off.gif',
	
	// cache
	_cachedFunc_refreshContent: null,
	
	initialize: function(data, marker, container) {
		this.data = data || {};
		this.marker = marker;
		this.container = $(container);
		this.prepareData();
		
		// HACK delay until DOM is constructed by mesq.List
		setTimeout(function() {
			try{this.data.ID = Number(this.container.down('div').id.replace(/Place/,''));} catch(e) {}
			this.setup();
		}.bind(this), REDRAW_DELAY_SECS);
		
		this._cachedFunc_refreshContent = this.refreshContent.bind(this);
	},
	
	/**
	 * Sets up event handlers for the marker, toggles pushpin status
	 * and hover events. 
	 */
	setup: function() {
		if(!window.app.isPrint() && !this.container.hasClassName('error')) {
			// marker
			GEvent.addListener(this.marker, "click", this.openInfoWindow.bind(this));

			// open infowindow on marker
			Event.observe(this.container, 'click', this.openInfoWindow.bind(this));
			
			// toggle pinned status
			if(this.container.down('.pushpin a')) Event.observe(this.container.down('.pushpin a'), 'click', this.togglePushpin.bind(this));
			
			// hovers
			Event.observe(this.container, 'mouseover', this.onMouseOver.bind(this));
			Event.observe(this.container, 'mouseout', this.onMouseOut.bind(this));
			GEvent.addListener(this.marker, 'mouseover', this.onMouseOver.bind(this));
			GEvent.addListener(this.marker, 'mouseout', this.onMouseOut.bind(this));
		} 
		
		if(window.app.isPrint()) {
			this.container.down('.pushpin').hide();
			this.container.down('div').insert(
				{ top: '<div class="marker"><img src="' + this.marker.getIcon().image + '" /></div>' }
			);
		}
		
		if(this.hasDeals()) {
			this.container.addClassName('hasDeals');
		} else {
			this.container.removeClassName('hasDeals');
		}
	},
	
	/**
	 * Recomputes distance to current map centerpoint, updates the marker status
	 * according to current deal count (useful e.g. after deleting the last remaining
	 * deal on a place).
	 */
	refresh: function() {
		if(window.app.debug) console.debug("ListItem.refresh() called");
		if(window.app.debugprofile) console.time('mesq.ListItem.refresh()');
		
		if(this.pinned) this.pin();
		
		var distanceNode = this.container.down('.distance');
		if(typeof distanceNode != 'undefined') {
			if(window.app.map.searchEnabled()) {
				if(this.data.distanceMilesRounded) {
					distanceNode.update('(' + this.data.distanceMilesRounded + ' miles)');
				} else {
					distanceNode.update('');
				}
			} else {
				distanceNode.hide();
			}
		}
		
		if(!window.app.isPrint()) {
			this.marker.setImage(this.hasDeals() ? MARKER_DEALS : MARKER_NORMAL);
		}
		
		if(window.app.isPrint()) {
			this.container.down('.pushpin').hide();
		}
		
		if(this.hasDeals()) {
			this.container.addClassName('hasDeals');
		} else {
			this.container.removeClassName('hasDeals');
		}
		
		if(window.app.debugprofile) console.timeEnd('mesq.ListItem.refresh()');
	},
	
	getID: function() {
		return this.data.ID;
	},
	
	onMouseOver: function(e) {
		if(window.app.isPrint()) return false;
		this.marker.setImage(this.hasDeals() ? MARKER_DEALS_HOVER : MARKER_NORMAL_HOVER);
		this.container.addClassName('hover');
	},
	
	onMouseOut: function(e) {
		if(window.app.isPrint()) return false;
		this.marker.setImage(this.hasDeals() ? MARKER_DEALS : MARKER_NORMAL);
		this.container.removeClassName('hover');
	},
	
	hasDeals: function() {
		return (this.data.Deals && this.data.Deals.length > 0);
	},
	
	maximize: function(e) {
		// leave middle-mouse-button intact for tabbing
		if(e && !e.isLeftClick()) return true;
		
		app.map.gmap.maximizeExtInfoWindow();
		
		this.setupInfoWindows();
		
		if(e) Event.stop(e);
		return false;
	},

	restore: function(e) {
		// leave middle-mouse-button intact for tabbing
		if(e && !e.isLeftClick()) return true;
		
		app.map.gmap.restoreExtInfoWindow();
		
		if(e) Event.stop(e);
		return false;
	},
	
	prepareData: function() {
		// round distance one digit
		this.data.distanceMilesRounded = Math.round(this.data.distanceMiles*10)/10;
	},
	
	/**
	 * Gets enough information to uniquely identify the place
	 * without too much overhead - handy for client-side storage
	 * in cookies.
	 */
	getSpec: function() {
		return {
			lat: this.data.lat,
			lng: this.data.lng,
			ID: Number(this.data.ID)
		};
	},
	
	computeDistanceFromCenter: function(centerPoint) {
		var point = new GLatLng(this.data.lat,this.data.lng);
		this.data.distanceKm = (point.distanceFrom(centerPoint)/1000);
		this.data.distanceKmRounded = Math.round(this.data.distanceKm*10)/10;
		this.data.distanceMiles = (point.distanceFrom(centerPoint)/1000) * CONVERT_KM_MILES;
		this.data.distanceMilesRounded = Math.round(this.data.distanceMiles*10)/10;
	},
	
	/**
	 * Requests both standard and maximized templates in raw HTML
	 * via ajax.
	 */
	loadInfoWindows: function() {
		new Ajax.Request(
			'place/itemdatajson/' + this.data.ID,
			{
				onSuccess: this.loadInfoWindows_onSuccess.bind(this),
				onFailure: this.loadInfoWindows_onFailure.bind(this)
			}
		);
	},
	
	/**
	 * Overrides any existing info window HTML templates,
	 * and corrects any height differences with a weird
	 * homebaked onload-counter.
	 */
	loadInfoWindows_onSuccess: function(response) {
		var data = response.responseText.evalJSON();
		this.data = data;
		
		var oldHeight = this.iwContentNode.getHeight(); 
		this.iwContentNode.update(data.infoWindowHTML);
		this.iwMaxContentNode.update(data.infoWindowMaxHTML);
		
		var iw = app.map.gmap.getExtInfoWindow();
		iw.redraw(true);
		
		// tricksing the DOM into firing an onload event to properly get the infowindow height
		// and reposition it with image heights present
		var images = this.iwContentNode.select('img');
		var count = 0;
		images.each(function(el) {
			Event.observe(el, 'load', function(e) { 
				count++;
				//if(count >= images.length) {
					// need to resize the infowindow in case the dimensions changed
					var iw = app.map.gmap.getExtInfoWindow();
					iw.redraw(true);
				//}
				return true;
			}.bind(this));
			el = null;
		}.bind(this));
		
		this.setupInfoWindows();
		
		// refresh marker (needs to change if deals were added)
		this.refresh();
		
		this.container.fire('ListItem:load');
	},
	
	loadInfoWindows_onFailure: function(response) {
		var errorMsg = '<p class="error">' + this.strLoadingFailed + '</p>';
		this.iwContentNode.down('.loading').update(errorMsg);
		this.iwMaxContentNode.update(errorMsg);
		
		setTimeout(function() {
			var iw = app.map.gmap.getExtInfoWindow();
			iw.disableMaximize();
			iw.redraw();
		}, REDRAW_DELAY_SECS);
	},
	
	/**
	 * Opens the extinfowindow instance (not maximized),
	 * and adds event handlers for deal editing, maximizing,
	 * etc.
	 * 
	 * If no infowindow templates are found, this will trigger an ajax-request
	 * for the HTML content.
	 */
	openInfoWindow: function(e) {
		if(!this.iwContentNode || !this.iwMaxContentNode) this.loadInfoWindows();
		
		// see http://support.silverstripe.com/mesq/ticket/218
		// always load infowindow from server when reopening in IE
		// for some strange reason the DOM-reference doesn't persist...
		// @todo fix infowindow DOM persistence in IE
		var forceIELoad = (Prototype.Browser.IE);
		// we generate temporary infowindows from the json data
		// while waiting for the full rendering to be returned
		// from loadInfoWindows()
		this.iwContentNode = this.generateInfoWindow(forceIELoad);
		this.iwMaxContenNode = this.generateInfoWindowMax(forceIELoad);
		
		// @todo fix infowindow DOM persistence in IE
		if(Prototype.Browser.IE) this.loadInfoWindows(); 
		
		this.marker.openExtInfoWindow(
			window.app.map.gmap,
			'ExtInfoWindow',
			this.iwContentNode,
			{
				maxContent: this.iwMaxContentNode, 
             	maxTitle: this.strMaxTitle,
             	paddingX: 20, 
             	paddingY: 20,
             	beakOffset: 0
			}
		);	
		
		var iw = app.map.gmap.getExtInfoWindow();
		GEvent.addListener(iw, "extinfowindowmaximizeclick",this.setupInfoWindows.bind(this));
		GEvent.addListener(iw, "extinfowindowrestoreclick",this.setupInfoWindows.bind(this));

		// observe deal events
		Event.stopObserving(this.iwMaxContentNode, 'Deal:remove', this._cachedFunc_refreshContent);
		Event.observe(this.iwMaxContentNode, 'Deal:remove', this._cachedFunc_refreshContent);
		Event.stopObserving(this.iwMaxContentNode, 'Deal:vote', this._cachedFunc_refreshContent);
		Event.observe(this.iwMaxContentNode, 'Deal:vote', this._cachedFunc_refreshContent);

		this.container.fire('ListItem:selected', {obj:this});

		// can be called with marker or omitted paramter, we have to strictly check for event object
		if(e && e.stop) { Event.stop(e); return false; }
	},
	
	openInfoWindowMax: function() {
		this.openInfoWindow();
		
		app.map.gmap.maximizeExtInfoWindow();
	},
	
	/**
	 * Either returns an existing infowindow-node (loaded via ajax before),
	 * or generates its own HTML based on the {data} properties.
	 */
	generateInfoWindow: function(force) {
		if(window.app.debugprofile) console.time('mesq.ListItem.generateInfoWindow()');
		if(this.iwContentNode && !force) return this.iwContentNode;
		
		this.iwContentNode = new Element('div',{'class':'placeInfoWindow vcard'});  
		
		// main container
		var container = new Element('div',{'class':'gs-result gs-localResult'});
		this.iwContentNode.appendChild(container);
		
		// title
		var titleDiv = new Element('div',{'class':'gs-title'});
		container.appendChild(titleDiv);
		var titleLinkHref = (this.data.url) ? this.data.url : '/place/show/' + this.data.id;
		var titleLink = new Element('a',{'class':'gs-title','href':titleLinkHref,'target':'_blank'}).update(this.data.title);
		titleDiv.appendChild(titleLink);
		
		// address
		var addressDiv = new Element('div', {'class':'gs-address'});
		container.appendChild(addressDiv);
		if(this.data.streetAddress) addressDiv.appendChild(new Element('div', {'class':'gs-street'}).update(this.data.streetAddress));
		if(this.data.city) addressDiv.appendChild(new Element('div', {'class':'gs-city'}).update(this.data.city));
		if(this.data.region) addressDiv.appendChild(new Element('div', {'class':'gs-region'}).update(','+this.data.region));
		if(this.data.country) addressDiv.appendChild(new Element('div', {'class':'gs-country'}).update(this.data.country));
		
		// phone
		if(this.data.phoneNumbers) {
			var phoneDiv = new Element('div', {'class':'gs-phone'});
			container.appendChild(phoneDiv);
			if(this.data.phoneNumbers && this.data.phoneNumbers.length) {
					this.data.phoneNumbers.each(function(el) {
					phoneDiv.appendChild(new Element('div', {'class':'tel'}).update(el.number));
				});
			}
		}
		
		// loading animation (overwritten by updating in loadInfoWindows_onSuccess())
		container.insert('<div class="loading"><img src="themes/mesq/images/ajax-loader.gif" />Loading</div>');
		
		// events
		Event.observe(titleLink, 'click', this.maximize.bind(this));
		
		if(window.app.debugprofile) console.timeEnd('mesq.ListItem.generateInfoWindow()');
		
		return this.iwContentNode;
	},
	
	/**
	 * Returns an existing infowindow-node (loaded via ajax before).
	 * Does NOT try to scaffold from existing data property, as the syntax
	 * gets too complex and we usually don't have enough data attached anyway.
	 */
	generateInfoWindowMax: function(force) {
		if(window.app.debugprofile) console.time('mesq.ListItem.generateInfoWindowMax()');
		if(this.iwMaxContentNode && !force) return this.iwMaxContentNode;
		
		this.iwMaxContentNode = new Element('div',{'class':'gs-result gs-localResult'});

		// loading animation (overwritten by updating in loadInfoWindows_onSuccess())
		this.iwMaxContentNode.insert('<div class="loading"><img src="themes/mesq/images/ajax-loader.gif" />Loading</div>');

		if(window.app.debugprofile) console.timeEnd('mesq.ListItem.generateInfoWindowMax()');

		return this.iwMaxContentNode;
		
	},
	
	generateInfoWindowMax_onComplete: function(response) {
		var addLinks = this.iwMaxContentNode.select('.add a');
		addLinks.each(function(el) {
			//Event.observe(el, 'click', function(e) {});
		});
	},
	
	refreshContent: function() {
		this.loadInfoWindows();
		/*
		var iwContentNodeHolders = $$('.placeDetail');
		if(!iwContentNodeHolders) return false;
		
		// HACK Google doesn't provide an API to update its markup properly...
		this.iwMaxContentNode = this.generateInfoWindowMax(true);
		iwContentNodeHolders[0].replace(this.iwMaxContentNode);
		*/
	},
	
	/**
	 * Adds event hanlders for placelists, auguments deals with a new mesq.Deal
	 * instance.
	 */
	setupInfoWindows: function() {
		// need to wait for DOM-node being constructed
		setTimeout(function() {
			if(window.app.debugprofile) console.time('mesq.ListItem.setupInfoWindows()');
			
			if(window.app.placeListManager==null){
				//hide the entire link
				$A([this.iwContentNode.down('.addToPlaceList'), this.iwMaxContentNode.down('.addToPlaceList')]).each(function(el) {
					if(!el) return;
					el.style.display='none';
				}.bind(this));
			}
			
			window.app.myLightWindow.ss_refreshLinks();
			// seeqlists (disabled via css for all deal controllers)
			if($('PlaceListManager')) {
				$A([this.iwContentNode.down('.addToPlaceList'), this.iwMaxContentNode.down('.addToPlaceList')]).each(function(el) {
					if(!el) return;
					Event.stopObserving(el, 'click');
					Event.observe(el, 'click', this.addToPlaceList.bind(this));
				}.bind(this));
			}
			
			// deals
			this.iwContentNode.select('.deal').each(function(el) {
				var deal = new mesq.Deal(el, this);
			}.bind(this));
			this.iwMaxContentNode.select('.deal').each(function(el) {
				var deal = new mesq.Deal(el, this);
			}.bind(this));
			
			// gmap
			var iw = app.map.gmap.getExtInfoWindow();
			iw.enableMaximize();
			
			// more info
			$A([this.iwContentNode.down('.moreInfo'), this.iwContentNode.down('.deals h3 a')]).each(function(el) {
				if(!el) return;
				Event.observe(el, 'click', function(e) {
					app.map.gmap.maximizeExtInfoWindow();
					Event.stop(e);
				}.bind(this));
			}.bind(this));
			
			if(window.app.debugprofile) console.timeEnd('mesq.ListItem.setupInfoWindows()');
		}.bind(this), REDRAW_DELAY_SECS);
	},
	
	addToPlaceList: function(e) {
		var el = Event.element(e);
		var selectContainer = el.up('div');
		
		// avoid creating mulitple dropdowns
		if(selectContainer.down('select')) {
			e.stop();
			return false;
		}
		
		
		if(window.app.isLoggedIn()) {
			// if no list exists, prompt to create one
			var lists = window.app.placeListManager.getLists();
			if(lists.length == 0) {
				var newListName = window.prompt(this.strEnterListName);
				window.app.placeListManager.addPlaceList(false, newListName, this.addToPlaceList_onAdded.bind(this, selectContainer));
			} else {
				this.addToPlaceList_onAdded(selectContainer);
			}
		}
		
		
		if(e) { Event.stop(e); return false; }
	},
	
	addToPlaceList_onAdded: function(selectContainer) {
		var lists = window.app.placeListManager.getLists();
		
		// if only one list available, add it directly
		if(lists.length == 1) {
			window.app.placeListManager.addToPlaceList(this, lists[0].getID());
			var list = window.app.placeListManager.getListByID(lists[0].getID()); 
			list.expand();
			selectContainer.update('<span class="note">' + this.strAddedPlace + '</span>');
			new Effect.ScrollTo('PlaceListManager');
			
			// refresh infowindows to reflect seeqlist count
			this.loadInfoWindows();
		} else {
			var html = '';
			html += '<select>';
			html += '<option>&nbsp;</option>';
			lists.each(function(list) {
				html += '<option value="' + list.getID() + '">' + list.getTitle() + '</option>';
			});
			html += '</select>';
			selectContainer.insert(html);
			
			Event.observe(selectContainer.down('select'), 'change', this.addToPlaceList_onChange.bind(this));
		}
	},
	
	addToPlaceList_onChange: function(e) {
		var el = Event.element(e);
		var container = el.up('div');
		window.app.placeListManager.addToPlaceList(this, $F(el));
		var list = window.app.placeListManager.getListByID($F(el)); 
		list.expand();
		
		container.update('<span class="note">' + this.strAddedPlace + '</span>');
		new Effect.ScrollTo('PlaceListManager', {afterFinish: function(e) {
			new Effect.Highlight(el);
		}});
		
		// refresh infowindows to reflect seeqlist count
		this.loadInfoWindows();
		
		e.stop();
	},
	
	getDealHTML: function() {
		return this.data.dealListItemHTML;
	},
	
	togglePushpin: function(e) {
		(this.pinned) ? this.unpin() : this.pin();
		
		var pinned = this.pinned; // needs to be a copy
		this.pinned = !this.pinned;
		
		this.container.fire('ListItem:pinned', {obj: this});
		
		if(e) Event.stop(e);;
		return false;
	},
	
	pin: function() {
		var img = this.container.select('.pushpin img')[0];
		img.src = img.src.replace(/_off/,'_on');			
		this.container.addClassName('pinned');
	},
	
	unpin: function() {
		var img = this.container.select('.pushpin img')[0];
		img.src = img.src.replace(/_on/,'_off');
		this.container.removeClassName('pinned');
	},
	
	lat: function() {
		return this.data.lat;
	},

	lng: function() {
		return this.data.lng;
	}
	
	/**
	 * Needed for sort()
	 */
	/*
	toString: function() {
		return (this.pinned) ? Number(this.data.distanceMiles) : 100 + Number(this.data.distanceMiles);
	}
	*/
	
});