/* 
 * Name: FireTokenizer (JavaScript).
 * Author: Jonas Höglund (FireFly, firefly.nu).
 * Date: 2010-01-04.
 */

if (typeof(FF) == 'undefined') FF = {};

FF.TokenizerConstants = {
	IGNORE: {
		type: 'ignore',
		key: '*IGNORE'
	},
	STRING: {
		type: 'string',
		key: '*STRING'
	},
	UNKNOWN: {
		type: 'unknown',
		key: '*UNKNOWN'
	},
	ROOT: {
		type: 'container',
		recurse: true,
		key: '*ROOT'
	},
	
	
	createTextPrototype: function(key, text) {
		return {
			type: 'regex',
			key: key,
			matcher: text,
			match: function(str) {
				if (str.indexOf(text) == 0)
					return str.substr(0, text.length);
				else
					return null;
			}
		};
	},
	createRegexPrototype: function(key, regex) {
		return {
			type: 'regex',
			key: key,
			matcher: regex,
			match: function(str) {
				if (str.match(this.matcher))
					return str.substr(0, str.length - str.
							replace(this.matcher, '').length);
				else
					return null;
			}
		};
	},
	createContainerPrototype: function(key, start, end,
			recurse) {
		return {
			type: 'container',
			key: key,
			recurse: recurse,
			start: start,
			end: end,
			match: function(str) {
				if (str.indexOf(start) == 0) {
					var rec=1;
					
					for (var i=1; i<str.length && rec > 0; i++) {
						var sub = str.substring(i);
						
						if (sub.indexOf(end) == 0) {
							rec--;
							i += end.length - 1;
						} else if (sub.indexOf(start) == 0) {
							rec++;
							i += start.length - 1;
						}
					}
					
					if  (rec == 0) {
						return str.substr(start.length,
								i - end.length*2);
					} else {
						throw new Error("Parsing error: Unmatched " +
								"container: '" + start + "', '" +
								end + "'.");
					}
				} else {
					return null;
				}
			}
		};
	}
};

FF.Tokenizer = function() {
	this.protos = [];
	this.depth = 1;
	
	this.addTextPrototype = function(key, text) {
		this.protos.push(FF.TokenizerConstants.
				createTextPrototype(key, text));
	}
	this.addRegexPrototype = function(key, regex) {
		this.protos.push(FF.TokenizerConstants.
				createRegexPrototype(key, regex));
	}
	this.addContainerPrototype = function(key, start, end, recurse) {
		this.protos.push(FF.TokenizerConstants.
				createContainerPrototype(key, start, end, recurse));
	}
	
	this.setPrototypes = function(protos) {
		this.protos = protos;
	}
	
	this.createToken = function(proto, value, extras) {
		var desc = typeof(value) == 'object' ? '[...]' :
				"'" + value + "'";
		
		var out = {
			proto: proto,
			key: proto.key,
			value: value,
			toString: function() {
				return '[' + this.key + ']: ' +
						desc;
			}
		}
		
		if (typeof(extras) != 'undefined') {
			for (var attr in extras)
				out[attr] = extras[attr];
		}
		
		return out;
	}
	
	this.tokenize = function(str) {
		var out = [];
		
		var sub, match;
		var length = -1, unknownLength = 0;
		var token;
		
		for (var i=0; i<str.length; i++) {
			sub = str.substr(i);
			
			for (var j=0; j<this.protos.length; j++) {
				length = -1;
				
				match = this.protos[j].match(sub);
				if (match != null) {
					if (unknownLength > 0) {
						token = this.createToken(FF.TokenizerConstants.UNKNOWN,
								str.substr(i-unknownLength, i));
					}
					
					if (this.protos[j].type == 'container') {
						this.depth++;
						
						var subtokens;
						if (this.protos[j].recurse) {
							subtokens = this.tokenize(
									match).value;
						} else {
							subtokens = [this.createToken(
									FF.TokenizerConstants.STRING, match)];
						}
						
						this.depth--;
						token = this.createToken(this.protos[j],
								subtokens, {depth: this.depth,
								raw: this.protos[j].start +
								match + this.protos[j].end});
						
						length = match.length + this.protos[j].
								start.length + this.protos[j].
								end.length;
					} else {
						token = this.createToken(
								this.protos[j], match);
						length = match.length;
					}
					
					if (token.key != FF.TokenizerConstants.IGNORE)
						out.push(token);
					
					i += length - 1;
					last = i;
					break;
				}
			}
			
			if (length < 0)
				unknownLength++;
		}
		
		return this.createToken(FF.TokenizerConstants.ROOT, out);
	}
}

FF.Highlighter = {
	highlight: function(str, hler) {
		var tokenizer = new FF.Tokenizer();
		tokenizer.setPrototypes(hler.prototypes);
		
		var tk = tokenizer.tokenize(str);
		var out = '';
		
		for (var i=0; i<tk.value.length; i++)
			out += this.hlToken(tk.value[i], hler.format);
		
		return out;
	},
	
	hlToken: function(tk, format) {
		var style = '', out = '';
		
		if (typeof(format[tk.key]) != 'undefined') {
			for (var k in format[tk.key]) {
				var attr;
				
				if (k == 'weight' || k == 'style' || k == 'size')
					attr = 'font-' + k;
				else
					attr = k;
				
				style += attr + ':' + format[tk.key][k] + ';';
			}
		}
		
		if (tk.proto.type == 'container') {
			out += tk.proto.start;
			
			for (var i=0; i<tk.value.length; i++)
				out += this.hlToken(tk.value[i], format);
			
			out += tk.proto.end;
			out = this.style(out, style);
		} else {
			out += this.style(tk.value, style);
		}
		
		return out;
	},
	
	style: function(str, style) {
		return style == '' ? str : '<span style="' +
				style + '">' + str + '</span>';
	}
}