/************************** Spreadsheet **************************/

// options
var allowconflicts = false;
var debug = true;
var casesensitive = true;
var valuecount = 0;

// represent cells with a hashtable of cell objects
var cells = {};

// One cell object for each widget in the speadsheet.
function cell (name, type, style, negs, poss, negsupports, possupports, negsupportx, possupportx, component, value, conflictset, forceset) {
	this.name = name;   // string
	this.type = type;   // collection object or named type, e.g. 'number'
	this.style = style;  // string for selector,textbox,etc
	this.negs = negs;   // function
	this.poss = poss;   // function
	this.negsupports = negsupports;   // function
	this.possupports = possupports;   // function
	this.negsupportx = negsupportx;   // function
	this.possupportx = possupportx;   // function
	this.component = component;   // collection
    this.userset = false;
	if (value === undefined) this.deleteVal();
	else { this.value = value; this.setGUIVal(); this.userset = true; }
	if (conflictset === undefined) this.clearConflictset();   // collection
	else this.conflictset = conflictset;
	if (forceset === undefined) this.clearForceset();   // collection
	else this.forceset = forceset;
}

cell.prototype.updateVal = function (internal) {
	var html = document.getElementById(this.name) 
	if (!internal) this.userset = true;
	this.forceset = new set();
	this.conflictset = new set();
	switch (this.style) {
		case 'selector': 
			if (html.value === 'undefined') this.value = undefined;
			else this.value = Number(html.value); 
			break;
		case 'textbox': 
			this.value = uglyname(html.value); 
			break;
		case 'checkbox':
			this.value = html.checked ? uglyname('true') : uglyname('false'); 
			break; }
	if (this.value === undefined) this.userset = false;
	return this; }
			
cell.prototype.getVal = function() { return this.value; }

cell.prototype.deleteVal = function() { 
	function maknot (v) { 
		return prettyname(v) === 'true' ? uglyname('false') : uglyname('true'); }

	this.clearForceset();
	this.clearConflictset();   // should be redundant
	switch (this.style) {
		case 'selector': this.value = undefined; break;
		case 'textbox': this.value = uglyname(''); break; 
		case 'checkbox': 
			if (this.value === undefined) this.value = uglyname('false'); 
			else this.value = maknot(this.value); 
			break; }}

cell.prototype.setGUIVal = function () {
	var htmlcell = document.getElementById(this.name);
	switch (this.style) {
		case 'selector': 
			if (this.value === undefined) htmlcell.options[0].selected = true;
			else
				for (var i = 0; i < htmlcell.options.length; i++) 
					if (htmlcell.options[i].value == this.value)
						htmlcell.options[i].selected = true; 
			break;
		case 'textbox': htmlcell.value = prettyname(this.value); break;
		case 'checkbox': htmlcell.checked = this.value; break; 
	}}

cell.prototype.userVal = function () { return this.userset; }
cell.prototype.forcedVal = function () { return (this.value !== undefined) && !this.userset; }
cell.prototype.hasVal = function () { return this.value !== undefined; }
cell.prototype.clearConflictset = function () { this.conflictset = new set(); }
cell.prototype.clearForceset = function () { this.forceset = new set(); }

cell.prototype.toString = function() {
		return "cell(name: " + this.name + ", value: " + this.value + ", conflictset: " + this.conflictset + ")"; }


// initialize spreadsheet and internal datastructures
function initspread (cellarray) {
	var cell;
	for (var i = 0; i < cellarray.length; i++)
		cells[cellarray[i].name] = cellarray[i];
	for (var i = 0; i < cellarray.length; i++) {
		cell = cellarray[i].name;
		force_values(cell);
		update_conflicts(cell);
		update_colors(cell); }}

// Interface functions for external consumption
function findcell(cell) { return cells[cell]; }
function cellvalue(cell) { return findcell(cell).getVal(); }
function hascellvalue(cell) { return cellhasvalue(cell); }
function cellhasvalue(cell) { 
	var obj = findcell(cell);
	return obj.hasVal() && obj.userVal(); }
function cellvalueforced(cell) { return findcell(cell).forcedVal(); }


// Canonicalizing names
function cellname (cell) { return cell.toLowerCase(); }
function valuename (value) { return value; }  //value.toLowerCase(); }

function prettyname (value) { 
	if (value === 'undefined') return prettynames[undefined];
	else return prettynames[value]; }

function capitalize (str) {
	str = str.toLowerCase();
	return str.charAt(0).toUpperCase() + str.slice(1); }

function uglyname (value) {
	if (!casesensitive) value = value.toLowerCase();
	if (!(value in uglynames)) { 
		v = create_symbol(value);
		prettynames[v] = value;
		uglynames[value] = v; }
	else v = uglynames[value];
	return v; }		
		
function create_symbol(val) {
	valuecount++;
	return (valuecount-1); }

/**********************************************************************************/
/*************************** Generic Component Behavior ***************************/
/**********************************************************************************/

// switch-style
function changeany(cell, userset) {
	var obj = findcell(cell);
	switch (obj.style) {
		case 'selector': selector_change(cell,userset); break;
		case 'textbox': textbox_change(cell,userset); break; 
		case 'checkbox': checkbox_change(cell,userset); break; }}

function focusany(cell) {
	var obj = findcell(cell);
	switch (obj.style) {
		case 'selector': selector_focus(cell); break;
		case 'textbox': textbox_focus(cell); break; 
		case 'checkbox': checkbox_change(cell); break; }}

function mouseoverany(cell) {
	var obj = findcell(cell);
	switch (obj.style) {
		case 'selector': selector_mouseover(cell); break;
		case 'textbox': textbox_mouseover(cell); break; 
		case 'checkbox': checkbox_mouseover(cell); break; }}

function mouseoutany(cell) {
	var obj = findcell(cell);
	switch (obj.style) {
		case 'selector': selector_mouseout(cell); break;
		case 'textbox': textbox_mouseout(cell); break; 
		case 'checkbox': checkbox_mouseout(cell); break; }}

/************************************************************************************/
/*************************** Standard Behavioral Routines ***************************/
/************************************************************************************/


/*************************** Value Forcing ***************************/

function force_values (cell) {	
	// find all cells in this component that don't have a value.
	var obj = findcell(cell);
	var candidates = obj.component.mapcar(function(x) { 
		var o = findcell(x);
		if (o.userVal() && o.hasVal()) return false; else return x; });
	//alert(candidates);
	candidates = candidates.toSet().remove(false);
	
	candidates.mapc(update_force);
	candidates.mapc(setGUIValue);
}


// for cell, set value and forceset either to value or delete both
function update_force (cell) {
	var obj = findcell(cell);
	// if there's a single positive value entailed, set that value
	if (obj.possupports === undefined) return false;
	var res = obj.possupports('?x');   // a set of <val, suppset> 
	var vals = res.mapcar(function (x) { return x.first(); }).toSet();
	//alert("update_force(" + cell + ") finds positive values: " + vals);
	if (vals.size() !== 1) { obj.deleteVal(); return false; }
	obj.value = vals.element(0).element(0);  

	// if that value is negatively entailed, delete it
	if (obj.negsupportx !== undefined && !obj.negsupportx(obj.value).empty()) {
		obj.deleteVal();
		return false; }

	// otherwise, store the forceset
	obj.forceset = ds.nunion(res.mapcar(function (x) { return x.second(); }));	
	return cell; }

function setGUIValue(cell) { findcell(cell).setGUIVal(); }


/*************************** Conflicts ***************************/

// Update the conflicts for the given cell, and for each conflicting cell apply the given function.
// Often called during onchange.	
		
function update_conflicts(cell) { 
	findcell(cell).component.mapc(function (x) { 
		 findcell(x).conflictset = compute_conflictset(x); }); }
		 
function compute_conflictset (cell) {
	//alert("updating conflicts");
	var cellobj = findcell(cell);
	var res;
	var negsupps = new set();
	
	// if cell object has been emptied (value 'unknown'), can't be in conflict
	//   This saves us a call to negsupports, which can be relatively expensive.
	if (!cellobj.hasVal()) return new set();
	
	// negsupports returns a set of <tuple(v1,...,vn), set(pred1,...,predn)>
	if (cellobj.negsupports != undefined) 
		negsupps = cellobj.negsupports(cellobj.value);

	res = ds.nunion(negsupps.mapcar(function (x) { return x.second(); }));	
	
	// for case when cell value is just disallowed
	if (!negsupps.empty() && res.empty()) res = new set(cell);
	return res; }

// called during onFocus event
function tooltip_conflicts(cell) {
	var h = build_sfc_hash(cell);
	var s = "";
	for (var v in h) {
			switch (h[v]) {
				case 'c': s += prettyname(v) + " ****\n";  break;
				case 's': s += prettyname(v) + " ++++\n";  break;
				case 'f': s += prettyname(v) + " ----\n";  break; }}
	document.getElementById(cell).title = s; }

function identity (x) { return x; }

function build_sfc_hash (cell) {
	var cellobj = findcell(cell);
	var negs = cellobj.negs;
	var poss = cellobj.poss;
	var negvalues = new set();
	var posvalues = new set();
	// negs/poss returns a set of expr(v1,...,vn), where n is always 1
	if (negs != undefined) 
		negvalues = negs('?x').mapcar(function(x) { return x.element(0) }).toSet();
	//if (poss != undefined) alert(poss('?x') instanceof set);
	if (poss != undefined) 
		posvalues = poss('?x').mapcar(function(x) { return x.element(0) }).toSet();

	// make sets that are mutually exclusive
	var stipulated, forbidden, conflict;
	conflict = posvalues.intersection(negvalues);
	stipulated = posvalues.difference(conflict);
	forbidden = negvalues.difference(conflict);
	var h = {};
	conflict.mapc(function (x) { h[x] = 'c'; });
	stipulated.mapc(function (x) { h[x] = 's'; });
	forbidden.mapc(function (x) { h[x] = 'f'; });
	//alert("Conflicts: " + conflict);
	//alert("Stipulated: " + stipulated);
	//alert("Forbidden: " + forbidden);
	return h; }

function findSelected (select) {
	var res = hashbag();  // use as a set
	for (var i = 0; i < select.options.length; i++) 
		if (select.options[i].selected) 
			res.adjoin(select.options[i].value);
	return res; }

/*************************** Colors ***************************/

function update_colors (cell) {
	findcell(cell).component.mapc(function(x) {
		var obj = findcell(x);
		if (!obj.conflictset.empty()) paintred(x);
		else if (obj.forcedVal()) painttan(x);
		else paintwhite(x); }); }

function painttan(cell) { highlight(cell, "#cc9966"); }
function paintbrown(cell) { highlight(cell, "663300"); }
function paintred(cell) { highlight(cell,"red"); }
function paintwhite(cell) { highlight(cell, "white"); }
function paintpurple(cell) { highlight(cell, "purple"); }

function highlight_supportset (cell) {
	var obj = findcell(cell);
	if (!obj.conflictset.empty()) obj.conflictset.mapc(paintpurple);
	else if (!obj.forceset.empty()) obj.forceset.mapc(paintbrown); }	

function highlight(cell, color) {
	document.getElementById(cell + "box").style.backgroundColor = color; }

function colorAllRed() {
	for (var c in cells) {
		if (cells.hasOwnProperty(c)) {
			highlight(cells[c].name, "red"); }}}

/*************************************************************************/
/*************************** Selector Behavior ***************************/
/*************************************************************************/

/************* Focus *************/

function selector_focus(cell) {
	if (allowconflicts == true) { 
		selector_setValueStyles_conflicts(cell, build_sfc_hash(cell)); }
	else { 	
		selector_setValueStyles_noconflicts(cell, build_sfc_hash(cell)); }}

// annotate each value with its status (conflict, stipulated, forbidden)
function selector_setValueStyles_conflicts (cell,h) {
	var cellobj = findcell(cell);
	var cellhtml = document.getElementById(cellobj.name);
	if (cellhtml.nodeName == 'SELECT') {
		for (var i = 0; i < cellhtml.options.length; i++) {
			var opt = cellhtml.options[i];
			switch (h[opt.value]) {
				case 'c': opt.text = prettyname(opt.value) + " ****";  break;
				case 's': opt.text = prettyname(opt.value) + " ++++";  break;
				case 'f': opt.text = prettyname(opt.value) + " ----";  break;
				default: opt.text = prettyname(opt.value); }}}}
	
// differs because we remove values and perform no annotations
function selector_setValueStyles_noconflicts (cell,h) {
	var cellobj = findcell(cell);
	var cellhtml = document.getElementById(cellobj.name);
	if (cellhtml.nodeName == 'SELECT') {
		var j = 1;  // start with empty 
		var selected = findSelected(cellhtml);		
		// includes nonforbidden values 
		cellobj.type.mapc(function (val) {
			if (h[val] != 'f') {
				cellhtml.options[j] = new Option(prettyname(val), val);
				if (selected.member(val)) 
					cellhtml.options[j].selected = true;
				// debugging
				//opt = cellhtml.options[j];
				//switch (h[cellobj.type[i]]) {
				//	case 'c': opt.text = opt.text + " ****";  break;
				//	case 's': opt.text = opt.text + " ++++";  break; }
				j++; }});
		cellhtml.options.length = j; }}

/************* Change, Mouseover, Mouseout *************/

// mouseover
function selector_mouseover(cell) { 
	var msgbox = document.getElementById('msg');
	if (msgbox != undefined) msgbox.value = findcell(cell).conflictset;
	highlight_supportset(cell); }

// mouseout
function selector_mouseout(cell) { 
	update_colors(cell); }

// change
function selector_change(cell) {
	// userset is assumed true if not provided
	// selector_cleanup(cell);  // see below for why we leave this out.
	selector_mouseout(cell);
	findcell(cell).updateVal();
	force_values(cell);
	update_conflicts(cell);
	update_colors(cell); }

// we don't call either onchange because if a user (a) changes the cell 
//   and (b) without changing focus hits the drop-down list again, 
//   the drop-down action does not retrigger the onfocus event.
// In short, we need a new widget.
function selector_cleanup (cell) {
	var cellhtml = document.getElementById(cell);
	var opt = cellhtml.options[cellhtml.selectedIndex];
	opt.text = prettyname(opt.value);}

function selector_cleanup2 (cell) {
	var cellhtml = document.getElementById(cell);
	var opt;
	for (var i = 0; i < cellhtml.options.length; i++) {
		opt = cellhtml.options[i];
		opt.text = prettyname(opt.value); }}
	
// TODO: FIGURE OUT WHERE TO RESET FORCESET.  SOMEWHERE IN FORCE_VALUES
/************************************************************************/
/*************************** Textbox Behavior ***************************/
/************************************************************************/

// change
function textbox_change (cell) {	
	textbox_mouseout(cell);
	var old,curr;
	var obj = findcell(cell);
	old = obj.getVal();
	obj.updateVal();
	//alert(cell + ".forceset = " + obj.forceset);
	curr = obj.getVal();
	if (old !== curr) {  
		// update universe
		univ.remove(new expr(old));
		univ.add(new expr(curr));
		force_values(cell);
		update_conflicts(cell); 
		update_colors(cell); }}
	
// mouseover
function textbox_mouseover(cell) { selector_mouseover(cell); }

// mouseout
function textbox_mouseout(cell) { selector_mouseout(cell); }

// focus
function textbox_focus (cell) { }


/*************************************************************************/
/*************************** Checkbox Behavior ***************************/
/*************************************************************************/

function checkbox_change (cell) { 
	checkbox_mouseout(cell);
	var obj = findcell(cell);
	obj.updateVal();
	force_values(cell);
	update_conflicts(cell); 
	update_colors(cell);  }

function checkbox_mouseover (cell) { selector_mouseover(cell); }
function checkbox_mouseout (cell) { selector_mouseout(cell); }
function checkbox_focus (cell) { }

	
/*************************************************************/
/*************************** Scrap ***************************/
/*************************************************************/

// Each time the select box is clicked, the values in that box can be altered
//   to show which values will produce conflicts by calling Clickcell.
//  Version where values are reordered.
/*
function focuscell(cell) {
	var cellobj = findcell(cell);
	var negs = cellobj.negs;
	var poss = cellobj.poss;
	var negvalues = empty();
	var posvalues = empty();
	if (negs != undefined) {
		negvalues = negs(); }
	if (poss != undefined) {
		posvalues = poss(); }
	//alert("neg values: " + negvalues);
	//alert("pos values: " + posvalues);
	
	var goodvalues, badvalues;
	if (posvalues.length == 0) {
		badvalues = negvalues;
		goodvalues = setdiff(cellobj.type, negvalues); }
	else if (posvalues.length == 1) {
		badvalues = setdiff(cellobj.type, posvalues);
		goodvalues = posvalues; }
	else {
		badvalues = cellobj.type;
		goodvalues = empty(); }
		
	setgoodbadcell(goodvalues, badvalues, cellobj); }

function setgoodbadcell(good, bad, cellobj) {
	//alert("Good: " + good);
	var cellhtml = document.getElementById(cellobj.name);
	if (cellhtml.nodeName == 'SELECT') {
		for (var i = 0; i < cellhtml.options.length; i++) {
			if (findq(cellhtml.options[i].value, bad)) {
				cellhtml.options[i].text = cellhtml.options[i].text.toUpperCase() + "****"; }
			else {
				cellhtml.options[i].text = prettyname(cellhtml.options[i].value); }}}}

// Version where we move entries around in select box.  
//  Bug alert: doesn't properly update the selectedIndex property.
function setgoodbadcell2(good, bad, cellobj) {
	//alert("Good: " + good);
	var cellhtml = document.getElementById(cellobj.name);
	if (cellhtml.nodeName == 'SELECT') {
		var cnt = 0;
		// start options list with unknown value
		if (cnt >= cellhtml.options.length) {
			cellhtml.options[cnt] = new Option(); }
		cellhtml.options[cnt].value = '';
		cellhtml.options[cnt].text = '';
		cnt++;
		// then add the good values
		for (var i = 0; i < good.length; i++) {
			if (cnt >= cellhtml.options.length) {
				cellhtml.options[cnt] = new Option(); }
			cellhtml.options[cnt].value = good[i];
			cellhtml.options[cnt].text = prettyname(good[i]);
			cnt++; }
		// then add a separator and the bad values
		if (bad.length > 0) {
			// add in a separator
			if (cnt >= cellhtml.options.length) {
				cellhtml.options[cnt] = new Option(); }
			cellhtml.options[cnt].value = '';
			cellhtml.options[cnt].text = cellobj.separator;
			cnt++;
			// place the bad values at the bottom
			for (var i = 0; i < bad.length; i++) {
				if (cnt >= cellhtml.options.length) {
					cellhtml.options[cnt] = new Option(); }
				cellhtml.options[cnt].value = bad[i];
				cellhtml.options[cnt].text = prettyname(bad[i]);
				cnt++; }}
			cellhtml.options.length = cnt; }}

function buildseparator (type) {
	var s = "";
	var p;
	var max = 0;
	for (var i = 0; i < type.length; i++) {
		p = prettyname(type[i]);
		if (p.length > max) { max = p.length; }}
	for (var i = 0; i < max; i++) {
		s += "-"; }
	return s;}

*/

