function find_parent ( element, match ) {
	var node;
	
	node = element;
	while ( node.parentNode && node != node.parentNode ) {
		if ( match(node) ) {
			return node;
		}
		
		node = node.parentNode;
	}
	
	return null;
}

function find_descendents ( element, match, count ) {
	var node, found = [];
	
	// count 0 means find all.
	
	node = element.firstChild;
	while ( true ) {
		if ( match(node) ) {
			found.push(node);
			
			if ( count && count <= found.length ) {
				return found;
			}
		}
		
		if ( node.firstChild ) {
			node = node.firstChild;
		} else {
			while ( ! node.nextSibling ) {
				node = node.parentNode;
				
				if ( node == element ) {
					return found;
				}
			}
			
			node = node.nextSibling;
		}
	}
}

function find_descendent ( element, match ) {
	var found = find_descendents(element, match, 1);
	
	if ( found.length ) {
		return found[0];
	}
	
	return null;
}

function match_tag ( tag_name ) {
	tag_name = tag_name.toUpperCase();
	
	return function ( node ) {
		return node.nodeName == tag_name;
	};
}

function match_class ( css_class ) {
	return function ( node ) {
		return has_css_class(node, css_class);
	};
}

function match_node ( to_find ) {
	return function ( node ) {
		return node === to_find;
	};
}

function next_sibling ( element ) {
	var node = element.nextSibling;
	
	// Test for name ! is for IE 5.5: it thinks comments are elements.
	while ( node && ( node.nodeType != 1 || node.nodeName == "!" ) ) {
		node = node.nextSibling;
	}
	
	return node;
}

function previous_sibling ( element ) {
	var node = element.previousSibling;
	
	// Test for name ! is for IE 5.5: it thinks comments are elements.
	while ( node && ( node.nodeType != 1 || node.nodeName == "!" ) ) {
		node = node.previousSibling;
	}
	
	return node;
}

function submit_parent_form ( element ) {
	var form = find_parent(element,
			function (e) { return e.tagName == "FORM"; });
	
	if ( validate_form(form) ) {
		form.submit();
	}
}

function validate_form ( form ) {
	var i, valid = true, messages = [], do_alert = false, errors, match,
		error_class = null, no_alert = false, no_messages = false;
	
	errors = form.getAttribute("errors");
	if ( /\bnomessages\b/i.exec(errors) ) {
		no_messages = true;
	}
	
	if ( /\bnoalert\b/i.exec(errors) ) {
		no_alert = true;
	}
	
	match = /\bclass=(\S*)/i.exec(errors)
	if ( match ) {
		error_class = match[1];
	}
	
	function on_error ( message, element, error_element ) {
		add_css_class(element, "error");
	}
	
	function on_valid ( element, error_element ) {
		remove_css_class(element, "error");
	}
	
	for ( i = form.elements.length - 1 ; i >= 0 ; i-- ) {
		valid = validate_element(form.elements[i], on_error, on_valid)
			&& valid;
	}
	
	document.getElementById("validation_message").style.visibility
		= valid ? "hidden" : "visible";
	
	return valid;
}

function validate_element ( element, on_error, on_valid ) {
	var what = element.getAttribute("validate"),
		initial = element.getAttribute("initial_value"),
		value = element.value,
		error_element, match, onfirm;
	
	if ( ! what ) {
		return true;
	}
	
	error_element = document.getElementById(element.name + "_error");
	
	function error ( message ) {
		try {
			element.focus();
			element.select();
		} catch ( e ) {
			// Ignore errors.
		}
		
		on_error(message, element, error_element);
	}
	
	if ( initial && value == initial ) {
		value = "";
	}
	
	// This can be used to make sure initial_value is set. This is meant to
	// work together with erase_initial().
	if ( /\bhas_initial\b/i.exec(what) && ! initial ) {
		initial = element.value;
		value = "";
	}
	
	if ( /\brequired?\b/i.exec(what) && value == "" ) {
		error("Required.");
		return false;
	}
	
	if ( /\bemail\b/i.exec(what) && value
			&& ! /^[^ \t@]+@[^ \t@]+\.[^ \t@.]+$/i.exec(value) ) {
		error("Not a valid email address.");
		return false;
	}
	
	match = /\bnochange=(\S+)/i.exec(what);
	if ( match && value == match[1] ) {
		// No changes -- don't do further validation.
		on_valid(element, error_element);
		
		return true;
	}
	
	match = /\bconfirm=(\S+)/i.exec(what);
	if ( match ) {
		confirm = document.getElementById(match[1]);
		if ( ! confirm ) {
			confirm = document.getElementsByName(match[1])[0];
		}
		
		if ( confirm && confirm.value != value ) {
			error("Fields do not match.");
			return false;
		}
	}
	
	on_valid(element, error_element);
	
	return true;
}

// Called onfocus, sets up the onblur event. If you use the validation
// functions above, be sure to set the has_initial flag in the validate
// attribute. That way, if the field was never focussed, the validation will
// be able to tell that the field is actually blank.
function erase_initial ( field ) {
	field = field || this;
	var initial = field.getAttribute("initial_value");
	
	if ( ! field.onblur ) {
		field.onblur = replace_initial;
	}
	
	if ( ! initial ) {
		field.setAttribute("initial_value", field.value)
		field.onblur = replace_initial;
		field.value = "";
	} else if ( field.value == initial ) {
		field.value = "";
	}
	
}

// Generally this is set up by erase_initial.
function replace_initial () {
	if ( this.value == "" ) {
		this.value = this.getAttribute("initial_value") || "";
	}
}

function has_css_class ( node, class_name ) {
	return new RegExp("\\b" + class_name + "\\b").exec(node.className);
}

function add_css_class ( node, class_name ) {
	if ( node.className ) {
		if ( has_css_class(node, class_name) ) {
			return;
		}
		
		node.className = node.className + " " + class_name;
	} else {
		node.className = class_name;
	}
}

function remove_css_class ( node, class_name ) {
	if ( node.className ) {
		node.className = node.className.replace(
			new RegExp("\\b" + class_name + "\\b"), "");
	}
}
