/*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, strict: true, newcap: true, immed: true */
/*global $: false, $chk: false, $clear: false, $each: false, $pick: false, $H: false, $A: false, Class: false, Element: false, Events: false, Fx: false, GEvent: false, GIcon: false, GLatLng: false, GLatLngBounds: false, GMap2: false, GOverlay: false, GPoint: false, GSize: false, G_DEFAULT_ICON: false, G_MAP_MARKER_MOUSE_TARGET_PANE: false, G_MAP_MARKER_PANE: false, G_MAP_MARKER_SHADOW_PANE: false, Math: false, Options: false, Request: false, console: false */

var SimpleMarker = new Class({
	Implements: Options,

	overlay: null,

	point: null,
	map: null,
	options: {
		icon: null
	},

	icon: null,
	iconElements: false, 
	/* iconElements: {
		image: null,
		shadow: null,
		transparent: null,
		imageMap: null
	}, */

	initialize: function (point, options) {
		this.initialize = this.initializeOverlay;

		this.overlay = new GOverlay();

		this.point = point;
		this.options.icon = G_DEFAULT_ICON;
		this.setOptions(options);

		this.icon = this.options.icon;
	},

	initializeOverlay: function (map) {
		this.map = map;
		this.createElements();
	},

	createElements: function () {
		if (!this.iconElements) {
			this.iconElements = $H();
			var click = GEvent.callbackArgs(GEvent, GEvent.trigger, this, 'click'),
				mapName = SimpleMarker.getNextMapName();

			this.iconElements.image = new Element('img', {
				src: this.icon.image,
				width: this.icon.iconSize.width,
				height: this.icon.iconSize.height,
				styles: {position: 'absolute'}
			});
			this.iconElements.shadow = new Element('img', {
				src: this.icon.shadow,
				width: this.icon.shadowSize.width,
				height: this.icon.shadowSize.height,
				styles: {position: 'absolute'}
			});
			this.iconElements.transparent = new Element('img', {
				src: this.icon.transparent,
				width: this.icon.iconSize.width,
				height: this.icon.iconSize.height,
				styles: {position: 'absolute'}
			});
			this.iconElements.imageMap = new Element('map', {id: mapName, name: mapName}).set('html',
				'<area alt="" shape="poly" coords="' + this.icon.imageMap.join() + '" href="#" />'
			);
			this.iconElements.imageMap.getElement('area').addEvent('click', function (event) {
				event.stop();
				click();
			});
			this.iconElements.transparent.useMap = '#' + mapName;
		}

		if (this.map) {
			this.map.getContainer().appendChild(this.iconElements.imageMap);
			this.map.getPane(G_MAP_MARKER_SHADOW_PANE).appendChild(this.iconElements.shadow);
			this.map.getPane(G_MAP_MARKER_PANE).appendChild(this.iconElements.image);
			this.map.getPane(G_MAP_MARKER_MOUSE_TARGET_PANE).appendChild(this.iconElements.transparent);
		}
	},
	redraw: function (force) {
		var pixelPoint,
			styles;

		// If we don't have to move the marker, then return
		if (!force) {
			return;
		}

		// Calculate the relative pixel point of the marker's geographical point
		pixelPoint = this.map.fromLatLngToDivPixel(this.point);

		// Account for anchor point
		pixelPoint.x -= this.icon.iconAnchor.x;
		pixelPoint.y -= this.icon.iconAnchor.y;

		this.zindex = GOverlay.getZIndex(this.point.lat());
		styles = {
			left: pixelPoint.x,
			top: pixelPoint.y,
			zindex: this.zindex
		};

		this.iconElements.image.setStyles(styles);
		this.iconElements.shadow.setStyles(styles);
		this.iconElements.transparent.setStyles(styles);
	},

	remove: function () {
		this.iconElements.image.parentNode.removeChild(this.iconElements.image);
		this.iconElements.shadow.parentNode.removeChild(this.iconElements.shadow);
		this.iconElements.transparent.parentNode.removeChild(this.iconElements.transparent);
		this.iconElements.imageMap.parentNode.removeChild(this.iconElements.imageMap);
	},

	copy: function () {
		return new SimpleMarker(this.point, this.options);
	},

	openInfoWindow: function (element, options) {
		options.pixelOffset = this.icon.infoWindowAnchor;
		if (this.map) {
			this.infoWindow = this.map.openInfoWindow(this.point, element, options);
		} else {
			return false;
		}
	},
	closeInfoWindow: function () {
		if (this.infoWindow) {
			this.infoWindow.hide();
			this.infoWindow = null;
		}
	},

	getElements: function () {
		return this.iconElements;
	},

	getLatLng: function() {
		return this.point;
	},

	getIcon: function() {
		return this.icon;
	}
});
SimpleMarker.getNextMapName = function () {
	this.mapNumber = (this.mapNumber || 0) + 1;
	return 'SimpleMarker_map_' + this.mapNumber;
};

var Rotator = new Class({
	Implements: [Events, Options],

	queue: [],
	callback: null,
	timer: false,

	options: {
		autoStart: true,
		delay: 1000
	},

	initialize: function (callback, queue, options) {
		this.callback = callback;
		this.setOptions(options);

		$A(queue).each(function(item) {
			this.addToEnd(item);
		}, this);

		if (this.options.autoStart) {
			this.start();
		}
	},

	run: function () {
		if (this.queue.length !== 0) {
			var next = this.queue.pop();
			this.queue.unshift(next);
			this.callback(next);
		}
		return this;
	},
	start: function () {
		if (this.isRunning()) {
			return;
		}

		// Set up the periodical callback
		this.timer = this.run.periodical(this.options.delay, this);
		// fn.periodical() wont run the function till the delay has passed, so we run it manually once
		this.run();

		return this;
	},
	stop: function () {
		$clear(this.timer);
		this.timer = false;
		return this;
	},
	restart: function () {
		this.stop();
		this.start();
		return this;
	},
	isRunning: function () {
		return this.timer !== false;
	},

	addNext: function (item) {
		this.queue.push(item);
		if (this.queue.length === 1 && this.isRunning()) {
			this.restart();
		}
		return this;
	},
	addToEnd: function (item) {
		this.queue.unshift(item);
		if (this.queue.length === 1 && this.isRunning()) {
			this.restart();
		}
		return this;
	},
	remove: function (item) {
		if (this.contains(item)) {
			this.queue.splice(this.queue.indexOf(item), 1);
		}
	},
	removeAll: function (item) {
		this.queue.erase(item);
	},
	contains: function (item) {
		return this.queue.contains(item);
	}

});

Fx.Transitions.extend({
	Jump: function (p) {
		var num = 1 - (1 - p) * (Math.abs(Math.sin(p * 4 * Math.PI)));
		return num;
	}
});

String.implement({
	linkify: function () {
		// modified from TwitterGitter by David Walsh (davidwalsh.name)
		// courtesy of Jeremy Parrish (rrish.org)
		return this.replace(/(https?:\/\/[\w\-:;?&=+.%#\/]+)/gi, '<a href="$1">$1</a>')
			.replace(/(^|\W)@(\w+)/g, '$1<a href="http://twitter.com/$2">@$2</a>')
			.replace(/(^|\W)#(\w+)/g, '$1#<a href="http://search.twitter.com/search?q=%23$2">$2</a>');
	}
});
Array.implement({
	shuffle: function () {
		var shuffled = [],
			l = this.length,
			i;

		while (l > 0) {
			l = l - 1;
			i = $random(0, l);

			shuffled.push(this[i]);
			delete(this[i]);
		}
		
		return shuffled;
	}
});

var LocationFeed = new Class({
	location: null,
	twitters: null,
	events: null,

	point: null,

	feedMap: null,

	iconUrl: '/images/event_markers/',

	twitter: {
		search: 'http://search.twitter.com/search.json',
		user: 'http://api.twitter.com/1/users/show.json',
		query: {},
	},

	first: true,

	refreshDelay: 60000,

	maxEvents: 2,

	initialize: function(location, feedMap) {
		this.location = location.Location;
		this.point = new GLatLng(this.location.latitude, this.location.longitude);
		this.feedMap = feedMap;
		this.id = 'location_' + this.location.id;
		this.icon = this.createIcon();

		this.twitters = $H();
		this.events = $H();

		location.TwitterFeed.each(function(twitter) {
			this.twitters[twitter.twitter_user] = {
				info: twitter,
				user: twitter.user,
				tweet: twitter.user.status
			};
			if (twitter.user && twitter.user.status) {
				this.twitter.since_id = twitter.user.status.id;
			}
		}, this);

		location.Event.each(function(event_info) {
			var event = {info: event_info}
			event.start = Date.parse(event.info.start);
			event.end = Date.parse(event.info.end);
			event.stage = this.getEventStage(event);
			this.events[event_info.id] = event;
		}, this);

		this.feedMap.addFeed(this.id, this);
		this.start();
	},

	createIcon: function() {
		var icon = new GIcon();
		icon.image = this.iconUrl + 'image.png';
		icon.shadow = this.iconUrl + 'shadow.png';
		icon.iconSize = new GSize(16, 16);
		icon.shadowSize = new GSize(24, 16);
		icon.iconAnchor = new GPoint(8, 8);
		icon.infoWindowAnchor = new GSize(0, -8);
		icon.printImage = icon.image;
		icon.mozPrintImage = this.iconUrl + 'printImage.gif';
		icon.printShadow = this.iconUrl + 'printShadow.gif';
		icon.transparent = this.iconUrl + 'transparent.png';
		icon.imageMap = [12,1,13,2,13,3,12,4,11,5,15,6,15,7,15,8,15,9,11,10,11,11,12,12,13,13,13,14,12,15,3,15,2,14,2,13,3,12,4,11,4,10,0,9,0,8,0,7,0,6,4,5,3,4,2,3,2,2,3,1];

		return icon;
	},

	/**
	 * Starts the automatic updating of events
	 */
	start: function () {
		this.stop();
		this.refreshData();
		this.refresher = this.refreshData.periodical(this.refreshDelay, this);
		return this;
	},
	/**
	 * Stops the automatic updating of events
	 */
	stop: function () {
		$clear(this.refresher);
		return this;
	},

	/**
	 * Checks the events to see if they need updating
	 */
	refreshData: function () {
		var count = 2,
			changed = false,
			callback = (function(hasChanged) {
				count--;
				changed = changed || hasChanged;
				if (count === 0) {
					this.refreshPopup(!this.first && changed);
					this.first = false;
				}
			}).bind(this);

		this.refreshTwitters(callback);
		this.refreshEvents(callback);
		return this;
	},

	refreshPopup: function(announce) {
		this.feedMap.updateFeed(this.id, this.toElement(), $pick(announce, true));
	},

	refreshTwitters: function (callback) {
		if (!this.request && this.twitters.getLength() !== 0) {
			var query = this.twitter.since_id ? {since_id: this.twitter.since_id} : {};

			this.twitters.each(function(twitter) {
				query.q = (query.q ? query.q + " OR " : '') + 'from:' + twitter.info.twitter_user;
			});

			this.request = new Request.JSONP({
				url: this.twitter.search,
				data: query,
				onSuccess: function (json) {
					// Parse the refresh_url query string, after stripping off the ?
					this.twitter.since_id = json.max_id || null;
					this.request = false;

					// Update the tweets
					(json.results || []).each(function(tweet) {
						this.twitters[tweet.from_user].tweet = tweet;
					}, this);

					callback(json.results && json.results.length !== 0);

				}.bind(this),
				onFailure: function() {
					callback(false);
				}
			}).send();
		} else {
			callback(false);
		}
		return this;
	},

	refreshEvents: function(callback) {
		var changed = false;
		$each(this.events, function (event, id) {
			var stage, diffWords, announce;

			stage = this.getEventStage(event);
			diffWords = stage.words;
			changed = changed || event.stage && (stage.status != event.stage.status);

			event.stage = stage;
		}, this);

		callback(changed);

		return this;
	},

	toElement: function() {
		var twitter = this.createTwitterHtml(),
		    event = this.createEventsHtml(),
			elements = [twitter, event].filter(function(item) {
				return item && item.getChildren().length !== 0;
			}),
			wrapper = new Element('div', {'class': 'map_infobox_popup'}).adopt(elements);

		return elements.length === 0 ? null : wrapper;
	},

	createTwitterHtml: function() {
		var tweets = [];
		this.twitters.each(function(twitter) {
			tweets.push(this.createTweetHtml(twitter));
		}, this);

		tweets.clean();

		if (tweets.length === 0) {
			return null;
		} else {
			return new Element('div', {'class': 'events'}).adopt(tweets);
		}
	},

	/**
	 * Creates a HTML Element displaying the tweet data
	 *
	 * @param feed The feed the tweet belongs to
	 * @param tweet The tweet to display
	 * @return A HTML Element with the feed details
	 */
	createTweetHtml: function (feed) {
		if (!feed.tweet) return null;
		var tweet = feed.tweet;
			text = new Element('p', {html: tweet.text.linkify()}),
			footer = new Element('p', {'class': 'small'}).adopt([
				new Element('a', {href: 'http://twitter.com/' + feed.user.screen_name + '/status/' + tweet.id, text: 'View tweet'}),
				new Element('span', {text: ' - ' + Date.parse(tweet.created_at).timeDiffInWords()})
			]),
			avatar = new Element('img', {src: feed.user.profile_image_url}),
			container = new Element('div', {'class': 'twitter'}).adopt([avatar, text, footer]);
		return container;
	},

	createEventsHtml: function() {
		var events = [],
			sortedEvents = $H(this.events).getValues(),
			i;

		sortedEvents.sort(function(e1, e2) {
			if (e1.stage.show === e2.stage.show) {
				return e2.start.getTime() - e1.start.getTime();
			} else {
				return (e1.stage.show ? -1 : 1);
			}
		});

		sortedEvents.each(function(event) {
			events.push(this.createEventHtml(event));
		}, this);

		events.clean();
		events = events.slice(0, Math.min(events.length, this.maxEvents));

		if (events.length === 0) {
			return null;
		} else {
			return new Element('div', {'class': 'events'}).adopt(events);
		}
	},

	/**
	 * Creates a HTML Element displaying the event data
	 *
	 * @param event The event to show
	 * @return A HTML Element with the feed details
	 */
	createEventHtml: function (event) {
		var stage = event.stage,
			title = new Element('p').grab(new Element('strong', {text: event.info.title})),
			description = new Element('p', {html: event.info.description}),
			time = (stage.status == 'running' ? 'happening now!' : event.start.timeDiffInWords()),
			footer = new Element('p', {'class': 'small'}).adopt([
				new Element('a', {href: '/events/view/' + event.info.id + '/', text: 'View event'}),
				new Element('span', {text: ' - ' + time})
			]),
			container = new Element('div').adopt([title, description, footer]);
		return container;
	},

	getEventStage: function (event) {
		var now = new Date(),
			startDiff = event.start.diff(now, 'minute'),
			endDiff = event.end.diff(now, 'minute'),
			words = event.start.timeDiffInWords();

		if (endDiff > 0) {
			return {status: 'over', show: false, words: words};
		} else if (startDiff >= 0) {
			return {status: 'running', show: true, words: words};
		} else if (startDiff >= new Date().increment('minute', 5).diff(now, 'minute')) {
			return {status: 'imminent', show: true, words: words};
		} else if (startDiff >= new Date().increment('minute', 30).diff(now, 'minute')) {
			return {status: 'soon', show: true, words: words};
		} else if (startDiff >= new Date().increment('day', 1).diff(now, 'minute')) {
			return {status: 'coming up', show: true, words: words};
		} else {
			return {status: 'ages away', show: false, words: words};
		}
	}
});

var FeedMap = new Class({
	map: null,
	mapElement: null,

	rotator: null,
	idleRotator: null,

	feeds: {},
	currentFeed: null,

	initialize: function (options) {
		this.mapElement = $(options.map);
		
		// Create and set up the map
		this.map = new GMap2(this.mapElement);
		this.map.setCenter(options.bounds.getCenter(), this.map.getBoundsZoomLevel(options.bounds));
		this.map.setUIToDefault();

		// Create the rotator
		this.rotator = new Rotator(this.showFeed.bind(this), [], {delay: 10000});

		// Add a click listener to the whole map to close info windows and stop the rotator
		GEvent.addListener(this.map, 'click', function () {
			// Stop the rotator
			this.rotator.stop();
			$clear(this.idleRotator);
			// Queue it up to start it again in a minute
			this.idleRotator = this.rotator.start.delay(60 * 1000, this.rotator);
		}.bind(this));

		// Add the supplied feeds
		$H(options.feeds || {}).each(function (feed, key) {
			this.addFeed(key, feed);
		}, this);
	},

	addFeed: function (key, feed) {

		feed.onMap = false;
		feed.marker = new SimpleMarker(feed.point, {icon: feed.icon});
		feed.marker.feed = feed;

		GEvent.addListener(feed.marker, 'click', function () {
			this.showFeed(key);
		}.bind(this));

		this.feeds[key] = feed;
		if (this.feeds[key].contents) {
			this.updateFeed(key, this.feeds[key].contents);
		}

		return this;
	},

	/**
	 * Updates a feeds contents
	 *
	 * @param key The key of the feed to update
	 * @param contents Optional. The new contents. If empty, the feed is removed from the map
	 * @param announce Optional. If this change should be announced. Defaults to true
	 */
	updateFeed: function (key, contents, announce) {

		var feed = this.feeds[key];

		contents = $pick(contents, false);
		announce = $pick(announce, true);

		// Argument defaults
		
		if (contents) {
			// Add the icon to the map if it doesnt exist already
			if (!feed.onMap) {
				//feed.marker.initializeOverlay(this.map);
				this.map.addOverlay(feed.marker);
				feed.onMap = true;
			}

			feed.contents = new Element('div', {'class': 'map_infobox_popup'}).grab(contents);
			feed.time = new Date();

			// Make the icon bounce, to annouce new contents
			if (announce) {
				$clear(feed.bounce);
				feed.bounce = function () {
					var elements = feed.marker.getElements();
					var duration = $random(800, 1200);
					new Fx.Tween(elements.image, {duration: duration, property: 'margin-top', transition: Fx.Transitions.Jump}).start(-20, 0);
					new Fx.Tween(elements.shadow, {duration: duration, property: 'margin-top', transition: Fx.Transitions.Jump}).start(-20, 0);
					new Fx.Tween(elements.shadow, {duration: duration, property: 'margin-left', transition: Fx.Transitions.Jump}).start(20, 0);
				}.periodical($random(1200, 1700));

				this.rotator.removeAll(key);
				this.rotator.addNext(key);
			} else {
				if (!this.rotator.contains(key)) {
					this.rotator.addToEnd(key);
				}
			}

		} else {

			// Remove the feed from the map if it doesnt have any contents
			if (feed.onMap) {
				this.map.removeOverlay(feed.marker);
				feed.marker.remove();
				feed.onMap = false;
			}

			feed.contents = null;
			this.rotator.removeAll(key);
			$clear(feed.bounce);
		}
	},


	/**
	 * Shows the popup for the feed. Hides any feeds currently being displayed.
	 *
	 * @param key The key of the feed to display
	 */
	showFeed: function (key) {
		var feed = this.feeds[key];

		this.hideFeed();
		this.currentFeed = feed;

		if (this.currentFeed) {
			this.currentFeed.marker.openInfoWindow(this.currentFeed.contents || this.empty, {maxWidth : 400});
			$clear(this.currentFeed.bounce);
		}

		return this;
	},

	/**
	 * Hides the current feed popup
	 */
	hideFeed: function () {
		if (this.currentFeed) {
			this.currentFeed.marker.closeInfoWindow();
			this.currentFeed = null;
		}

		return this;
	},

	getMap: function () {
		return this.map;
	},

	shuffleFeeds: function () {
		this.rotator.queue = this.rotator.queue.shuffle();
	}
});

