// Guybrush talker
// Kevin Vance 2009-09-19
// http://guybrush.kvance.com/


//
// Constants
//

var TALK_SPEED = 100; // milliseconds

var BASE_TEXT_Y = 252; // pixels, Plot the bottom line of text here.
var TEXT_HEIGHT = 30;  // pixels

var TALK_STATE_MOUTH_CLOSED_NORMAL = 0;	// Talking head state enumeration
var TALK_STATE_MOUTH_OPENED_NORMAL = 1;
var TALK_STATE_MOUTH_CLOSED_UP     = 2;
var TALK_STATE_MOUTH_OPENED_UP     = 3;
var TALK_STATE_MOUTH_CLOSED_DOWN   = 4;
var TALK_STATE_MOUTH_OPENED_DOWN   = 5;
var TALK_STATE_NONE                = 6;


//
// Global variables
//

var backgroundDiv = null;	// Hardcoded div elements
var screenDiv = null;
var headDiv = null;
var sigDiv = null;

var headCenter = null;		// The center of Guybrush's head on the x-axis
var wrapLength = null;		// How many pixels before a word wraps
var lastWrapLength = null;	// The wrap length from the last resize event

var letters = [];			// Collection of all letter divs
var currentText = null;		// The currently printed text

var talkTimer = null;		// Talking head timer ID
var talkState = TALK_STATE_NONE;	// Current talking state
var nextUtterance = [];		// List of things to say


//
// Entry point
//

function guyStart()
{
	// Find hardcoded elements
	backgroundDiv = document.getElementById('background');
	screenDiv = document.getElementById('screen');
	headDiv = document.getElementById('head');
	sigDiv = document.getElementById('sig');

	// Set up screen position
	window.onresize = repositionScreen;
	repositionScreen();

	// Calculate the center of Guybrush's head.
	headCenter = Math.floor(headDiv.offsetLeft + headDiv.offsetWidth/2);

	// Start downloading the tweets.
	var req = new XMLHttpRequest();
	req.open('GET', 'http://guybrush.kvance.com/tweets.json', true);
	req.onreadystatechange = function()
	{
		if(req.readyState == 4) {
			if(req.status == 200) {
				loadTweets(req.responseText);
			} else {
				utter("HTTP Error " + req.status + ".");
				utter("I hope I'm not broken too badly.");
			}
		}
	}
	req.send(null);
}


//
// Tweet loader
//

function loadTweets(json)
{
	tweets = eval(json);
	for(var i = 0; i < tweets.length; i++) {
		utter(tweets[i]);
	}
}


//
// Talking animation
//

function startTalking()
{
	stopTalking();
	headDiv.style.backgroundPosition = "0px 0px";
	talkState = TALK_STATE_MOUTH_CLOSED_NORMAL;
	talkTimer = setInterval(talkTick, TALK_SPEED);
}

function stopTalking()
{
	if(talkTimer) {
		clearInterval(talkTimer);
		talkTimer = null;
		headDiv.style.backgroundPosition = "0px 0px";
		talkState = TALK_STATE_NONE;
	}
}

function talkTick()
{
	// Advance to the next talking state.
	switch(talkState) {
		case TALK_STATE_MOUTH_OPENED_NORMAL:
			// Pick a new closed state (0, 2, or 4).
			talkState = Math.floor(Math.random() * 3) * 2;
			break;

		case TALK_STATE_MOUTH_OPENED_UP:
		case TALK_STATE_MOUTH_OPENED_DOWN:
			talkState = TALK_STATE_MOUTH_CLOSED_NORMAL;
			break;

		case TALK_STATE_MOUTH_CLOSED_NORMAL:
			talkState = TALK_STATE_MOUTH_OPENED_NORMAL;
			break;

		case TALK_STATE_MOUTH_CLOSED_UP:
			talkState = TALK_STATE_MOUTH_OPENED_UP;
			break;

		case TALK_STATE_MOUTH_CLOSED_DOWN:
			talkState = TALK_STATE_MOUTH_OPENED_DOWN;
			break;
	}

	// Display the newly chosen state.
	var xOffset, yOffset;
	switch(talkState) {
		case TALK_STATE_MOUTH_CLOSED_NORMAL:
			xOffset = 0;
			yOffset = 0;
			break;
		case TALK_STATE_MOUTH_OPENED_NORMAL:
			xOffset = 33;
			yOffset = 0;
			break;
		case TALK_STATE_MOUTH_CLOSED_UP:
			xOffset = 0;
			yOffset = 33;
			break;
		case TALK_STATE_MOUTH_OPENED_UP:
			xOffset = 33;
			yOffset = 33;
			break;
		case TALK_STATE_MOUTH_CLOSED_DOWN:
			xOffset = 0;
			yOffset = 66;
			break;
		case TALK_STATE_MOUTH_OPENED_DOWN:
			xOffset = 33;
			yOffset = 66;
			break;
	}
	headDiv.style.backgroundPosition = xOffset + "px " + yOffset + "px";
}


//
// Character rendering
//

// Plot this character to these coordinates.
function putch(c, x, y)
{
	var info = getCharInfo(c);
	if(info) {
		var letr = document.createElement('div');
		letr.className = 'letter';
		letr.style.left  = x + 'px';
		letr.style.top   = y + 'px';
		letr.style.width = info.width + "px";
		letr.style.backgroundPosition = "-" + info.offset + "px 0px";
		screenDiv.appendChild(letr);
		letters.push(letr);
	}
}

// Print this string to these coordinates.
function puts(s, x, y)
{
	for(var i = 0; i < s.length; i++) {
		var c = s.charAt(i);
		if(c == ' ') {
			x += 9;
		} else {
			var info = getCharInfo(c);
			if(info) {
				putch(c, x, y);
				x += info.width - 3; // Allow outline overlap
			}
		}
	}
}

// Print this block of text, using word wrap.
function wrapAndPrint(text)
{
	var lines = [];
	var words = text.split(/ +/);
	var currentLine = words[0];

	for(var i = 1; i < words.length; i++) {
		// Add the next next space and word.
		var endingChar = currentLine.slice(-1);
		var space;
		if(endingChar == '.' || endingChar == '!' || endingChar == '?') {
			space = "  ";
		} else {
			space = " ";
		}
		var lineWithNextWord = currentLine + space + words[i];

		// See if it fits.
		if(stringWidth(lineWithNextWord) < wrapLength) {
			currentLine = lineWithNextWord;
		} else {
			lines.push(currentLine);
			currentLine = words[i];
		}
	}
	// Add the last line.
	lines.push(currentLine);

	// Calculate the top position.
	var lineHeight = TEXT_HEIGHT - 3; // Allow outline overlap
	var y = BASE_TEXT_Y - (lines.length-1) * lineHeight;

	// Print all the lines.
	for(var i = 0; i < lines.length; i++) {
		var line = lines[i];
		var width = stringWidth(line);
		var x = headCenter - width/2;
		puts(line, x, y);
		y += lineHeight;
	}

	currentText = text;
}


// Clear all text from the screen.
function clearText()
{
	for(var i = 0; i < letters.length; i++) {
		screenDiv.removeChild(letters[i]);
	}
	letters = [];
	currentText = null;
}

// Return the width in pixels of this string.
function stringWidth(s)
{
	var total = 0;

	for(var i = 0; i < s.length; i++) {
		var c = s.charAt(i);
		var info = getCharInfo(c);
		if(info)
			total += info.width - 3; // Allow outline overlap
	}

	return total;
}

// Return offset and width info for this character.
function getCharInfo(c)
{
	// We don't have square brackets.  Try angled ones!
	if(c == '[')      { c = "<"; }
	else if(c == ']') { c = ">"; }

	var code = c.charCodeAt(0);
	var x1 = charset[code];
	var x2 = charset[code+1];
	if(typeof(x1) == 'undefined' || typeof(x2) == 'undefined') {
		return null;
	} else {
		width = x2 - x1;
	}
	return {offset: x1, width: width};
}


//
// Utterance management
//

function utter(text)
{
	nextUtterance.push(text);

	if(talkState == TALK_STATE_NONE) {
		startTalking();
		timeToSpeak();
	}
}

function timeToSpeak()
{
	clearText();

	var text = nextUtterance.shift();
	if(text) {
		wrapAndPrint(text);
		// Calculate the delay based on the text length.
		var delay = 2000 + text.length / 60 * 2000;
		setTimeout(timeToSpeak, delay);
	} else {
		stopTalking();
	}
}


//
// Screen manipulation
//

function repositionScreen()
{
	var viewport = getViewportSize();

	// Center the screen on the x-axis.
	var xDiff = viewport.width - backgroundDiv.offsetWidth;
	backgroundDiv.style.left = Math.floor(xDiff / 2) + "px";

	// Make the sig visible if offscreen.  Update the word wrap length.
	var fullWrap = backgroundDiv.offsetWidth - 189;
	if(xDiff > 0) {
		sigDiv.style.left = "10px";
		wrapLength = fullWrap;
	} else {
		sigDiv.style.left = (10 - xDiff / 2) + "px";
		wrapLength = Math.min(fullWrap, viewport.width - 99);
	}

	// If the wrap length changed, reprint the current text.
	if(currentText && lastWrapLength && lastWrapLength != wrapLength) {
		var lastText = currentText;
		clearText();
		wrapAndPrint(lastText);
	}
	lastWrapLength = wrapLength;

	// Center the screen on the y-axis.  If too small, make the bottom visible.
	var yDiff = viewport.height - backgroundDiv.offsetHeight;
	if(yDiff > 0) {
		backgroundDiv.style.top = Math.floor(yDiff / 2) + "px";
	} else {
		backgroundDiv.style.top = yDiff + "px";
	}
}


//
// Helper functions
//


// Return the viewport size
// From: http://andylangton.co.uk/articles/javascript/get-viewport-size-javascript/
function getViewportSize()
{
	var width;
	var height;
	if(typeof window.innerWidth != 'undefined') {
		width = window.innerWidth,
		height = window.innerHeight
	} else if (typeof document.documentElement != 'undefined' &&
	           typeof document.documentElement.clientWidth != 'undefined' &&
	           document.documentElement.clientWidth != 0) {
		width = document.documentElement.clientWidth,
		height = document.documentElement.clientHeight
	} else {
		width = document.getElementsByTagName('body')[0].clientWidth,
		height = document.getElementsByTagName('body')[0].clientHeight
	}
	return {width: width, height: height}
}


