// DataGrouper
// 2009 Rensselaer Polytechnic Institute - ewp.rpi.edu

// Reads in data from XML or the DOM, constructs a filter
// control, and selectively displays output based on matching
// filter objects.

// Requires Prototype 1.6
var ClassFilter = Class.create({
				// css class, label for checkbox, label for output, enabled by default
	initialize: function(klass, label, groupLabel, enabled) {
		if (!groupLabel) { groupLabel = label; }
		this.className = klass;
		this.label = label;
		this.groupLabel = groupLabel;
		this.enabled = enabled == true;
	},
	test: function(element) {
		if (this.enabled == false) return false;
		if (!(element = $(element))) return false;
		return element.hasClassName(this.className);
	},
	process: function(elements) {
		var ret = new Array();
		
		var l = elements.length;
		for (var i=0; i<l; ++i) {
			var el = $(elements[i]);
			if (el.hasClassName(this.className)) {
				ret.push(el);
			}
		}
		return ret;
	},
	setFrom: function(event) {
		var element = Event.element(event);
		if (this.enabled != element.checked) {
			this.enabled = element.checked;
			element.fire("DataGrouper:filterChanged");
		}
	},
	getControl: function() {
		var wrapper = new Element('li');
		var ctr = new Element('input', {type:'checkbox', checked:this.enabled});
		ctr.observe('click', this.setFrom.bind(this));
		this.control = ctr;
		wrapper.insert(ctr);
		wrapper.insert(new Element('label', {'for':ctr.identify()}).update(this.label));
		return wrapper;
	}
});

var Group = Class.create({
	initialize: function(label, filters, enabled) {
		if (filters) { this.setFilters(filters) }
		this.label = label;
		this.anyControl = new Element('li');
		this.anyCheck = new Element('input', {type:'checkbox', disabled:true, checked:true});
		this.anyControl.insert(this.anyCheck);;
		this.anyControl.insert( new Element('label', {'for':this.anyCheck.identify()}).update("Any") );
		this.anyCheck.observe('click', this.enable.bind(this));
		this.updateEnabledState();
	},
	setFilters: function(filters) {
		this.filters = $A(filters).clone();
	},
	setFrom: function(event) {
		var element = Event.element(event);
		var enabled = element.checked;

		if (enabled) {
			//only fire event once, only fire if newly enabled
			element.fire("DataGrouper:groupChanged", this);
		}
	},
	getControl: function(name) { /* get control used in GroupSet */
		var wrapper = new Element('li');
		var ctr = new Element('input', {type:'radio', name:name});
		ctr.observe('click', this.setFrom.bind(this));
		wrapper.insert(ctr);
		wrapper.insert(new Element('label', {'for':ctr.identify()}).update(this.label));
		return wrapper;
	},
	toHTML: function() { /* get html for gruop */
		var div = new Element('div', {'class':'DataGrouperFilterSet'});
		div.observe('DataGrouper:filterChanged', this.updateEnabledState.bind(this));
		div.insert('<h4>'+this.label+'</h4>');
		var list = new Element('ul');
		div.insert(list);
		list.insert(this.anyControl);
		var l = this.filters.length;
		for (var i=0; i<l; ++i) {
			list.insert(this.filters[i].getControl());
		}
		return div;
	},
	enable: function(event) {
		if (this.anyCheck.checked == true) {
			var l = this.filters.length;
			
			for (var i=0; i<l; ++i) {
				this.filters[i].control.checked = true;
				this.filters[i].enabled = true;
			}
			
			this.anyCheck.fire("DataGrouper:filterChanged");
		}
	},
	updateEnabledState: function() {
		var l = this.filters.length;
		var anyCheckbox = this.anyCheck;
		var enabled = true;
		var disabled = true;
		for (var i=0; i<l; ++i) {
			var state = this.filters[i].enabled;
			enabled = enabled && state;
			disabled = disabled && !state;
		}
		if (enabled || disabled) { //all one way or the other
			anyCheckbox.disable();
			anyCheckbox.checked = true;
		} else {
			anyCheckbox.enable();
			anyCheckbox.checked = false;
		}
		this.allDisabled = disabled;
		this.allEnabled = enabled;
	},			
	test: function(element) {
		//If no filters are enabled, always match
		if (this.allDisabled) return true;
		var l = this.filters.length;
		for (var i=0; i<l; ++i) {
			if (this.filters[i].test(element)) {
				return true;
			}
		}
		return false;
	}
});

var GroupSet = Class.create({
	initialize: function(groups, filterHeader, groupHeader) {
		if (groups) { this.setGroups(groups) }
		this.filterHeader = filterHeader || "Filter";
		this.groupHeader = groupHeader || "Group By";
	},
	setGroups: function(groups) {
		this.groups = $A(groups).clone();
		this.selectedItem = this.groups[0];
	},
	setSelected: function(event) {
		this.selectedItem = event.memo;
	},
	toHTML: function() {
		var div = new Element('div');
		var groupby = new Element('div', {'class':'DataGrouperGroupSet'});
		groupby.insert('<h3>'+this.groupHeader+'</h3>');
		var radioname = div.identify()+'Radio';
		var l = this.groups.length;
		var list = new Element('ul');
		groupby.insert(list);
		for (var i=0; i<l; ++i) {
			var ctr = this.groups[i].getControl(radioname);
			list.insert(ctr);
		}
		var filterby = new Element('div', {'class':'DataGrouperFilterSet'});
		filterby.insert('<h3>'+this.filterHeader+'</h3>');
		for (var i=0; i<l; ++i) {
			filterby.insert(this.groups[i].toHTML());
		}
		div.insert(filterby);
		div.insert(groupby);
		div.insert(new Element('br', {'class':'end'}));
		div.observe("DataGrouper:groupChanged", this.setSelected.bind(this)); //update selectedItem when changed
		return div;
	}
});

var DataGrouper = Class.create({
	initialize: function(container, data, groupSet, formatter) {
		Element.extend(container);
		
		this.groupSet = groupSet;
		if (Prototype.Browser.IE) {
			var l = data.length;
			this.data = new Array(l);
			for (var i=0; i<l; ++i) {
				var el = data[i];
				this.data[i] = new Element(el.nodeName, {className:el.className}).update(el.innerHTML);
			}
		} else {
			this.data = $A(data);
		}
		
		
		this.controller = new Element('div', {'class':'DataGrouperController'})
		container.update(this.controller); //empty div first
		this.content = new Element('div', {'class':'DataGrouperContent'});
		container.insert(this.content);
		
		this.formatter = formatter;
		
		container.observe('DataGrouper:groupChanged', this.update.bind(this));
		container.observe('DataGrouper:filterChanged', this.update.bind(this));
		
		this.output();
	},
	output: function() {
		var html = this.groupSet.toHTML();
		this.controller.update(html);
		this.controller.select("input[type='radio']")[0].checked = true;
		this.update();
	},
	update: function() {
		this.content.update();
		var matches = new Array();
		var dl = this.data.length;
		var gl = this.groupSet.groups.length;
		
		for (var d=0; d<dl; ++d) {
			var el = this.data[d];
			var matched = true;
			var g=0;
			//El must belong in all groups
			while (matched && g<gl) {
				var group = this.groupSet.groups[g++];
				if (group==this.groupSet.selectedItem) continue; //check later
				matched = group.test(el);
			}
			if (matched) {
				//Only push if still matched
				matches.push(el);
			}
		}
			
		var group = this.groupSet.selectedItem;
		var l = group.filters.length;
		
		var noneFound = true;
		
		for (var f=0; f<l; ++f) {
			var filter = group.filters[f];
			var result;
			
			if (group.allDisabled || filter.enabled) { //if group is disabled or filter is enabled, run as normal
				result = filter.process(matches);
			} else { //group is enabled, but this filter isn't, so use null set
				result = [];
			}
			if (result.length > 0) {
				noneFound = false;
				var div = this.formatter.format(filter, result);
				this.content.insert(div);
			}
		}
		
		if (noneFound) {
			this.content.insert('<h2>No results found.</h2><p><em>Try setting less specific filters.</em></p>');
		}
	}
});

var GroupFormatterUL = Class.create({
	format: function(filter, items) {
		var ret = new Element('div', {'class':'DataGroup'});
		ret.insert('<h2>'+filter.groupLabel+'</h2>');
		var ul = new Element('ul');
		ret.insert(ul);
		var l = items.length;
		for (var i=0; i<l; ++i) {
			ul.insert('<li>'+items[i].innerHTML+'</li>');
		}
		if (l==0) {
			ul.insert('<em>(no matches)</em>');
		}
		ret.insert(new Element('br', {'class':'end'}));
		return ret;
	}
});