/*
 * @depends mootools-1.2.4-core.js
 * @depends mootools-1.2.4.4-more.js
 *
 * Created: 2010-08-20
 * Nathan Reed (c) 2010
 */

var _DATA_COOKIE = 'stream-data';
var _LANG_COOKIE = 'stream-lang';
var _RATE_SUFFIX = " tweets/min";

var Ui = {
	init: function() {

		this.column_count = 0;
		this.max_cols = 5;
		this.streams = $H();

		// set the language, either from the #hashtag or the cookie
		// default to english if we have neither
		this.lang = location.hash.replace('#', '') || Cookie.read(_LANG_COOKIE) || 'en';

		// set the selected lang link as inactive, save the language to the cookie
		$('ll-'+this.lang).href = '#';
		$('ll-'+this.lang).setStyle('color', 'black');
		Cookie.write(_LANG_COOKIE, this.lang, {duration: 7});


		// read the cookie data to set up the page as it was last time
		var json_cookie = Cookie.read(_DATA_COOKIE);
		if($defined(json_cookie)) {
			this.fromJson(json_cookie);
		}

		// start getting trending topics. If there are no columns on the page
		// when we are done, then add the top 3 TT.
		new TrendingTopics({
			onSuccess: function(data) {
				var trending_count = 3;
				Ui.load_trending(data[0].trends);

				if(this.column_count == 0) {
					for(var i=0; i < trending_count; i++) {
						this.add_column(data[0].trends[i].name, false);
					}
				}
			}.bind(Ui)
		});

	},

	start_search: function(elem, stream_id) {
		var query = $(elem).value.trim();

		if(query != "") {
			this.streams[stream_id].start_feed(query);
		}
	},

	toggle_pause: function(elem, stream_id) {
		this.streams[stream_id].toggle_pause();
		var is_paused = this.streams[stream_id].is_paused;

		if(is_paused) {
			$(elem).innerHTML = 'resume';
		} else {
			$(elem).innerHTML = 'pause';
		}
	},

	clear_search: function(stream_id) {
		this.streams[stream_id].clear();
		$('t-search-' + (stream_id)).value = "";
		$('t-rate-' + (stream_id)).innerHTML = "0" + _RATE_SUFFIX;
	},

	add_column: function(search_query, will_save) {

		if(!$defined(will_save)) {
			// do we save this new column to the state cookie?
			will_save = true;
		}

		if(this.column_count >= this.max_cols) {
			return;
		}

		search_query = search_query || '';
		var column_id = 'C' + (Math.random() * 1000).round().toString();

		// add the column to the dom, and create the object to handle it.
		var new_elem = new JsTemplate('tmpl-tweet-col').render({'column_id': column_id, 'query':search_query}).inject('t-columns');

		this.streams[column_id] = new TweetStream('tweet-list-'+column_id, {
			'rate_elem': 't-rate-'+column_id,
			'lang':this.lang,
			'max_displayed':16,
			'popular_list': 'pop-tweets-'+column_id,
			'popular_alert': 'pop-tweets-alert-'+column_id
		}).start_feed(search_query);

		// we set this now instead of in the constructor so that we don't have to
		// worry about it getting triggered on the initial search
		this.streams[column_id].options.onChange = this.query_change.bind(this);
		this.column_count++;

		// resize all the columns to the correct width
		// edit: don't think this is needed anymore, just have them al at 400px?
		//var new_width = (90 / this.column_count).round() + '%';
		//$$('.tweet-col').setStyle('width', new_width);

		if(will_save) {
			// set the focus on the textbox we just added

			this.save_state();
		}

		if(search_query == "" && will_save) {
			// focus on the just added textbox, but only if it has been
			// added by the user
			$('t-search-'+column_id).focus();
		}
	},

	delete_column: function(stream_id) {
		this.streams[stream_id].stop();
		this.streams.erase(stream_id);

		$('tc-' + stream_id).dispose();

		this.column_count--;
		this.save_state();
	},

	query_change: function(stream, new_query) {
		// one of the searches has changed. save the change to the cookie
		this.save_state();
	},

	toggle_popular: function(stream_id) {
		var stream = this.streams[stream_id];
		stream.popular_visible = !stream.popular_visible;

		if(stream.popular_visible) {
			$('pop-tweets-alert-'+stream_id).innerHTML = 'click to hide popular tweets';
			$('pop-tweets-'+stream_id).setStyle('display', '');
		} else {
			$('pop-tweets-'+stream_id).setStyle('display', 'none');
			$('pop-tweets-alert-'+stream_id).innerHTML = stream.popular_alert_text;
		}
	},

	// serialize the current state of the streams to json so that they
	// can be saved to a cookie
	toJson: function() {
		var stream_meta_data = [];

		this.streams.each(function(item) {
			stream_meta_data.push({'query':item.search_query});
		});

		return JSON.encode(stream_meta_data);
	},

	// takes a json object and creates all the streams needed
	fromJson: function(json_data) {
		if($type(json_data) == 'string') {
			json_data = JSON.decode(json_data);
		}

		if($type(json_data) != 'array') {
			return this;
		}

		json_data.each(function(item) {
			// create each column
			this.add_column(item.query, false);
		}.bind(Ui));

		return this;
	},

	save_state: function() {
		// serialize the column state and save to cookie for next time
		Cookie.write(_DATA_COOKIE, this.toJson(), {duration: 7});
	},

	change_language: function(lang_code) {
		// set the hashtag, reload the page.
		// is this the best way to set the language? im not sure...
		window.location = '#' + lang_code;
		window.location.reload();
	},

	load_trending: function(trend_list) {
		var max_trending = 4;

		trend_list.each(function(item, index) {
			if(index >= max_trending) {
				return;
			}

			var new_link = new Element('a', {
				'href': 'javascript:void(0)',
				'text': item.name,
				'events': {
					'click': function() { Ui.add_column(item.name); }
				}
			});

			$('tt-list').grab(new_link)

			if(index != trend_list.length-1 && index != max_trending-1) {
				$('tt-list').appendText(' | ');
			}
		});

	}
}

// TweetStream: handles searching for a query, and then adding that data to the page.
//
// This ties the searcher and DivList classes together. On the page an instance of this
// represents a single search or a single column. This class actually handles the
// rendering of the element and adding it to the DOM. The format of the tweet is handled
// using a html template stored somewhere on the page. (Usually inside a script element)
//
// usage (will search for:
//     new TweetStream('tweet-list-2', {'rate_elem': 't-rate'}).start_feed(search_query);
//
var TweetStream = new Class({
	initialize: function(elem, options) {
		this.search_query = "";
		this.first_load = true;
		this.is_paused = false;
		this.refresh_delay = 60000;
		this.popular_visible = false;
		this.element = $(elem);
		this.options = options || {};
		this.popular_alert_text = "";


		this.tweet_template = this.options.tweet_template || 'tmpl-tweet';
		this.options.onChange = this.options.onChange || $empty;
		this.options.lang = this.options.lang || 'en';
		this.options.popular_list = $(this.options.popular_list) || null;
		this.options.popular_alert = $(this.options.popular_alert) || null;

		if($defined(this.options.rate_elem)) {
			this.rate_elem = $(this.options.rate_elem);
		}

		// the stream reliles on two things, a div list to display the tweets
		// and a interface to the twitter search api (using jsonp)
		this.element.empty();

		this.tweet_list = new DivList(this.element, {
			'max_items': this.options.max_displayed || 25,
			'onAdd': this.add_tweet.bind(this)
		});

		this.searcher = null;
	},

	start_feed: function(search_query) 	{
		if(search_query == "") {
			return this;
		}

		this.search_query = search_query;

		if($defined(this.searcher)) {
			this.tweet_list.empty();
			this.searcher.stop();
			this.searcher = null;
		}

		this.first_load = true;
		this.element.innerHTML = "<div class='t-load-msg'>loading...</div>";

		this.clear_popular();

		this.searcher = new TwitterSearch({
			'query':search_query,
			'onComplete': this.tweets_received.bind(this),
			'refresh_delay': this.refresh_delay - 1000,
			'lang': this.options.lang
		}).start();

		this.options.onChange(this, search_query);

		return this;
	},

	clear_popular: function() {
		if(this.options.popular_list != null) {
			this.popular_visible = false;
			this.options.popular_list.empty();

			this.options.popular_list.setStyle('display', 'none');
			this.options.popular_alert.setStyle('display', 'none');
		}
	},

	tweets_received: function(data, rate_tps) {
		// we might have go no results, so just quit now. but remember to remove
		// the loading message if it is the inital load.
		if(!$defined(data.results) || data.results.length == 0) {
			if(this.first_load == true) {
				this.element.innerHTML = "<div class='t-load-msg'>no results found...</div>";
			}

			return;
		}

		// show the tweet rate in the header. we must convert from tweets/sec to t/minute
		if($defined(this.rate_elem)) {
			this.rate_elem.innerHTML = (rate_tps * 60).round(2) + _RATE_SUFFIX;
		}

		// we want to hold back about a minutes worth of tweets. (but show at least one straight away)
		// this way it looks like we are getting tweets all the time, but we still display some immediately
		// if it is not a very active query
		var slow_count = (rate_tps * 60).toInt().limit(0, data.results.length-1);

		// we want to add the tweets one by one with a delay between them, this way it
		// looks like we are continuously receiving data. ideally the delay should last
		// exactly until the next request is made.
		data.results.reverse();
		if(this.first_load == true) {
			this.first_load = false;
			this.element.empty();

			// on the first load we want to do things slightly differently.
			// so lets add a bunch of tweets straight up, but leave a few to
			// be added in on delay.
			for(var i=0; i < data.results.length - slow_count; i++) {
				this.tweet_list.add_item(data.results[i]);
			}

			this.tweet_list.add_slowly(data.results.fromIndex(data.results.length - slow_count), this.refresh_delay);
		} else {
			this.tweet_list.add_slowly(data.results, this.refresh_delay);
		}

		// set the popular tweets alert bar. Click on this to show pop tweets
		if(data.popular_results.length > 0 && this.options.popular_alert != null) {
			if(data.popular_results.length == 1) {
				var count_str =  ' popular tweet. ';
			} else {
				var count_str =  ' popular tweets. ';
			}

			this.popular_alert_text = data.popular_results.length + count_str + 'Click to show.';
			this.options.popular_alert.setStyle('display', '');
			this.options.popular_alert.innerHTML = this.popular_alert_text;
		}

		// add the popular tweets. They will be hidden by default.
		data.popular_results.each(function(tweet) {
			tweet.popular_tag = '<span class="pop-tweet-tag"><b>popular</b> ' + tweet.metadata.recent_retweets + '+ retweets</span>';
			this.add_popular(tweet);
		}.bind(this));
	},

	add_tweet: function(tweet, list_element) {
		list_element = list_element || this.tweet_list;

		// turn the urls in the text into proper links
		// link @names
		// link #hashtags
		tweet.text = tweet.text.replace(/(\s)*(http:\/\/[^\s]+)(\s)*/g, '$1<a href="$2">$2</a>$3');
		tweet.text = tweet.text.replace(/(@(\w+))/g, '<a href="http://twitter.com/$2">$1</a>');
		tweet.text = tweet.text.replace(/(#(\w+))/g, '<a href="javascript:Ui.add_column(\'$1\')">$1</a>');
		tweet.created_time = Date.parse(tweet.created_at).format('%X');

		var jst = new JsTemplate(this.tweet_template);
		var elem = jst.render(tweet).inject(list_element, 'top');

		elem.setStyle('opacity', '0').fade();
	},

	add_popular: function(tweet) {
		if(this.options.popular_list != null) {
			this.add_tweet(tweet, this.options.popular_list);
		}
	},

	toggle_pause: function() {
		this.is_paused = !this.is_paused;

		if(this.is_paused) {
			this.tweet_list.pause();
		} else {
			this.tweet_list.resume();
		}

	},

	stop: function() {
		if(this.searcher != null) {
			this.searcher.stop();
			this.is_paused = true;
			this.search_query = "";
		}
	},

	clear: function() {
		this.searcher.stop();
		this.tweet_list.empty();
		this.clear_popular();
		this.search_query = "";
		this.options.onChange(this, this.search_query);
	}
});

// uses JsonP to search twitter, firing off the passed in callback when results arrive
//
// usage (searches for "justin beiber" every 60 secs, runs the callback when it gets results):
// 		this.searcher = new TwitterSearch({
//			'query':'justin beiber',
//			'onComplete': this.tweets_received.bind(this),
//			'refresh_delay': 60000
//		}).start();
//
var TwitterSearch = new Class({
	initialize: function(options) {
		this.timer_id = -1;
		this.base_url = 'http://search.twitter.com/search.json';

		this.options = options || {};
		this.refresh_delay = options.refresh_delay || 60*1000;
		this.tweets_received_fn = options.onComplete || $empty();

		// TODO: allow user to pass in something in 'options', that will
		// go straight in here to be passed to twitter.
		this.params = new Hash({
			'result_type':'mixed',
			'rpp': this.options.result_count || 32,
			'lang': this.options.lang || 'en',
			'q': this.options.query || ""
		});

		// this mootools class will take care of all the JsonP details for us.
		// no need to set the url at this stage, it will be set dynamically when
		// the search is run.
		this.json_request = new Request.JSONP({
			url: "",
			callbackKey: 'callback',
			onComplete: this.tweets_received.bind(this)
		});
	},

	run_search: function() {
		this.json_request.options.url = this.base_url + '?' + this.params.toQueryString();
		this.json_request.send();
	},

	tweets_received: function(data) {
		// twitter sends a nice for refreshing with the since_id parameter
		// already baked in. Set the parameter url to this for the next refresh
		if($defined(data.results) && data.results.length > 0) {
			this.refresh_params = data.refresh_url.replace('?', '').parseQueryString();
			this.params.extend(this.refresh_params);
		}

		// filter out the popular results, and place them in their own spot...
		data.popular_results = this.filter_tweets(data.results, 'popular');
		data.results = this.filter_tweets(data.results, 'recent');

		// run the user callback
		this.tweets_received_fn(data, this.tweet_rate(data.results));
	},

	start: function() {
		this.run_search();

		$clear(this.timer_id);
		this.timer_id = this.run_search.periodical(this.refresh_delay, this);

		return this;
	},

	stop: function() {
		$clear(this.timer_id);
		return this;
	},

	// we use the difference in timestamps between the first and last results
	// the calculate the tweet rate. (in tps)
	tweet_rate: function(tweet_list) {
		if(tweet_list == null || tweet_list.length == 0) {
			return 0;
		}

		var tweetCount = tweet_list.length;
		var firstTweet = Date.parse(tweet_list.getFirst().created_at);
		var lastTweet = Date.parse(tweet_list.getLast().created_at);
		var tweetSpread = lastTweet.diff(firstTweet, 'second');

		return (tweetSpread != 0)? tweet_list.length /tweetSpread : 0;
	},

	filter_tweets: function(tweet_list, type) {
		return tweet_list.filter(function(item) {
			return (item.metadata.result_type == type);
		});
	}
});

var TrendingTopics = new Class({
	initialize: function(options) {
		this.options = options || {};
		this.options.url = this.options.url || 'https://api.twitter.com/1/trends/1.json'
		this.options.onSuccess = this.options.onSuccess || $empty;

		this.refresh();
	},

	refresh: function() {
		new Request.JSONP({
			url: this.options.url,
			callbackKey: 'callback',
			onSuccess: this.load_complete.bind(this)
		}).send();
	},

	load_complete: function(data) {
		this.options.onSuccess(data);
	}
});

// A list of Div elements.
//
// This class sloves two problems. We can't keep adding tweets to the page
// forever, because we eventually run out of memory. This class keeps track of
// that, and remove the last div from 'elem' once we have enough.
//
// The second problem is that when the tweets arrive from the api, we don't want
// to add them all at once every minute or so, it would look stupid, so this class
// has an 'add_slowly' function that takes an array and calls 'add_item' for
// each item in the array over the period specified. Ideally we are adding the
// last item just as the new data is arriving.
//
// The actual rendering and adding of the *element* to the DOM is done by the owner
// through a callback.
var DivList = new Class({
	initialize: function(elem, options) {
		this.elem = $(elem);
		this.options = options || {};

		this.max_items = this.options.max_items || 32;
		this.add_item_callback = this.options.onAdd || $empty();

		this.item_count = 0;
		this.is_paused = false;
		this.timer_id = -1;
	},

	add_slowly: function(item_list, over_interval_ms) {
		if(!$defined(item_list)) {
			return;
		}

		over_interval_ms = over_interval_ms || 60000;
		var base_delay = over_interval_ms / item_list.length;

		$clear(this.timer_id);
		item_list.resetPointer();
		this.timer_id = this.add_next.periodical(base_delay, this, [item_list]);

	},

	add_next: function(item_list) {
		var item = item_list.getNext();

		if(this.is_paused) {
			return;
		}

		if(!$defined(item)) {
			// we have reached the end of the list, so cancel the timer
			$clear(this.timer_id);
			this.timer_id = -1;
			return;
		}

		this.add_item(item);
	},

	add_item: function(item) {
		this.item_count++;

		if(this.item_count > this.max_items) {
			this.elem.getLast().dispose();
		}

		this.add_item_callback(item);
	},

	empty: function() {
		$clear(this.timer_id);
		this.elem.empty();
		this.item_count = 0;
	},

	stop: function() {
		$clear(this.timer_id);
	},

	pause: function() {
		this.is_paused = true;
	},

	resume: function() {
		this.is_paused = false;
	},

	toElement: function() {
		return this.elem;
	}

});

/*
 * JsTemplate: convert template into an element object.
 *
 * example:
 * <!-- this goes in <head>, the browser will ignore it if the type is not text/javascript -->
 * <script id='tmpl-entry' type='text/html'>
 *   <div>
 *     <b>#{title}<b>
 *     <div>#{text}</div>
 *   </div>
 * </script>
 *
 * // replaces #{title} & #{text} with the actual text in the data object, then
 * // inserts the element into <body>
 * new JsTemplate('tmpl-entry').render({title: 'test', text:'test text'}).inject('body');
 *
 * License: MIT-Style License
 * Nathan Reed (c) 2010
 */
var JsTemplate = new Class({
	initialize: function(elem) {
		this.element = $(elem);
		this.regex = /\\?#\{([^{}]+)\}/g; // matches '#{keyword}'
	},

	render: function(data) {
		if($defined(this.element)) {
			// replaces #{name} with whatever is in data.name
			// but first we need to normalize the innerHTML
			var html_string = this.element.innerHTML.clean().trim(); // collapses mulitple whitespace down to single spaces
			var is_td = false;

			if(html_string.test(/^<td/i)) {
				// tables are treated differently. we are not allow to just parse and insert
				// them willy nilly.
				html_string = "<table><tbody><tr>" + html_string + "</tr></tbody></table>";
				is_td = true;
			}

			var e = new Element('div', {'html': html_string.substitute(data, this.regex)});

			if(is_td) {
				return e.getFirst('table').getFirst('tbody').getFirst('tr').getFirst();
			} else {
				return e.getFirst();
			}
		}
	}
});

Array.implement({
	// returns all the items in the array with an index
	// greater than i;
	fromIndex: function(i) {
		return this.filter(function(item, index) {
			return (index >= i);
		});
	},

	// methods below allow us to use Array as an ennumerable type
	// to step through the array. For all I know Array already has these
	// under a different name, I just don't know what it is
	resetPointer: function() {
		this.current_item = 0;
		return this;
	},

	getNext: function() {
		return this[this.current_item++];
	},

	getFirst: function() {
		return this[0];
	},

	getLast: function() {
		return this[this.length-1];
	}
});

