/******************************************************************
 * File indira.js
 *****************************************************************/

/**
 * Scan the document on load to find all "image buttons" (i.e. either
 * input[type="image"] or a+img combos and create ImageButton
 * instances for them.
 *
 * NOTE: to change the way css class names are used to encode
 * ImageButton behavior, extend or re-implement BaseImageButtonFactory
 * and override this function.
 */
function indiraScanDoc () {
    Indira.scanDoc(new BaseImageButtonFactory());
}

// Don't use as onUnload handler (that would expect an event first
// arg).  This is useful to call from JS to profile the cleanup
// process.
function indiraClear (deep, displayTime) {
    var t1 = new Date().getTime();
    Indira.releaseResources(deep);
    if (displayTime) {
        var t2 = new Date().getTime();
        alert("Released resources in " + (t2 - t1) + " msec.");
    }
}

// onUnload function -- half-hearted attempt to work around browser
// memory leaks
function indiraShallowClear (event) {
    Indira.releaseResources(false);
}

// onUnload function -- thorough (but maybe slow) workaround for
// browser memory leaks
function indiraDeepClear (event) {
    Indira.releaseResources(true);
}

function indiraReady () {
    return Indira.SCANNED;
}

function indiraSetMode (id, mode) {
    if (!Indira.SCANNED) return;
    var but = Indira.getImageButton(id);
    if (!but) {
        alert("ERROR: no image button with id '" + id + "'");
        return;
    }
    // else
    but.setMode(mode);
}

function indiraSetModeIfEnabled (id, mode) {
    if (!Indira.SCANNED) return;
    var but = Indira.getImageButton(id);
    if (!but) {
        alert("ERROR: no image button with id '" + id + "'");
        return;
    }
    // else
    if (but.mode != "g")
        but.setMode(mode);
}

function indiraSetFamily (id, family) {
    if (!Indira.SCANNED) return;
    var but = Indira.getImageButton(id);
    if (!but) {
        alert("ERROR: no image button with id '" + id + "'");
        return;
    }
    // else
    but.setFamily(family);
}

function indiraSetOnBeforeScan (fn) {
    Indira.ON_BEFORE_SCAN = [fn];
}

function indiraSetOnAfterScan (fn) {
    Indira.ON_AFTER_SCAN = [fn];
}

function indiraAddOnBeforeScan (fn) {
    if (!Indira.ON_BEFORE_SCAN)
        indiraSetOnBeforeScan(fn);
    else {
        Indira.ON_BEFORE_SCAN.push(fn);
    }
}

function indiraAddOnAfterScan (fn) {
    if (!Indira.ON_AFTER_SCAN)
        indiraSetOnAfterScan(fn);
    else {
        Indira.ON_AFTER_SCAN.push(fn);
    }
}

// FIXME - this should get moved to a standard library somewhere.
// Add an event to a listener interface, without disrupting prev. setting.
// See http://www.scottandrew.com/weblog/articles/cbs-events,
// http://www.bobbyvandersluis.com/articles/unobtrusiveshowhide.php
function addEvent (obj, evType, fn) {
 if (obj.addEventListener) {
   obj.addEventListener(evType, fn, true);
   return true;
 } else if (obj.attachEvent) {
   return obj.attachEvent("on" + evType, fn);
 } else {
   return false;
 }
}

// FIXME: it is hard to override indiraScanDoc when it is mentioned
// explicitly here...  
// 
// Now add onload event so this will all get executed
addEvent (window, 'load', indiraScanDoc);
addEvent (window, 'unload', indiraDeepClear);

/******************************************************************
 * class Indira
 *****************************************************************/

function Indira () {
    // nothing
}

/**
 * static vars
 */

// look up global families via image file basename
Indira.IMAGE_FAMILIES = new Object();
// look up image button via dom element ID
Indira.IMAGE_BUTTONS = new Object();
// have we scanned this document yet?
Indira.SCANNED = false;
// callback
Indira.ON_BEFORE_SCAN = null;
// callback
Indira.ON_AFTER_SCAN = null;
// callback function for debugging
Indira.DEBUG = null;

/**
 * Scan document for Indira-managed images, using supplied Image
 * Button factory.  Will not scan the same document more than once,
 * even if this function is called multiple times.  Calls the before-
 * and after-scan callback functions if they have been set (see
 * indiraSetBeforeScan() and indiraSetAfterScan() above).a
 */
Indira.scanDoc = function (factory) {
    if (Indira.SCANNED) { return; };
    Indira.callbackAll(Indira.ON_BEFORE_SCAN);
    var but;
    // alert(document.images.length + " images in document");
    for (var i = 0; i < document.images.length; i++) {
        // alert("image " + i + " src=" + document.images[i].src);
        but = factory.createImageButton(document.images[i]);
        if (but) {
            Indira.IMAGE_BUTTONS[but.getId()] = but;
        }
    }
    // At least in Firefox, <input type="image"> doesn't get included in
    // the form.elements array, so we have to look for image inputs this way:
    if (document.getElementsByTagName) {
	var inputs = document.getElementsByTagName("input");
	for (var j = 0; j < inputs.length; j++) {
	    if (inputs[j].type == 'image') {
                but = factory.createImageButton(inputs[j]);
                if (but) {
                    Indira.IMAGE_BUTTONS[but.getId()] = but;
                }
	    }
	}
    }
    Indira.SCANNED = true;
    Indira.callbackAll(Indira.ON_AFTER_SCAN);
};

/**
 * Call callback function if it has been set.
 */
Indira.callback = function (fn) {
    if (typeof fn == "function") {
        fn.call();
    }
};

/**
 * Call callback function if it has been set.
 */
Indira.callbackAll = function (fns) {
    if (fns) {
        for (var i = 0; i < fns.length; i++) {
            var fn = fns[i];
            if (typeof fn == "function") {
                fn.call();
            }
        }
    }
};

/**
 * clean up (to avoid mem leaks)
 */
Indira.releaseResources = function (deep) {
    if (deep) {
        for (var b in Indira.IMAGE_BUTTONS) {
            Indira.IMAGE_BUTTONS[b].removeEventHandlers();
        }
    }
    Indira.IMAGE_FAMILIES = new Object();
    Indira.IMAGE_BUTTONS = new Object();
};

/**
 * static factory method
 *
 * @basename see ImageFamily constructor
 * @ext see ImageFamily constructor
 * @states see ImageFamily constructor
 */
Indira.getFamilyCreate = function (basename, ext, states) {
    var fam = Indira.IMAGE_FAMILIES[basename];
    if (!fam) {
        // alert("new family: '" + basename + "', '" + ext + "', '" + states + "'");
        fam = new ImageFamily(basename, ext, states);
        Indira.IMAGE_FAMILIES[basename] = fam;
    }
    return fam;
};

Indira.getImageButton = function (id) {
    return Indira.IMAGE_BUTTONS[id];
};

/**
 * call user's debugging callback function, if any
 */
Indira.debug = function (txt) {
    if (typeof Indira.DEBUG == "function") {
        Indira.DEBUG.call(this, txt);
    }
};

/**
 * static event handlers
 */
Indira.out = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.debug("out");
    Indira.setImageState(this, '');
};

Indira.over = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.debug("over");
    Indira.setImageState(this, 'o');
};

Indira.down = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.debug("down");
    Indira.setImageState(this, 'd');
};

Indira.up = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.debug("up");
    Indira.setImageState(this, 'o');
};

Indira.outLink = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.debug("outLink");
    Indira.setChildImageState(this, '');
};

Indira.overLink = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.debug("overLink");
    Indira.setChildImageState(this, 'o');
};

Indira.downLink = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.debug("downLink");
    Indira.setChildImageState(this, 'd'); 
};

Indira.upLink = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.debug("upLink");
    Indira.setChildImageState(this, 'o'); 
};

Indira.blurLink = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.debug("blurLink");
    Indira.setChildImageState(this, '');
};

Indira.focusLink = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.debug("focusLink");
    Indira.setChildImageState(this, 'o', '');
};

Indira.focusInput = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.setImageState(this, 'o', '');
};

Indira.keyupLink = function (e) {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    if (Indira.isReturnKey(e)) {
        Indira.debug("keyupLink");
        // alert("keyup is return");
        Indira.setChildImageState(this, 'o');
    } 
    return true;
};

Indira.keydownLink = function (e) {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    if (Indira.isReturnKey(e)) {
        Indira.debug("keydownLink");
        // alert("keydown is return");
        Indira.setChildImageState(this, 'd');
    } 
    return true;
};

Indira.focusLabel = function () {
    if ((typeof Indira) == "undefined") return;
    if (!Indira.SCANNED) return;
    Indira.setChildImageState(this, 'o', '');
};


/**
 * @param fromState if specified means only perform the state change
 * if the image is currently in fromState.  This was added
 * specifically to enable preventing focus from overriding mousedown
 * events.
 */
Indira.setImageState = function (obj, state, fromState) {
    // alert("setImageState('" + obj.id + "','" + state + "')");
    var but = Indira.getImageButton(obj.id);
    // alert("setImageState('" + obj.id + "','" + state + "'), but = " + but);
    if (!but) { return; }
    if (fromState === undefined || but.getLastRequestedState() == fromState) {
        but.requestState(state);
    }
};

Indira.isReturnKey = function (e) {
    if (!e) { var e = window.event; }
    var code;
    if (e.keyCode) {
        code = e.keyCode;
    } else if (e.which) {
        code = e.which;
    }
    if (code == 13) { return true; }
    return false;
};

/**
 * set the state of the FIRST child of type IMG.  If your link
 * contains more than one image and the first one is not the
 * indira-relevant one for this link, then this will not work.
 * @param fromState if specified means only perform the state change
 * if the image is currently in fromState.  This was added
 * specifically to enable preventing focus from overriding mousedown
 * events.
 */
Indira.setChildImageState = function (obj, state, fromState) {
    var img = obj.getElementsByTagName("IMG")[0];
    if (!img) { return; }
    // alert("setImageState('" + obj.id + "','" + state + "')");
    var but = Indira.getImageButton(img.id);
    // alert("setImageState('" + obj.id + "','" + state + "'), but = " + but);
    if (!but) { return; }
    if (fromState === undefined || but.getLastRequestedState() == fromState) {
        but.requestState(state);
    }
};

/******************************************************************
 * class BaseImageButtonFactory
 *
 * Examines the IMG or INPUT object's properties to create a new
 * ImageButton subclass as follows:
 * 
 * @class contains two types of info: the "initial FMS (family, mode
 * and state)" classname and 0 or more "family specification"
 * classnames. 
 *
 * Initial FMS: has the form "_init_<famSubId>_<mode>_<state>" where
 * <famSubId> is either "" or the alphanumeric subId of a non-default
 * image family, <mode> is either "" or in "gs", <state> is either ""
 * or one of "odgs".  The minimal form (and BY FAR the most common) is
 * "_init___" which may be left unspecified.
 *
 * Examples:
 *
 * 1. "_init__s_" for default family, selected mode, no image
 * state (i.e. no selected or "o" state was available in this family).
 *
 * 2. "_init_x__" for family "x" active mode, no initial state
 *
 * 3. "_init_x_s_o" for family "x", selected mode, state "o"
 *
 * Family Specification: has the form "_<famSubId>_<states>" and
 * simply defines what image states are available in that family.  The
 * minimal example is "__", which may also be left unspecified.
 * This indicates that the default family has no special image states.
 *
 * Examples:
 *
 * 1. "__od" means the default family contains "o" and "d"
 * states
 *
 * 2. "_x_" means family "x" contains no special states
 *
 * 3. "_x_os" means family "x" contains "o" and "s" states
 *
 *
 * @src contains the url of the initial image.  It is crucial that
 * this correspond to the information contained in the Initial FMS
 * classname, since that information is used to parse the SRC to
 * arrive at the "proto-basename" for the whole group of families
 * denoted by this image.  If the initial img@src or input@src is out
 * of sync with the Initial FMS classname, then all hell will break
 * loose.
 *
 * OTHER CRUCIAL ASSUMPTIONS:
 *
 * 1. all images in all families in the group represented by a single
 * ImageButton must (a) have the same extension/type and (b) be in the
 * same path
 *
 * 2. the image family and image state are encoded transparently in
 * the final image filename (NOT the indira id)
 *
 * 3. family subId may not be "o", "d", "s" or "g", because file
 * naming conventions would create a conflict: "item_o.gif" could mean
 * the "o" state of the default family or the default state of the "o"
 * family.
 *
 *****************************************************************/

/**
 * Constructor
 *
 */
function BaseImageButtonFactory () {
    this.init();
}

/**
 * regex members, may be overridden in subclass to change the
 * way classnames are used to encode ImageButton specs.
 */
BaseImageButtonFactory.prototype.FAMILY_REGEX = /^_([a-z0-9]*)_([odsg]*)$/;
BaseImageButtonFactory.prototype.INIT_STATE_REGEX = /\b_init_([a-z0-9]*)_([sg]*)_([odsg]*)\b/;

/**
 * initializer
 */
BaseImageButtonFactory.prototype.init = function () {
    // do nothing here in base class
};

/**
 * return [familyId, mode, state] where mode is "" or in "sg", and
 * state is "" or in "odsg".  NOTE mode can be "s" while state is "o"
 * if no special "s" image state is available for the selected mode.
 */
BaseImageButtonFactory.prototype.getInitFamilyModeState = function (className) {
    // alert("parsing init FMS from '" + className + "'");
    var initInfo = this.INIT_STATE_REGEX.exec(className);
    // if none found (the normal case, assume defaults)
    if (!initInfo || initInfo.length === 0) {
        // alert("no init FMS found");
        // [familyId, mode, state]
        return ["", "", ""];
    }
    // else
    // alert("init FMS: ['" + initInfo[1] + "','" + initInfo[2] + "','" + initInfo[3] + "']");
    return [initInfo[1], initInfo[2], initInfo[3]];
};

/**
 * returns either an instance of ImageButton (subclass) or null if
 * this element has no potentially interesting behavior
 */
BaseImageButtonFactory.prototype.createImageButton = function (domElt) {
    var id = domElt.id;
    // alert (id + " has src = " + domElt.src + " and realSrc = " + domElt.realSrc);
    // alert("maybe createImageButton: " + id + ", '" + domElt.className + "'");
    // if no ID, then not an indira-managed object
    if (!id) { return null; }

    // if it's not a button or linked image, then don't need any of
    // this, so return null
    if (!this.isImageButton(domElt)) { return null; }
    // alert("doing createImageButton: " + id + ", '" + domElt.className + "'");

    var cl = domElt.className;
    // TODO: if cl is empty, then really the only use of indira would
    // be to disable the link itself (i.e. for disabled or selected
    // mode).  Since this capability is probably pretty rarely needed,
    // maybe we should provide a way for the HTML to promise this code
    // that image's link will never need to be disabled, thus
    // bypassing the need to create any ImageButton for it at all.

    // first figure out the ext, basename and initial state
    var fms = this.getInitFamilyModeState(cl);
    // alert("'" + id + "' has init mode of '" + fms[1] + "'");
    var src;
    // if pngbehavior is in use, real src will be stashed in realSrc property
    if (domElt.realSrc) {
	src = domElt.realSrc; 
    } else {
	src = domElt.src;
    }
    var dotPos = src.lastIndexOf('.');
    var ext = src.substring(dotPos + 1);
    var famExt = this.maybeUnderscore(fms[0]);
    var stateExt = this.maybeUnderscore(fms[2]);
    var protoBasename = src.substring(0, dotPos - (famExt.length + stateExt.length));
    // alert("ext='" + ext + "', famExt='" + famExt + "', stateExt='" + stateExt + "', protoBasename='" + protoBasename + "', mode='" + fms[1] + "'");
    // now decode the family specs
    var families = new Object();
    var cls = cl.split(' ');
    for (var i = 0; i < cls.length; i++) {
        var info = this.FAMILY_REGEX.exec(cls[i]);
        // is this a family spec class? if not, skip
        if (!info || info.length === 0) { continue; }
        var famSubId = info[1];
        families[famSubId] = Indira.getFamilyCreate(protoBasename + this.maybeUnderscore(famSubId), ext, info[2]);
    }
    // make sure the default gets in there even if unspecified
    if (!families[""]) {
        families[""] = Indira.getFamilyCreate(protoBasename, ext, "");
    }
    
    // var buf = "FAMILIES:\n";
    // for (var f in families) {
    //    buf += "  '" + f + "': " + families[f].toString() + "\n";
    // }
    // alert(buf);

    return this.createImageButtonInternal(domElt, families, fms[0], fms[1]);

};

/**
 * PROTECTED: create the actual ImageButton subclass instance.
 * Override (or modify) this method if you need to add support for
 * more types of ImageButtons.
 */
BaseImageButtonFactory.prototype.createImageButtonInternal = function (domElt, families, initFamSubId, initMode) {
    if (this.isLinkedImage(domElt)) {
        return new LinkedImageButton(domElt.id, families, initFamSubId, initMode);
    } 
    if (this.isLabelImage(domElt)) {
        return new LabelImageButton(domElt.id, families, initFamSubId, initMode);
    } 
    // else must be input image button
    return new InputImageButton(domElt.id, families, initFamSubId, initMode);
};


/**
 * PROTECTED: is this domElt something that we need to make an
 * ImageButton (subclass) instance for?  Override (or modify) this
 * method if you need to add support for more types of ImageButtons.
 */
BaseImageButtonFactory.prototype.isImageButton = function (domElt) {
    return (this.isInputButton(domElt) || this.isLinkedImage(domElt) || this.isLabelImage(domElt));
};


/**
 * is domElt an input[@type='image']
 */
BaseImageButtonFactory.prototype.isInputButton = function (domElt) {
    // NOTE: acc. to http://www.quirksmode.org/dom/w3c_core.html,
    // nodeName always returns UPPER CASE, so we don't need to worry here
    return (domElt.nodeName == "INPUT");
};

/**
 * is domElt an HTML 'IMG' element whose parent is a link ('A'
 * element)
 *
 * NOTE: currently the IMG must have A as its *immediate parent*, not
 * just and ancestor.  This could easily be relaxed if necessary.
 */
BaseImageButtonFactory.prototype.isLinkedImage = function (domElt) {
    // NOTE: acc. to http://www.quirksmode.org/dom/w3c_core.html,
    // nodeName always returns UPPER CASE, so we don't need to worry here
    return (domElt.nodeName == "IMG" && domElt.parentNode.nodeName == "A");
};


/**
 * is domElt an HTML 'IMG' element whose parent is a label ('LABEL'
 * element)
 *
 * NOTE: currently the IMG must have LABEL as its *immediate parent*, not
 * just and ancestor.  This could easily be relaxed if necessary.
 */
BaseImageButtonFactory.prototype.isLabelImage = function (domElt) {
    // NOTE: acc. to http://www.quirksmode.org/dom/w3c_core.html,
    // nodeName always returns UPPER CASE, so we don't need to worry here
    return (domElt.nodeName == "IMG" && domElt.parentNode.nodeName == "LABEL");
};


BaseImageButtonFactory.prototype.maybeUnderscore = function (str) {
    return (str ? "_" + str : "");
};


/******************************************************************
 * class ImageFamily requires Indira
 *****************************************************************/

/**
 * Constructor
 *
 * @param basename includes path + filename up to right before where
 * the state suffix would go.  I.e., it INCLUDES the family subID.
 * (e.g. "/icons/item_c_s.gif" has basename "/icons/item_c")
 * @param ext the extension (image file format)
 * @param states for example "ods" means there are explicit images
 * available for the OVER ("_o"), DOWN ("_d") and SELECTED ("_s")
 * states. 
 */
function ImageFamily (basename, ext, states) {
    this.init(basename, ext, states);
}

/**
 * initializer
 */
ImageFamily.prototype.init = function (basename, ext, states) {
    this.basename = basename;
    this.extension = ext;
    this.states = states;
    this.loaded = false;
    this.preloads = new Object();
};


/**
 * return states as string such as "odsg"
 */
ImageFamily.prototype.getStates = function () {
    return this.states;
};

/**
 * for debugging
 */
ImageFamily.prototype.toString = function () {
    var buf = "[FAMILY basename='" + this.basename + "', extension='" + this.extension;
    buf += ", states='" + this.states + "']";
    return buf;
}        

/**
 * 
 */
ImageFamily.prototype.preloadStates = function () {
    if (this.loaded) { return; }
    this.loaded = true;
    // Save original image source under "" state
    var orig = new Image();
    orig.src = this.basename + "." + this.extension;
    this.preloads[""] = orig;

    // Preload and save other states
    for (var i = 0; i < this.states.length; i++) {
        var state = this.states.charAt(i);
        var newObj = new Image();
        newObj.src = this.basename + '_' + state + "." + this.extension;
        this.preloads[state] = newObj;
    }
};
    
/**
 * 
 */
ImageFamily.prototype.getStateSource = function (state) {
    var preload = this.preloads[state];
    if (preload) {
        return preload.src;
    }
    // else
    return null;
};


/******************************************************************
 * class ImageButton
 *****************************************************************/

/******************************************************************
 * Constructor
 *****************************************************************/
function ImageButton (id, families, initFamSubId, initMode) {
    if (id !== undefined)
        this.init(id, families, initFamSubId, initMode);
}

/**
 * initializer
 */
ImageButton.prototype.init = function (id, families, initFamSubId, initMode) {
    // Indira.debug("ImageButton.init('" + id + "'," + families + ",'" + initFamSubId + "','" + initMode + "')");
    this.id = id;
    this.families = families;
    this.familyId = "";
    this.family = null;
    this.examineInitialDOM();
    this.setModeInternal("");
    this.requestedState = "";
    // used as a flag for performance
    this.hasEventHandlers = false;
    // after this, this.family is guaranteed non-null or an error
    // alert will be shown 
    this.setFamily(initFamSubId);
    this.setMode(initMode);
};

/**
 * look at initial DOM before indira does any mangling, in order to
 * cache link props
 */
ImageButton.prototype.examineInitialDOM = function () {
    // do nothing by default
};

/**
 * get the ID of this object, which is the same as the img or input id
 */
ImageButton.prototype.getId = function () {
    return this.id;
};

/**
 * get the state that the image is currently in
 */
ImageButton.prototype.getLastRequestedState = function () {
    return this.requestedState;
};

/**
 * set the current family, making sure it is a legal
 */
ImageButton.prototype.setFamily = function (famSubId) {
    var origFamId = this.familyId;
    this.setFamilyInternal(famSubId);
    if (this.familyId != origFamId) {
        var curState = this.getLastRequestedState();
        this.setMode(this.mode, true);
        // get back to orig state if poss
        this.requestState(curState);
    }
};

/**
 */
ImageButton.prototype.setFamilyInternal = function (famSubId) {
    if (!famSubId) { famSubId = ""; }
    var newFam = this.families[famSubId];
    Indira.debug("setting family of " + this.id + " to '" + famSubId + "' (" + newFam + ")");
    if (!newFam) {
        if (famSubId == "") {
            alert("ERROR: ImageButton['" + this.id + "'] has no default family.");
        }
        this.setFamilyInternal("");
        return;
    }
    // else
    this.familyId = famSubId;
    this.family = newFam;
};

/**
 * get the DOM IMG or INPUT object
 */
ImageButton.prototype.image = function () {
    return document.getElementById(this.id);
};
    
/**
 * set the href and target of this object, but if the link is
 * currently disabled, DO NOT enable it (just save the href and target
 * for when the link is re-enabled)
 */
ImageButton.prototype.setHrefAndTarget = function (href, target) {
    alert("ERROR: Abstract method ImageButton.setHrefAndTarget() called.");
};

/**
 * @param mode is one of "" (active), "g" (disabled), "s" (selected).
 * By design, the mode ID corresponds to the default image STATE to
 * request for the mode.
 * @param force by default, don't set img state if the new mode is the
 * same as the old mode, unless force is true (this is case when the
 * family has changed and must reset the image state with the current
 * mode but new family)
 */
ImageButton.prototype.setMode = function (mode, force) {
    var img = this.image();
    Indira.debug("setting mode of " + this.id + " to '" + mode + "'");
    if (mode == "g" || mode == "s") {
        this.deactivate();
    } else if (mode == "") { // activate 
        if (force) {
            // when switching families make sure previous family's
            // event handlers are wiped first or there may be
            // bleed-through if the new family has fewer states.
            // (because of the graceful state fallback mechanism this
            // would actually never be noticed though...)
            this.removeEventHandlers();
        }
        this.activate();
    } else {
        alert("ERROR: Unknown mode '" + mode + "' requested: ignoring.");
        return;
    }
    // don't do a requestState unless the mode is actually changing
    // or (if force == true) the family has changed and we need to 
    // reset the state no matter what
    if (force || this.mode != mode) {
        this.setModeInternal(mode);
        // since the default state for the mode is the same as the mode
        // ID, it is meaningful to requestState(mode)
        this.requestState(mode);
    }
};

/**
 * override this if you want something else to happen when mode
 * property changes.
 */
ImageButton.prototype.setModeInternal = function (mode) {
    this.mode = mode;
};

/**
 * Switch state to the requested state if possible, otherwise try
 * reasonable alternatives
 */
ImageButton.prototype.requestState = function (state) {
    Indira.debug(this.id + ": requestState('" + state + "')");
    this.requestedState = state;
    switch (state) {
        case '':
            this.setState('');
            break;
        case 'o':
            this.setState('o', '');
            break;
        case 'd':
            this.setState('d', 'o', '');
            break;
        case 's':
            this.setState('s', 'o', '');
            break;
        case 'g':
            this.setState('g', '');
            break;
    }
};

/**
 * setState(s1, s2, ... sN): tries each state in turn, setting img to
 * that state if avail, else moving on to next.  If none are
 * available, do nothing.
 */
ImageButton.prototype.setState = function (varargs) {
    // alert(this.id + ": setState(" + varargs + ").  args=" + arguments.length + ", args[0]=" + arguments[0]);
    var fam = this.family;
    for (var i = 0; i < arguments.length; i++) {
        var stateSrc = fam.getStateSource(arguments[i]);
        // alert("fam = " + fam.toString() + ", fam['" + arguments[i] + "'] = " + stateSrc);
        if (stateSrc) {
            this.image().src = stateSrc;
            break; // done
        }
    }
};

/**
 */
ImageButton.prototype.addEventHandlers = function () {
    alert("ERROR: Abstract method ImageButton.addEventHandlers() called.");
};

/**
 */
ImageButton.prototype.removeEventHandlers = function () {
    alert("ERROR: Abstract method ImageButton.removeEventHandlers() called.");
};

// --------------------------------------------------------------
// utility methods
// --------------------------------------------------------------

/**
 * In IE, can't just use element.setAttribute().  See discussion near
 * the end of the following:
 * http://www.alistapart.com/articles/jslogging
 */
ImageButton.prototype.setAttribute = function (elt, att, val) {
    var attUcase = att.toUpperCase();
    // if the node's class already exists
    // then replace its value
    if (elt.getAttributeNode(att)) {
        for (var i = 0; i < elt.attributes.length; i++) {
            var attrName = elt.attributes[i].name.toUpperCase();
            if (attrName == attUcase) {
                elt.attributes[i].value = val;
            }
        }
        // otherwise create a new attribute
    } else if (val !== undefined && val !== null) {
        elt.setAttribute(att, val);
    }
};


ImageButton.prototype.removeAttribute = function (elt, att) {
    // superstitious: set the attribute value to undefined first just
    // in case the removal doesn't work
    this.setAttribute(elt, att, undefined);
    elt.removeAttribute(att);
};

/******************************************************************
 * class LinkedImageButton extends ImageButton
 *****************************************************************/

/******************************************************************
 * Constructor
 *****************************************************************/
function LinkedImageButton (id, families, initFamSubId, initMode) {
    this.init(id, families, initFamSubId, initMode);
}
LinkedImageButton.prototype = new ImageButton();
LinkedImageButton.prototype.constructor = LinkedImageButton;
LinkedImageButton.superclass = ImageButton.prototype;

LinkedImageButton.prototype.CURRENT_MODE_REGEX = / ?\bindira_mode_[sg]?\b/;

/**
 * initializer
 */
LinkedImageButton.prototype.init = function (id, families, initFamSubId, initMode) {
    // alert("LinkedImageButton.init('" + id + "'," + families + ",'" + initFamSubId + "','" + initMode + "')");
    LinkedImageButton.superclass.init.call(this, id, families, initFamSubId, initMode);
};

/**
 * look at initial DOM before indira does any mangling, in order to
 * cache link props
 */
LinkedImageButton.prototype.examineInitialDOM = function () {
    var lnk = this.link();
    // these can't be just held in the dom element because must delete
    // these from the dom element in order to disable
    this.href = lnk.href;
    // Indira.debug("initial DOM: " + this.id + ": href = '" + lnk.href + "'");
    this.target = lnk.target;
    this.onclick = lnk.onclick;
};

/**
 * get the DOM A object
 */
LinkedImageButton.prototype.link = function () {
    return this.image().parentNode;
};

/**
 * PUBLIC: For (rare) external use to alter the href and target of the
 * LinkedImageButton's link.  This method is smart enough to not
 * enable a disabled link, but to still keep the new info for the next
 * time the link is enabled.
 */
LinkedImageButton.prototype.setHrefAndTarget = function (href, target) {
    this.href = href;
    this.target = target;
    var lnk = this.link();
    if (this.isLinkEnabled(lnk)) {
        lnk.href = href;
        this.setLinkTarget(lnk, target);
    }
};

/**
 * PUBLIC: For (rare) external use to alter the onclick handler of the
 * LinkedImageButton.  This method is smart enough to not enable a
 * disabled link, but to still keep the new info for the next time the
 * link is enabled.
 */
LinkedImageButton.prototype.setOnClick = function (onclick) {
    this.onclick = onclick;
    var lnk = this.link();
    if (this.isLinkEnabled(lnk)) {
        lnk.onclick = onclick;
    }
};

/**
 * intercept changing of mode to update css class with "_mode_X" class
 */
LinkedImageButton.prototype.setModeInternal = function (mode) {
    this.setModeClass(this.link(), mode);
    this.mode = mode;
};

/**
 * PROTECTED: Template method called in setMode().
 */
LinkedImageButton.prototype.deactivate = function () {
    var img = this.image();
    var lnk = img.parentNode;
    // disable the link and set the image to disabled state
    if (this.isLinkEnabled(lnk)) {
        this.href = this.disableLink(lnk);
        lnk.onclick = null;
    } 
    this.removeEventHandlers();
};


/**
 * PROTECTED: Template method called in setMode().
 */
LinkedImageButton.prototype.activate = function () {
    var img = this.image();
    var lnk = img.parentNode;
    // Indira.debug(this.id + ": activating...");
    if (!this.isLinkEnabled(lnk)) {
        // Indira.debug(this.id + ": link was disabled");
        lnk.href = this.href;
        // Indira.debug(this.id + ": tried setting link href to '" + this.href + "'");
        // Indira.debug(this.id + ": link href is actually '" + lnk.href + "'");
        this.setLinkTarget(lnk, this.target);
        lnk.onclick = this.onclick;
    } 
    this.family.preloadStates();
    this.addEventHandlers();
};

/**
 * 
 */
LinkedImageButton.prototype.removeEventHandlers = function () {
    // short-circuit if possible
    if (!this.hasEventHandlers) { return; };
    var lnk = this.link();
    lnk.onmouseover = null;
    lnk.onfocus     = null;
    lnk.onmouseout  = null;
    lnk.onblur      = null;
    lnk.onmousedown = null;
    lnk.onkeydown   = null;
    lnk.onmouseup   = null;
    lnk.onkeyup     = null;
    this.hasEventHandlers = false;
};


/**
 * 
 */
LinkedImageButton.prototype.addEventHandlers = function () {
    var states = this.family.getStates();
    var lnk = this.link();
    for (var i = 0; i < states.length; i++) {
        var state = states.charAt(i);
        switch (state) {
        case 'o':
            this.hasEventHandlers = true;
            lnk.onmouseover = Indira.overLink;
            lnk.onfocus     = Indira.focusLink;
            lnk.onmouseout  = Indira.outLink;
            lnk.onblur      = Indira.blurLink;
            break;
        case 'd':
            this.hasEventHandlers = true;
            lnk.onmousedown = Indira.downLink;
            lnk.onkeydown   = Indira.keydownLink;
            lnk.onmouseup   = Indira.upLink;
            lnk.onkeyup     = Indira.keyupLink;
            break;
        }
    }
};


// -------------------------------------------------------------
// utility methods, 
//
// - could be global, but put them inside this class for namespace
//   protection.
//
// - could be static, but didn't want the verbosity of
//   LinkedImageButton.isLinkEnabled(), etc.
// -------------------------------------------------------------

LinkedImageButton.prototype.isLinkEnabled = function (elt) {
    return (elt.href || elt.onclick);
};

LinkedImageButton.prototype.setLinkTarget = function (elt, target) {
    if (target) {
        elt.target = target;
    } else {
        this.removeAttribute(elt, "target");
    }
};

LinkedImageButton.prototype.disableLink = function (elt) {
    var href = elt.href;
    this.removeAttribute(elt, "href");
    return href;
};

LinkedImageButton.prototype.setModeClass = function (elt, mode) {
    var cl = "indira_mode_" + mode;
    var currentCl = elt.className;
    if (!currentCl) {
        currentCl = cl;
    } else {
        // remove existing
        currentCl = currentCl.replace(this.CURRENT_MODE_REGEX, "");
        if (!currentCl) {
            currentCl = cl;
        } else {
            currentCl += " " + cl;
        }
    }
    // alert("new class = '" + currentCl + "'");
    elt.className = currentCl;
};


/******************************************************************
 * class InputImageButton extends ImageButton
 *****************************************************************/

/******************************************************************
 * Constructor
 *****************************************************************/
function InputImageButton (id, families, initFamSubId, initMode) {
    this.init(id, families, initFamSubId, initMode);
}
InputImageButton.prototype = new ImageButton();
InputImageButton.prototype.constructor = InputImageButton;
InputImageButton.superclass = ImageButton.prototype;

/**
 * initializer
 */
InputImageButton.prototype.init = function (id, families, initFamSubId, initMode) {
    // alert("InputImageButton.init('" + id + "'," + families + ",'" + initFamSubId + "','" + initMode + "')");
    InputImageButton.superclass.init.call(this, id, families, initFamSubId, initMode);
};


/**
 * get the DOM FORM object
 */
InputImageButton.prototype.form = function () {
    var par = this.image().parentNode;
    while (par) {
        if (par.nodeName == "FORM") {
            return par;
        }
        // else
        par = par.parentNode;
    }
    // none found
    alert("ERROR: InputImageButton object has no form ancestor.");
    return null;
};

/**
 *   
 */
InputImageButton.prototype.setHrefAndTarget = function (href, target) {
    this.href = href;
    this.target = target;
    var frm = this.form();
    frm.action = href;
    if (target) {
        frm.target = target;
    } else {
        frm.target = "_self";
    }
};

/**
 */
InputImageButton.prototype.deactivate = function () {
    // alert("deactivating");
    var img = this.image();
    // disable button and set the image to disabled state
    img.disabled = true;
    // this.setAttribute(img, "disabled", "true");
    this.removeEventHandlers();
};

/**
 */
InputImageButton.prototype.activate = function () {
    // alert("activating");
    var img = this.image();
    // activate button and set the image to active state
    img.disabled = false;
    // this.setAttribute(img, "disabled", "false");
    this.family.preloadStates();
    this.addEventHandlers();
};


/**
 * 
 */
InputImageButton.prototype.removeEventHandlers = function () {
    // short-circuit if possible
    if (!this.hasEventHandlers) { return; };
    var inp = this.image();
    inp.onmouseover = null;
    inp.onfocus     = null;
    inp.onmouseout  = null;
    inp.onblur      = null;
    inp.onmousedown = null;
    inp.onkeydown   = null;
    inp.onmouseup   = null;
    inp.onkeyup     = null;
    this.hasEventHandlers = false;
};



/**
 * 
 */
InputImageButton.prototype.addEventHandlers = function () {
    var states = this.family.getStates();
    var inp = this.image();
    for (var i = 0; i < states.length; i++) {
        var state = states.charAt(i);
        switch (state) {
        case 'o':
            this.hasEventHandlers = true;
            inp.onmouseover = Indira.over;
            inp.onfocus     = Indira.focusInput;
            inp.onmouseout  = Indira.out;
            inp.onblur      = Indira.out;
            break;
        case 'd':
            this.hasEventHandlers = true;
            inp.onmousedown = Indira.down;
            inp.onkeydown   = Indira.down;
            inp.onmouseup   = Indira.up;
            inp.onkeyup     = Indira.up;
            break;
        }
    }
};


/******************************************************************
 * class LabelImageButton extends ImageButton
 *****************************************************************/

/******************************************************************
 * Constructor
 *****************************************************************/
function LabelImageButton (id, families, initFamSubId, initMode) {
    this.init(id, families, initFamSubId, initMode);
}
LabelImageButton.prototype = new ImageButton();
LabelImageButton.prototype.constructor = LabelImageButton;
LabelImageButton.superclass = ImageButton.prototype;

/**
 * initializer
 */
LabelImageButton.prototype.init = function (id, families, initFamSubId, initMode) {
    // alert("LabelImageButton.init('" + id + "'," + families + ",'" + initFamSubId + "','" + initMode + "')");
    LabelImageButton.superclass.init.call(this, id, families, initFamSubId, initMode);
};


/**
 * get the DOM LABEL object
 */
LabelImageButton.prototype.label = function () {
    return this.image().parentNode;
};

/**
 */
LabelImageButton.prototype.deactivate = function () {
    this.removeEventHandlers();
};

/**
 */
LabelImageButton.prototype.activate = function () {
    this.family.preloadStates();
    this.addEventHandlers();
};


/**
 * 
 */
LabelImageButton.prototype.removeEventHandlers = function () {
    // short-circuit if possible
    if (!this.hasEventHandlers) { return; };
    var img = this.image();
    var lab = this.label();
    img.onmouseover = null;
    lab.onfocus     = null;
    img.onmouseout  = null;
    lab.onblur      = null;
    img.onmousedown = null;
    img.onkeydown   = null;
    img.onmouseup   = null;
    img.onkeyup     = null;
    this.hasEventHandlers = false;
};



/**
 * 
 */
LabelImageButton.prototype.addEventHandlers = function () {
    var states = this.family.getStates();
    var img = this.image();
    var lab = this.label();
    for (var i = 0; i < states.length; i++) {
        var state = states.charAt(i);
        switch (state) {
        case 'o':
            this.hasEventHandlers = true;
            img.onmouseover = Indira.over;
            lab.onfocus     = Indira.focusLabel;
            img.onmouseout  = Indira.out;
            lab.onblur      = Indira.out;
            break;
        case 'd':
            this.hasEventHandlers = true;
            img.onmousedown = Indira.down;
            img.onkeydown   = Indira.down;
            img.onmouseup   = Indira.up;
            img.onkeyup     = Indira.up;
            break;
        }
    }
};
