//
// Auto Complete
//
function ac(input_container_id, url_string, callback, select_list) {
	// Initial Variables.
	this.base_url = url_string;
	this.callback = callback;
	this.num_results = 0; // Updated in onreadystatechange
	this.selected = 0;
	this.max_results = 20;
	this.max_result_width = 0;

	// This is to keep track of the input fields events
	// The events it will track are
	// onkeypress, onkeydown, onkeyup
	this.previous_event = null;

	// Specific elements that you want to hide when the dropdown is visible.
	// This is here because some browsers do NOT seem to yield to z-index.
	this.hide_list = Array();
	if(select_list) {
		for(id in select_list) {
		    exclude_element = document.getElementById(select_list[id]);
		    if(exclude_element) {
                this.hide_list[this.hide_list.length] = exclude_element;
		    }
		}
	}

	var select_tags = document.getElementsByTagName("select");
	for (var i = 0; i < select_tags.length; i++) {
        this.hide_list[this.hide_list.length] = select_tags[i];
	}

	// input element we are autocompleting on
	this.input_field = document.getElementById(input_container_id);
	
	// Do not go any further if you can't find the input field.
	if(!this.input_field) {
		return(false);
	}

	// Add a variable to keep track if it has focus or not
	this.input_field.has_focus = false;

	// Add a reference to self
	this.input_field.ac = this;

    // need to set the inital current value
	this.current_value = this.input_field.value;

    // need to set the inital current value
	this.sent_value = "";

	// Keep original function definition
	this.input_field.ac_onkeypress_original = this.input_field.onkeypress;
	this.input_field.ac_onkeydown_original = this.input_field.onkeydown;
	this.input_field.ac_onkeyup_original = this.input_field.onkeyup;
	this.input_field.ac_onfocus_original = this.input_field.onfocus;
	this.input_field.ac_onblur_original = this.input_field.onblur;

	// install event handlers
	this.input_field.onkeypress = this.onkeypress;
	this.input_field.onkeydown = this.onkeydown;
	this.input_field.onkeyup = this.onkeyup;
	this.input_field.onfocus = this.onfocus;
	this.input_field.onblur = this.onblur;

	// Create a popup layer we will display results in
	// Set the css class for the dropdown.
	// Add this node right after the input_field
	this.result = document.createElement('div');
	this.result.className = "ac_dropdown";
    this.result.style.minWidth = this.input_field.clientWidth + 1 + "px";
	this.hide_dropdown();

	// Work with prepopulated fields.
	if(this.input_field.value) {
    	this.update(this.input_field.value, false);
	}

	this.position_dropdown();

	// Add this to the root node so that the container will not
	// be affected by any position: relatives. This will make it
	// easier to properly position the dropdown
	document.body.appendChild(this.result);

	//this.input_field.parentNode.insertBefore(this.result, this.input_field.nextSibling);

    this.input_field.setAttribute("autocomplete", "off");
}

ac.prototype.total_offset = function(element, property) {
	var total = 0;
	while(element) {
		total += element[property];
		element = element.offsetParent;
	}
	return total;
}

//
// I've noticed that this event happens much faster
// than the other events.
//
// Infact so fast that if you press a key the input box
// is updated AFTER this function is called.
//
// Thus you can't put the update function in here
// because the this.value will not have the updated value.
//
// This is a good candidate for trapping the "enter" key
// because this function will trap the keypress before
// the forum is submitted.
//
// I have tried to trap the enter key on the "onkeydown"
// event, and it failed to work in Opera.
//
ac.prototype.onkeypress = function(e) {
    if (!e) { e = window.event; }
	var c = e.keyCode;
	if (c == 0) { c = e.charCode; }
	switch (c) {
        case 13: // enter
            // If there is a dropdown, then hide it and don't submit the form
            // If there isnt a dropdown, then submit the form.
	        if(this.ac.result.style.visibility == "visible") {

				this.ac.update_selection();

				/*
    			var nodes = this.ac.result.childNodes;
    			var selection = nodes[this.ac.selected].firstChild.innerHTML.replace(/(<([^>]+)>)/ig, "");
    			this.ac.update(selection, false);
    			this.ac.current_value = selection;
    			this.value = selection;
				this.ac.callback(this.value, nodes[this.ac.selected].item_id);
				// category_tree.change(nodes[this.ac.selected].item_id);
				*/
	        }
			break;
        case 38: // up
            if(this.ac.previous_event != "onkeydown") {
    			this.ac.select_prev();
            }
            break;
        case 40: // down
            if(this.ac.previous_event != "onkeydown") {
    			this.ac.select_next();
            }
            break;
		default:
            break;
	}

    // Call original onkeypress function
	if(this.ac.input_field.ac_onkeypress_original) {
        this.ac.input_field.ac_onkeypress_original(e);
	}

	// This should be the last line of this event.
    this.ac.previous_event = "onkeypress";

    // Need to put this block outside of the "case 13"
    // because of the callback to the "original onkeypress" function
    // We do not want to lose the ability to press enter when auto completing.
    if((this.ac.result.style.visibility == "visible") && (c == 13)) {
    	this.ac.hide_dropdown();
        return(false);
    }
}

//
// OnKeyDown
//
// For Arrow down and hold
//    Works in
//       [Windows] [IE]
//       [Windows] [FF]
//
ac.prototype.onkeydown = function(e) {
    // Call original onkeydown function
	if(this.ac_onkeydown_original) {
        this.ac_onkeydown_original(e);
	}

	if (!e) { e = window.event; }
	var c = e.keyCode;
	if (c == 0) { c = e.charCode; }
	switch (c) {
        case 9:    // tab
			this.ac.hide_dropdown();
    		break;
        case 27:   // esc
			this.ac.hide_dropdown();
    		break;
        case 38:   // up
            if(this.ac.previous_event != "onkeypress") {
				this.ac.select_prev();
			}
    		break;
    	case 40:   // down
            if(this.ac.previous_event != "onkeypress") {
				if(this.ac.result.style.visibility == "hidden") {
					// Shows the dropdown when a user presses the down arrow in the field
					// This is here just in case the dropdown isn't visible
					this.ac.show_dropdown();
				} else {
					this.ac.select_next();
				}
			}
    		break;
    	default:
    		break;
	}

	// This should be the last line of this event.
    this.ac.previous_event = "onkeydown";
}

ac.prototype.onkeyup = function(e) {
    // Call original onkeyup function
	if(typeof this.ac_onkeyup_original == "function") {
        this.ac_onkeyup_original(e);
	}

    if (!e) { e = window.event; }
	var c = e.keyCode;
	if (c == 0) { c = e.charCode; }

	// Needed for local scope of variables.
	var this_object = this;
	var param = this.value;

    param = encodeURIComponent(param); 

    // Keeping track of the current value that is being requested
    this.ac.current_value = param;

	switch (c) {
        case 13:   // enter
       		// Do not update on enter key.
			break;
		default:
            // This will wait 300 milliseconds before executing this script.
            // Reason:
            //   When this function is called it will check if the parameter
            //   is different than the current value in the input box.
            //   If its different then it won't send a request else send a request.
			window.setTimeout(
				function() {
					this_object.ac.update(param, true);
				}, 300);
            break;
	}

	// This should be the last line of this event.
    this.ac.previous_event = "onkeyup";
}

ac.prototype.onfocus = function(e) {
    // Call original onfocus function
	if(typeof this.ac_onfocus_original == "function") {
        this.ac_onfocus_original(e);
	}
	this.has_focus = true;

	// check if the selection in the box is different in any way.

	if(this.ac.current_value != this.value) {
		// Do an update on the value in the background.
		this.ac.current_value = this.value;
		this.ac.update(this.value, true);
		return;
	}

	if(this.ac.current_value == "" || this.ac.num_results == 0 ) {
		this.ac.hide_dropdown();
		return;
	}

	this.ac.show_dropdown();
}

ac.prototype.onblur = function(e) {
    // Call original onblur function
	if( typeof this.ac_onblur_original == "function") {
        this.ac_onblur_original(e);
	}

	this.has_focus = false;
	this.ac.hide_dropdown();

	// Do an update on the value in the background.
	this.ac.update(this.value, false);
}

//
// This function is called by the onkeyup function.
//
// Notes: You must declare the HttpRequest object in here
//        If you do not, when update is called a second time
//        There is a chance that you might accidentally
//        stomp over the current request.
//
// Visible is a parameter to make the dropdown visible
// after doing the RPC update on the drop down.
//
ac.prototype.update = function(param, visible) {
	// Only send a RPC if the value has changed.
	if(param == this.sent_value) {
		return;
	}

	// Only send a RPC it matches the value in the input field.
	if(param != this.current_value) {
		return;
	}

	this.sent_value = param;

	// branch for native XMLHttpRequest object
    if (window.XMLHttpRequest) {
		var req = new XMLHttpRequest();
    // branch for IE/Windows ActiveX version
    } else if (window.ActiveXObject) {
		var req = new ActiveXObject("Microsoft.XMLHTTP");
    }

	var url = this.base_url + param;
	req.open("GET", url, true);

	// Declare a local reference to this object
	// This variable will be used in the onreadystatechange
	var ac = this;
	var visible = visible;

    req.onreadystatechange = function() {
		// Some integrity checking code
		// Makes sure that we are not updating data when we aren't supposed to.
		if(param != ac.current_value) {
			return;
		}

		// readyState 4 means the request is finshed loading.
		if (req.readyState == 4 && req.status == 200) {	
			ac.empty_items();
			
			// Remove the loading image.
			ac.input_field.style.backgroundImage = "";


			// Need to check if the data that came back is XML, JSON, or Text...
			// Or we need to abstract this so that it can be more flexible
			if(req.responseText) {
				try {
					var elements = eval("(" + req.responseText + ")");

					ac.build_dropdown(elements);
					if((visible == true) && ac.input_field.has_focus) {
						ac.show_dropdown();
					}
				} catch(e) {
					;
				}
			}
		}
	};

	// Display the loading image.
	// This is currently static.
	// this.input_field.style.backgroundImage = "url('/web_images/loading.gif')";
	// this.input_field.style.backgroundRepeat = "no-repeat";
	// this.input_field.style.backgroundPosition = (this.input_field.clientWidth - 63 - 5) + "px center";

	req.send(null);
}

ac.prototype.build_dropdown = function(elements) {
	this.num_results = elements.length;
	// Do not show anything if the input box is empty.
	// Do not show anything if there are no results.
	if(this.current_value == "" || this.num_results == 0) {
		this.hide_dropdown();
		return;
	}

    var i=0;
    this.max_result_width = 0;
    // while(e = elements.item(i++)) {
	// Apparently a for loop is much better than this approach...
	for(i=0; i<this.num_results; i++) {
		e = elements[i];
        var div = this.create_entry();

		// Add the result value to the div box.
		// This function can be overloaded
		// based on how you want the result to appear.
		this.entry_result(div, e);

/*
		if(e.title == this.current_value) {
			this.callback(e.title, e.id);
		}
*/

		if(i >= this.max_results) {
			/*
            var div_more = document.createElement("div");
            div_more.className = "ac_more_results";
            div_more.innerHTML = "Too many results to show...";
            this.result.appendChild(div_more);
			*/
            break;
		}
    }

	this.selected = 0;
}

// This creates the container for the resulting text
// This will not add the text to the container.
//
// Only add event handlers to this function.
ac.prototype.create_entry = function() {
    var div = document.createElement("div");
	div.className = "ac_element";

    // Allows the "div" element to access the input_field object.
    div.input_field = this.input_field;
	div.ac = this;

    // Event Handlers
	div.onmouseover = function() {
		this.style.cursor = "pointer";
		// Remove the current hilighed element.
		// This way no two elements are hilighted.
		//
		// Order of events, make sure you clear the current selection
		// and then mark the new one as selected, else you will not be
		// able to hilight the this.ac.selected element.
		// bascially keep this section above the line below.
		var nodes = this.ac.result.childNodes;
		nodes[this.ac.selected].className = "ac_element ac_unselected";

		this.className = "ac_element ac_selected";
	}

	div.onmouseout = function() {
		this.className = "ac_element ac_unselected";

		// This will keep the current, keyboard selection
		// var nodes = this.ac.result.childNodes;
		// nodes[this.ac.selected].className = "ac_element ac_selected";
	}

	div.onmousedown = function() {
		this.input_field.value = this.firstChild.innerHTML.replace(/(<([^>]+)>)/ig,"");
		this.ac.update(this.input_field.value, false);

		this.ac.update_selection(this);

		// .replace(/(<([^>]+)>)/ig,"") // strips all HTML tags...

		/*
        this.input_field.value = this.firstChild.innerHTML.replace(/(<([^>]+)>)/ig,"");
		this.ac.update(this.input_field.value, false);
		this.ac.current_value = this.input_field.value;

//		this.ac.callback(this.input_field.value, nodes[this.ac.selected].item_id);
//		category_tree.change(this.item_id);
		
		*/
	}

	return(div);
}

// where you add the text...
ac.prototype.entry_result = function(div, item) {
//	var username = item.firstChild.nodeValue;
//	var description = item.getAttribute("description"); // STATIC
	var username = item.title;
	var description = "";

	// Might want to add more error checking here
	// This detect if the value that was passed is blank
	// Might want to add something that states this value is missing in this row.
	if(username.length == 0) {
	   return;
	}

	// http://xkr.us/js/regexregex
    var current_value = decodeURIComponent(this.current_value);
	current_value = current_value.replace(/([\\\^\$*+[\]?{}.=!:(|)])/g,"\\$1");
	var current_value_reg = new RegExp("^(" + current_value + ")", 'i');

	if(username.match(current_value_reg)) {
    	username = username.replace(current_value_reg, '<span style="font-weight: bold;">$1</span>');
	} else {
	    // Special exception for a dash
	    if(current_value[0] == '-') {
	        // This will do a straight sub string search
	        // Note that this string assues that there is a dash in front of it.
    	    var dash_reg = new RegExp("(" + current_value + ")", 'i');
        	username = username.replace(dash_reg, '<span style="font-weight: bold;">$1</span>');
	    } else {
            // This will try to do match with a dash in the front
    	    var dash_reg = new RegExp("(-" + current_value + ")", 'i');
        	username = username.replace(dash_reg, '<span style="font-weight: bold;">$1</span>');
	    }
	}

	// only hilight if the "search value" is at the start of a word.
	var desc_array = description.split(" ");
	var temp = "";
	var sub_length = 0;
	var desc_length = description.length;
	for (var i in desc_array) {
	    // Example:
	    //     Username: Foo Bar Baz Qux
	    //    AC String: Foo B
	    //
	    // Algorithim:
	    //       Checks: Foo
	    //       Checks: Foo Bar
	    //       Checks: Foo Bar Baz
	    //       Checks: Foo Bar Baz Qux
	    //
	    var sub_desc = description.substring(parseInt(sub_length) + parseInt(i));

        if(sub_desc.match(current_value_reg)) {
            temp += sub_desc.replace(current_value_reg, '<span style="font-weight: bold;">$1</span>');
            break;
        } else {
            temp += desc_array[i] + " ";
        }

	    sub_length += desc_array[i].length;
	}

	description = temp;

    var div_username = document.createElement("span");
    var div_description = document.createElement("span");

	div_username.className = "ac_value";
	div_description.className = "ac_info";

	// the "&nbsp;" are here just in case description is blank.
	div_username.innerHTML = username;
	div_description.innerHTML = "&nbsp;";

	// Do not change the order of this.
    // The value of the left element is used for
    // the value that is placed in the input field
	div.appendChild(div_username); // the left element
	div.appendChild(div_description); // the right element

	// Add this node to its parent.
	this.result.appendChild(div);

	var node_width = (div_username.offsetWidth + div_description.offsetWidth);
	if( node_width > this.max_result_width ) {
	   this.max_result_width = node_width;
	   this.result.style.width = (node_width + 20)+"px";
	}

	// Extenion to this function that needs to be separated.
	// by a user defined function to add in more functionality
	div.item_id = item.id;
}

// Empties all of the items in the drop down.
ac.prototype.empty_items = function() {
	while(this.result.hasChildNodes()) {
		this.result.removeChild(this.result.firstChild);
	}

	this.hide_dropdown();
}

ac.prototype.show_dropdown = function() {
	var nodes = this.result.childNodes;

	if(nodes.length == 0) {
		this.hide_dropdown();
		return;
	}

	// This is a fix that detects if there is a SELECT
	// element that will appear on top of the dropdown.
	//
	// If there is a SELECT element it will then hide it.
	for(var i=0; i<this.hide_list.length; i++) {
		var temp = this.hide_list[i]; // the element that will be hidden

        if ( (this.findPosY(this.result) < (this.findPosY(temp) + temp.offsetHeight ) ) &&
           ( ((this.findPosY(this.result) + this.result.offsetHeight) > this.findPosY(temp) ) ) &&
           ( this.findPosX(this.result) < (this.findPosX(temp) + temp.offsetWidth ) ) &&
           ( ((this.findPosX(this.result) + this.result.offsetWidth) > this.findPosX(temp) )) ) {
            temp.style.visibility = "hidden";
		} else {
			temp.style.visibility = "visible";
		}
	}

	nodes[this.selected].className = "ac_element ac_selected";

	// Re calculate the dropdown each time the drop down
	// becomes visible because, if there was some dhtml
	// that changed the placement of some HTML elements
	// then this dropdown might not appear in the correct location.
	this.position_dropdown();

	this.result.style.visibility = "visible";
}

ac.prototype.hide_dropdown = function() {

	// for(id in this.hide_list) {
	for(var i=0; i<this.hide_list.length; i++) {
		var temp = this.hide_list[i];
		temp.style.visibility = "visible";
	}

	this.result.style.visibility = "hidden";
}

ac.prototype.position_dropdown = function() {
	this.result.style.top = this.total_offset(this.input_field,'offsetTop') +
	                        (this.input_field.offsetHeight - 1 ) + "px";

	this.result.style.left = (this.total_offset(this.input_field,'offsetLeft')) + "px";
}

ac.prototype.findPosY = function(obj) {
	var curtop = 0;
	if (obj.offsetParent) {
		while (obj.offsetParent) {
			curtop += obj.offsetTop
			obj = obj.offsetParent;
		}
	} else if (obj.y) {
		curtop += obj.y;
	}

	return curtop;
}

ac.prototype.findPosX = function(obj) {
	var curleft = 0;
	if (obj.offsetParent) {
		while (obj.offsetParent) {
			curleft += obj.offsetLeft
			obj = obj.offsetParent;
		}
	} else if (obj.x) {
		curleft += obj.x;
	}

	return curleft;
}

ac.prototype.select_next = function() {
	if(this.selected < this.num_results-1 && (this.selected < this.max_results-1) ) {
		var nodes = this.result.childNodes;
		nodes[this.selected].className = "ac_element";
		this.selected++;
		nodes[this.selected].className = "ac_element ac_selected";
	}
}

ac.prototype.select_prev = function() {
	if(this.selected > 0) {
		var nodes = this.result.childNodes;
		nodes[this.selected].className = "ac_element";
		this.selected--;
		nodes[this.selected].className = "ac_element ac_selected";
	}
}

ac.prototype.update_selection = function(selection) {
	if(!selection) {
		var nodes = this.result.childNodes;
		var selection = nodes[this.selected];
	}

	// Use the first element in the dropdown.
	var val = selection.firstChild.innerHTML.replace(/(<([^>]+)>)/ig, "");
	var id = selection.item_id;

	this.update(val, false);
	this.current_value = val;
	this.input_field.value = val;

	this.callback(val, id);
}