function SpeciesSelector(activateCallback, gotListCallback) {
	this.input = null;
	this.outputHead = null;
	this.outputScrollContainer = null;
	this.outputBody = null;
	
	this.autoResize = true;
	
	this.activateCallback = activateCallback;
	this.gotListCallback = gotListCallback;
	this.openGroupsOnMiddleBar = true;
	
	this.taxonsRightIcon = null;
	
	this.searchResultSize = 0;
	this.taxonList = {};
	this.taxonListIds = {};
	this.treePath = [];

	this.currentSearchURL = '';
	this.currentSearchParams = {};
	
	this.baseSearchURL = '';
	this.baseSearchParams = {};
	
	this.arrowKeyRepeatTimeout = null;
	
	this.currentHilight = null; /* null: input, >= 0: taxons in list, < 0: path */
	this.pathStack = [];
	
	
	var userAgent = navigator.userAgent.toLowerCase();
    this.isIE = (userAgent.indexOf('msie') != -1 && userAgent.indexOf('opera') == -1 &&
                        userAgent.indexOf('webtv') == -1);
	this.needPngHack = false;
    if (this.isIE) {
        var version = parseFloat(userAgent.substring(userAgent.indexOf('msie ') + 5));
        if (version >= 5.5 && version < 7) {
            this.needPngHack = true;
        }
    }	
	
	this.nurGefunden = 0;
	
	this.answerLength = 30; /* set in server */	
   	this.partsRequested = {};
   	this.scrollRequest = -1;
   	this.checkScrollRequestTimeout = null;
   	
	this.entryHeight = 40; /* pixels */
	this.entryIndent = 10; /* pixels */
	this.entryWidth = 535; /* pixels */
	
	this.lastSearchRequestValue = null;
	this.lastSearchRequestNumber = -1;
	
	this.aktionsId = -1;
	this.year = -1;
}
SpeciesSelector.prototype = {
	registerCallbacks: function() {
		var lthis = this;
				
		this.input = document.getElementById('speciesInput');
		this.input.value = 'nach Artnamen suchen';
		this.input.onkeyup = function(e) { return lthis.inputKeyUp(e || window.event); }
		this.input.onfocus = function(e) { lthis.inputHasFocus = true; lthis.setHilight(null); }
		this.input.onblur = function(e) { window.setTimeout(function() {lthis.inputHasFocus = false;}, 10); };
		this.input.value = 'nach Artnamen suchen';
		this.input.focus();
		this.input.select();
		this.outputHead = document.getElementById('speciesListHead');
		this.outputScrollContainer = document.getElementById('speciesListScrollContainer');
		this.outputScrollContainer.onscroll = function() { lthis.containerScrolled(); };
		this.outputScrollContainer.onmousemove = function(e) { lthis.containerMouseMoved(e); };
		this.outputBody = document.getElementById('speciesList');
		
		document.onkeydown = function(e) { return lthis.keyDown(e || window.event); };
		document.onkeypress = function(e) { return lthis.keyPress(e); };
		document.onkeyup = function(e) { return lthis.keyUp(e || window.event); };
	},
	
	inputKeyUp: function(e) {
            	this.setArrowKeyUp();
        switch (e.keyCode) {
            case 13: /* return */
                this.searchSpecies(true);
                return false;
            case 27: /* esc */
            	this.input.select();
                return false;
            case 38: /* up */
            	return false;
            case 40: /* down */
            	return false;
            case 33: /* pgup */
            	this.moveHilight(-7);
            	return false;
            case 34: /* pgdown */
            	this.moveHilight(7);
            	return false;
        }
        var lthis = this;
        window.setTimeout(function() { lthis.searchSpecies(); }, 100)
    },
    
    keyDown: function(e) {
        switch (e.keyCode) {
            case 38: /* up */
            	this.setArrowKeyDown(-1);
            	return false;
            case 40: /* down */
            	this.setArrowKeyDown(1);
            	return false;
        }
    },
    
    keyUp: function(e) {
            	this.setArrowKeyUp();
    	if (this.inputHasFocus) return;
        switch (e.keyCode) {
            case 13: /* return */
            case 32: /* space */
            	this.activateTaxon(this.currentHilight);
                return false;
            case 27: /* esc */
            	this.setHilight(null);
            	this.input.select();
                return false;
            case 38: /* up */
            	return false;
            case 40: /* down */
            	return false;
            case 33: /* pgup */
            	this.moveHilight(-7);
            	return false;
            case 34: /* pgdown */
            	this.moveHilight(7);
            	return false;
            case 37: /* left */
            	this.closeTaxon(-1);
            	return false;
            case 39: /* right */
            	this.openTaxon(this.currentHilight);
            	return false;
        }
    },
    
    keyPress: function(e) {
    	/* disable automatic scrolling by up/down keys */
    	var charCode = 0;
    	if (e == null) {
    		charCode = window.event.keyCode;
    	} else {
    		charCode = e.charCode;
    	}
    	e = e || window.event;
    	if (e.keyCode == 38 || e.keyCode == 40)
    		return false;
    	
    	if (!this.inputHasFocus) {
    		if (charCode >= 65 && charCode <= 90) {
	        	this.input.value = String.fromCharCode(charCode + 32);
    		} else if (charCode >= 97 && charCode <= 122) {
	        	this.input.value = String.fromCharCode(charCode);
    		} else {
    			return;
    		}
        	this.input.focus();
        	this.inputHasFocus = 1;
        	return false;
        }
    },
    
    setArrowKeyDown: function(key) {
    	this.moveHilight(key);
    	if (this.arrowKeyRepeatTimeout != null) {
    		window.clearTimeout(this.arrowKeyRepeatTimeout);
    	}
    	var lthis = this;
    	this.arrowKeyRepeatTimeout = window.setTimeout(function() { lthis.arrowKeyRepeat(key); }, 800);
    },
    
    setArrowKeyUp: function() {
    	if (this.arrowKeyRepeatTimeout != null) {
    		window.clearTimeout(this.arrowKeyRepeatTimeout);
    		this.arrowKeyRepeatTimeout = null;
    	}    	
    },
    
    arrowKeyRepeat: function(key) {
   		this.moveHilight(key);
   		var lthis = this;
    	this.arrowKeyRepeatTimeout = window.setTimeout(function() { lthis.arrowKeyRepeat(key); }, 100);
    },
    
    resizeOutputList: function() {
    	if (!this.autoResize) return;
    	var pos = getAbsolutePosition(this.outputScrollContainer);
    	var innerSize = getInnerSize();
    	this.outputScrollContainer.style.height = (innerSize[1] - pos[1] - 30) + 'px';
    },
    
    hideList: function() {
    	this.outputScrollContainer.style.display = 'none';
    },
    
    showList: function() {
       	this.outputScrollContainer.style.display = 'block';
    },
    
    startNewOutput: function(url, params) {
    	this.treePath = [];
		this.updateTreePath();
		
    	this.baseSearchURL = url;
    	this.baseSearchParams = params;
    	this.pathStack = [];
    	this.setHilight(null);
    	this.lastSearchRequestValue = '';

		this.loadSearchResults(url, params, {});
    },
    
    showAllKingdoms: function() {
    	this.nurGefunden = 0;
    	this.startNewOutput('zeige subtaxa', {id: 0, aktion: this.aktionsId, year: this.year, nurGefunden: this.nurGefunden});
    },
    
    showSpeciesFound: function() {
    	this.nurGefunden = 1;
    	this.startNewOutput('zeige subtaxa', {id: 0, aktion: this.aktionsId, year: this.year, nurGefunden: this.nurGefunden});
    },

	activateTaxon: function(taxonInList) {
		var taxon = this.getTaxonByIndex(taxonInList);
		if (taxon == null) return;
		
		if (taxonInList < 0) {
			this.closeTaxon(taxonInList);
		} else if (taxonInList >= 0 && (taxon.findable || (!this.openGroupsOnMiddleBar))) {
			this.activateCallback(taxon);
		} else if (taxon.hasChildren) {
			this.openTaxon(taxonInList);
		}
	},
	
	closeTaxon: function(taxonInList) {
		if (taxonInList >= 0) return;
		if (taxonInList <= -this.treePath.length) {
			if (this.pathStack.length == 0) return;
			
			this.treePath = [];
			var infoForLoad = this.pathStack.pop();
			this.setHilight(infoForLoad.hilight);
			this.pathStack = [];
			this.updateTreePath();
			
	    	this.loadSearchResults(this.baseSearchURL, this.baseSearchParams, infoForLoad);
		} else {
			this.openTaxon(taxonInList - 1);
		}
	},
	
	openTaxon: function(taxonInList) {
		var taxon = this.getTaxonByIndex(taxonInList);
		if (taxon == null || !taxon.hasChildren) return;
		
		var infoForLoad = {};
		
		if (taxonInList < 0) {
			if (taxonInList < -1) {
				infoForLoad = this.pathStack[-taxonInList - 2];
				this.treePath = this.treePath.slice(-taxonInList - 1);
				this.setHilight(infoForLoad.hilight);
				this.pathStack = this.pathStack.slice(-taxonInList - 1);
			}
		} else {
			var wasHilit = (this.currentHilight == taxon.index);
			taxon.index = -1;
			this.pathStack.unshift({hilight: this.currentHilight,
									scrollTop: this.outputScrollContainer.scrollTop,
									height: this.outputBody.clientHeight,
									resultSize: this.searchResultSize});
			if (wasHilit) {
				this.currentHilight = -1;
			}
			this.treePath.unshift(taxon);
			for (var i = 1; i < this.treePath.length; i ++) {
				this.treePath[i].index = -i - 1;
			}
		}
		this.updateTreePath();
		
    	this.loadSearchResults('zeige subtaxa', {id: taxon.id, aktion: this.aktionsId, year: this.year, nurGefunden: this.nurGefunden}, infoForLoad);
	},

	moveHilight: function(amount) {
		var newHilight;
		if (this.currentHilight == null) {
			if (amount > 0) {
				newHilight = -this.treePath.length + amount - 1;
			} else {
				return;
				//this.setHilight(this.searchResultSize - amount);
			}
		} else {
			newHilight = this.currentHilight + amount;
		}
		if (newHilight >= this.searchResultSize) {
			newHilight = this.searchResultSize - 1;
		}
		if (newHilight <= -this.treePath.length - 1) {
			newHilight = null;
		}
		if ((newHilight < 0 || newHilight == null) && this.currentHilight >= 0) {
			this.outputScrollContainer.scrollTop = '0px';
		}
		this.setHilight(newHilight);
	},
	
	setHilight: function(i, noScroll) {
		this.unhilight();
		if (i == null) {
			this.currentHilight = null;
			this.input.focus();
			return;
		}
		this.input.blur();
		this.currentHilight = i;
		if (i >= 0 && noScroll != true) {
			this.scrollIntoView(i);
		}
		var t = this.getHilightTaxon();
		if (t != null) {
			t.hilight();
		}
	},
	
	unhilight: function() {
		var t = this.getHilightTaxon();
		if (t != null) t.unhilight();
	},
	
	scrollIntoView: function(index) {
		var scroll = this.outputScrollContainer.scrollTop / this.entryHeight;
		var height = Math.floor(this.outputScrollContainer.clientHeight / this.entryHeight);
		if (scroll > index - 1) {
			scroll = index - 1;
		}
		if (scroll < index - height + 1) {
			scroll = index - height + 1;
		}
		if (scroll < 0) scroll = 0;
		this.outputScrollContainer.scrollTop = scroll * this.entryHeight;
		this.containerScrolled();
	},
	
	getTaxonByIndex: function(i) {		
		if (i < 0) {
			return this.treePath[-i - 1];
		} else if (i >= 0) {
			return this.taxonList[i];
		}		
	},
	
	getHilightTaxon: function() {
		if (this.currentHilight == null) return null;
		return this.getTaxonByIndex(this.currentHilight);
	},
    
    searchSpecies: function(force) {
    	if (this.input.value == this.lastSearchRequestValue ||
    			(this.input.value.length < 3)) return;

		this.nurGefunden = 0;
		this.startNewOutput('suche taxon', {name: this.input.value, aktion: this.aktionsId, year: this.year});
    },
    
    updateTreePath: function() {
    	clearNode(this.outputHead);
    	var j = 0;
    	for (var i = this.treePath.length - 1; i >= 0; i --) {
    		this.treePath[i].topPos = 0;
    		this.treePath[i].indent = j * this.entryIndent;
    		this.treePath[i].updateHTML();
    		this.outputHead.appendChild(this.treePath[i].getHTMLNode());
    		j ++;
    	}
    	var lthis = this;
    	window.setTimeout(function() { lthis.resizeOutputList();}, 10);
    },
    
    loadSearchResults: function(url, params, dimensionInfo) {
    	this.currentSearchURL = url;
    	this.currentSearchParams = params;
    	this.lastSearchRequestNumber += 1;
    	this.lastSearchRequestValue = '';
    	this.clearResultList();
    	
    	if (dimensionInfo.height != null)
    		this.outputBody.style.height = dimensionInfo.height + 'px';
    	if (dimensionInfo.resultSize != null)
    		this.searchResultSize = dimensionInfo.resultSize;
    	if (dimensionInfo.scrollTop != null) {
		    this.outputScrollContainer.scrollTop = dimensionInfo.scrollTop;
		    this.containerScrolled();
    	}

		var pos = 0;
		if (this.currentHilight != null && this.currentHilight > 0) {
			pos = this.currentHilight;
		}
		this.requestAtIndex(pos);
    },

    reloadSearchResults: function(offset) {
    	var params = this.currentSearchParams;
     	params['offset'] = offset; /* this is not a copy! */
		params['searchNumber'] = this.lastSearchRequestNumber;
    	var lthis = this;
     	var req = makeHTTPRequest('/ajax/' + this.currentSearchURL, urlencode(params),
    				function(request, success) { lthis.speciesSearched(request, success); });
    },
    
    speciesSearched: function(request, success) {
    	if (!success) return;
    	
    	var errors = request.responseXML.getElementsByTagName('error');
    	if (errors.length > 0) {
    		var msgs = errors[0].getElementsByTagName('message');
    		alert("Fehler bei der Datenbankabfrage: " + getNodeContent(msgs[0]));
    		return;
    	}
    	
    	var list = request.responseXML.getElementsByTagName('list')[0];
    	this.gotListCallback(list);
    	if (list.getAttribute('searchNumber') != this.lastSearchRequestNumber) return;

    	this.searchResultSize = list.getAttribute('numResults') - 0;
    	this.outputBody.style.height = (this.searchResultSize * this.entryHeight) + 'px';
    	
    	var offset = list.getAttribute('offset') - 0;
    	var items = request.responseXML.getElementsByTagName('item');
        for (var i = 0; i < items.length; i ++) {
        	if (this.taxonList[offset + i] != null) {
        		this.taxonList[offset + i].updateInformation(items[i]);
        	} else {
	        	var taxon = new TaxonInList(items[i], offset + i, this);
	        	taxon.topPos = (offset + i) * this.entryHeight;
	        	taxon.indent = this.treePath.length * this.entryIndent;
    	    	this.outputBody.appendChild(taxon.getHTMLNode())
	        	this.taxonList[offset + i] = taxon;
	        	this.taxonListIds[taxon.id] = taxon;
        	}
    	}
    	t = this.getHilightTaxon();
    	if (t != null) t.hilight();
    },
   
    clearResultList: function() {
    	clearNode(this.outputBody);
    	this.outputBody.style.height = '0px';
    	this.outputBody.style.top = '0px';
    	this.taxonList = {};
    	this.taxonListIds = {};
    	this.partsRequested = {};
    	this.scrollRequest = -1;
    },
    
    containerScrolled: function() {
    	var scrollPos = this.outputScrollContainer.scrollTop;
    	this.scrollRequest = Math.floor((scrollPos + this.outputScrollContainer.clientHeight * 0.5) / this.entryHeight);
    	if (this.checkScrollRequestTimeout == null) {
    		this.checkScrollRequest();
    	}
    },
    
    checkScrollRequest: function() {
    	if (this.scrollRequest < 0) {
    		this.checkScrollRequestTimeout = null;
    		return;
    	}
    	
    	this.requestAtIndex(this.scrollRequest);
    	this.scrollRequest = -1;
    	var lthis = this;
    	this.checkScrollRequestTimeout = window.setTimeout(function() { lthis.checkScrollRequest(); }, 500);
    },
    
    requestAtIndex: function(index) {
    	var part = Math.floor(index / this.answerLength);
    	this.requestPart(part - 1);
    	this.requestPart(part);
    	this.requestPart(part + 1);
    },
    
    requestPart: function(part) {
    	if (part < 0) return;
    	if (part * this.answerLength > this.searchResultSize) return;
    	if (this.partsRequested[part] == true) return;
    	
    	this.partsRequested[part] = true;
   		this.reloadSearchResults(part * this.answerLength);
    },
    
    containerMouseMoved: function(e) {
    	var mousePos = getEventPosition(e);
    	var listPos = getAbsolutePosition(this.outputScrollContainer);
    	var y = mousePos[1] - listPos[1] + this.outputScrollContainer.scrollTop;
    	
    	var index = Math.floor(y / this.entryHeight);
    	if (this.currentHilight != index && this.getTaxonByIndex(index) != null) {
    		this.setHilight(index, true);
    	}
    },

	updateInformation: function(items) {
    	for (var i = 0; i < items.length; i ++) {
    		var item = items[i];
	    	var t = this.taxonListIds[item.getAttribute('id') - 0];
	    	if (t != null) {
				t.updateInformation(item);
				t.updateHTML();
	    	}
	    	this.updateInformationInTree(item);
    	}		
	},
	
	updateInformationInTree: function(xmlData) {
		var id = xmlData.getAttribute('id') - 0;
		for (var i = 0; i < this.treePath.length; i ++) {
			if (this.treePath[i].id == id) {
				this.treePath[i].updateInformation(xmlData);
				this.treePath[i].updateHTML();
			}
		}
	}	
}

function TaxonInList(xml, index, speciesSelector) {
	this.speciesSelector = speciesSelector;
	this.updateInformation(xml);
	this.index = index;
	
	this.indent = 0;
	this.topPos = 0;
	
	this.node = null;
	this.leftPart = null;
	this.leftPartImage = null;
	this.arrow = null;
	this.middlePart = null;
	this.nameNode = null;
	this.synNode = null;
	this.higherNode = null;
	this.rightIcon = null;
	
	this.isHilit = false;
}

TaxonInList.prototype = {
	updateInformation: function(xml) {
		this.id = xml.getAttribute('id') - 0;
		this.name = getNodeContent(xml.getElementsByTagName('name')[0]);
		this.synonyms = [];
		this.hasChildren = (xml.getAttribute('hasChildren') != 0);
		this.findable = (xml.getAttribute('findable') != 0);
		this.found = (xml.getAttribute('found') != 0);
		this.subFound = (xml.getAttribute('sub_found') != 0);
		this.numFound = xml.getAttribute('num_found');
		this.higherRank = xml.getAttribute('higher_rank');
		
		var synonyms = xml.getElementsByTagName('synonym');
		for (var i = 0; i < synonyms.length; i ++) {
			this.synonyms.push(getNodeContent(synonyms[i]));
		}		
	},
	
	getHTMLNode: function() {
		if (this.node == null) {
			var lthis = this;
			this.node = document.createElement('div');
			this.node.className = 'taxonInList';
			this.node.onmousemove = function() {
				if (lthis.index < 0 && lthis.speciesSelector.currentHilight != lthis.index)
						lthis.speciesSelector.setHilight(lthis.index); };

			this.leftPartImage = document.createElement('img');
			this.leftPartImage.src = '/imgs/blank.gif';
			this.leftPartImage.className = 'taxonLeftImage';
			this.leftPartImage.alt = '';
			this.node.appendChild(this.leftPartImage);
						
	 		this.leftPart = document.createElement('div');
			this.leftPart.className = 'taxonLeft';
	 		this.leftPart.onclick = function() { this.blur(); lthis.leftClicked(); return false; };
			this.node.appendChild(this.leftPart);
			
			if (this.hasChildren) {
				this.arrow = document.createElement('div');
				this.arrow.className = 'taxonArrow';
				this.leftPart.appendChild(this.arrow);
			}

			this.middlePart = document.createElement('div');
			this.middlePart.className = 'taxonMiddle';
	 		this.middlePart.onclick = function() { this.blur(); lthis.middleClicked(); return false;};
			this.node.appendChild(this.middlePart);

			this.nameNode = document.createElement('div');
			this.nameNode.className = 'taxonName';
			this.middlePart.appendChild(this.nameNode);

		    this.synNode = document.createElement('div');
		    this.synNode.className = 'synonymList';
		    this.middlePart.appendChild(this.synNode);
		    
		    this.higherNode = document.createElement('div');
		    this.higherNode.className = 'higherRank';
		    this.middlePart.appendChild(this.higherNode);
		    
		    if (this.speciesSelector.taxonsRightIcon != null) {
		    	this.rightIcon = document.createElement('img');
		    	this.rightIcon.className = 'taxonRightIcon';
		    	this.rightIcon.src = this.speciesSelector.taxonsRightIcon;
		    	this.rightIcon.onclick = function() { this.blur(); lthis.iconClicked(); return false; };
		    	this.middlePart.appendChild(this.rightIcon);
		    }
		    
		    this.rightPart = document.createElement('div');
		    this.rightPart.className = 'taxonRight';
		    this.node.appendChild(this.rightPart);
	 		
			this.updateHTML();
		}
		return this.node;
	},
	
	updateHTML: function() {
		if (this.index < 0) {
			this.node.style.position = 'relative';
			if (this.arrow != null) {
				if (this.speciesSelector.needPngHack) {
					this.arrow.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/imgs/tax_left_arrowd.png')";
				} else {
					this.arrow.style.backgroundImage = 'url(/imgs/tax_left_arrowd.png)';
				}
			}
		} else {
			this.node.style.position = 'absolute';
			if (this.arrow != null) {
				if (this.speciesSelector.needPngHack) {
					this.arrow.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/imgs/tax_left_arrowr.png')";
				} else {
					this.arrow.style.backgroundImage = 'url(/imgs/tax_left_arrowr.png)';
				}
			}
		}
		var width = this.speciesSelector.entryWidth - this.indent;
		this.node.style.top = this.topPos + 'px';
		this.node.style.left = this.indent + 'px';
		this.node.style.width = width + 'px';
		this.middlePart.style.width = (width - 36 - 6) + 'px';
		this.rightPart.style.left = (width - 6) + 'px';

		this.setBackgroundImages();

		var alt = '';
		if (this.hasChildren) {
			if (this.index < 0) {
				alt = 'v';
			} else {
				alt = '>';
			}
		}
		if (this.found || this.subFound) {
			alt += ' gef';
		}
		this.leftPartImage.alt = alt;

		clearNode(this.nameNode);
		/*if (this.hasChildren) {*/
			this.nameNode.appendChild(document.createTextNode(this.name +
								' (' + this.numFound + ')'));
		/*} else {
			this.nameNode.appendChild(document.createTextNode(this.name));
		}*/
		clearNode(this.synNode);
	    this.synNode.appendChild(document.createTextNode(this.synonyms.join(', ')));
	    
	    clearNode(this.higherNode);
	    this.higherNode.appendChild(document.createTextNode(this.higherRank));
	},
	
	getBackgroundURL: function(name, found) {
		var img = 'url(/imgs/tax_' + name + '_';
		if (found) img += 'f';
		else img += 'n';
		if (this.isHilit) img += 'h';
		else img += 'n';
		img += '.png)';
		return img;
	},
	
	setBackgroundImages: function() {
		this.leftPart.style.backgroundImage = this.getBackgroundURL('left', this.subFound);
		this.middlePart.style.backgroundImage = this.getBackgroundURL('mid', this.found);
		this.rightPart.style.backgroundImage = this.getBackgroundURL('right', this.found);
	},
	
	hilight: function() {
		this.isHilit = true;
		this.setBackgroundImages();
	},
	
	unhilight: function() {
		this.isHilit = false;
		this.setBackgroundImages();
	},
	
	leftClicked: function() {
		if (!this.hasChildren) {
			this.middleClicked();
			return;
		}
		if (this.index >= 0) {
			this.speciesSelector.openTaxon(this.index);
		} else {
			this.speciesSelector.closeTaxon(this.index);
		}
	},
	
	middleClicked: function() {
		if (this.rightIcon == null)
			this.speciesSelector.activateTaxon(this.index);
	},
	
	iconClicked: function() {
		this.speciesSelector.activateTaxon(this.index);
	}
}

