hyphenate = (function() {
	var MAX_LINES = 30;
	var CHAR_WIDTH = {
		" ": 0.795, "!": 0.783, '"': 1.200, "%": 2.550, "&": 1.878, "'": 0.795,
		"(": 0.786, ")": 0.786, "*": 1.179, ",": 0.795, "-": 0.951, ".": 0.795,
		":": 0.795, ";": 0.795, "?": 1.179, "@": 2.334,
		"A": 1.848, "B": 1.584, "C": 1.755, "D": 1.911, "E": 1.398, "F": 1.305,
		"G": 1.986, "H": 1.914, "I": 0.708, "J": 0.912, "K": 1.695, "L": 1.293,
		"M": 2.553, "N": 1.974, "O": 2.076, "P": 1.446, "Q": 2.076, "R": 1.575,
		"S": 1.395, "T": 1.413, "U": 1.914, "V": 1.740, "W": 2.712, "X": 1.731,
		"Y": 1.686, "Z": 1.425,
		"[": 0.780, "\\": .849, "]": 0.780, "^": 0.618, "_": 1.500, "`": 0.618,
		"a": 1.434, "b": 1.587, "c": 1.302, "d": 1.587, "e": 1.437, "f": 0.909,
		"g": 1.587, "h": 1.578, "i": 0.618, "j": 0.636, "k": 1.362, "l": 0.618,
		"m": 2.376, "n": 1.578, "o": 1.620, "p": 1.587, "q": 1.587, "r": 0.936,
		"s": 1.119, "t": 0.942, "u": 1.566, "v": 1.278, "w": 2.250, "x": 1.287,
		"y": 1.281, "z": 1.221,
		"{": 0.783, "|": 0.651, "}": 0.783, "~": 0.618,
		"¡": 0.783, "©": 2.388, "«": 1.263, "®": 2.388, "¯": 0.618, "°": 0.618,
		"»": 1.263, "¿": 1.179,
		"À": 1.848, "Á": 1.848, "Â": 1.848, "Ã": 1.848, "Ä": 1.848, "Å": 1.848,
		"Æ": 1.848, "Ç": 1.755, "È": 1.398, "É": 1.398, "Ê": 1.398, "Ë": 1.398,
		"Ì": 0.708, "Í": 0.708, "Î": 0.708, "Ï": 0.708, "Ð": 1.911, "Ñ": 1.974,
		"Ò": 2.076, "Ó": 2.076, "Ô": 2.076, "Õ": 2.076, "Ö": 2.076, "Ø": 2.076,
		"Ù": 1.914, "Ú": 1.914, "Û": 1.914, "Ü": 1.914, "Ý": 1.686, "Þ": 1.446,
		"ß": 1.599, "à": 1.434, "á": 1.437, "â": 1.434, "ã": 1.434, "ä": 1.434,
		"å": 1.434, "æ": 2.328, "ç": 1.302, "è": 1.437, "é": 1.437, "ê": 1.437,
		"ë": 1.437, "ì": 0.618, "í": 0.618, "î": 0.618, "ï": 0.618, "ð": 1.620,
		"ñ": 1.578, "ò": 1.620, "ó": 1.620, "ô": 1.620, "õ": 1.620, "ö": 1.620,
		"ø": 1.620, "ù": 1.566, "ú": 1.566, "û": 1.566, "ü": 1.566, "ý": 1.281,
		"þ": 1.587, "ÿ": 1.281
	};
	var DEFAULT_CHAR_WIDTH = 1.59;

	var CLEANUP_RE = / {2,}|\t/g;
	var WORD_RE = /^([^a-zäöüß]*)([a-zäöüß]{4,})([^a-zäöüß]*)$/i;
	var PARTS_RE = /[\u2027\u2013\u2014]/;
	var STRIP_RE = /^\s*(.*?)\s*$/g;
	var TRAILER = "                                                                .";

	var wordCache = {};

	function calculateWidth(string) {
		var rv = 0;
		for (var i in string.split("")) {
			rv += CHAR_WIDTH[string.charAt(i)] || DEFAULT_CHAR_WIDTH;
		}
		return rv;
	}

	function doUpdate(inputText, maxWidth, stripNewlines, callback) {
		if (stripNewlines) inputText = inputText.replace(/\r?\n/g, " ");
		inputText = inputText.replace(CLEANUP_RE, " ");
		if (!inputText.length) return 0;
		var inputLines = inputText.split(/\n/);

		function nextLine(inputLineNumber, totalLineCount) {
			if (inputLineNumber >= inputLines.length) { // done with all lines
				return;
			}
			hyphenateText(inputLineNumber, totalLineCount, inputLines[inputLineNumber]);
		}
		nextLine(0, 0);

		function hyphenateText(inputLineNumber, totalLineCount, inputLine) {
			var words = inputLine.split(" ");
			var lines = [""];

			function hyphenateWord(idx, word) {
				if (!wordCache[word]) {
					$.get("/hyph/" + encodeURIComponent(word[2]) + "/", function(data) {
						var parts = data.split(PARTS_RE);
						parts[0] = word[1] + parts[0];
						parts[parts.length - 1] = parts[parts.length - 1] + word[3];
						wordCache[word] = parts;
						append(idx, word[0], parts);
					});
				} else {
					append(idx, word[0], wordCache[word]);
				}
			}

			function nextWord(idx) {
				if (idx >= words.length) { // done with all words
					addLine(false, true);
					return;
				}
				var word = words[idx];
				if (word.length > 0) {
					append(idx, word);
				} else {
					nextWord(idx + 1);
				}
			}
			nextWord(0);

			function append(idx, inputText, parts, breakline, hyphen) {
				if (breakline === undefined) breakline = true;
				if (hyphen === undefined) hyphen = false;
				if (!inputText) return true;

				// store the previous content of the current line, so that it can
				// be restored if the additional text does not fit on the line
				var lineNo = lines.length - 1;
				var prevLine = lines[lineNo];
				var newLine = lines[lineNo] += (prevLine.length ? " " : "") + inputText;

				if (calculateWidth(newLine + (hyphen ? "-" : "")) > maxWidth - (lineNo > 0 ? 0 : 3)) {
					// restore the previous content of the current line
					lines[lineNo] = prevLine;
					if (!breakline) return false;

					// if we have hyphenation hints for the string, use them
					if (parts && parts.length > 1) {
						var pos = parts.length;
						while (pos-- > 1) {
							var frag = parts.slice(0, pos).join("");
							if (append(idx, frag, parts, false, true)) {
								addLine(true);
								var rest = parts.slice(pos, parts.length);
								append(idx, rest.join(""), rest);
								return true;
							}
						}
						addLine();
						return append(idx, inputText, parts);
					} else if (parts == null && inputText.match(WORD_RE)) {
						hyphenateWord(idx, inputText.match(WORD_RE));
					} else if (!prevLine.length) { // very long word
						if (lineNo > 0) lines.pop();
						return append(idx, inputText, inputText.split(""));
					} else {
						addLine();
						return append(idx, inputText);
					}
					return true;
				} else if (breakline) {
					nextWord(idx + 1);
				}
				return true;
			}

			function addLine(hyphen, isLast) {
				fillLine(lines.length - 1, hyphen, isLast);
				lines.push("");
				if (isLast) nextLine(inputLineNumber + 1, totalLineCount + lines.length - 1);
			}

			function fillLine(lineNo, hyphen, isLast) {
				callback(lines[lineNo].replace(STRIP_RE, "$1"), totalLineCount + lineNo,
				         hyphen, isLast ? inputLineNumber == inputLines.length - 1 : false);
			}
		}
	}

	return doUpdate;
})();
