// Copyright (C) 2007-2010 Bristle Software, Inc.
// 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 1, or (at your option)
// any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc.

/******************************************************************************
* com.bristle.jslib.MsgBox.js
*******************************************************************************
* Purpose:
*       This file contains utility routines that manage one or more popup 
*       windows, like a fancier version of "MsgBox" or "alert".  For 
*       advantages of this over the standard JavaScript alert() function, see:
*               http://bristle.com/Tips/JavaScript.htm#popup_messages
* Usage:
*       - The typical scenario for using this file from an HTML file is:
*         <script language='JavaScript' src='com.bristle.jslib.MsgBox.js'></script>
*         Call the various functions that reside here.
* Assumptions:
*       - The file "com.bristle.jslib.Util.js" has already been loaded.
* Effects:
*       - None.
* Anticipated Changes:
* Notes:
* Implementation Notes:
* Portability Issues:
* Revision History:
*   $Log$
******************************************************************************/

// Create the "namespace" to hold the functions in this file.
com.bristle.jslib.MsgBox = {};

// Cached handle to window used for reporting errors, and debug info.
com.bristle.jslib.MsgBox.objMsgBox = null;

/******************************************************************************
* Shows a message in the specified MsgBox window, waiting and retrying if 
* necessary for the window to be fully created first.
*
*@param objMsgBox         Handle to the MsgBox window.
*@param strMessage        String to display in the window.
*@param blnClearPrevious  Boolean specifying whether to clear previous contents 
*                         of the window before appending the new message.
*@param intRetryCount     Number of times to retry, if necessary.
******************************************************************************/
com.bristle.jslib.MsgBox.showMessageEventually =
function(objMsgBox, strMessage, blnClearPrevious, intRetryCount)
{
    try
    {
        if (blnClearPrevious)
        {
            objMsgBox.document.getElementById("divMessage").innerHTML = 
                                                                strMessage;
        }
        else
        {
            objMsgBox.document.getElementById("divMessage").innerHTML += 
                                                                strMessage;
            // Scroll OK button into view to show the end of the total 
            // message since we appended to it.  Generally a good idea.
            // If not, we could add a new param to give the caller more 
            // control over whether to scroll.
            com.bristle.jslib.Util.scrollIntoView
                                (objMsgBox.document.getElementById("cmdOK"));
        }
    }
    catch (exception)
    {

        // MsgBox window is not yet ready to write to the DIV.  Try again soon.
        if (intRetryCount > 0)
        {
            intRetryCount--;

            // Capture the pointer to the MsgBox in a temporary global 
            // variable that will be available to this function when called
            // via setTimeout().  Can't just pass it as a param because 
            // setTimeout() takes either a parameterless function pointer or 
            // a string containing the text of a function call, including 
            // parameter, as its parameter.  There is no way to pass it a 
            // function and the parameter values of the function unless the 
            // parameter values can be stringized into a string literal for 
            // the call and de-stringized inside the called function, which 
            // an object reference cannot.
            strTempMsgBoxVariableName = 
                                com.bristle.jslib.Util.generateUniqueVarName();
            eval (strTempMsgBoxVariableName + " = objMsgBox;");

            // Same problem as above, except that it's a string, not an object.
            // No easy way to dump a string into a string literal and pull it 
            // back out w/o dealing with escaping quote chars and such.  Easier
            // to just do what we did for the object above.
            strTempMessageVariableName = 
                                com.bristle.jslib.Util.generateUniqueVarName();
            eval (strTempMessageVariableName + " = strMessage;");

            // No need to do anything tricky for numbers and booleans.  Just
            // insert their literal values into the string literal.
            var strCallSelf = "com.bristle.jslib.MsgBox.showMessageEventually"
                              + "(" 
                              + strTempMsgBoxVariableName
                              + "," 
                              + strTempMessageVariableName
                              + ","
                              + blnClearPrevious
                              + ","
                              + intRetryCount
                              + ")";
                                
            window.setTimeout(strCallSelf, 50, "JavaScript");
            //?? Is it safe to clear the values of the temp variables here?
            //?? Yes, if the setTimeout call has already parsed the string
            //?? and created a function call passing copies of the parameter 
            //?? values.  No, if it has only stored the string for parsing 
            //?? after the specified timeout.
            //?? So, where can we clear the temp variables and release any
            //?? memory their values may be consuming?  Could pass an extra
            //?? parameter when calling via setTimeout, to act as a flag to 
            //?? tell this function to release the value.  Still, how would 
            //?? it do so?  It would have the value passed to it, but inside 
            //?? the function it wouldn't know the name of the global variable 
            //?? whose value was passed to it.  Would have to also pass the 
            //?? string name of the global variable and do an eval() to clear 
            //?? it inside like the eval() we do to set it here.  Sounds 
            //?? clunky.  For now, tolerate the small memory leak.
        }
        else 
        {
            // Give up after specified number of retries and rethrow the 
            // exception.
            // ?? Where does this go when called by setTimeout()?  
            // ?? Not back to caller of setTimeout().  Any better ideas?
            alert ("Error:  com.bristle.jslib.MsgBox.showMessageEventually()"
                   + " failed in repeated attempts to display this message:\n\n" 
                   + strMessage);
            throw exception;
        }
    }
}

/******************************************************************************
* Reports a message to the user in a browser window, optionally clearing the 
* previous contents of the current message window, if any, optionally popping 
* up a new window, and optionally setting focus to the window.  
*?? Don't forget to close *all* such windows when the main window closes.
*
*@param strMessage        String to display in the window.
*@param blnClearPrevious  Boolean specifying whether to clear previous contents 
*                         of the window before appending the new message.
*                         Optional.  Default: false
*@param blnNewWindow      Boolean specifying whether to create a new window 
*                         instead of using any previously used MsgBox window 
*                         that may be cached.
*                         Optional.  Default: false
*@param blnOrphanWindow   Boolean specifying whether to orphan the window after 
*                         using it, instead of caching a handle to it for reuse 
*                         with future messages.
*                         Optional.  Default: false
*@param blnSetFocus       Boolean specifying whether to set the keyboard
*                         focus to the error message window.
*                         Optional.  Default: true
*@param blnRetry          Boolean specifying whether to retry once if the 
*                         operation fails the first time.  Useful since the 
*                         user may have manually closed the currently cached 
*                         window.
*                         Optional.  Default: true
******************************************************************************/
com.bristle.jslib.MsgBox.blnMSGBOX_CLEAR_PREVIOUS = true;
com.bristle.jslib.MsgBox.blnMSGBOX_NEW_WINDOW     = true;
com.bristle.jslib.MsgBox.blnMSGBOX_ORPHAN_WINDOW  = true;
com.bristle.jslib.MsgBox.blnMSGBOX_SET_FOCUS      = true;
com.bristle.jslib.MsgBox.blnMSGBOX_RETRY          = true;
com.bristle.jslib.MsgBox.intMsgBoxCounter         = 0;
com.bristle.jslib.MsgBox.reportPopupMessage =
function(strMessage, 
         blnClearPrevious, 
         blnNewWindow, 
         blnOrphanWindow, 
         blnSetFocus, 
         blnRetry)
{
    // Fill in default parameter values.
    if (typeof (blnClearPrevious) == "undefined")
    {
        blnClearPrevious = false;
    }
    if (typeof (blnNewWindow) == "undefined")
    {
        blnNewWindow = false;
    }
    if (typeof (blnOrphanWindow) == "undefined")
    {
        blnOrphanWindow = false;
    }
    if (typeof (blnSetFocus) == "undefined")
    {
        blnSetFocus = true;
    }
    if (typeof (blnRetry) == "undefined")
    {
        blnRetry = true;
    }

    // Reusing the same window name all the time causes the same window.
    // to be used.  Generating a new name creates a new window.
//?? Could use "_blank" instead of generating unique names?
    var strWindowName = "MsgBox" 
                      + (blnNewWindow 
                         ? com.bristle.jslib.MsgBox.intMsgBoxCounter++ 
                         : "");

    // Make a copy of the handle to the existing MsgBox.  Depending 
    // on blnOrphanWindow, we may or may not update the global handle.
    var objLocalMsgBox = com.bristle.jslib.MsgBox.objMsgBox;

    // Pop up a new window, if requested to, or if there is no cached one,
    // or if the cached one was closed, to show the message.
//?? Need array of window handles, not just single 
//?? com.bristle.jslib.MsgBox.objMsgBox?
    if (blnNewWindow || (objLocalMsgBox == null) || objLocalMsgBox.closed)
    {
        objLocalMsgBox = window.open
                ("about:blank"          // Open the window empty to avoid 
                                        // relying on the ability to load a 
                                        // page from any server.  May be 
                                        // opening this window to report an 
                                        // error that the server is down.
                 ,strWindowName
                 ,   "fullscreen=no"
                 + ", top=100"
                 + ", left=100"
                 + ", width=600"
                 + ", height=600"
                 + ", resizable=yes"
                 + ", scrollbars=yes"
                 + ", location=yes"     // Input field for typing a URL
                 + ", titlebar=yes"     // Limited situation.  See docs.
                 + ", directories=yes"  // Directory buttons toolbar
                 + ", menubar=yes"
                 + ", toolbar=yes"
                 + ", status=yes"       // Status bar
                 + ", copyhistory=no"
                 + ", channelmode=no"   // Theater mode showing channel 
                                        // band
                );

        objLocalMsgBox.document.write
        ( "\n<html>"
        + "\n <head>"
        + "\n  <title id='objTitle'></title>"
        + "\n </head>"
        + "\n <body id='objBody'"
        + "\n       >"
        + "\n  <form>"
        + "\n   <table align='center'>"
        + "\n    <tr>"
        + "\n     <td>"
        + "\n      <div id='divMessage'>"
        + "\n      </div>"
        + "\n     </td>"
        + "\n    </tr>"
        + "\n   </table>"
        + "\n   <p>"
        + "\n   <table align='center'>"
        + "\n    <tr>"
        + "\n     <td>"
        + "\n      <button id='cmdOK'"
        + "\n              onclick='window.close()'"
        + "\n              >"
        + "\n       OK"
        + "\n      </button>"
        + "\n     </td>"
        + "\n    </tr>"
        + "\n   </table>"
        + "\n  </form>"
        + "\n </body>"
        + "\n</html>"
        );
        objLocalMsgBox.document.close();
    }

    try
    {
        // Show window if it is behind another window.  Also, un-minimize
        // it if necessary.
        // Note:  This may also affect the timing of things.  It is possible
        //        that deleting this line will cause the following code to 
        //        attempt to update divMessage before it is fully created.
        if (blnSetFocus)
        {
            objLocalMsgBox.window.focus();
        }

        // Try up to 100 times to write the message to the MsgBox, waiting
        // asynchronously between attempts, so the MsgBox has a chance to 
        // get fully created first.
        // Note:  Usually succeeds on first try.  Sometimes 2nd if the 
        //        window was not yet created.  I've seen 6th when loading
        //        the MsgBox HTML file from a slow remote server and not 
        //        yet having it cached locally by the browser.  100 should
        //        be more than enough.  The limit is only to prevent an 
        //        infinite loop if something unexpected happens.
        com.bristle.jslib.MsgBox.showMessageEventually
                (objLocalMsgBox, strMessage, blnClearPrevious, 100);

        // Update the global handle to the MsgBox unless the caller 
        // wants this new window orphaned.
        if (!blnOrphanWindow)
        {
            com.bristle.jslib.MsgBox.objMsgBox = objLocalMsgBox;
        }

    }
    catch (exception)
    {
        // Some error occurred.  Perhaps the user closed the window 
        // before we finished operating on it.  This is especially
        // likely when the window is showing a previous error 
        // message, and the user clicks OK before we show the next 
        // error message.  Try one more time with the simplest set
        // of options, then give up.
        if (blnRetry)
        {
            com.bristle.jslib.MsgBox.reportPopupMessage
                            (strMessage, 
                             com.bristle.jslib.MsgBox.blnMSGBOX_CLEAR_PREVIOUS, 
                             com.bristle.jslib.MsgBox.blnMSGBOX_NEW_WINDOW,
                             blnOrphanWindow,
                             blnSetFocus,
                             !com.bristle.jslib.MsgBox.blnMSGBOX_RETRY);
        }
        else
        {
            alert("Error occurred while trying to show error message:\n\n"
                + strMessage);
        }
    }
}

/******************************************************************************
* Reports an error message to the user.
*
*@param strMessage     Message to show the user.  This may be the entire 
*                      message or may be a higher level description of the
*                      error contained in objException.
*@param objException   Error object or com.bristle.jslib.Exception.Exception 
*                      object containing additional details to show the user.  
*                      Optional.  Default: no additional details
*                      Can be omitted or explicitly specified as null when
*                      no additional details are available.
*@param blnSetFocus    Boolean specifying whether to set the keyboard
*                      focus to the error message window.  
*                      Optional.  Default: true
******************************************************************************/
com.bristle.jslib.MsgBox.reportError =
function(strMessage, objException, blnSetFocus)
{
    var strError = strMessage;
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(objException))
    {
        // No details to add to the message.
    }
    else
    {
        // Add error details to the message.
        strError = strError
            + "<br /><br />Error details:"
            + com.bristle.jslib.Util.formatScriptError(objException)
            ;
    }

    // Pop up a window to show the error message wrapped in a border.
    com.bristle.jslib.MsgBox.reportPopupMessage
            (com.bristle.jslib.Util.formatWithBorder(strError)
            ,!com.bristle.jslib.MsgBox.blnMSGBOX_CLEAR_PREVIOUS
            ,!com.bristle.jslib.MsgBox.blnMSGBOX_NEW_WINDOW
            ,!com.bristle.jslib.MsgBox.blnMSGBOX_ORPHAN_WINDOW
            ,blnSetFocus
            );
}

/******************************************************************************
* Closes the window used to report an error message to the user.
******************************************************************************/
com.bristle.jslib.MsgBox.closeErrorWindow =
function()
{
    if (com.bristle.jslib.MsgBox.objMsgBox)
    {
        com.bristle.jslib.MsgBox.objMsgBox.close();
    }
}

/******************************************************************************
* Display a debug message.
******************************************************************************/
com.bristle.jslib.MsgBox.debugPrint =
function(strMessage)
{
    strMessage = com.bristle.jslib.Util.replaceAll("" + strMessage, "\n", "<br />");
    com.bristle.jslib.MsgBox.reportPopupMessage
            (strMessage + "<br />"
            ,!com.bristle.jslib.MsgBox.blnMSGBOX_CLEAR_PREVIOUS
            ,!com.bristle.jslib.MsgBox.blnMSGBOX_NEW_WINDOW
            ,!com.bristle.jslib.MsgBox.blnMSGBOX_ORPHAN_WINDOW
            ,!com.bristle.jslib.MsgBox.blnMSGBOX_SET_FOCUS
            ,!com.bristle.jslib.MsgBox.blnMSGBOX_RETRY
            );
}

/******************************************************************************
* Test the specified assertion, reporting a popup error message if not true.
*
*@see com.bristle.jslib.Exception.assert
******************************************************************************/
com.bristle.jslib.MsgBox.assert =
function(strAssertion, strDescription, strSource)
{
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString
                                                (strDescription))
    {
        strDescription = strAssertion;
    }

    try
    {
        com.bristle.jslib.Exception.assert
                    (strAssertion, strDescription, strSource);
    }
    catch(exception)
    {
        com.bristle.jslib.MsgBox.reportError
                        ("Failed Assertion: " + strDescription, exception);
    }
}

