/*

Layer-based Menus as Reimagined by a Student of the Dark Technical Arts

Copyright 2002, HFD LLC, All Rights Reserved.
Author: Tripp Lilley

*/


/*
	Check to see whether or not our libraries loaded successfully. If
	not, update a global failure counter so that we'll know to ignore
	all other menu processing.

*/
var library_load_failures = 0;
for (var idx in menu_libraries) {
	var missing_libs = "";
	
	if (! library_loaded(menu_libraries[idx])) {
		library_load_failures += 1;
		missing_libs += "\n  " + menu_libraries[idx];
	}
}
if (library_load_failures) {
	debug.writeln("missing:" + missing_libs);
}


// Container for all of the menus we define
var menus = new Object();

// some global state
var menus_initialized = 0;

var menus_deferred = new Object();
menus_deferred.menu = null;
menus_deferred.menu_item = null;
menus_deferred.menu_context = null;


var menus_menu_context = new Array();
menus_menu_context.top = function () {
	if (this.length <= 0) {
		return null;
	}

	return this[this.length - 1];
}
// TODO: factor this out into a library so we don't clutter compliant browsers
if (! menus_menu_context.pop) {
    menus_menu_context.pop = function () {
	if (this.length <= 0) {
	    return null;
	}
	var popped = this[this.length - 1];
	this.length -= 1;
	return popped;
    }
}
// TODO: factor this out into a library so we don't clutter compliant browsers
if (! menus_menu_context.push) {
    menus_menu_context.push = function (value) {
	this[this.length] = value;
	return this.length;
    }
}





/*
   These are how we determine event propagation order and behave
   consistently regardless of the order and regardless of browser
   quirks in the event implementation.

   Assuming the stacking order is:

          TARGET   /\  |
           ...     |   |
          BOTTOM   |  \/
                 cap bub

   Events "capture" from BOTTOM to TARGET, then "bubble" from TARGET
   to BOTTOM, at least according to the DOM Level 2 event
   model. However, not every browser seems to handle this consistenly,
   or to necessarily do the "right" thing with event.cancelBubble and
   event.stopPropagation. Or I don't understand fully what they're
   supposed to be doing, which is equally possible.

   Anyway, my challenge is to devise a mechanism whereby BOTTOM's
   event handler can contain "default" behaviour which TARGET's event
   handler can, effectively, cancel.

   Part of this is already in place in the form of the oneshot_event
   timer callback we set in menu_hide. Unfortunately, since that's set
   in BOTTOM, it depends on being cancelled by TARGET later in the
   event propagation, or being cancelled by another handler, in the
   case of the gap rollover problem it was originally devised to
   fix.

   Well, expecting TARGET to cancel BOTTOM's oneshot_event call is
   going to fail if, say, TARGET's handler gets called first, as it
   seems to do in Opera 6 (at least). So, instead of relying solely on
   TARGET cancelling the oneshot_event, we're going to make a
   mechanism that doesn't even post the oneshot_event -if- BOTTOM
   happens to be called -after- TARGET in the -same- event
   propagation.

   However, we can't do something simple like tack a new field onto
   the event object to say that we've visited one or the other method
   during the event, since it's possible that we'll be running under
   an older browser that only supports the DOM Level 0 (aka "legacy")
   event model. That model doesn't have event objects, as far as I
   know.

   So instead we'll set a flag in each function that the other
   function will use to determine whether it's "coming or going". If
   no flags are set, the function knows it's "coming". If one flag is
   set, the function knows it's "going". The function that determines
   it's "going" is responsible for cleaning up the flag state.

*/

var menus_proporder_from_hide = 0;
var menus_proporder_from_show = 0;


// Used to zero-out menu behaviour when the requisite libraries don't load
function nop () {}


/*
	menu_init (menu_name, rollovers):

	Looks for an object named "[menu_name]_position_anchor" (where
	[menu_name] is the menu_name argument, without the square
	brackets). Bases the new menu rendering position on the position
	of the menu_name_position_anchor object.

*/

// TODO: generalize init for multiple menus


function menu_init (menu_name, rollovers) {
	debug.writeln("menu_init: [" + menu_name + "]");
	if (library_load_failures) {
		// TODO: remove this alert
		debug.writeln("menu_init: aborting [" + menu_name + "]");
		menu_hide = nop;
		menu_show = nop;
		menu_cancel_hide = nop;
		return;
	}

	var menu_item_list = get_global(menu_name + "_menu");
	var menu = new Object();
	menu.name = menu_name;
	menus[menu.name] = menu;
	menu.visible_item = null;
	menu.items = new Object();

	errors = menu_get_menu_position_and_size(menu);
	if (errors) {
		menus_initialized = 0;
		return;
	}


	for (var idx in menu_item_list) {
		menu_item = new Object();
		menu_item.menu = menu;

		menu_item.name = menu_item_list[idx][0];
		menu_item.buttons = new Array(menu_item_list[idx][1]);
		debug.writeln("menu_item init: " + menu_item.name + "(" + menu_item_list[idx][1] + " buttons)");


		/* the menu layer */
		menu_item.layer_id = menu_item.name + "MenuLayer";
		menu_item.layer = get_object_by_id(menu_item.layer_id);
		menu_set_item_position_and_size(menu_item);



		/* the menu button image(s) */

		// NOTE: Because of the Super Duper Temporal Slide Technology
		// (translation: allowing users to do a sloppy mouse from the
		// menu target over to the menu,) we have to support the
		// possibility of having multiple images for a single logical
		// menu button.

		debug.writeln("building " + menu_item.buttons.length + " buttons");
		for (var button_idx = 0; button_idx < menu_item.buttons.length; button_idx++) {
			button = new Object();
			button.name = menu_item.name + "MenuButton" + button_idx;
			button.object = get_object_by_name(button.name);
			button.modes = new Object();
			button.modes.inactive = new Image();
			button.modes.inactive.src = button.object.src;
			debug.writeln("button.modes.inactive.src: " + button.modes.inactive.src);

			if (rollovers) {
				// Preload the "active" image
				button.modes.active = new Image();
				button.modes.active.src = button.modes.inactive.src.replace("MenuButton", "MenuButtonActive");
				debug.writeln("button.modes.active.src: " + button.modes.active.src);
			}
			else {
				button.modes.active = button.modes.inactive;
			}

			menu_item.buttons[button_idx] = button;
		}

		debug.writeln("calling menu_platform_item_setup");
		menu_platform_item_setup(menu_item);

		menu.items[menu_item.name] = menu_item;
		hide_object(menu_item.layer);
	}

	menu_platform_init(menu);

	debug.writeln("menu_init: success");
	menus_initialized = 1;
	return;
}

function menu_get_menu_position_and_size (menu) {
	menu_position_anchor = get_objects_by_name(menu.name + "_menu_position_anchor");
	if (menu_position_anchor.length != 1) {
		debug.writeln("menu_init: couldn't find menu_position_anchor image");
		return 1;
	}
	menu.position_anchor = menu_position_anchor[0];
	menu.position = get_object_position(menu.position_anchor);
	menu.size = get_object_size(menu.position_anchor);
	return 0;
}

function menu_set_item_position_and_size (menu_item) {
	set_object_position(menu_item.layer, menu_item.menu.position.x, menu_item.menu.position.y);
	set_object_size(menu_item.layer, menu_item.menu.size.width, menu_item.menu.size.height);
	debug.writeln("layer [" + menu_item.layer_id + "] (" + menu_item.menu.position.x + ", " + menu_item.menu.position.y + ") " + menu_item.menu.size.width + " x " + menu_item.menu.size.height);
}


function menu_hide (note) {
	debug.writeln("menu_hide ## " + note);
	if (! menus_menu_context.length) {
	    return;
	}
	if (! menus_menu_context.top().visible_item) {
	    return;
	}

	if (menus_proporder_from_show) {
	    menus_proporder_from_show = 0;
	    return;
	}

	// When we get a hide command, we actually sleep for a few
	// milliseconds (less than the magic 100ms threshold, though)
	// before we carry out the hide. When we -do- the hide, we
	// check to see if we still should.

	// See, the problem is that some times the menu buttons and
	// the menu targets are separated by minute gaps (usually one
	// or two pixels) caused by HTML's inexact control over table
	// positioning and adjacency. So what we do is, whenever there's
	// a Mouse event over top of a "safe" area, we set a flag saying
	// that everything's safe, and not to hide the menu. Whenever
	// we go over a "dangerous" area, we clear that flag and start
	// a timer. If the timer expires and the flag is still clear,
	// then we hide the menu, figuring that the user spent long
	// enough in the "danger" zone that they really did trigger the
	// hide. However, if the flag is set to true by the time the
	// hide timer expires, we assume that the user was just crossing
	// the gap, and we undo the hide event.

	// Long comments like this convince me that I need to write a
	// JavaScript preprocessor that will strip out comments.

	debug.writeln("menu_hide: marking contexts for hide");
	for (var idx in menus_menu_context) {
		menus_menu_context[idx].actually_hide = 1;
	}
	menus_proporder_from_hide = 1;
	oneshot_event("_do_hide", _do_hide, 75);
}


function _do_hide () {
	// Walk down from the top of the context stack, hiding and popping
	// any visible menu items that still have "actually_hide" set. If
	// we reach one that doesn't have a visible_item or an
	// actually_hide flag set, then the hiding is finished.

	debug.writeln("_do_hide");
	var context = menus_menu_context.top();
	while (context) {
		if (! context.visible_item) {
			debug.writeln("_do_hide: no visible_item in context");
			return;
		}
		if (! context.actually_hide) {
			debug.writeln("_do_hide: actually_hide false in context [" + context.menu_context + "]");
			return;
		}

		debug.writeln("<font color=\"red\">_do_hide: hiding in context [" + context.menu_context + "]</font>");
		hide_object(context.visible_item.layer);
		menus_set_button_mode(context.visible_item, "inactive");
		context.visible_item = null;

		menus_menu_context.pop();
		context = menus_menu_context.top();
	}
	debug.writeln("_do_hide: fallthrough");
}


function menu_cancel_hide (menu, menu_item, menu_context, note) {
	debug.writeln("menu_cancel_hide: " + menu + ", " + menu_item + ", " + menu_context + " ## " + note);
	context = context_find(menu_context);
	if (context) {
		debug.writeln("menu_cancel_hide: found context; disabling hide");
		context.actually_hide = 0;
	}


	// TODO: figure out what this shit is doing :)

	if ((! menu) && (! menu_item)) {
		menus_deferred.menu = null;
		menus_deferred.menu_item = null;
		menus_deferred.menu_context = null;
	}

	if ((menu != menus_deferred.menu)
		|| (menu_item != menus_deferred.menu_item)
		|| (menu_context != menus_deferred.menu_context)
		) {
		menus_deferred_menu = null;
		menus_deferred_menu_item = null;
	}

	if (menus_proporder_from_hide)
		menus_proporder_from_hide = 0;
	else
		menus_proporder_from_show = 1;
}

function context_is_active (menu_context) {
	return context_find (menu_context);
}

function context_find (menu_context) {
	for (var idx in menus_menu_context) {
		if (menus_menu_context[idx].menu_context == menu_context) {
			return menus_menu_context[idx];
		}
	}
	return null;
}	

// TODO: reorder delay and menu_context
function menu_show (menu, menu_item, delay, menu_context) {
	debug.writeln("<font color=\"red\">menu_show: " + menu + ", " + menu_item + ", " + delay + ", " + menu_context + "</font>");
	// Pretty much no matter what, we want to cancel any pending hide
	// These are part of the hide / show logic
	for (var idx in menus_menu_context) {
		menus_menu_context[idx].actually_hide = 0;
	}
	if (menus_proporder_from_hide)
		menus_proporder_from_hide = 0;
	else
		menus_proporder_from_show = 1;


	// A bunch of this code is somewhat similar to what goes on in
	// _do_menu_show. Need to consider how to refactor it out so it's
	// not happening twice.


	// If this is the first menu target we've hit in this context, no
	// need to bother with the timer and all that mess. Just go ahead
	// and start the show.

	//if (! context_is_active(menu_context)) {
	//	return _do_menu_show(menu, menu_item, menu_context);
	//}


	// If the context is active, then we need to check to see whether
	// or not this context is "on top". If it's "on top", then a
	// reshow is a no-op. If we're not "on top", but this -is- a
	// reshow, then it's a simple hide of all contexts "above" this
	// one.

	var top_menu_context = menus_menu_context.top();
	if (top_menu_context && top_menu_context.menu_context == menu_context) {
		// okay, this context is "on top". Is this a reshow?
		if ((top_menu_context.menu == menu)
			&& (top_menu_context.menu_item == menu_item)) {

			// It's a reshow. That's a no-op.
			return;
		}
	}



	// It's not a reshow of a menu on this context level. It might
	// still be a reshow of a menu "under" this one, but we don't care
	// any more because it's not a strict no-op. The logic in
	// menu_show will handle updating the context stack and hiding any
	// obscuring contexts if necessary.
		
	menus_deferred.menu = menu;
	menus_deferred.menu_item = menu_item;
	menus_deferred.menu_context = menu_context;

	oneshot_event("_deferred_menu_show", _deferred_menu_show, delay);
}

function _deferred_menu_show () {
	if (!menus_deferred.menu) { return; }
	if (!menus_deferred.menu_item) { return; }
	if (!menus_deferred.menu_context) {  return; }

	_do_menu_show(menus_deferred.menu, menus_deferred.menu_item, menus_deferred.menu_context);
}

function _do_menu_show (menu, menu_item, menu_context) {
	debug.writeln("<font color=\"red\">_do_menu_show: " + menu + ", " + menu_item + ", " + menu_context + "</font>");
	// These are just sanity checking and namespace setup
	menu = menus[menu];
	if (! menu) { debug.writeln("<font color=\"orange\">can't find menu object</font>"); return; }

	menu_item = menu.items[menu_item];
	if (! menu_item) { debug.writeln("<font color=\"orange\">can't find menu_item object</font>"); return; }


	// Make sure we have the latest position and size info
	errors = menu_get_menu_position_and_size(menu);
	if (errors) {
	    return;
	}
	menu_set_item_position_and_size(menu_item);


	// Show the new menu and button
	var item_being_shown = menu_item;
	menus_set_button_mode(menu_item, "active");
	show_object(menu_item.layer);


	// Hiding the old stuff depends on the context:

	// If the context is active on the stack, then we need to walk
	// through the stack, hiding anything that's "above" the context
	// (which includes hiding both the active menu items on each
	// "above" context and hiding the contexts themselves).

	// As a consequence of this, we'll be popping the target from the
	// stack, too. That's just so we don't have to put an extra
	// conditional in. We "always" push the target context onto the
	// top of the stack for simplicity, knowing that by the time we
	// get there, it will always be off of the stack.

	// If the context is not active on the stack, then we just need to
	// add it to the stack (which, as described above, we always
	// do). We don't worry about hiding anything "underneath" it,
	// since its z-position ought to take care of that for us.

	// TODO: seems like we're doing too much popping and pushing and
	// still not ending up with a "clean" algorithm. Might as well go
	// back to the special case, since that doesn't require us to pay
	// special attention to the "item_being_shown", below.

	if (context_is_active(menu_context)) {
		var top_menu_context = menus_menu_context.top();
		while (top_menu_context) {
			// We don't actually want to hide the item we're showing,
			// so we've got a little sanity check for that here.
			if ((top_menu_context.visible_item)
				&& (top_menu_context.visible_item != item_being_shown)) {
				hide_object(top_menu_context.visible_item.layer);
				menus_set_button_mode(top_menu_context.visible_item, "inactive");
			}

			menus_menu_context.pop();
			if (top_menu_context.menu_context == menu_context) {
				break;
			}
			top_menu_context = menus_menu_context.top();
		}
	}


	var context = Object();
	
	context.menu = menu;
	context.menu_item = menu_item;
	context.menu_context = menu_context;
	context.visible_item = menu_item;
	
	menus_menu_context.push(context);


	// We do this here instead of in menu_init because some browsers
	// release captured events under certain circumstances. I think.
	set_object_event(menu_item.layer, "mouseover",
					 function (event) {menu_cancel_hide(menu, menu_item, menu_context, "mouseover event");});

	set_object_event(menu_item.layer, "mousemove",
					 function (event) {menu_cancel_hide(menu, menu_item, menu_context, "mousemove event");});

	set_object_event(menu_item.layer, "mouseout",
					 function (event) {menu_hide("mouseout event");});
}

function menus_set_button_mode (menu_item, mode) {
	for (var idx in menu_item.buttons) {
		menu_item.buttons[idx].object.src = menu_item.buttons[idx].modes[mode].src;
	}
}



/* This is for Emacs' benefit:
 * Local Variables:  ***
 * mode: Java        ***
 * End:              ***
 */
