function SubjectSelector(clientObjectId, listContainer, pathContainer, valueCSV, resultsPane, selectedValues, settings){
	var that = this;
	var isInit = true;
	listContainer = $('#'+listContainer);
	pathContainer = $('#'+pathContainer);
	resultsPane = $('#'+resultsPane);
	var body = resultsPane.parent();
	var header = body.next();
	var summary = $('ul', header);
	valueCSV = $('#'+valueCSV);
	this.selectedValues = [];
	$.each(selectedValues || [], function(){
		for (var i=0;i<that.selectedValues.length;i++)
			if (this.nodeId == that.selectedValues[i].nodeId)
				return;
		that.selectedValues.push(this);
	});
	var inSearch = false;
	var initialResults = listContainer[0].innerHTML;//this is restored when "everything" is clicked - it can be overwritten
	var originalResults = initialResults;//so this one is here to maintain the true start point
	var working = false;
	var selectedMap = {};
 
	var settings = jQuery.extend({
		maxSelections: 1,
		quantifierTitle: 'Enter Value',
		sortable: false,
		quantify: false,
		selectionType: 'flat',
		collapsable: false,
		quantifyOptions: null,
		emptySummary: 'Nothing selected, click the plus to add items.',
		searchFunction: function(a,b,c,d,e){
			return sv.getSubjectSearch(a,b,c,d,e);
		}
	}, settings);
	if (settings.searchInput){
		if (settings.searchButton){
			if (settings.selectionType == 'combobox'){
				listContainer.mousedown(function(event){
					if (event.target.tagName == 'UL'){
						blockHide = true;
						setTimeout(function(){
							settings.searchInput.focus();
							blockHide = false;
						}, 100);
					}
				});
				settings.searchButton.click(function(e){
					if (isBoxShown)
						that.hideSearchBox();
					else{
						if (inSearch)
							that.gotoRoot();
						that.showSearchBox(true);
					}
					e.preventDefault();
				});
			}else{
				settings.searchButton.click(function(e){
					that.search(settings.searchInput.val());
					e.preventDefault();
				});
			}
		}
		settings.searchInput.keypress(function(e){
			if ((e.keyCode || e.which) == 13){
				var toClick = $('li.KeySelect a span',listContainer);
				if (toClick.length == 0){
					fromKeyPress = true;
					toClick = $('li.KeySelect a',listContainer);
				}
				if (toClick.length > 0){
					toClick.click();
					settings.searchInput.val('');
					that.hideSearchBox();
				}else{
					that.search(settings.searchInput.val());
				}
				e.preventDefault();
			}
		});
		settings.searchInput.keyup(function(e){
			switch (e.keyCode || e.which){
			case 38: // up
				that.moveKeySelect(-1);
				e.preventDefault();
				break;
			case 40: // down
				that.moveKeySelect(1);
				e.preventDefault();
				break;
			case 39: // right
				var toClick = $('li.KeySelect a.HasChildren',listContainer);
				if (toClick.length > 0){
					fromKeyPress = true;
					toClick.click();
					e.preventDefault();
				}
				break;
			case 37: // left
				var toClick = $('li a.SelectorBack',listContainer);
				if (toClick.length > 0){
					fromKeyPress = true;
					toClick.click();
					e.preventDefault();
				}
				break;
			case 27: // escape
				active = -1;
				that.moveSelect(0);
				that.hideSearchBox();
				break;
			default:
				if (settings.selectionType == 'combobox'){
					setTimeout(function(){
						if (settings.searchInput.val().length >= 2){
							that.search(settings.searchInput.val());
							that.showSearchBox();
						}else if (inSearch)
							that.hideSearchBox();
							//that.drillNode({nodeId:0, nodeName:'Everything'});
					}, 25);
				}
			}
		});
	}

	var path = [{nodeId:0, nodeName:'Everything'}];
	$('.SelectorToggleCollapse', header).click(function(event){
		event.preventDefault();
		event.stopPropagation();
		$(this).toggleClass('SelectorToggleExpanded');
		that.buildSummary();
		body.slideToggle('normal');
		summary.toggle('normal');
	});
	if (settings.collapsable){
		$(document).click(function(event){
			if (body.attr('offsetHeight') > 0){
				for (var elem=event.target;elem!=null;elem=elem.parentNode)
					if (elem == body[0])
						return;
				$('.SelectorToggleCollapse', header).click();	
			}
		})
	}
	if (settings.quantify){
		var vals = $('#'+settings.qualifierField).val().split('\n');
		for (var i=0;i<vals.length;i++){
			var val = vals[i].split('\t');
			var nodeId = parseInt(val[0])
			for (var j=0;j<that.selectedValues.length;j++)
					if (that.selectedValues[j].nodeId == nodeId)
						that.selectedValues[j].quantifier = val[1];
		}
	} 
	this.buildSummary = function(){
		summary.empty();
		var found = false
		var depth = 999;
		$.each(that.selectedValues, function(){
			if (this.selectable && (this.nodeDepth||-1) <= depth){
				if (!found){
					found = true;
					depth = this.nodeDepth;
				}
				summary.append($('<li></li>').html(this.acronym ? '<acronym title="'+this.nodeName+'">'+this.acronym+'</acronym>' : this.nodeName));
			}
		});
		if (!found)
			summary.append($('<li></li>').text(settings.emptySummary));
		$('li:last-child', summary).addClass('Last');
	}
	this.drillNode = function(node, newPath, replacePath){
		if (working)
			return;
		var foundItem = -1;
		if (settings.selectionType == 'combobox'){
			that.showSearchBox();
			settings.searchInput.focus();
		}
		for(var i=0;i<path.length;i++)
			if(path[i].nodeId == node.nodeId)
				foundItem = i;
		if (foundItem >= 0){
			lastSearch = null;
			if (path[foundItem].nodeId == 0){
				path = [{nodeId:0, nodeName:'Everything'}];
				that.animateIn(initialResults, inSearch);
				that.rebuildPath();
				inSearch = false;
				return;
			}
			if (foundItem == path.length - 1 && !inSearch)
				return this.selectNode(node);
			for (;path.length>foundItem+1;path.pop());
			if (path[foundItem].results){
				that.rebuildPath();
				that.animateIn(path[foundItem].results, inSearch);
				inSearch = false;
			}else{
				working = true;
				sv.getChildSubjectsList(clientObjectId, path[foundItem].nodeId, settings.maxDepth, function(response){
					path[foundItem].results = response.result;
					that.rebuildPath();
					inSearch = false;
					that.animateIn(response.result, inSearch);
				}); 
			}
		}else{
			working = true;
			sv.getChildSubjectsList(clientObjectId, node.nodeId, settings.maxDepth, function(response){
				if ((inSearch || replacePath) && newPath)
					path = newPath;
				node.results = response.result;
				path.push(node);
				that.rebuildPath();
				inSearch = false;
				that.animateIn(response.result, true);
			});
		}
	}
	this.toggleNode = function(node, newPath, skipRegen){
 		if (node.nodeId == 0)
			return;
		for (i=0;i<that.selectedValues.length; i++)
			if (that.selectedValues[i].nodeId == node.nodeId )
				return that.removeNode(node.nodeId);
		that.selectNode(node, newPath, skipRegen);
	}
	this.selectNode = function(node, newPath, skipRegen){
		if (!node) return;
		if (!node.selectable || node.nodeId == 0) return;
		if (settings.selectionType == 'combobox')
			settings.searchInput.focus();
		if (settings.maxSelections == 1)
			that.selectedValues = [];
		else if (that.selectedValues.length >= settings.maxSelections && settings.maxSelections != 0)
			return alert('You are only allowed '+settings.maxSelections+' selections - please delete one first.');
		for (var i=0;i<that.selectedValues.length;i++)
			if (that.selectedValues[i].nodeId == node.nodeId)
				return;
		node.path = inSearch ||((newPath || {}).length > 0) ? newPath : jQuery.makeArray(path);
		if (settings.hierarchical && node.path.length > 0){
			var parentPath = $.makeArray(node.path);
			var parentNode = parentPath.pop(parentPath);
			that.selectNode(parentNode, parentPath, true);
		}	
		that.selectedValues.push(node);
		if (!skipRegen)
			that.regenerateSelected();
		that.markSelected();
		clearTimeout(hideBoxTimeout);
		hideBoxTimeout = null;
		settings.searchInput.focus();
		if (settings.maxSelections == 1)
			that.hideSearchBox();
	}
	var lastNodeIdList;
	this.changeEverything = function(nodeIdList){
		if (working || nodeIdList == lastNodeIdList)
			return;
		lastNodeIdList = nodeIdList;
		working = true;
		sv.getSubjectsList(clientObjectId, nodeIdList, settings.maxDepth, function(response){
			path = [{nodeId:0, nodeName:'Everything'}];
			that.rebuildPath();
			inSearch = false;
			initialResults = response.result;
			that.animateIn(response.result, true);
		});
		that.markSelected();
	}
	this.gotoRoot = function(){
		that.drillNode({nodeId:0, nodeName:'Everything'});
	}
	var joinedSubjectReturn = function(response){
		working = false;
		if (response.result == '')
			that.restoreEverything();
		else{
			path = [{nodeId:0, nodeName:'Everything'}];
			that.rebuildPath();
			inSearch = false;
			initialResults = response.result;
			that.animateIn(response.result, true);
		}
	}
	this.showJoinedSubjectsFromName = function(nodeName, nodeTypeId, subjectTypeId){
		var cleartime = function(){
			if (that.lastWait){
				clearTimeout(that.lastWait);
				that.lastWait = null;
			}
		}
		var todo = function(){
			cleartime();
			if (working)
				return that.lastWait = setTimeout(todo, 200);
			working = true;
			sv.getJoinedSubjectsListFromName(clientObjectId, nodeName, nodeTypeId, subjectTypeId, settings.maxDepth, joinedSubjectReturn);
		}
		cleartime();
		that.lastWait = setTimeout(todo, 100);
	}	
	this.showJoinedSubjects = function(nodeId, subjectTypeId){
		var cleartime = function(){
			if (that.lastWait){
				clearTimeout(that.lastWait);
				that.lastWait = null;
			}
		}
		var todo = function(){
			cleartime();
			if (working)
				return that.lastWait = setTimeout(todo, 200);
			working = true;
			sv.getJoinedSubjectsList(clientObjectId, nodeId, subjectTypeId, settings.maxDepth, joinedSubjectReturn);
		}
		cleartime();
		that.lastWait = setTimeout(todo, 100);
	}
	this.restoreEverything = function(){
		if (working || originalResults == initialResults)
			return;
		lastNodeIdList = null;
		path = [{nodeId:0, nodeName:'Everything'}];
		that.rebuildPath();
		that.animateIn(originalResults, true);
		initialResults = originalResults;
		inSearch = false;
		that.markSelected();
	}
	this.buildQuantifier = function(indx, currentItem, beforeLink){
		if (currentItem == null || (currentItem || {}).length == 0)
			return;
		var qf = that.selectedValues[indx].quantifier;
		if (qf == null || qf == '')
			qf = settings.quantifierTitle;
//			qf = '['+settings.quantifierTitle+']';
		var quant;
		//create the quantifier field
		//if we don't have any options then it's an input
		var haschild = $('a', currentItem).hasClass('HasChildren') ? ' HasChildren' : '';
		if (settings.quantifyOptions == null)
			quant = $.INPUT({type: 'text', value: qf, className: 'Quantifier'+haschild})
				.focus(function(){if (this.value == '['+settings.quantifierTitle+']') this.value = '';})
				.blur(function(){if (this.value == '') this.value = '['+settings.quantifierTitle+']';});
		//otherwise it's a select box
		else{
			quant = $("<select></select>").attr({className: 'Quantifier'+haschild});
			quant.append(
				$("<option></option>").attr({value: ''}).text(settings.quantifierTitle)
//				$("<option></option>").attr({value: ''}).text('['+settings.quantifierTitle+']')
			);
			for (var opin=0;opin<settings.quantifyOptions.length;opin++)
				quant.append(
					$("<option></option>")
						.attr({value: settings.quantifyOptions[opin].id})
						.text(settings.quantifyOptions[opin].text)
				);
			quant.val(that.selectedValues[indx].quantifier||'');
		}
		currentItem[beforeLink ? 'prepend' : 'append'](
			quant
			.change(function(){that.rebuildQualifiers()})
		);
		that.selectedValues[indx].quantifierElem = quant;
	}
	this.ensureSelected = function(path, indx){
		var i=settings.hierarchical ? 0 : path.length-1;
		var currentItem, ul;
		for (;i<path.length; i++){
			//don't show the first node on sirel stuff
			if (path[i].nodeId != 0 && path[i].selectable){
				if (settings.selectionType == 'tickonly'){
					if (settings.quantify){
						var wrapper = $('li[rel*='+that.selectedValues[indx].nodeId.toString()+']', listContainer);
						that.buildQuantifier(indx, wrapper, true);
					}
				}else if (selectedMap[path[i].nodeId.toString()] != null){
					currentItem = selectedMap[path[i].nodeId.toString()];
				}else if (!path[i].selectable && path[i].nodeId != 0){
					//if it's not selectable, set the path to its parent
					if (currentItem)
						selectedMap[path[i].nodeId.toString()] = currentItem;
				}else{
					currentItem = $.LI({nodeId: path[i].nodeId});
					var title = $.SPAN({className: 'SelectedSubjectHandle'});
					title.append(path[i].acronym ? '<acronym title="'+path[i].nodeName+'">'+path[i].acronym+'</acronym> ' : path[i].nodeName + ' ');
					currentItem.append(title);
					//if this is the first item in the path then we can add it to the root
					//(we don't display "everything" in the results; id=0)
					var topLevel = false;
					if (i==0 || !settings.hierarchical? true : path[i-1].nodeId == 0){
						//debugger;
						ul = resultsPane;
						topLevel = true;
					}else{
						//find the list that we want to add it to
						var parent = selectedMap[path[i-1].nodeId.toString()] || resultsPane.parent();
						if (parent){
							ul = $('ol,ul', parent);
							topLevel = true;
							for (var j=0;j<i;j++)
								if (path[j].selectable)
									topLevel = false;
						}else{
							ul = resultsPane;
							topLevel = true;
						}
						//if there's a list already below the parent item
						if (ul.length > 0)
							ul = $(ul[0]);
						//else there's not a sub list under the parent so create a new one
						else {
							ul = $.UL();
							if (settings.hierarchical)
								ul.addClass('SubItemList');
							//ul = settings.sortable ? $.OL(): $.UL();
							parent.append(ul);
						}
					}
					if (topLevel)
						currentItem.addClass('SelectedSubject');
					currentItem.addClass('Selector'+path[i].nodeNameClean);
					selectedMap[path[i].nodeId.toString()] = currentItem;
					
					//if it's a sirel selector then don't show the quantifier if it's not the top
					if (settings.quantify && (topLevel || settings.selectionType != 'sirel'))
						that.buildQuantifier(indx, currentItem, false);

					currentItem.append(
						$.A({href: '#'}).append('<img src="/Templates/Sirel/sirel_images/smallcross.png" alt="remove" /> ').click(function(){that.removeNode(that.selectedValues[indx].nodeId); return false;})
					);
					ul.append(currentItem);
				}
			}
		}
	}
	this.regenerateSelected = function(){
		resultsPane.empty();
		$('select', listContainer).remove();
		$('input', listContainer).remove();
		selectedMap = {'0':resultsPane};
		var csv = '';

		that.selectedValues = that.selectedValues.sort(function(a,b){
			return a.nodeDepth - b.nodeDepth;
		});
		for(var i=0;i<that.selectedValues.length;i++){
			if (i>0)
				csv+=',';
			csv += that.selectedValues[i].nodeId;
			var fullPath = $.makeArray(that.selectedValues[i].path);
			fullPath.push(that.selectedValues[i]);
			that.ensureSelected(fullPath, i)
		}
		valueCSV.val(csv);
		try{
			if (!isInit)			
				valueCSV.change();
		}catch(err){
		}
		that.rebuildQualifiers();
		if (settings.sortable){
			resultsPane.Sortable({
				accept : 'SelectedSubject',
				handle: 'span.SelectedSubjectHandle',
				activeclass : 'SelectedSubjectHover',
				hoverclass : 'sortablehover',
				helperclass : 'sorthelper',
				opacity: 	0.5,
				axis: 'vertically',
				fit :	true,
				onStop : function(){
					var newValues = [];
					var items = $('li', resultsPane);
					var csv = '';
					for (var i=0;i<items.length;i++){
						if (i>0)
							csv+=',';
						csv += items[i].nodeId;
						for (var j=0;j<that.selectedValues.length;j++)
							if (items[i].nodeId == that.selectedValues[j].nodeId)
								newValues.push(that.selectedValues[j]);
					}
					valueCSV.val(csv);
					try{
						if (!isInit)
							valueCSV.change();
					}catch(err){
					}
					that.selectedValues = newValues;
					that.rebuildQualifiers();
				}
			});
		}
	}
	this.rebuildQualifiers = function(){
		var value = '';
		for (var i=0;i<that.selectedValues.length; i++){
			if (that.selectedValues[i].quantifierElem){
				if (value.length > 0)
					value += '\n';
				that.selectedValues[i].quantifier = that.selectedValues[i].quantifierElem.val();
				value += that.selectedValues[i].nodeId + '\t' + that.selectedValues[i].quantifier;
			}
		}
		$('#'+settings.qualifierField).val(value);
	}
	this.removeNode = function(nodeId){
		that.selectedValues = jQuery.grep(that.selectedValues, function(node,i){
			//remove from list if the node we're removing is the current one...
			if (nodeId == node.nodeId)
				return false;
			//...or if it's the parent of the current one
			if (settings.hierarchical)
				for (var j=0;j<node.path.length;j++)
					if (nodeId == node.path[j].nodeId)
						return false;
			return true;
		})
		that.regenerateSelected();
		that.markSelected();
	}
	this.rebuildPath = function(){
		pathContainer.empty();
		var first = true;
		$.each(path, function (){
			var tother = this;
			var link = $('<a href="#"></a>')
				.html(tother.acronym ? '<acronym title="'+tother.nodeName+'">'+tother.acronym+'</acronym>' : tother.nodeName)
				.click(function(event){
					that.drillNode(tother);
					event.stopPropagation();
					return false;
				});
			if (!first)
				pathContainer.append(' &gt; ');
			first = false;
			pathContainer.append(link);
		});
	}
	var lastSearch = null;
	var searchTimeout = null;
	this.search = function(term){
		if (lastSearch == term)
			return;
		if (searchTimeout){
			clearTimeout(searchTimeout);
			searchTimeout = null;
		}
		searchTimeout = setTimeout(function(){
			working = true;
			settings.searchInput.addClass('Searching');
			lastSearch = term;
			settings.searchFunction(clientObjectId, term, settings.subjectTypeId, settings.maxDepth, function(response){
				if (lastSearch == term){
					inSearch = true;	
					settings.searchInput.removeClass('Searching');			
					that.animateIn(response.result, true, true);
					if (settings.selectionType == 'combobox'){
						that.showSearchBox();
						active = 0;
						that.moveKeySelect(0);
					}
				}
			});
		}, 400);
	}
	this.animateIn = function(ulHtml, toLeft, fromSearch){
		active = fromKeyPress ? 0 : -1;
		fromKeyPress = false;
		working = true;
		var scrollTop = listContainer[0].scrollTop;
		var widthpx = listContainer[0].clientWidth + 'px';
		var temp = $(document.createElement('div'));
		temp.append(ulHtml)
		var newul = $('ul', temp);
		newul.css({
			'left': (toLeft ? '': '-') + widthpx,
			'position': 'absolute'
		});
		var oldul = $('ul', listContainer);
		oldul.css({position:'absolute',top: '-'+scrollTop+'px'});
		listContainer[0].scrollTop = 0;
		listContainer.append(newul);
		newul = $('ul:nth-child(2)', listContainer);
		if ((inSearch && settings.selectionType != 'combobox') || path.length>1 )
			newul.prepend($('<li></li>').append($('<a class="SelectorBack"><span></span> Back</a>').click(
				function(event){
					event.stopPropagation();
					if (inSearch){
						inSearch = false;
						that.gotoRoot();
					}else
						that.drillNode(path[path.length-2]);
				}
			)))
		//set up the qualifiers
		if (settings.quantify && settings.selectionType == 'tickonly'){
			for (var i=0; i<that.selectedValues.length; i++){
				var wrapper = $('li[rel*='+that.selectedValues[i].nodeId.toString()+']', newul);
				that.buildQuantifier(i, wrapper, true);
			}
		}
		if (body[0].offsetHeight > 0 || (settings.selectionType == 'combobox' && !fromSearch)){
			newul.css('overflow','hidden');
			newul.animate({'left':'0'});
			var lastOverflow = listContainer.css('overflow');
			listContainer.css('overflow', 'hidden');
			oldul.css('overflow','hidden');
			oldul.animate({marginLeft:(toLeft ? '-': '') + widthpx},{complete:function(){
				newul.css('overflow-y','auto');
				newul[0].scrollTop = 0;
				listContainer.css('overflow', lastOverflow);
				$(this).remove();
				newul.css({'top': 0, 'position': 'static', 'left': 0});
				working = false;
				that.moveKeySelect(0);
			}});
		}else{
			oldul.remove();
			newul.css({'top': 0, 'position': 'static', 'left': 0});
			working = false;
			that.moveKeySelect(0);
		}
		this.markSelected();
	}
	var hideBoxTimeout = null;
	var isBoxShown = false;
	var boxMoved = false;
	var blockBox = false;
	var blockHide = false;
	this.showSearchBox = function(){
		if (!blockBox){
			if (settings.selectionType == 'combobox'){
				if (hideBoxTimeout){
					clearTimeout(hideBoxTimeout);
					hideBoxTimeout = null;
				}
				listContainer.show();
				isBoxShown = true;
				settings.searchInput.focus();
			}
		}
	}
	this.hideSearchBox = function(){
		if (!blockHide && !hideBoxTimeout && isBoxShown && settings.selectionType == 'combobox')
			hideBoxTimeout = setTimeout(function(){
				listContainer.hide();
				isBoxShown = false;
				hideBoxTimeout = null;
			}, 200);
	}
	var active = -1;
	this.moveKeySelect = function(step){
		var lis = $("li", listContainer);
		if (!lis) 
			return;
		active += step;
		if (active < -1) {
			active = -1;
		} else if (active >= lis.size()) {
			active = lis.size() - 1;
		}
		lis.removeClass('KeySelect');
		if (active >= 0){
			$(lis[active]).addClass('KeySelect'); 
			//check to see we can see the thing that's selected
			var selected = $("li.KeySelect", listContainer);
			var ul = selected.parent();
			if (selected.height() + selected[0].offsetTop - ul[0].scrollTop > listContainer[0].scrollHeight)
				ul[0].scrollTop = ul[0].scrollTop + selected.height();
			else if (selected[0].offsetTop < ul[0].scrollTop)
				ul[0].scrollTop = ul[0].scrollTop - selected.height();
		}
	}
	var fromKeyPress = false;
	this.setMaxSelections = function(max){
		settings.maxSelections = max;
	}
	this.clear = function(){
		that.hideSearchBox();
		blockBox = true;
		that.selectedValues = [];
		that.gotoRoot();
		that.restoreEverything();
		that.regenerateSelected();
		that.buildSummary();
		blockBox = false;
	}
	this.markSelected = function(){
		var elems = $('ul li', listContainer);
		for (var i=0; i<elems.length; i++){
			var el = $(elems[i]);
			el.removeClass('ToggleTicked');
			for (var j=0;j<that.selectedValues.length;j++)
				if (parseInt(el.attr('rel')) ==  that.selectedValues[j].nodeId)
					el.addClass('ToggleTicked');	
		}
	}
	if (settings.searchInput && settings.selectionType == 'combobox'){
		settings.searchInput.blur(that.hideSearchBox);
		//settings.searchInput.focus(that.showSearchBox);
		if (settings.searchButton){
			//settings.searchButton.blur(that.hideSearchBox);
			//settings.searchButton.focus(that.showSearchBox);
			settings.searchButton.click(function(){
				settings.searchInput.val('');
				settings.searchInput.focus();
				if (inSearch)
					that.gotoRoot();
				that.showSearchBox();
			});
		}
		listContainer.blur(that.hideSearchBox);
		listContainer.focus(that.showSearchBox);
	}
	this.rebuildPath();
	this.regenerateSelected();
	this.markSelected();
	this.buildSummary();
	isInit = false;
	return this;
}