/* ANSI Console Library
 * Copyright 2005, Steve Blinch
 *
 * Part of the BBS Web Terminal
 * http://bbs.blitzaffe.com
 *
 * Not licensed for redistribution
 */

NS4 = (document.layers) ? 1 : 0;
IE4 = (document.all) ? 1 : 0;
W3C = (document.getElementById) ? 1 : 0;

var entcount = 0;

// overwrites a string with 'newstring' at position 'startpos'
String.prototype.replacechars = function(startpos,newstring) {
	
	/*
	var s = this;
	var regex = new RegExp('^(.{'+startpos+'}).{0,'+newstring.length+'}(.*)$');
	while (s.length<startpos) s += ' ';
	return s.replace(regex,"$1"+newstring+"$2");
	*/
	
	var thislen = this.length;
	
	if (startpos+newstring.length>thislen) newstring = newstring.substring(0,thislen-startpos);
	var trailerpos = startpos+newstring.length;
	return this.substring(0,startpos) + newstring + this.substring(trailerpos,thislen);
}

/*
function isArray(a) {
    return (typeof(a)=='object') && (a.constructor == Array);
}

Array.prototype.copyFrom = function(src) {
	for (var i=0; i<src.length; i++) {
		if (isArray(src[i])) {
			this[i] = new Array();
			this[i].copyFrom(src[i]);
		} else {
			this[i] = src[i];
		}
	}
}
*/

// constructor
function ANSIConsole(elementid) {
	this.lineelements = new Array();

	this.linetext = new Array();
	this.linestyles = new Array();
	this.lineentities = new Array();
	this.currentclass = 'reset';
	
	this.translatecharset = false;
	
	this.cols = 80;
	this.rows = 24;
	this.x = 0;
	this.y = 0;
	
	this.currentintensity = 0;
	this.currentforeground = 37;
	this.currentbackground = 40;
	
	this.rowheight = 14;
	
	this.entityregex = /(&#[0-9]{1,5};)/;

	this.console = document.getElementById(elementid);
	if (!this.console) {
		alert("Error: Cannot locate console element: "+elementid);
	} else {
		this.build();
//		this.test();
	}
}

ANSIConsole.prototype.benchmark = function(bm_begin) {
	if (bm_begin) {
		this.starttime = new Date().getTime();
	} else {
		endtime = new Date().getTime();
		this.benchmarktime = endtime - this.starttime;
	}
}

/*

// clunky as hell, but it does the job
ANSIConsole.prototype.snapshot_save = function() {
	this.savelineelements = new Array();
	this.savelineelements.copyFrom(this.lineelements);

	this.savelinetext = new Array();
	this.savelinetext.copyFrom(this.linetext);

	this.savelinestyles = new Array();
	this.savelinestyles.copyFrom(this.linestyles);

	this.savelineentities = new Array();
	this.savelineentities.copyFrom(this.lineentities);
	
	this.savecurrentclass = this.currentclass;
	this.savetranslatecharset = this.translatecharset;
	this.savex = this.x;
	this.savey = this.y;

	this.savecurrentintensity = this.currentintensity;
	this.savecurrentforeground = this.currentforeground;
	this.savecurrentbackground = this.currentbackground;
}

ANSIConsole.prototype.snapshot_restore = function() {
	this.lineelements = new Array();
	this.lineelements.copyFrom(this.savelineelements);
	this.savelineelements = null;

	this.linetext = new Array();
	this.linetext.copyFrom(this.savelinetext);
	this.savelinetext = null;

	this.linestyles = new Array();
	this.linestyles.copyFrom(this.savelinestyles);
	this.savelinestyles = null;

	this.lineentities = new Array();
	this.lineentities.copyFrom(this.savelineentities);
	this.savelineentities = null;
	
	this.currentclass = this.savecurrentclass;
	this.translatecharset = this.savetranslatecharset;
	this.x = this.savex;
	this.y = this.savey;

	this.currentintensity = this.savecurrentintensity;
	this.currentforeground = this.savecurrentforeground;
	this.currentbackground = this.savecurrentbackground;
	
	this.redraw();
}
*/

ANSIConsole.prototype.redraw = function() {
	for (var i=0; i<this.rows; i++) this._draw_row(i);
}

ANSIConsole.prototype.get_element_width = function(el) {
	/*if (el.style.width) return el.style.width; // commented to avoid percentage widths
	else */if (el.style.pixelWidth) return el.style.pixelWidth;
	else if (el.offsetWidth) return el.offsetWidth;
	else if (document.defaultView && document.defaultView.getComputedStyle) {
		w = document.defaultView.getComputedStyle(el,'').getPropertyValue('width');
		return w.replace(/px/,'');
    }
}

ANSIConsole.prototype.get_element_height = function(el) {
	if (el.style.Height) return el.style.Height;
	else if (el.style.pixelHeight) return el.style.pixelHeight;
	else if (el.offsetHeight) return el.offsetHeight;
	else if (document.defaultView && document.defaultView.getComputedStyle) {
		w = document.defaultView.getComputedStyle(el,'').getPropertyValue('height');
		return w.replace(/px/,'');
    }
}

ANSIConsole.prototype.get_element_top = function(el) {
	w = '0';
	if (el.style.top) w = el.style.top;
	else if (el.style.pixelTop) w = el.style.pixelTop;
	else if (el.offsetTop) w = el.offsetTop;
	else if (document.defaultView && document.defaultView.getComputedStyle) {
		w = document.defaultView.getComputedStyle(el,'').getPropertyValue('top');
    }
    if (w.replace) {
    	return w.replace(/px/,'');
    } else {
    	return w;
    }
}

ANSIConsole.prototype.get_element_left = function(el) {
	w = '0';
	/* if (el.style.left) return el.style.left;
	else */ if (el.style.pixelLeft) w = el.style.pixelLeft;
	else if (el.offsetLeft) w = el.offsetLeft;
	else if (document.defaultView && document.defaultView.getComputedStyle) {
		w = document.defaultView.getComputedStyle(el,'').getPropertyValue('left');
    }
    if (w.replace) {
    	return w.replace(/px/,'');
    } else {
    	return w;
    }
}



// clear a row in the console buffer
ANSIConsole.prototype._clear_row = function(y) {
	this.linetext[y] = '';
	this.lineentities[y] = new Array();
	for (var x=0; x<this.cols; x++) {
		this.linetext[y] += ' ';
		this.linestyles[y][x] = this.currentclass;
		this.lineentities[y][x] = '';
	}
}

// draw a row from the console buffer to the browser
ANSIConsole.prototype._draw_row = function(y) {
	if (this.buffering) {
		this.invalidated[y] = true;
		return true;
	}
	
	var res = "";
	var currentclass = "";
	var c = "";
	var openspan = false;
	
	for (var x=0; x<this.cols; x++) {
		
		if (this.linestyles[y][x]!=currentclass) {
			currentclass = this.linestyles[y][x];
			if (openspan) res += "</span>";
			res += "<span class='"+currentclass+"'>";
			openspan = true;
		}
		//c = this.linetext[y][x];

		/*if ( (!this.linetext[y]) || (!this.linetext[y].charAt) ) {
			alert('offscreen: '+y);
			return;
		}
		*/
		c = IE4 ? this.linetext[y].charAt(x) : this.linetext[y][x];
		switch(c) {
			case "&":
				if (this.translatecharset) c = this.lineentities[y][x];
				break;
			case " ": c = "&nbsp;"; break;
			case "<": c = "&lt;"; break;
			case ">": c = "&gt;"; break;
		}
		res += c;
	}
	if (openspan) res += "</span>";
	//alert(res);
	this.lineelements[y].innerHTML = res;
}

ANSIConsole.prototype._cursor_down = function(lines) {
	if (this.y+lines<this.rows) {
		this.y += lines;
	} else {
		lines = ( (this.y+1) + lines) - this.rows;
		this.y = this.rows-1;
		
		for (var i = 0; i<lines; i++) this._scroll_down();
	}
}

ANSIConsole.prototype._scroll_down = function() {
	for (var y=1; y<this.rows; y++) {
		for (var x=0; x<this.cols; x++) {
			this.linestyles[y-1][x] = this.linestyles[y][x];
		}
		if (this.translatecharset) this.lineentities[y-1] = this.lineentities[y];
		this.linetext[y-1] = this.linetext[y];
		this._draw_row(y-1);
	}
	this._clear_row(this.rows-1);
	this._draw_row(this.rows-1);
}

ANSIConsole.prototype._ansicolor = function(s) {
	if (s=='0') {
		this.currentintensity = 0;
		this.currentforeground = 37;
		this.currentbackground = 40;
	} else if (s=='1') {
		this.currentintensity = 1;
	} else if (s=='0;1') {
		this.currentintensity = 1;
		this.currentforeground = 37;
		this.currentbackground = 40;
	} else {
		var pieces = s.split(/;/);
		
		for (var i=0; i<pieces.length; i++) {
			pi = parseInt(pieces[i]);
			if (pi>=40) {
				this.currentbackground = pi;
			} else if (pi>=30) {
				this.currentforeground = pi;
			} else {
				this.currentintensity = pi;
				if (this.currentintensity==0) this.currentbackground = 40;
			}
		}
	}
	
	this.currentclass = 'ci'+this.currentintensity+'f'+this.currentforeground+'b'+this.currentbackground;	
}

ANSIConsole.prototype.clear = function() {
	for (var y=0; y<this.rows; y++) {
		this._clear_row(y);
		this._draw_row(y);
	}
	this.x = 0;
	this.y = 0;
}

ANSIConsole.prototype._clear_eol = function() {
	var y = this.y;
	this.linetext[y] = this.linetext[y].substring(0,this.x);
	for (var x=this.x; x<this.cols; x++) {
		this.linetext[y] += ' ';
		this.linestyles[y][x] = this.currentclass;
		if (this.lineentities[y][x]) this.lineentities[y][x] = '';
	}
	this._draw_row(y);
}

// not all of ANSI X3.64 is implemented here; just the most common stuff
// (eg: only the codes that were actually in common use by BBS software)
ANSIConsole.prototype._ansiparse = function(s) {
	var code = s.substring(s.length-1,s.length);
	var s = s.substring(1,s.length-1);
	switch(code) {
		case 'm':
			// color
			this._ansicolor(s);
			break;
		case 's':
			// save position
			this.savedx = this.x;
			this.savedy = this.y;
			break;
		case 'J':
			// clear screen;
			this.clear();			
			break;
		case 'u':
			// restore position
			this.x = this.savedx;
			this.y = this.savedy;
			break;
		case 'B':
			// move cursor down
			var n = Math.min(s.length ? parseInt(s) : 1,this.rows-this.y);

			this._cursor_down(n);
			break;
		case 'f':
		case 'H':
			// reposition cursor
			var pieces = s.split(/;/);
			if (pieces.length==2) {
				this.gotoxy(parseInt(pieces[1]),parseInt(pieces[0]));
			}
			break;
		case 'C':
			// move cursor right
			var n = Math.min(s.length ? parseInt(s) : 1,this.cols-this.x);
			this.x += n;

//			n += this.x;
//			this.x = (n % this.cols);
//			this._cursor_down(Math.floor(n / this.cols));
			
			break;
		case 'A':
			// move cursor up
			var n = Math.min(s.length ? parseInt(s) : 1,this.y);
			this.y -= n;
			break;
		case 'D':
			// move cursor left
			var n = Math.min(s.length ? parseInt(s) : 1,this.x);
			this.x -= n;
			
//			this.x = (Math.abs(n) % this.cols);
//			this.y -= Math.floor(Math.abs(n) / this.cols);
			break;
		case 'G':
			var n = s.length ? parseInt(s) : 0;
			this.x = n;
			break;
		case 'K':
			// erase to the end of the line
			this._clear_eol();
			break;
	}
}

// build the interface
ANSIConsole.prototype.build = function() {
	var console_top = this.get_element_top(this.console);
	var console_left = this.get_element_left(this.console);
	
	for (var y=0; y<this.rows; y++) {
		this.linestyles[y] = new Array();
		this.lineentities[y] = new Array();
		//for (var x=0; x<this.cols; x++) this.linestyles[x] = '';
		
		var el = document.createElement('div');
		el.style.position = 'relative';
		el.id = 'cs'+y;
		/*
		el.style.top = (console_top + (this.rowheight - (IE4 ? 1 : 0) ) * y) + 'px';
		el.style.left = console_left + 'px';
		*/

//		el.style.top = (this.rowheight * y) + 'px';
		el.style.top = '0px';


		el.style.height = this.rowheight + 'px';
		el.style.left = '0px';


		el.style.width = '640px';
		el.style.backgroundColor = 'black';
		el.className = 'reset';
		el.innerHTML = '';
		this.lineelements[y] = el;
		this.console.appendChild(el);
		
		this._clear_row(y);
		this._draw_row(y);
	}
	this.console.style.width = this.get_element_width(el)+'px';
	this.console.style.height = (this.rowheight * this.rows )+'px';
//	alert(this.console.style);
}

ANSIConsole.prototype.buffer = function(dobuffer) {
	this.buffering = dobuffer;
	if (!this.buffering) {
		for (var y in this.invalidated) this._draw_row(y);
	}
	this.invalidated = new Array();
}

ANSIConsole.prototype.gotoxy = function(x,y) {
	if (y>this.rows) y = this.rows;
	if (x>this.cols) x = this.cols;
	if (y<1) y = 1;
	if (x<1) x = 1;
	
	this.x = x-1;
	this.y = y-1;
}

// gets the length of the string, while accounting for HTML entities (eg: &amp; = 1 character)
String.prototype.charlength = function() {
	var s = this.replace(/&#([0-9]{1,5});/g,'x');
	return s.length;
}

// returns a substring of the string from pstart to pend, while accounting for HTML entities (eg: &amp; = 1 character)
String.prototype.charsubstring = function(pstart,pend) {

	var i = 0;
	var c = '';
	var charpos = 0;
	
	var copybuf = "";
	var copylen = 0;
	
	while (i<this.length) {
		c = this.charAt(i);
		
		if (c=='&') {
			var teststr = this.substring(i,i+7);
			var testmatch = this.entityregex.exec(teststr);
			if (testmatch) {
				var entity = testmatch[1];
				
				c = entity;
				i += c.length - 1;
			}
		}
		if ( (charpos>=pstart) && (charpos<=pend) ){
			copylen++;
			copybuf += c;
		}
		charpos++;
		
		i++;
	}
	
	return copybuf;
}

// display a string in the console at the current position in the current color
ANSIConsole.prototype.display = function(s) {
	var i = 0;
	
	var slength = s.length;
	var outputtxt = "";
	
	var insertx = this.x;

	while (i<slength) {
//		if (!this.linestyles[this.y]) alert('failed: '+this.y);
		this.linestyles[this.y][this.x] = this.currentclass;
//		c = IE4 ? s.charAt(i) : s[i];
		c = s.charAt(i);
		
		if ( (this.translatecharset) && (c=='&') ) {
			var teststr = s.substring(i,i+7);
			var testmatch = this.entityregex.exec(teststr);
			if (testmatch) {
				var entity = testmatch[1];
				i += entity.length - 1;
				
				this.lineentities[this.y][this.x] = entity;
			} else {
				this.lineentities[this.y][this.x] = '&';
			}
		}
		
		
		outputtxt += c;

		if ( (this.x+1>this.cols-1) || (i==slength-1) ) {
			this.linetext[this.y] = this.linetext[this.y].replacechars(insertx,outputtxt);
			insertx = this.x;
			outputtxt = "";
		}
		this.x++;
		i++;
		
		
		
		if (this.x>this.cols-1) {
			this.x = 0;
			insertx = 0;
			outputtxt = "";
			
			this._draw_row(this.y);
			if (this.y<this.rows-1) {
				this.y++;
			} else {
				// if we're at the bottom of the screen, scroll one line
				this._scroll_down();
			}			
		}

	}
	this._draw_row(this.y);
	return true;
}

// display an ANSI-formatted string
ANSIConsole.prototype.write = function(s,cr) {
	var pieceregex = /\[[0-9\;]{0,12}[CDJmuHsBfKAG]/;
	var pieces = s.split(/\x1B/);
	for (var i=0; i<pieces.length; i++) {
		if (!pieces[i].length) continue;
		
		if (pieces[i].charAt(0)=='[') {
			var ansicode = pieces[i].match(pieceregex);
			isansi = (ansicode!=null);
		} else {
			isansi = false;
		}
		if (isansi) {
			var s = pieces[i].substring(ansicode[0].length,pieces[i].length);
			this._ansiparse(ansicode[0]);
		} else {
			s = pieces[i];
		}
		
		this.display(s);
	}
	
	if (cr) {
		this.x = 0;
		
		if (this.y<this.rows-1) {
			this.y++;
		} else {
			// if we're at the bottom of the screen, scroll one line
			this._scroll_down();
		}			
	}
}

ANSIConsole.prototype.test = function() {
	
	for (var i=1; i<=24; i++) {
		this.gotoxy(1,i);
		this.display(i.toString());
	}

	this.gotoxy(10,10);
	this.write("\1B[1;34mtesting \1B[2J\1B[1;32mtesty \1B[1;34mtest...\1B[0m test!");

	this.gotoxy(10,11);
	this.write("\1B[1;32mtesting \1B[1;34mhey-hey\1B[0m");

	this.gotoxy(15,15);
	this.write("\1B[1;34;44manother test \1B[5;5Hstill testing\1B[s");

	this.gotoxy(25,23);
	this.write("wrap \1B[5C\1B[3Dthis \1B[0m\1B[utesty test");
	
	this.gotoxy(50,17);
	this.write("Test.\1B[10D\1B[K");

}