
/* ======================= */

var BASEURL = 'http://joshleejosh.com/games/twagnetic/';
var SOURCE_URL_BASE = 'http://twitter.com/';

var POEM_LINE_TOLERANCE = 24; // pixels

var QUERY_DELIMITER = '&';
var TWEET_URL_BASE = 'http://twitter.com/home?status=';
var TWEET_PRE = 'Twagnetic poetry: ';
var TWEET_POST = ' (http://tr.im/tSy0)';
var LINK_BLURB = 'link';
var TWEET_BLURB = 'tweet it!';

var COLOR_FIXED = '#fff';
var COLOR_SELECTED = '#99f';
var COLOR_PLACED = '#fff';

var BG_COLORS = [ 'f66', 'f96', 'ff6', '6f6', '6ff', '69f', 'fcf', 'f6f', 'ccc', 'fff', '000' ];
var HEXDIGITS = ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'];
var DEBUG_MAGIC_POEM = 'no rad twagnetic hit';

var HELP_MESSAGES = [
	'Enter your Twitter username into the text box above, and hit the "magnetize" button to turn your tweets into magnetic poetry.',
	'Drag words into the box on the left to create poems. Drag words out of the box to return them to the rack.',
	'Click on the colored boxes below to change the background color. Click on "' + LINK_BLURB + '" for a bookmarkable version of your poem, or click on "' + TWEET_BLURB + '" to post your poem on Twitter.',
	'If you ask for tweets from a private account, Twitter will ask for your login info. This site never stores any of your information.'
];

/* ======================= */

var theDebug = false;
var theUsername = '';
var theCorpus = new Array();
var theDragee = null;

var theFridgeExtents = new Object();
theFridgeExtents.x = 0;
theFridgeExtents.y = 0;
theFridgeExtents.width = 0;
theFridgeExtents.height = 0;
theFridgeExtents.maxX = 0;
theFridgeExtents.maxY = 0;

/* ======================= */

function initialize() {
	$('#twitter_username').keypress(onUsernameKey);
	log('1');
	preMagnetize();
	log('2');
	initializeColorButtons();
	log('3');

	var line = $('#words').text();
	log('4');
	mungeLine(line);
	log('5');
	mungeCorpus();
	log('6');
	resetWords();
	log('7');

	var url = new String(document.location);
	var qi = url.indexOf('?');
	if (qi > -1) {
		var qs = url.substr(qi+1);
		deserializeFridge(qs);
	}
	log('8');

	refresh();
	log('9');

	if (theDebug) {
		colorFridge('cb_1');
		colorFridge('cb_3');
		colorFridge('cb_5');
	}

}

function initializeColorButtons() {
	$('#colorbuttons').empty();
	for (i in BG_COLORS) {
		var e = $(document.createElement('input'));
		var id = 'cb_' + i;
		e.attr('type', 'submit');
		e.attr('value', '');
		e.attr('class', 'colorbutton');
		e.attr('style', 'background-color:#' + BG_COLORS[i]);
		e.attr('id', id);
		e.attr('onClick', 'colorFridge(\'' + id + '\');');
		$('#colorbuttons').append(e);
	}
	$('#colorbuttons').append('<p id="permalink">&nbsp;</p>');
}

// "main" function; updates words, poems, and links whenever neccessary.
function refresh() {
	updateFridgeExtents();
	var poem = guessPoem();

	if (poem.length > 0) {
		$('#whiteboard').empty().append('<p>' + poem.replace(/\//g, '<br/>'));
		$('#whiteboard').append('</p>');
		$('#whiteboard').css('font-weight', 'bold');
	} else {
		$('#whiteboard').css('font-weight', 'normal');
	}

	if (poem == DEBUG_MAGIC_POEM) {
		theDebug = !theDebug;
	}

	wipeDanglingWords();

	var purl = serializeFridge(poem);

	if (theDebug)
		dumpFridgeExtents();

	if (poem.length > 0) {
		var plink = ' <a href="?' + escape(purl) + '">' + LINK_BLURB + '</a> / ';
		$('#permalink').empty().append(plink);
		var turl = TWEET_URL_BASE + escape(TWEET_PRE) + escape(poem) + escape(TWEET_POST);
		var tlink = ' <a href="' + turl + '" target="_blank">' + TWEET_BLURB + '</a> '
			+ ' (' + (140-poem.length-TWEET_PRE.length-TWEET_POST.length)
					+ ' characters remaining)';
		$('#permalink').append(tlink);
	} else {
		$('#permalink').empty();
		fridgeHelp();
	}
}

/* ======================= */

function getUserTweets() {
	var username = $('#twitter_username').val();
	username = username.replace(/^@/, ''); // just in case someone left it on.
	fillErUp(username);
	if (theDebug)
		getLimits();
}

function fillErUp(username) {
	if (username != theUsername) {
		theUsername = username;
		theCorpus.splice(0, theCorpus.length);

		var f = function(data) {
			$.each(data, function(i, item) {
					var line = item.text;
					mungeLine(line);
					});
			reshuffleWords();
		}

		//u = SOURCE_URL_BASE + 'statuses/friends_timeline/' + username + '.json?callback=?';
		u = SOURCE_URL_BASE + 'statuses/user_timeline/' + username + '.json?callback=?';
		$.getJSON(u, f);
	} else {
		resetWords();
	}
}

function getLimits() {
	var g = function(data) {
		log('(' + data.remaining_hits + ' calls remaining)');
	}
	u = SOURCE_URL_BASE + 'account/rate_limit_status.json?callback=?';
	$.getJSON(u, g);
}

/* ======================= */

function serializeFridge(poem) {
	var out = '';

	var cs = $('#fridge').css('background-color');
	out = serializeColor(cs) + QUERY_DELIMITER + out;

	$('.word').each(function() {
		if ($(this).css('position') == 'absolute') {
			var s = $(this).text();
			var p = getWordExtents($(this));
			var x = d2h(p.fridgeX, 3);
			var y = d2h(p.fridgeY, 3);
			s = s.replace(QUERY_DELIMITER, '\\a');
			out += x+y+s+QUERY_DELIMITER;
			if (theDebug)
				$('#whiteboard').append(s + ': '
					+ p.fridgeX + ',' + p.fridgeY
					+ ' / '
					+ p.x + ',' + p.y
					+ ' / '
					+ x + ',' + y
					+ '<br/> ');
		}
	});

	out = out.replace(new RegExp(QUERY_DELIMITER+'$'), '');
	return out;
}

function deserializeFridge(ins) {
	updateFridgeExtents();
	ins = unescape(ins);
	var a = ins.split(QUERY_DELIMITER);

	var cs = a.shift();
	$('#fridge').css('background-color', deserializeColor(cs));

	for (i in a) {
		a[i] = a[i].replace('\\a', QUERY_DELIMITER);
		if (a[i].length > 6) {
			var xs = a[i].substr(0, 3);
			var ys = a[i].substr(3, 3);
			var ss = a[i].substr(6);
			var x = h2d(xs);
			var y = h2d(ys);
			var w = makeWord(ss);
			moveToFridgeRelative(w, x, y);
			w.css('background-color', COLOR_PLACED);
		}
	}
}

/* ======================= */

function ColorParser(cs) {
	this.source = cs;
	this.r = 0;
	this.g = 0;
	this.b = 0;
	this.hex = '000000';
	this.hex3 = '000';

	var s = this.source;
	if (startsWith(s, 'rgb')) {
		var a = s.match(/\d+/g);
		this.r = a[0];
		this.g = a[1];
		this.b = a[2];
	} else {
		if (startsWith(s, '#')) {
			s = s.substr(1);
		}
		var m = s.match(/[^01234567890ABCDEFabcdef]+/);
		if (!m || m.length == 0) {
			if (s.length == 3) {
				this.r = h2d(s.substr(0,1)+s.substr(0,1));
				this.g = h2d(s.substr(1,1)+s.substr(1,1));
				this.b = h2d(s.substr(2,1)+s.substr(2,1));
			} else {
				// do your best if the hex string is of bad length.
				while (s.length < 6) {
					s = s + '0';
				}
				this.r = h2d(s.substr(0,2));
				this.g = h2d(s.substr(2,2));
				this.b = h2d(s.substr(4,2));
			}
		} else {
			log('ERROR: Bad color string "' + s + '"');
		}
	}
}

function testParseColor() {
	var f = function(s) {
		var p = new ColorParser(s);
		return '[' + p.r + ',' + p.g + ',' + p.b + ']';
	};
	log('tsc1:' + f('rgb(255, 51, 153)'));
	log('tsc2a:' + f('#c69'));
	log('tsc2b:' + f('c69'));
	log('tsc3a:' + f('#CC3366'));
	log('tsc3b:' + f('CC3366'));
	log('tsc4a:' + f('66CC8'));
	log('tsc4b:' + f('#0A'));
	log('tsc5:' + f('red'));
}

function serializeColor(cs) {
	var c = new ColorParser(cs);
	var rgb = '' + d2h(c.r,2) + d2h(c.g,2) + d2h(c.b,2);
	return rgb;
}

function deserializeColor(cs) {
	return '#'+cs;
}

function colorFridge(sourceId) {
	var sourceC = $('#'+sourceId).css('background-color');
	var seriC = serializeColor(sourceC);
	var deserC = deserializeColor(seriC)
	$('#fridge').css('background-color', deserC);
	log(' ['+sourceId+':'+deserC+']');
	refresh();
}

/* ======================= */


function guessPoem() {
	var wa = new Array();
	var i = 0;
	$('.word').each(function() {
		if ($(this).css('position') == 'absolute') {
			var x = $(this).offset().left;
			var y = $(this).offset().top;
			if (x > theFridgeExtents.x
					&& y > theFridgeExtents.y
					&& x < theFridgeExtents.x + theFridgeExtents.width
					&& y < theFridgeExtents.y + theFridgeExtents.height) {
				wa[i] = $(this);
				i++;
			}
		}
	});

	if (wa.length > 0) {

		var table = new Array();
		table.unshift(new Array(wa.shift()));
		for (i in wa) {
			var p = getWordExtents(wa[i]);
			// find or make my row.
			var row = -1;
			for (j in table) {
				var jp = getWordExtents(table[j][0]);
				if (Math.abs(jp.y - p.y) < POEM_LINE_TOLERANCE) {
					row = j;
					break;
				}
			}

			if (row >= 0) {
				// add to this row.
				var col = -1;
				for (j in table[row]) {
					var jp = getWordExtents(table[row][j]);
					if (jp.x > p.x) {
						col = j;
						table[row].splice(j, 0, wa[i]);
						break;
					}
				}
				if (col < 0) {
					table[row].push(wa[i]);
				}
			} else {
				// add a new row.
				row = -1;
				for (j in table) {
					var jp = getWordExtents(table[j][0]);
					if (jp.y > p.y) {
						row = j;
						table.splice(j, 0, new Array(wa[i]));
						break;
					}
				}
				if (row < 0) {
					table.push(new Array(wa[i]));
				}
			}

		}

		var rows = new Array();
		for (i in table) {
			var s = map(table[i], function(e) { return e.text(); }).join(' ');
			rows.push(s);
		}

		var out = rows.join(' / ');
		return out;

	} else {
		$('#whiteboard').empty();
		return '';
	}
}

function wipeDanglingWords() {
	$('.word').each(function() {
		var wex = getWordExtents($(this));
		if (wex.x < theFridgeExtents.x || wex.y < theFridgeExtents.y
				|| wex.x > theFridgeExtents.maxX || wex.y > theFridgeExtents.maxY) {
			resetWord($(this));
		}
	});
}

/* ======================= */

function resetWord(elem) {
	elem.css('background-color', COLOR_FIXED);
	elem.css('position', 'static');
	elem.css('top', '0');
	elem.css('left', '0');
}

function resetWords() {
	$('.word').each(function() {
		resetWord($(this));
	});
	refresh();
}

function resortWords() {
	theCorpus.sort();
	mungeCorpus();
	resetWords();
}

function reshuffleWords() {
	shuffleCorpus();
	mungeCorpus();
	resetWords();
}

function mungeWord(s) {
	var out = s;
	//if (startsWith(out, '@'))
		//out = '';
	if (startsWith(out, 'http:'))
		out = '';
	out = trimPunctuation(out);
	out = out.toLowerCase();
	return out;
}

function mungeLine(ins) {
	var s = unescape(ins);
	s = s.replace(/\s+/g, ' ');
	var a = s.split(' ');
	for (i in a) {
		var word = mungeWord(a[i]);
		if (word.length > 0)
			pushNoRepeat(theCorpus, word);
	}
}

function makeWord(s) {
	var e = $(document.createElement('div'));
	e.attr('class', 'word');
	e.empty().append(s);
	resetWord(e);
	magnetize(e);
	$('#words').append(e);
	return e;
}

function mungeCorpus() {
	$('#words').empty();
	for (w in theCorpus) {
		makeWord(theCorpus[w]);
	}
}

function shuffleCorpus() {
	for (var i=0; i<20; i++) {
		for (var j=0; j<theCorpus.length; j++) {
			var r = Math.floor(Math.random() * theCorpus.length);
			var t = theCorpus[r];
			theCorpus[r] = theCorpus[j];
			theCorpus[j] = t;
		}
	}
}

/* ======================= */

function moveTo(elem, x, y) {
	elem.css('position', 'absolute');
	elem.css('top', '' + y + 'px');
	elem.css('left', '' + x + 'px');
}
function moveToFridgeRelative(elem, fx, fy) {
	var x = fx + theFridgeExtents.x;
	var y = fy + theFridgeExtents.y;
	moveTo(elem, x, y);
}
function pinToPointer(elem, evt) {
	moveTo(elem, evt.pageX-10, evt.pageY-10);
}

function onWordMouseDrag(evt) {
	if (theDragee) {
		pinToPointer(theDragee, evt);
	}
}

function onWordMouseDown(evt) {
	$(this).css('background-color', COLOR_SELECTED);
	pinToPointer($(this), evt);
	theDragee = $(this);
};

function onWordMouseUp(evt) {
	$(this).css('background-color', COLOR_PLACED);
	theDragee = null;
	refresh();
};

function onWindowResize(evt) {
	refresh();
}

function magnetize(elem) {
	elem.mousedown(onWordMouseDown);
	elem.mouseup(onWordMouseUp);
	elem.disableTextSelect();
}

// Magnetize any existing words, because it's more fun.
function preMagnetize() {
	$('.word').each(function() {
		magnetize($(this));
	});
}

function getWordExtents(w) {
	var out = new Object();
	out.x = w.offset().left;
	out.y = w.offset().top;
	out.width = w.width();
	out.height = w.height();
	out.fridgeX = out.x - theFridgeExtents.x;
	out.fridgeY = out.y - theFridgeExtents.y;
	return out;
}

function updateFridgeExtents() {
	theFridgeExtents.x = $('#fridge').offset().left;
	theFridgeExtents.y = $('#fridge').offset().top;
	theFridgeExtents.width = $('#fridge').width();
	theFridgeExtents.height = $('#fridge').height();
	theFridgeExtents.maxX = theFridgeExtents.x + theFridgeExtents.width;
	theFridgeExtents.maxY = theFridgeExtents.y + theFridgeExtents.height;
	return theFridgeExtents;
}

/* ======================= */

function onUsernameKey(evt) {
	if (evt.which == 13)
		getUserTweets();
}

function dumpFridgeExtents() {
	$('#whiteboard').append('<br/><br/> '
			+ 'fridge: (' + theFridgeExtents.x + ',' + theFridgeExtents.y + ') '
			+ '(' + theFridgeExtents.width + ',' + theFridgeExtents.height + ') '
			);
}

function log(e) {
	if (theDebug) {
		$('#debug').append(' ' + e);
	}
}

function doHelp() {
	alert(HELP_MESSAGES.join('\n\n'));
}

function fridgeHelp() {
	for (i in HELP_MESSAGES)
		$('#whiteboard').append('<p>' + HELP_MESSAGES[i] + '</p>');
}

/* ======================= */
/* Boring utility functions. */

function trimPunctuation(s) {
	var out = s;
	out = out.replace(/&(#?\w+);/g, 'ENTITY$1YTITNE');
	out = out.replace(/^\W+/, '');
	out = out.replace(/\W+$/, '');
	out = out.replace(/ENTITY(#?\w+)YTITNE/, '&$1;');
	return out;
}

function startsWith(s, t) {
	var u = s.substr(0, t.length);
	return (u == t);
}

function pushNoRepeat(a, s) {
	var found = false;
	for (i in a) {
		if (a[i] == s)
			found = true;
	}
	if (!found)
		a.push(s);
}

function d2h(d, pad) {
	var dn = Math.floor(new Number(d));
	var out = '';
	while (dn > 0) {
		var res = Math.floor(dn/16);
		var rem = (dn/16) - res;
		var hexdigit = HEXDIGITS[rem * 16];
		out = '' + hexdigit + out;
		dn = res;
	}
	while (out.length < pad) {
		out = '0' + out;
	}
	return out;
}

function h2d(h) {
	var out = 0;
	var a = h.split('');
	var power = 1;
	for (var i=a.length-1; i>=0; i--) {
		var ai = -1;
		for (j in HEXDIGITS) {
			if (HEXDIGITS[j] == a[i].toUpperCase())
				ai = j;
		}
		if (ai > -1) {
			out += ai * power;
		} else {
			log('h2d: could not find bad digit [' + a[i] + ']');
		}
		power *= 16;
	}
	return out;
}

function map(a, f) {
	var out = new Array();
	for (i in a) {
		out.push(f(a[i]));
	}
	return out;
}

