
// Formatter object helper.  Basically it's just a wrapper around
// a function generator to help create more than just basic values
// for the output functions.
XF = new Object();
XF.xform = function(key, val_xform_func) {
    return function(row) {
	var value = row[key];
	return val_xform_func(value);
    }
};
XF.xform_row = function(key, val_xform_func) {
    return function(row) {
	return val_xform_func(row);
    }
};

// ajax output function

OutputFuncs = new Object();

// set single select
OutputFuncs.setSelect = function(element, value) {
    //Logger.debug("setting " + value + " of " + $A($(element).options));
    var opts = $A($(element).options);
    var index = -1;
    opts.detect(function(option, i) {
	//Logger.debug('checking ' + option.value);
	if (option.value == value) {
	    index = i;
	    return true;
	} else
	    return false;
    });
    $(element).selectedIndex = index;
}

// replace a TD containing text (or nothing) with different text.
// because IE is crap!
OutputFuncs.replaceTdText = function(td, text) {
    td = $(td);
    while (td.firstChild)
	td.removeChild(td.firstChild);
    if (text) {
	td.appendChild(document.createTextNode(text));
    } else {
	td.appendChild(document.createTextNode(""));
    }
}

// Generic table output
// Give it a matching list of column names and the order from which to pull
// valued from the results list to match.
// Third argument is a row creation function; see genericRowCreator for an
// example and prototype for it.
// For speed, you can provide a 4th argument, a per-row html creation function,
// which will enable the use of innerHTML to draw the table.
OutputFuncs.tableOutputFunction = function(col_names, col_list, row_creator, row_html_creator) {
// outputFunction: function(element, results, status, criteria, sql) 
    // if we have html creator, prefer that.  otherwise use old row creator.
    if (!row_creator) {
	if (!row_html_creator)
	    row_html_creator = OutputFuncs.genericRowHTMLCreator;
    }
    return function(element, results, status, criteria, sql) {
	Logger.info("tableOutputFunction with " + results.length + " rows");
	$A($(element).childNodes).each(function (node, i) {
	    // Logger.debug("removing child node " + node.toString());
	    $(element).removeChild(node);
	});
	if (results.length <= 0)
	    return;
	
	if (row_html_creator) {
	    var start = new Date();
	    // do this with string adds cause it is _MUCH_ faster in IE
	    Logger.debug("using html creator");
	    var sa = new Array(results.length);
	    sa.push("<TABLE><THEAD><TR>");
		
	    for (var i = 0; i < col_names.length; i++) {
		sa.push("<TH>");
		sa.push(col_names[i]);
		sa.push("</TH>");
	    }
	    sa.push("</TR></THEAD>\n<TBODY>\n");
	    var local_ref_creator = row_html_creator;
	    var local_col_list = col_list;
	    var local_results = results;
	    var d1 = new Date();
	    for (var j = 0; j < results.length; j++) {
		if (j % 10 == 0)
		    Logger.debug("added " + j + " rows");
		// this seems backwards but think 1-based
		if ((j % 2) == 0) {
		    sa.push("<TR class=\"oddrow\">");
		} else {
		    sa.push("<TR class=\"evenrow\">");
		}
		sa.push(local_ref_creator(local_col_list, local_results[j]));
		//sa.push("<td> test </td>");
		sa.push("</TR>");
	    }
	    var d2 = new Date();
	    Logger.warn("took " + (d1.valueOf() - d2.valueOf()) + " ms"); 
	    sa.push("</TBODY></TABLE>\n");
	    var childNode = document.createElement("DIV");
	    $(element).appendChild(childNode);
	    childNode.innerHTML = sa.join('');
	    var end = new Date();
	    Logger.warn("took " + (end.valueOf() - start.valueOf()) + " ms"); 
	    
	} // end HTML creation
	else {
	    var table = document.createElement("TABLE");
	    $(element).appendChild(table);
	    var header = table.createTHead();
	    var hrow = header.insertRow(0);
	    for (var i = 0; i < col_names.length; i++) {
		var cell = hrow.insertCell($A(hrow.cells).length);
		cell.innerHTML = col_names[i];
	    }
	    var body = document.createElement("TBODY");
	    table.appendChild(body);
	    for (var j = 0; j < results.length; j++) {
		var brow = row_creator(body, body.rows.length, col_list, results[j]);
		// this seems backwards but think 1-based
		if ((j % 2) == 0) {
		    $(brow).classNames().add("oddrow");
		} else {
		    $(brow).classNames().add("evenrow");
		}
	    }
	}
    }
};

// Generic row output, called by tableOutputFunction.  
// table = HTML table object
// position = position to insert row
// col_list = list of field names, or field xform functions, to use (in order)
// data = data (duh)
// xform = transformer to apply to all cells (columns), optional -- defaults to identity
// oddrow and evenrow classes will be applied.
OutputFuncs.genericRowCreator = function(table, position, col_list, data, xform) {
    // handle identity transform
    if (!xform) {
	xform = function(x) { return x };
    }
    var brow = table.insertRow(position);
    for (var k = 0; k < col_list.length; k++) {
	var cell = brow.insertCell(brow.cells.length);
	if (typeof(col_list[k]) == "function") 
	    cell.innerHTML = xform(col_list[k](data));
	else 
	    cell.innerHTML = xform(data[col_list[k]]);
    }
    return brow;
}

OutputFuncs.xform_identity = function(x) { return x };

// muuuuch faster version -- return generated HTML, assign to innerHTML.
// However, it can only be used to create a table from scratch.  Can't update thanks
// to IE sucking.  Also IE is such a dog that I have unrolled this loop & test.
OutputFuncs.genericRowHTMLCreator = function(col_list, data, xform) {
    var rv = new Array();
    var start = new Date();
    // handle identity transform
    if (!xform) {
	for (var k = 0; k < col_list.length; k++) {
	    if (typeof(col_list[k]) == "function") {
		rv.push("<TD>");
		rv.push(col_list[k](data));
		rv.push("</TD>");
	    } else {
		rv.push("<TD>");
		rv.push(data[col_list[k]]);
		rv.push("</TD>");
	    }
	}
    } else {
	for (var k = 0; k < col_list.length; k++) {
	    if (typeof(col_list[k]) == "function") {
		rv.push("<TD>");
		rv.push(xform(col_list[k](data)));
		rv.push("</TD>");
	    } else { 
		rv.push("<TD>");
		rv.push(xform(data[col_list[k]]));
		rv.push("</TD>");
	    }
	}
    }
    var end = new Date();
    Logger.info("row took " + (end.valueOf() - start.valueOf()) + " ms");
    return rv.join('');
    
}

// Use a single named fragment (an element already created) to populate a
// row expander that is shared among all rows in the table.  This also implies
// that the row expander can only be used on one row, so the selection of
// a new row will cause the expander to move.  Hooks are provided to set up the
// expander element (elementSetup).
// The "fragment" element will also gain a few methods -- highlight and unhighlight.
OutputFuncs.singleExpandingRowCreatorGenerator = function(fragment, elementSetup) {
    $(fragment).highlight = function() {
	var parent = $(fragment);
	while (parent.tagName.toLowerCase() != "tr") {
	    Logger.debug(parent.tagName);
	    parent = parent.parentNode;
	}
	if (parent) {
	    var pclasses;
	    if (parent.className) {
		pclasses = new Element.ClassNames($(parent));
		pclasses.add("output_row_highlight");
	    } else
		Logger.warn("parent element " + parent.id + " has no classes");
	    if ($(parent).previousSibling) {
		pclasses = new Element.ClassNames($(parent).previousSibling);
		if (parent.previousSibling.className) {
		    pclasses = new Element.ClassNames($(parent));
		    pclasses.add("output_row_highlight");
		} else
		    Logger.warn("parent element " + parent.id + " has no classes");
	    }
	}
    };

    $(fragment).unhighlight = function() {
	var parent = $(fragment);
	while (parent.tagName.toLowerCase() != "tr") {
	    Logger.debug(parent.tagName);
	    parent = parent.parentNode;
	}
	if (parent) {
	    var pclasses;
	    if (parent.className) {
		pclasses = new Element.ClassNames($(parent));
		pclasses.remove("output_row_highlight");
	    } else
		Logger.warn("parent element " + parent.id + " has no classes");
	    if ($(parent).previousSibling) {
		pclasses = new Element.ClassNames($(parent).previousSibling);
		if (parent.previousSibling.className) {
		    pclasses = new Element.ClassNames($(parent));
		    pclasses.remove("output_row_highlight");
		} else
		    Logger.warn("parent element " + parent.id + " has no classes");
	    }
	}
    };
    return function(table, position, col_list, data, xform) {
	// handle identity transform
	if (!xform) {
	    xform = function(x) { return x };
	}
	var brow = OutputFuncs.genericRowCreator(table, position, col_list, data, xform);
	var xrow = table.insertRow(brow.sectionRowIndex + 1);
	var xcell = xrow.insertCell(0);
	Logger.info("inserted cell and row");
	xcell.colSpan = brow.cells.length;
	$(xcell).hide();
	$(xrow).hide();
	var clickHandler = function(event) {
	    // if the fragment is visible, hide it and deselect its momma row
	    Logger.info("in row click handler");
	    // find parent row
	    var parent = $(fragment);
	    //Logger.info(parent.tagName);
	    while (parent.tagName.toLowerCase() != "tr") {
		Logger.debug(parent.tagName);
		parent = parent.parentNode;
	    }
	    Logger.warn(parent.parentNode.tagName);
	    if ($(fragment).visible() && $(parent).visible()) {
		Logger.info("fragment is visible");
		$(fragment).unhighlight();
		Logger.info("removed classes from rows");
		//new Effect.SlideUp($(fragment));
		$(parent).hide();
		if ($(fragment).parentNode.parentNode == xrow) {
		    //$(fragment).parentNode.removeChild(fragment);
		    $(fragment).hide();
		    Logger.info("removed fragment from old position");
		    return;
		} else {
		    //$(fragment).parentNode.removeChild(fragment);
		    //Logger.info("removed fragment from old position");
		}
	    } 
	    Logger.info("highlighting rows");
	    //Element.classNames(brow).add("output_row_highlight");
	    Logger.info("appending fragment");
	    //$(fragment).parentNode.removeChild($(fragment));
	    xcell.appendChild($(fragment));
	    //Element.classNames(xrow).add("output_row_highlight");
	    $(fragment).highlight();
	    Logger.info("setting up fragment data");
	    elementSetup($(fragment), data);
	    //new Effect.SlideDown($(fragment), {scaleFrom: 0, scaleTo: 100});
	    Logger.info("showing fragment");
	    $(fragment).show();
	    $(xcell).show();
	    $(xrow).show();
	    var pos = Position.cumulativeOffset(brow);
	    //Logger.debug("position is " + pos);
	    OutputFuncs.scrollIfNeeded($(fragment));
	    return false;
	}
	$A(brow.cells).each(function (cell, i) {
	    Event.observe(cell, 'click', clickHandler);
	    // can we hang this somewhere for detaching later?
	    // for now just rely on the page to do it on exit, thanks Prototype
	    // but if we had to do this a lot we would want to
	    // clear it ourselves
	});
	return brow;
    }
}

// scroll if the element position is not on the page.
// vertical scroll only
OutputFuncs.scrollIfNeeded = function(el) {
    var pos = Position.cumulativeOffset(el);

    try {
	var bottom = pos[1] + el.offsetHeight;
	var scrollTop = window.pageYOffset ? window.pageYOffset : (document.documentElement.scrollTop ? document.documentElement.scrollTop : 0);
	var scrollBottom = document.documentElement.scrollHeight ? document.documentElement.scrollHeight : 0;
	//cr010710 doesn't work with ie 8.0 
	//cr010710 var winHeight = window.innerHeight ? window.innerHeight : (document.documentElement.clientHeight ? document.documentElement.clientHeight : 0);
	//cr010710 add document.body.clientHeight to mix for ie 8
	//cr011110 modified earlier fix due to showing performance issues
	var n_result = window.innerHeight ? window.innerHeight : 0;
	var n_docel = document.documentElement ? document.documentElement.clientHeight : 0;
	var n_body = document.body ? document.body.clientHeight : 0;
	if (n_docel && (!n_result || (n_result > n_docel)))
		n_result = n_docel;
	var winHeight =  n_body && (!n_result || (n_result > n_body)) ? n_body : n_result;

	Logger.info("top of element is at " + pos[1] + ", bottom is at " + bottom + ", window scrollTop is " + scrollTop + ", bottom is " + (scrollTop + winHeight));
	var newYPos = scrollTop;
	if (bottom > (scrollTop + winHeight)) {
	    newYPos = bottom - winHeight - 1;
	} 
	if (pos[1] < scrollTop) {
	    newYPos = pos[1];
	} 
	Logger.info("new yPos = " + newYPos);
	window.scrollTo(0, newYPos);
	return newYPos;
    } catch (e) {
	Logger.warn(e);
	window.scrollTo(0, pos[1]);
	return pos[1];
    }
}


// facilitates the creation of "Expanding" rows, which are suitable for
// short lists or cases when the row contents are simple.  don't want
// to use this for a big table, or anything like that -- in that case, see singleExpandingRowCreatorGenerator
OutputFuncs.expandingRowCreatorGenerator = function(expansionRowCreator, styleBaseName) {
    if (!styleBaseName)
	styleBaseName = "output_row_highlight";
    return function(table, position, col_list, data, xform) {
	// handle identity transform
	if (!xform) {
	    xform = function(x) { return x };
	}
	var brow = OutputFuncs.genericRowCreator(table, position, col_list, data, xform);
	// create hidden expansion row
	// might want to do this the hard way to keep it hidden
	var xrow = expansionRowCreator(table, brow.sectionRowIndex + 1, col_list, data, xform);
	$(xrow).hide();
	// set up expansion event on click
	var clickHandler = function(event) {
	    // highlight this row
	    var bclasses = new Element.ClassNames(brow);
	    var xclasses = new Element.ClassNames(xrow);
	    //var bfirst = $($A($(brow).cells).first());
	    //var blast = $($A($(brow).cells).last());
	    //var xfirst = $($A($(xrow).cells).first());
	    //var xlast = $($A($(xrow).cells).last());
	    try {
	    var bfirst = $($(brow).cells[0]);
	    var blast = $($(brow).cells[$(brow).cells.length - 1]);
	    var xfirst = $($(xrow).cells[0]);
	    var xlast = $($(xrow).cells[$(xrow).cells.length - 1]);
	    if (!$(xrow).visible()) {
		bclasses.add(styleBaseName);
		bclasses.add(styleBaseName + "_static"); // in case they need to look different
		xclasses.add(styleBaseName);
		xclasses.add(styleBaseName + "_dynamic"); // (borders, etc.)
		bfirst.classNames().add("first");
		blast.classNames().add("last");
		xfirst.classNames().add("first");
		xlast.classNames().add("last");
	    } else {
		bfirst.classNames().remove("first");
		blast.classNames().remove("last");
		xfirst.classNames().remove("first");
		xlast.classNames().remove("last");
		bclasses.remove(styleBaseName);
		bclasses.remove(styleBaseName + "_static");
		xclasses.remove(styleBaseName);
		xclasses.remove(styleBaseName + "_dynamic");
	    }
	    } catch (ex) {
		alert(ex);
	    }
	    $(xrow).toggle();
	}
	$A(brow.cells).each(function (cell, i) {
	    Event.observe(cell, 'click', clickHandler);
	});
	return brow;
    }
}

OutputFuncs.targetSelf = function(element) {
    return element;
}

OutputFuncs.targetIsInRow = function(element) {
    var orig = element;
    Logger.debug("element is " + element);
    //while (element && !(element instanceof HTMLTableRowElement)) {
    while (element && !element.rowIndex) {
	element = element.parentNode;
	Logger.debug("element is " + element);
    }
    if (element)
	return element;
    else
	return orig;
}

OutputFuncs.popupDivs = new Object();

// might work...performs "surgery" on the named div, substituting in the
// innerHTML positional params of the format {$n$}...
// target_to_highlight_func defines how the action popup relates to its target
// element.  
OutputFuncs.popupActionDiv = function(div_name, params, target_to_highlight_func) {
    Logger.debug("popup action div");
    Logger.debug("div_name = " + div_name + ", params = " + params);
    if (!target_to_highlight_func) {
	target_to_highlight_func = OutputFuncs.targetSelf;
    }

    return function(event) {
	var target = Event.element(event);
	if (!$(div_name)) 
	    return;
	    
	if (!OutputFuncs.popupDivs[div_name]) {
	    OutputFuncs.popupDivs[div_name] = new Object();
	    OutputFuncs.popupDivs[div_name].html = $(div_name).innerHTML;
	}
	
	// find target too
	OutputFuncs.popupDivs[div_name].activeTarget = target_to_highlight_func($(target));
	OutputFuncs.popupDivs[div_name].oldBackground = OutputFuncs.popupDivs[div_name].activeTarget.style.backgroundColor;
	Logger.debug('target is ' + OutputFuncs.popupDivs[div_name].activeTarget );
	

	var html = OutputFuncs.popupDivs[div_name].html;
	var newHtml = html;
	Logger.debug("started with html " + newHtml);
	for (var i = 0; i < params.length; i++) {
	    var str = "{\\$" + i + "\\$}";
	    var re = new RegExp(str, "gm");
	    newHtml = newHtml.replace(re, params[i]);
	    Logger.debug("replaced " + str + "=" + re.toString() + " with " + params[i]);
	}
	Logger.debug("finished with html " + newHtml);
	$(div_name).innerHTML = newHtml;
	// align it with the event coordinates
	$(div_name).style.top = Event.pointerY(event) + "px";
	$(div_name).style.left = Event.pointerX(event) + "px";
	$(div_name).show();
	Element.setStyle($(target), { backgroundColor: $(div_name).style.backgroundColor});
    }
};

OutputFuncs.hideActionDiv = function(div_name) {
    $(div_name).hide();
    OutputFuncs.popupDivs[div_name].activeTarget.style.backgroundColor = OutputFuncs.popupDivs[div_name].oldBackground;
};

OutputFuncs.dateSortFuncGen = function(column, formatString) {
    return function(data, direction) {
	var order = new Array(data.length);
	for (var i = 0; i < order.length; i++) 
	    order[i] = i;
	if (direction == "ascending") {
	    var sortfunc = function (a, b) {
		var aa = data[a][column];
		var bb = data[b][column];
		if (aa) aa = Date.parseString(aa, formatString);
		if (bb) bb = Date.parseString(bb, formatString);
		if (aa) aa = aa.valueOf();
		if (bb) bb = bb.valueOf();
		if ((!aa) || (aa < bb))
		    return -1;
		else if ((!bb) || (aa > bb))
		    return 1;
		else
		    return 0;
	    };
	} else {
	    var sortfunc = function (a, b) {
		var aa = data[a][column];
		var bb = data[b][column];
		if (aa) aa = Date.parseString(aa, formatString);
		if (bb) bb = Date.parseString(bb, formatString);
		if (aa) aa = aa.valueOf();
		if (bb) bb = bb.valueOf();
		if ((!aa) || (aa < bb))
		    return 1;
		else if ((!bb) || (aa > bb))
		    return -1;
		else
		    return 0;
	    };
	}
	order.sort(sortfunc);
	return order;
    }
}


OutputFuncs.standardSortFuncGen = function(column) {
    return function(data, direction) {
	var order = new Array(data.length);
	for (var i = 0; i < order.length; i++) 
	    order[i] = i;
	if (direction == "ascending") {
	    var sortfunc = function (a, b) {
		var aa = data[a][column];
		var bb = data[b][column];
		if (aa && aa.toString) aa = aa.toString();
		if (bb && bb.toString) bb = bb.toString();
		if ((!aa) || (aa < bb))
		    return -1;
		else if ((!bb) || (aa > bb))
		    return 1;
		else
		    return 0;
	    };
	} else {
	    var sortfunc = function (a, b) {
		var aa = data[a][column];
		var bb = data[b][column];
		if (aa && aa.toString) aa = aa.toString();
		if (bb && bb.toString) bb = bb.toString();
		if ((!aa) || (aa > bb))
		    return -1;
		else if ((!bb) || (aa < bb))
		    return 1;
		else
		    return 0;
	    };
	}
	order.sort(sortfunc);
	return order;
    };
}

/* Table which can display a large number of rows of data.
 * Provides row counts and forward-back buttons.
 *
 */
OutputFuncs.SegmentedTable = Class.create();

OutputFuncs.SegmentedTable.prototype = {
    initialize: function(container, name) {
	this.data = [];
	this.headerOrder = [];
	this.headerSetup = {};
	this.activeOrder = [];
	this.rowDecorationFuncs = [];
	this.activeSortFunc = null;
	this.activeSortDirection = null;
	this.visibleRows = 10;
	this.visibleStart = 0;
	this.visibleEnd = this.visibleStart + this.visibleRows;
	this.container = container;
	this.tableClass = "segmented_table";
	this.name = name;
	this.drawnOnce = false;
	this.legalActions = {};
    },

    addHeader: function(dataKey, options) {
	if (!options) 
	    options = {};
	if (!options['text']) 
	    options['text'] = dataKey;
	if (!options['sortfunc'])
	    options['sortfunc'] = OutputFuncs.standardSortFuncGen(dataKey);
	this.headerSetup[dataKey] = options;
	this.headerOrder.push(dataKey);
    },

    getPrettyRange: function() {
	return { 
	    start: this.visibleStart + 1, 
	    total: this.activeOrder.length,
	    end: Math.min(this.activeOrder.length, this.visibleStart + this.visibleRows)
	};
    },
    
    previousPage: function() {
	return this.setFirstRow(Math.max(0, this.visibleStart - this.visibleRows));
    },

    nextPage: function() {
	return this.setFirstRow(Math.min(this.visibleRows + this.visibleStart, this.activeOrder.length - 1));
    },


    setFirstRow: function(startRow) {
	if ((startRow >= 0) && (startRow < this.data.length)) {
	    this.visibleStart = startRow;
	    this.visibleEnd = this.visibleStart + this.visibleRows;
	    this.scrolled();
	}
    },

    // make sure row & column highlights get drawn on a fresh table
    highlightCells: function() {
	for (var i = 0; i < this.headerOrder.length; i++) {
	    var c = this.headerOrder[i];
	    var colCells = document.getElementsByClassName(this.name + "_" + c);
	    if (this.headerSetup[c]['highlight'] != null) {
		$(this.headerSetup[c]['domId']).addClassName('highlight');
		if (colCells) {
		    colCells.each(function (cell) {
			cell.addClassName("highlight");
		    });
		}
	    } else {
		$(this.headerSetup[c]['domId']).removeClassName('highlight');
		if (colCells) {
		    colCells.each(function (cell) {
			cell.removeClassName("highlight");
		    });
		}
	    }
	}
	// and do the rows now
	if (this.rowDecorationFuncs && (this.rowDecorationFuncs.length > 0)) {
	    var visibleRows = this.getVisibleData();
	    for (var fi = 0; fi < this.rowDecorationFuncs.length; fi++) {
		this.rowDecorationFuncs[fi](visibleRows);
	    }
	}
    },

    // return an array of the visible data elements.
    getVisibleData: function() {
	var visibleRows = new Array(this.visibleRows);
	for (var j = 0, i = this.visibleStart; i < this.visibleEnd; i++, j++) {
	    visibleRows[j] = this.data[i];
	}
	return visibleRows;
    },

    // use this to add a row decoration function.  this must accept a list of
    // data rows.  the function is free to do
    // whatever it wants with those.  it's not limited to row decoration really.
    // be quick though -- it gets called every time the view is changed.
    addRowDecorationFunc: function(decoratorFunc) {
	if (this.rowDecorationFuncs == null)
	    this.rowDecorationFuncs = [ decoratorFunc ];
	else
	    this.rowDecorationFuncs.push(decoratorFunc);
    },

    sortByColumn: function (column, direction) {
	if (this.column == null) {
	    // clear sort
	    this.sort(null);
	    $H(this.headerSetup).keys().each(function (k) {
		this.headerSetup[k]['highlight'] = null;
	    }.bind(this));
	}
	if (this.headerSetup[column] && (direction == 'ascending' || direction == 'descending')) {
	    this.sort(this.headerSetup[column]['sortfunc'], direction);
	    $H(this.headerSetup).keys().each(function (k) {
		this.headerSetup[column]['highlight'] = (k == column);
	    }.bind(this));
	}
	this.highlightCells();
    },

    // sortfunc must map data indices to sort order.  called as
    // sortfunc(data).
    sort: function(sortfunc, direction) {
	if (sortfunc == null) {
	    sortOrder = new Array(this.data.length);
	    for (var i = 0; i < this.data.length; i++) 
		sortOrder[i] = i;
	    this.activeSortFunc = null;
	    this.activeSortDirection = null;
	    this.activeOrder = sortOrder;
	    this.draw();
	    return;
	}
	var newOrder = sortfunc(this.data, direction);
	if (newOrder) {
	    this.activeSortFunc = sortfunc;
	    this.activeSortDirection = direction;
	    this.activeOrder = newOrder;
	    this.draw();   
	}
    },

    htmlTableBounds: function(low, high, startVisible) {
	var tableHtml = "<table class=\"" + this.tableClass + "\""
	if (!startVisible)
	    tableHtml += " style=\"display: none;\""
	tableHtml += ">\n";
	// draw the headers
	var headerHtml = this.headerRowHtmlFunc();
	tableHtml += "<thead>" + headerHtml + "</thead>";
	var innerTableHtmlA = [];
	var rowIndex;
	for (var i = this.visibleStart; i < this.visibleEnd; i++) {
	    rowIndex = this.activeOrder[i];
	    innerTableHtmlA.push(this.innerRowHtmlFunc(this.data[rowIndex], i));
	}
	tableHtml += innerTableHtmlA.join('');
	tableHtml += "</table>\n";
	//Logger.debug("html is " + tableHtml);
	return tableHtml;
    },

    // clean up event handlers
    disconnectEvents: function() {
	// header cell sorting
	if (!this.toggleSort)
	    return;
	var i;
	for (i = 0; i < this.headerOrder.length; i++) {
	    var hSetup = this.headerSetup[this.headerOrder[i]];
	    var hId = hSetup['domId'];
	    var cell = $(hId);
	    if (cell) {
	//	Logger.debug('unhooking sort from cell ' + hId + '(' + cell + ')');
	      //$(cell).removeEventListener('click', this.toggleSort, false);
	      //$(cell).detachEvent('onclick', this.toggleSort);
		Event.stopObserving(hId, 'click', this.toggleSort);
	    }
	}
    },

    // attach event handlers
    connectEvents: function() {
	var i;
	if (!this.toggleSort) {
	    Logger.warn("creating toggleSort function");
	    this.toggleSort = function(event) {
		var el = Event.element(event);
		Logger.debug("toggle sort for element " + el.id);
		if (el.id) {
		    headerKeys = $H(this.headerSetup).keys();
		    for (var j = 0; j < headerKeys.length; j++) {
			var colName = headerKeys[j];
			var opts = this.headerSetup[colName];
			if (opts['domId'] == el.id) {
			    if (this.activeSortFunc == opts['sortfunc']) {
				if (this.activeSortDirection == 'ascending')
				    this.sortByColumn(colName, 'descending');
				else if (this.activeSortDirection == 'descending')
				    this.sortByColumn(null);
			    } else {
				this.sortByColumn(colName, 'ascending');
			    }

			}	
		    };
		}
	    }.bindAsEventListener(this);
	}
	for (i = 0; i < this.headerOrder.length; i++) {
	    var hSetup = this.headerSetup[this.headerOrder[i]];
	    var hId = hSetup['domId'];
	    var cell = $(hId);
	    if (cell && !hSetup['nosort']) {
		Event.observe(hId, 'click', this.toggleSort);
		
      //cell.attachEvent('onclick', this.toggleSort);
      //cell.addEventListener('click', this.toggleSort, false);
	    }
	}
    },

    draw: function() {
	this.disconnectEvents();
	$(this.container).innerHTML = null;
	$(this.container).innerHTML = this.htmlTableBounds(this.visibleStart, this.visibleEnd, true);
	this.highlightCells();
	// save header widths...
	if (!this.drawnFirstTime) {
	    this.drawnFirstTime = true;
	    for (var i = 0; i < this.headerOrder.length; i++) {
		var hName = this.headerOrder[i];
		var domId = this.headerSetup[hName]['domId'];
		var cell = $(domId);
		this.headerSetup[hName]['initialWidth'] = cell.getWidth();
	    }
	}
	this.connectEvents();
	this.evaluateLegalActions();
    },

    scrolled: function() {
	this.draw();
    },

    evaluateLegalActions: function() {
	if ((this.visibleStart + this.visibleRows) < this.activeOrder.length)
	    this.legalActions['Forward'] = true;
	else
	    this.legalActions['Forward'] = false;
	     
	if (this.visibleStart > 0)
	    this.legalActions['Back'] = true;
	else
	    this.legalActions['Back'] = false;

    },

    // I'd like to make this not change widths of columns on redraw,
    // but it doesn't appear that that is possible (short of forcing
    // column widths on all columns up front via css).
    // You can secretly override this if you need to, of course.  Just
    // make sure you set domId inside the headerSetup hash for each
    // header element you want to be clickable.
    headerRowHtmlFunc: function() {
	var htmlA = [];
	this.headerOrder.each(function (dataKey) {
	    var opts = this.headerSetup[dataKey];
	    opts['domId'] = this.tableClass + "_" + dataKey;
	    var title = "";
	    var style = "";
	    if (!opts['nosort'])
		title = 'title="Click to toggle sorting"';
	    if (opts['initialWidth']) 
		style = "style=\"width: " + opts['initialWidth'] + "px;\"";
		//style = "width=\"" + opts['initialWidth'] + "\"";
	    htmlA.push("<th " + title + " " + style + " id=\"" + opts['domId'] + "\">" + opts['text'] + "</th>");
	}.bind(this));
	return "<tr>" + htmlA.join('') + "</tr>\n";
    },

    // draw an inner table row(s) from a row of data
    // note: row might be empty or null
    innerRowHtmlFunc: function(datarow, index) {
	if (this.rowHtmlCreator) {
	    var trTag = "<tr class=\"" + (index % 2 == 0 ? "evenrow" : "oddrow") + "\">"; 
	    // OutputFuncs.genericRowHTMLCreator = function(col_list, data, xform) {
	    if (datarow) 
		return trTag + this.rowHtmlCreator(this.compatColList, datarow) + "</tr>\n";
	    else
		return trTag + "</tr>\n";
	} else {
	    return trTag + "<td>todo</td></tr>\n"
	}
    },

    fullDataUpdate: function(fullData) {
	var sortOrder;
	if (this.activeSortFunc) {
	    sortOrder = this.activeSortFunc(fullData, this.activeSortDirection);
	} else {
	    sortOrder = new Array(fullData.length);
	    for (var i = 0; i < fullData.length; i++) 
		sortOrder[i] = i;
	}
	this.data = fullData;
	this.activeOrder = sortOrder;
	this.draw();
    },

    serializeDisplayState: function() {
	var state = {
	    'headerOrder': this.headerOrder,
	    'activeOrder': this.activeOrder,
	    'activeSortFunc': this.activeSortFunc,
	    'activeSortDirection': this.activeSortDirection,
	    'visibleRows': this.visibleRows,
	    'visibleStart': this.visibleStart,
	    'visibleEnd': this.visibleEnd
	};
	var json = state.toJSONString();
	return json;
    },

    loadDisplayState: function(json) {
	Logger.debug("loading display state json=" + json);
	eval("var state = " + json);
	Logger.debug("loading display state 2 state = " + state);
	$H(state).keys().each(function (k) {
	    Logger.debug("loaded state value " + k + ": was " + state[k]);
	    this[k] = state[k];
	}.bind(this));
	this.draw();
    }
    
};
// 

