// -*- Mode: JavaScript; c-basic-offset: 8 -*-

// Dependencies: prototype v1.6

// Given a tree of labels and values, creates a new dynamic,
// hierarchal select element.  When the user chooses an option, the
// given input element's value is updated to reflect the option value.
// When the given input element's value is changed (with the change
// event), the dynamic select elements are updated to show the
// matching option selected, or the first leaf node, if there was no
// match in the tree.

// Each select element represents the list of children of some tree
// node.  Leaf nodes correspond to a simple "option" element under the
// select; when these option elements are selected, the target input
// element is updated.  Non-leaf nodes are also represented by option
// elements: when one is selected, another select element is displayed
// underneath, with options corresponding to the node's children.

// When a new select is displayed, its first option is automatically
// chosen.  This may result in more select elements being displayed at
// the same time, if that option corresponded to a non-leaf-node.  You
// may wish to always ensure the first child of any node is a
// placeholder leaf-node, such as ["", ""] or ["(select)", ""].

// All possible select elements are generated and inserted into the
// document tree at once, as children of the given placeholder
// element.  (Other existing children are not affected.)  select
// elements are shown and hidden on-demand.

// Format of the tree structure:
//   tree                 = option_list
//   option_list          = [option*]
//   option               = [label, value_or_option_list]
//   label                = string
//   value_or_option_list = value
//                        | option_list
//   value                = string

// Example -- Imagine the following tree:

// [["None", "none"],
//  ["Portmaster 3", [["PortMaster 3 with ComOS 3.7.1", "pm3_3.7.1"],
//		      ["Portmaster 3 with ComOS 3.7", "pm3_3.7"],
//		      ["Portmaster 3 with ComOS 3.5", "pm3_3.5"]]],
//  ["Portmaster 2", [["Portmaster 2 with ComOS 3.7", "pm2_3.7"],
//		      ["Portmaster 2 with ComOS 3.5", "pm2_3.5"]]],
//  ["IRX", [["IRX with ComOS 3.7R", "IRX_3.7R"],
//	     ["IRX with ComOS 3.5R", "IRX_3.5R"]]]]

// The user sees:
//   [None        ]

// The user clicks the select element and sees:
//   [None        ]
//    Portmaster 3
//    Portmaster 2
//    IRX

// The user selects "Portmaster 3."  The value of the target element
// is updated to contain the value "pm3_3.7.1".  The user sees:
//   [Portmaster 3]
//   [Portmaster 3 with ComOS 3.7.1]

// The user clicks the second select element and sees:
//   [Portmaster 3]
//   [Portmaster 3 with ComOS 3.7.1]
//    Portmaster 3 with ComOS 3.7
//    Portmaster 3 with ComOS 3.5

// The user clicks "Portmaster 3 with ComOS 3.7".  The value of the
// target element is updated to now contain the value "pm3_3.7".  The
// user now sees:
//   [Portmaster 3]
//   [Portmaster 3 with ComOS 3.7  ]


// Parameters:
//   tree: A tree, as defined above.
//   select_elt: The element of the hierarchal select element (or its
//               ID) to make dynamic.
//   target_input: The input element (or its ID) whose value is to be
//                 updated with selected options.  If null or not
//                 given, a new hidden input element is inserted, with
//                 the same name as the original select element
//                 (removed), and used for this purpose.
//   muffle_errors: Swallow exceptions.  Good for production, when the
//                  hierarchal select mechanism is not critical.
// To avoid any XHTML problems, even if your document is HTML, avoid
// upper-case tagnames.
function make_dynamic_select (tree, placeholder, target_input, muffle_errors) {
	try {
		_dynselect.make_dynamic_select (tree, $(placeholder), $(target_input));
	} catch (e) {
		if (! muffle_errors)
			throw e;
	}
}

var _dynselect =
(function () {
	var make_dynamic_select = function (tree, placeholder, target_input) {
		var root_select = option_list_to_select (placeholder, tree, target_input);
		target_input.dynselect_onchange = function () {
			// One never knows...
			if (target_input.dynselect_muffle_onchange)
				return;
			var old_value = target_input.value;
			if (! select_option_with (root_select, target_input.value, [])) {
				var old_muffle = target_input.dynselect_muffle_onchange;
				target_input.dynselect_muffle_onchange = true;
				// No match; choose first entry.
				root_select.selectedIndex = 0;
				root_select.dynselect_div.show ();
				root_select.dynselect_onchange ();
				// ...but don't overwrite new value!
				target_input.value = old_value;
				target_input.dynselect_muffle_onchange = old_muffle;
			}
		};
		target_input.dynselect_muffle_onchange = false;
		target_input.observe ("change", target_input.dynselect_onchange);
		target_input.dynselect_onchange ();
	};
	
	var option_list_to_select = function (container, options, target_input) {
		if (options.length === undefined)
			throw "Option list isn't an array.";
		if (options.length < 1)
			throw "Option list length is less-than 1.";
		
		var select = elt ("select");
		select.addClassName ("dynselect");
		select.dynselect_children = new Array ();
		select.dynselect_div = elt ("div", {"class": "dynselect"}, [select]);
		select.dynselect_div.hide ();
		container.appendChild (select.dynselect_div);
		select.dynselect_null_mask = new Array ();
		for (var i = 0; i < options.length; i++)
			add_option (select, container, options[i], target_input);
		
		select.dynselect_onchange = function () {
			if (! select.dynselect_div.visible ())
				// Who knows?
				return;
			hide_select_subtree (select, select.selectedIndex, true);
			var child = select.dynselect_children[select.selectedIndex];
			if (child === null) {
				if (! select.dynselect_null_mask[select.selectedIndex]) {
					var old_muffle = target_input.dynselect_muffle_onchange;
					target_input.dynselect_muffle_onchange = true;
					target_input.value = select.value;
					target_input.dynselect_muffle_onchange = old_muffle;
				}
			} else {
				child.selectedIndex = 0;
				child.dynselect_div.show ();
				child.dynselect_onchange ();
			}
		};
		Event.observe (select, "change", select.dynselect_onchange);
		return select;
	};
	var add_option = function (select, container, option, target_input) {
		if (option.length != 2)
			throw "Option isn't an array of length 2.";
		var label = option[0];
		var value;
		var child_select;
		var is_null;
		
		if ((typeof label) != "string")
			throw "Option label isn't of type string.";
		
		if ((typeof option[1]) == "string") {
			child_select = null;
			value = option[1];
			is_null = false;
		} else if (option[1] === null) {
			child_select = null;
			value = "";
			is_null = true;
		} else if (option[1].length !== undefined) {
			child_select = option_list_to_select (container, option[1], target_input);
			value = "";
			is_null = false;
		} else {
			throw "Second element of option isn't a value or an option_list.";
		}
		
		select.options[select.options.length] = new Option (label, value);
		select.dynselect_children.push (child_select);
		select.dynselect_null_mask.push (is_null);
	};
	
	var hide_select_subtree = function (select, except_idx, except_this) {
		if ((except_idx === undefined) || (except_idx === null))
			except_idx = -1;
		for (var i = 0; i < select.dynselect_children.length; i++) {
			if (i == except_idx)
				continue;
			if (select.dynselect_children[i] === null)
				continue;
			hide_select_subtree (select.dynselect_children[i]);
		}
		if (! except_this)
			select.dynselect_div.hide ();
	};
	
	var select_option_with = function (select, value, match_cbs) {
		for (var i = 0; i < select.options.length; i++) {
			var child = select.dynselect_children[i];
			if (child === null) {
				if ((select.options[i].value == value)) {
					match_cbs.invoke ("apply");
					select.selectedIndex = i;
					select.dynselect_div.show ();
					select.dynselect_onchange ();
					return true;
				}
			} else {
				var child_cbs = match_cbs.clone ();
				child_cbs.push (function () {
							select.selectedIndex = i;
							hide_select_subtree (select, i, true);
							select.dynselect_div.show ();
						});
				var result = select_option_with (child, value, child_cbs);
				if (result)
					return result;
			}
		}
		return false;
	};
	
	
	// Note: DOM functions below are hard-coded for use with XHTML DOM nodes.
	var XHTML_NS = "http://www.w3.org/1999/xhtml";

	var elt = function (tagname, attrs, children) {
		var elt;
		if (attrs)
			attrs = new Hash (attrs);
		else
			attrs = new Hash ();
		if (! children)
			children = new Array ();
		
		if (document.createElementNS !== undefined)
			elt = document.createElementNS (XHTML_NS, tagname);
		else if (document.createElement !== undefined)
			elt = document.createElement (tagname);
		else
			throw "Cannot create element: document.createElementNS and document.createElement are both undefined.";
		
		attrs.each (function (pair) {
				    set_attr (elt, pair.key, pair.value);
			    });
		children.each (function (child) {
				       elt.appendChild (child);
			       });
		
		return $(elt);
	};
	
	// When the given element is an XHTML element, returns its
	// localname.  Otherwise, returns a string of the form
	// "{NS_URI}LOCALNAME".
	var elt_name = function (elt) {
		if (elt.localName) {
			// DOM Level 2+ node.
			if (elt.namespaceURI != XHTML_NS)
				return "{" + elt.namespaceURI + "}" + elt.localName;
			else
				return elt.localName;
		} else {
			// DOM Level 1 node.
			return elt.tagName;
		}
	};
	
	var get_attr = function (elt, name) {
		if (elt.getAttributeNS !== undefined)
			return elt.getAttributeNS (null, name);
		else if (elt.getAttribute !== undefined)
			return elt.getAttribute (name);
		else
			throw "Cannot get attribute value: elt.getAttributeNS and elt.getAttribute are both undefined.";
	};
	
	var set_attr = function (elt, name, value) {
		if (elt.setAttributeNS !== undefined)
			elt.setAttributeNS (null, name, value);
		else if (elt.setAttribute !== undefined)
			elt.setAttribute (name, value);
		else
			throw "Cannot set attribute value: elt.setAttributeNS and elt.setAttribute are both undefined.";
	};
	
	var rm_attr = function (elt, name) {
		if (elt.removeAttributeNS !== undefined)
			elt.removeAttributeNS (null, name);
		else if (elt.removeAttribute !== undefined)
			elt.removeAttribute (name);
		else
			throw "Cannot remove attribute: elt.removeAttributeNS and elt.removeAttribute are both undefined.";
	};
	
	return {make_dynamic_select: make_dynamic_select};
}) ();
