/*
 * Object: PopupHelp
 *
 * Purpose: Provide runtime support for the popup help functionality.
 *
 * @author Boniface Lau
 */
var PopupHelp = (function() {

    var oLastHelpMsg; // The last displayed help message.

    var aWin; // All opened windows - some may have been closed by the
              // user.

    var oWin; // The last opened window.

    /*
     * Method: toggleMouseHelpMsg
     *
     * Description: Toggle the display of the help message that the
     *              mouse is on.
     *
     * @param object oNode The node that the mouse is on.
     */
    function toggleMouseHelpMsg(oNode) {
        var oHelpMsg = locateMouseHelpMsg(oNode);
        if (oHelpMsg) {
            oHelpMsg.className = ((oHelpMsg.className == 'popupHelpMsgShown')
                                  ? 'popupHelpMsgHidden'
                                  : 'popupHelpMsgShown');
        }
    }

    /*
     * Method: locateMouseHelpMsg
     *
     * Description: Locate the help message that the mouse is on.
     *
     * @param object oNode The node what the mouse is on.
     */
    function locateMouseHelpMsg(oNode) {
        var oHelpMsg = null;
        var oCurNode = oNode;
        while (oHelpMsg == null) {
            if ((oCurNode != null) &&
                (oCurNode.tagName.toLowerCase() == 'div') &&
                /popupHelpMsgShown|popupHelpMsgHidden/.test(oCurNode.className)) {
                oHelpMsg = oCurNode;
                break;
            }
            oCurNode = oCurNode.parentNode;
        }
        return oHelpMsg;
    }

    /*
     * Method: toggleNodeHelpMsg
     *
     * Description: Toggle the display of the help message associated
     *              with the specified node.
     *
     * @param object oNode The node for which a help message is to be
     *                     located.
     */
    function toggleNodeHelpMsg(oNode) {
        var oHelpMsg = locateNodeHelpMsg(oNode);
        if (oHelpMsg) {
            oHelpMsg.className = ((oHelpMsg.className == 'popupHelpMsgShown')
                                  ? 'popupHelpMsgHidden'
                                  : 'popupHelpMsgShown');
        }
    }

    /*
     * Method: toggleNextRowDisplay
     *
     * Description: Toggle the display of the table row right below
     *              the row containing the HTML tag calling this
     *              method.
     *
     * @param object oCurTag The HTML tag calling this method.
     */
    function toggleNextRowDisplay(oCurTag) {
        // Trace back to locate the current tag's enclosing TR tag.
        var oTr = oCurTag;
        while (oTr.tagName != 'TR') {
            oTr = oTr.parentNode;
        }

        // Trace back to locate the curent tag's enclosing TABLE tag.
        var oTbl = oTr;
        while (oTbl.tagName != 'TABLE') {
            oTbl = oTbl.parentNode;
        }

        // Toggle the visibility of the row that is right after the
        // current tag's enclosing table row.
        var aNextRowCell = oTbl.rows[(oTr.rowIndex + 1)].cells;
        for (var i=0; i<aNextRowCell.length; i++) {
            var oCell = aNextRowCell[i];
            oCell.style.display = ((oCell.style.display == 'none')
                                   ? // A hidden row. Show it.
                                     ((document.all && !window.opera)
                                      ? // IE
                                        'block'
                                      : // Gecko
                                        'table-cell')
                                   : // A visible row. Hide it.
                                     'none');
        }
        return false;
    }

    function toggleCurRowDisplay(oCurTag) {
        // Trace back to locate the current tag's enclosing TR tag.
        var oTr = oCurTag;
        while (oTr.tagName != 'TR') {
            oTr = oTr.parentNode;
        }

        // Trace back to locate the curent tag's enclosing TABLE tag.
        var oTbl = oTr;
        while (oTbl.tagName != 'TABLE') {
            oTbl = oTbl.parentNode;
        }

        // Toggle the visibility of the row that is right after the
        // current tag's enclosing table row.
        var aNextRowCell = oTbl.rows[oTr.rowIndex].cells;
        for (var i=0; i<aNextRowCell.length; i++) {
            var oCell = aNextRowCell[i];
            oCell.style.display = ((oCell.style.display == 'none')
                                   ? // A hidden row. Show it.
                                     ((document.all && !window.opera)
                                      ? // IE
                                        'block'
                                      : // Gecko
                                        'table-cell')
                                   : // A visible row. Hide it.
                                     'none');
        }
        return false;
    }

    /*
     * Method: configNodeHelpMsg
     *
     * Description: Configure the position of the specified node's
     *              help message to the mouse position.
     *
     * @param object oNode The object where the mouse is on.
     *
     * @param object oEvt The event object associated with this method
     *                    call.
     */
    function configNodeHelpMsg(bPosInPage, oNode, oEvt) {
        var iTop = (bPosInPage
                    ? getMouseInPageVerPos(oEvt)
                    : getEvtEleTop(oEvt));
        var iLeft = 0; // To avoid narrow column, always take the full
                       // available width.

        // In case user hasn't closed the last help display window.
        // Close it now.
        hideHelpMsg(this.oLastHelpMsg);

        this.oLastHelpMsg = locateNodeHelpMsg(oNode);
        this.oLastHelpMsg.style.top = iTop + "px";
        this.oLastHelpMsg.style.left = iLeft + "px";
    }

    function getMouseInPageVerPos(oEvt) {
        return ((typeof(oEvt.pageY) == 'undefined')
                ? // IE
                  getMouseVerInTableScroller(oEvt)
                : // Gecko. PageY already takes into account any
                  // vertical scrolling of the document body.
                  oEvt.pageY);
    }

    function getEvtEleTop(oEvt) {
        var oEle = (oEvt.srcElement
                    ? oEvt.srcElement
                    : oEvt.target);
        var iTop = 0;
        do {
            iTop += oEle.offsetTop;
            oEle = oEle.offsetParent;
            var sPos = (window.getComputedStyle
                        ? // DOM
                          document.defaultView.getComputedStyle(oEle, null)
                          .getPropertyValue('position')
                        : // IE
                          oEle.currentStyle['position']);
        } while (sPos != 'absolute');
        return iTop;
    }

    /*
     * Method: hideHelpMsg
     *
     * Description: Hide the specified help message.
     *
     * @param object oMsg Help message to hide.
     */
    function hideHelpMsg(oMsg) {
        if (oMsg && (oMsg.className == 'popupHelpMsgShown')) {
            oMsg.className = 'popupHelpMsgHidden';
        }
    }

    /*
     * Method: locateNodeHelpMsg
     *
     * Description: Locate the help message associated with the
     *              specified node.
     *
     * @param object oNode The node for which a help message is to be
     *                     found.
     */
    function locateNodeHelpMsg(oNode) {
        var oHelpMsg = null;

        // Travel up the tag chain to locate a tag with a help message
        // right next to it.
        var oCurNode = oNode;
        while (oHelpMsg == null) {
            if ((oCurNode.nextSibling != null) &&
                (oCurNode.nextSibling.tagName.toLowerCase() == 'div') &&
                /popupHelpMsgShown|popupHelpMsgHidden/.test(oCurNode.nextSibling.className)) {
                oHelpMsg = oCurNode.nextSibling;
                break;
            }
            oCurNode = oCurNode.parentNode;
        }
        return oHelpMsg;
    }

    function getMouseVerInTableScroller(oEvt) {
        var iPos =
            // Mouse vertical position (relative to the top of
            // document body).
            oEvt.clientY

            // The scrolled vertical distance inside the document
            // body. This translates the mouse vertical position into
            // one relative to the top of the document body's visible
            // area.
            + (((document.compatMode == "CSS1Compat")
                ? document.documentElement
                : document.body)
               .scrollTop)
            ;
        return iPos;
    }

    function getMouseHorInTableScroller(oEvt) {
        var iPos =
            // Mouse horizontal position (relative to the left of
            // document body).
            oEvt.clientX

            // The scrolled horizontal distance inside the document
            // body. This translates the mouse horizontal position
            // into one relative to the left of the document body's
            // visible area.
            + (((document.compatMode == "CSS1Compat")
                ? document.documentElement
                : document.body)
               .scrollLeft)
            ;
        return iPos;
    }

    /*
     * Method: locateScroller
     *
     * Description: Locate the table scroller covering the area of the
     *              current mouse position.
     *
     * @param object oCaller The object where the mouse is on.
     *
     * @return object The table scroller object.
     */
    function locateScroller(oCaller) {
        oScroller = oCaller;
        do {
            if ((typeof(oScroller.scrollTop) != 'undefined') &&
                (oScroller.scrollTop > 0)) {
                // Found the table scroller DIV housing the region
                // that the mouse is on.
                break;
            }
            oScroller = oScroller.parentNode;
        } while (oScroller.tagName != 'HTML');
        return oScroller;
    }

    /*
     * Method: loadWin
     *
     * Description: Load a window with the specified URL.
     *
     * @param object oParam Parameter object with the following
     *                      properties:
     *
     *               .sUrl URL to load.
     *
     *               .sName Optional. Name to be assigned to the
     *                      window that will be loaded with the
     *                      speified URL.
     *
     *               .sAttr Optional. Attribute to be assigned to the
     *                      window.
     *
     *               .bResizable Optional. Whether the window will be
     *                           resizable by the user.
     *                           Default is resizable.
     *
     *               .bScrollable Optional. Whether the window will be
     *                            scrollable by the user.
     *                            Default is scrollable.
     *
     *               .iWidth Optional. Window width (in pixel).
     *
     *               .iHeight Optional. Window height (in pixel).
     *
     *               .iPosX Optional. Window's X position (in pixel).
     *
     *               .iPosX Optional. Window's Y position (in pixel).
     */
    function loadWin(oParam) {
        if (typeof(this.aWin) == 'undefined') {
            // First-time initialization.
            this.aWin = [];
        }
        var sWinName = (oParam.sName
                        ? // A non-blank name was specified. Make sure
                          // that it has no embedded space.
                          oParam.sName.replace(/\s+/g,"")
                        : // No name specified. Use a default
                          // non-blank name to ensure that subsequent
                          // calls to load the window will not open up
                          // another new window.
                          "default");

        var bIsNewWin = (// A window never seen before.
                         (this.aWin[sWinName] == undefined)
                         ||
                         // A previously opened window is now closed.
                         this.aWin[sWinName].closed);

        var aAttr = new Array();
        if (typeof(oParam.sAttr) != 'undefined') {
            aAttr[aAttr.length] = oParam.sAttr;
        }

        if ((typeof(oParam.bResizable) == 'undefined')
            ||
            oParam.bResizable) {
            aAttr[aAttr.length] = "resizable";
        }

        if ((typeof(oParam.bScrollable) == 'undefined')
            ||
            oParam.bScrollable) {
            aAttr[aAttr.length] = "scrollbars";
        }

        var iWidth = ((oParam.iWidth > 0)
                      ? oParam.iWidth
                      : screen.availWidth - 40);
        aAttr[aAttr.length] = "width=" + iWidth;

        var iHeight = ((oParam.iHeight > 0)
                       ? oParam.iHeight
                       : screen.availHeight / 2);
        aAttr[aAttr.length] = "height=" + iHeight;

        var sWinAttr = aAttr.join(",");

        this.oWin = window.open(oParam.sUrl, sWinName, sWinAttr);
        this.aWin[sWinName] = this.oWin;
        if (bIsNewWin) {
            // Only position the window if it is a newly created one.
            // Otherwise, we risk interferring with the new position
            // set by the user.
            var iPosX;
            var iPosY;
            if ((oParam.iPosX >= 0) && (oParam.iPosY >= 0)) {
                // A position was specified for the window.
                iPosX = oParam.iPosX;
                iPosY = oParam.iPosY;
            } else {
                // No window position specified. Center it on the screen.
                iPosX = (screen.availWidth - iWidth) / 2;
                iPosY = (screen.availHeight - iHeight) / 2;
            }

            // IE6 does not always return after a window.moveTo().
            // That can cause trouble to the routine caller. So we use
            // setTimeout() to ensure that by the time IE is called to
            // move the window, we are already back to the caller.
            setTimeout("oUtil.oWin.moveTo(" + iPosX + "," + iPosY + ")", 100);
        }
        this.oWin.focus();
        return this.oWin;
    }

    /*
     * Export public properties and methods.
     */
    return {
        'toggleMouseHelpMsg' : toggleMouseHelpMsg,
        'toggleNodeHelpMsg': toggleNodeHelpMsg,
        'oWin': oWin,
        'loadWin': loadWin,
        'toggleNextRowDisplay': toggleNextRowDisplay,
        'toggleCurRowDisplay': toggleCurRowDisplay,
        'configNodeHelpMsg': configNodeHelpMsg
    }
})();
