/*
 * Copyright 2005 Tridium, Inc. All Rights Reserved.
 */

/**
 * hx
 *
 * @author    Andy Frank
 * @creation  5 Jan 05
 * @version   $Revision$ $Date$
 * @since     Baja 1.0
 */

////////////////////////////////////////////////////////////////
// DOM Extensions
////////////////////////////////////////////////////////////////

/**
 * Convenience for <code>document.getElementById(id)</code>.
 */
function $(id)
{
  return document.getElementById(id);
}

////////////////////////////////////////////////////////////////
// Hx
////////////////////////////////////////////////////////////////

var hx = new Hx();
function Hx()
{
////////////////////////////////////////////////////////////////
// Attributes
////////////////////////////////////////////////////////////////

  this.pollTimeout = 5000;
  this.ie = navigator.appName.toLowerCase() == "microsoft internet explorer";
  this.failure = false;
  this.dynamic = false;

  // Last mouse position
  this.mx = 0;
  this.my = 0;

  // Menu
  this.menuId = "_hxMenu";

  // Dialog Id
  this.dialogId = "_hxDialog";
  this.dialogCounter = 0;
  this.dialogOnload = null;

  // Error
  this.err_LostConnection   = "Session disconnected";
  this.errorInvokeCode      = null;
  this.lastException        = null;
  this.lastExceptionDetails = null;
  this.lastExceptionMessage = null;
  
  // Poll element array for sending data back on each poll request
  this.pollElementArray = new Array();

////////////////////////////////////////////////////////////////
// Lifecycle
////////////////////////////////////////////////////////////////

  this.started = function(dynamic, timeout)
  {
    this.pollTimeout = timeout;
    this.dynamic = dynamic;
    
    //firefox form caching fix
    if(document.forms[0] != null)
      document.forms[0].reset();

    // TODO - can hide needed error messages
    //window.onerror = this.doWindowError;
    document.body.onclick = this.closeMenu;
    if (dynamic) setTimeout("hx.poller()", this.pollTimeout);
  }

  this.stopped = function()
  {
  }

////////////////////////////////////////////////////////////////
// Polling
////////////////////////////////////////////////////////////////

  this.poller = function()
  {
    this.doPoll(true);
  }

  this.poll = function()
  {
   this.doPoll(false);
  }

  this.doPoll = function(repeat)
  {
    if (this.failure) return;
    var msg = new Message();
    msg.setHeader("Content-Type", "application/x-niagara-hx-update");
    msg.setHeader("Screen-Width", this.getScreenWidth());
    msg.setHeader("Screen-Height", this.getScreenHeight());
 
    var pollElementBody = "";
  
    if (this.pollElementArray.length > 0)
     pollElementBody = hx.encodeForm(document.body, msg, this.pollElementArray); 
      
  msg.send(window.location, pollElementBody, (repeat && this.dynamic)? this.pollHandlerRepeat : this.pollHandler);
  }

  this.pollHandler = function(resp)
  {
    var text = resp.responseText;
    try
    {
      eval(text);
    }
    catch (err)
    {
     if (text.substring(0,6) != "<html>")
      {
        text = text.replace(new RegExp("<", "gi"), "&lt;");
        text = text.replace(new RegExp(">", "gi"), "&gt;");
     }
      
      hx.doError(null, text, err);
    } 
  }
  
  this.pollHandlerRepeat = function(resp)
  {
    var text = resp.responseText;
    try
    {
      eval(text);
   setTimeout("hx.poller()", hx.pollTimeout);    
    }
    catch (err)
    {
     if (text.substring(0,6) != "<html>")
      {
        text = text.replace(new RegExp("<", "gi"), "&lt;");
        text = text.replace(new RegExp(">", "gi"), "&gt;");
     }
      
      hx.doError(null, text, err);
    }
  }  
  
  /**
  * @since Niagara 3.5
  **/
  this.getScreenHeight = function() 
  {
    if( typeof( window.innerWidth ) == 'number' ) {
      //Non-IE
      return window.innerHeight;
    } else if( document.documentElement && 
      ( document.documentElement.clientWidth || document.documentElement.clientHeight ) ) {
      //IE 6+ in 'standards compliant mode'
      return document.documentElement.clientHeight;
    } else if( document.body && ( document.body.clientWidth || document.body.clientHeight ) ) {
      //IE 4 compatible
      return document.body.clientHeight;
    }
    return 0;
  }  

  /**
  * @since Niagara 3.5
  **/
  this.getScreenWidth = function() 
  {
    if( typeof( window.innerWidth ) == 'number' ) {
      //Non-IE
      return window.innerWidth;
    } else if( document.documentElement && 
      ( document.documentElement.clientWidth || document.documentElement.clientHeight ) ) {
      //IE 6+ in 'standards compliant mode'
      return document.documentElement.clientWidth;
    } else if( document.body && ( document.body.clientWidth || document.body.clientHeight ) ) {
      //IE 4 compatible
      return document.body.clientWidth;
    }
    return 0;
  }  
  
  /**
  * Returns an array containing the provided elements bounds [top,left,height,width]
  * @since Niagara 3.5
  **/
  this.getElementBounds = function(obj)
  {
   var curleft = curtop = 0;
   if (obj.offsetParent) 
   {
    do 
    {
   curleft += obj.offsetLeft;
   curtop += obj.offsetTop;
    } while (obj = obj.offsetParent);
    return [curleft,curtop];
   } else
    return [0,0];
  } 

////////////////////////////////////////////////////////////////
// Events
////////////////////////////////////////////////////////////////

  /**
   * Fire the specified event.
   */
  this.fireEvent = function(path, eventId, event)
  {
    //hx.enterBusy();

    // If the value for a header is empty, the header will not
    // be set, so check if path is empty, and force it to be
    // something if it is.  HxView will look for this case, and
    // reset path to an empty string.
    if (path.length == 0) path = "*";

    if(event!=null)
    {
    // set mouse position
    var posx = 0;
    var posy = 0;
    {
     if (event.pageX || event.pageY)  
     {
      posx = event.pageX;
      posy = event.pageY;
     }
     else if (event.clientX || event.clientY)  
     {
      posx = event.clientX + document.body.scrollLeft
       + document.documentElement.scrollLeft;
      posy = event.clientY + document.body.scrollTop
       + document.documentElement.scrollTop;
     }
    }         
      this.mx = posx;
      this.my = posy;
   }

    var form = hx.encodeForm(document.body, null, null);
    var msg = new Message();
    msg.setHeader("x-niagara-hx-path", path);
    msg.setHeader("x-niagara-hx-eventId", eventId);
    msg.setHeader("Content-Type", "application/x-niagara-hx-event");
    msg.send(window.location, form, this.doFireEvent);
  }

  /**
   * Handle response to fireEvent.
   */
  this.doFireEvent = function(resp)
  {
    //hx.exitBusy();

    var text = resp.responseText;
    try
    {
      if (text.length > 0) eval(text);    
    }
    catch (err)
    {
      text = text.replace(new RegExp("<", "gi"), "&lt;");
      text = text.replace(new RegExp(">", "gi"), "&gt;");
      hx.doError(null, text, err);
    }
  }

////////////////////////////////////////////////////////////////
// HxGraphics
////////////////////////////////////////////////////////////////

  /**
   * Set the current brush as a color.
   */
  this.setColor = function(g, color)
  {
    g.strokeStyle = color;
    g.fillStyle = color;
  }
  
  /**
   * Set the current brush as a color.
   */
  this.setColor = function(g, red, green, blue, alpha)
  {
 g.strokeStyle = "rgba("+red+", "+green+", "+blue+", "+alpha+")";
 g.fillStyle = "rgba("+red+", "+green+", "+blue+", "+alpha+")";
  }  

  /**
   * Set the current brush as a linear gradient.
   */
  this.setLinearGradient = function(g, x1, y1, x2, y2, stops)
  {
   try
   {
     var grad = g.createLinearGradient(x1,y1,x2,y2);
     for (var i=0; i<stops.length; i++)
       grad.addColorStop(stops[i][0]/100, stops[i][1]);
     g.strokeStyle = grad;
     g.fillStyle = grad;
 } catch (err) {
  alert("hx.setLinearGradient: "+err);
 }
  }

  /**
   * Clip this rectangle.
   */
  this.clipRect = function(g, x, y, w, h)
  {
    g.beginPath();
    g.moveTo(x,   y);
    g.lineTo(x+w, y);
    g.lineTo(x+w, y+h);
    g.lineTo(x,   y+h);
    g.closePath();
    g.clip();
  }

  /**
   * Draws a line between the two points using the
   * current pen and brush.
   */
  this.strokeLine = function(g, x1, y1, x2, y2)
  {
    g.beginPath();
    g.moveTo(x1, y1);
    g.lineTo(x2, y2);
    g.closePath();
    g.stroke();
  }

  /**
   * Draws the text given by the specified string.
   */
  this.drawString = function(parentId, str, color, font, x, y)
  {
    var canvas = document.getElementById(parentId);
    var canvasParent = canvas.parentNode;

    if (hx.ie && canvasParent.style.position == "relative")
    {
      // sort of a hack since I usually depend on a
      // relative div above the canvas tag
      var s = canvasParent.style.paddingTop;
      if (s != null && s.length > 2) y += parseInt(s.substring(0, s.length-2));
    }
    else
    {
      // the normal rest of the world...
      x += canvas.offsetLeft;
      y += canvas.offsetTop;
    }

    var child = document.createElement("div");
    child.style.position = "absolute";
    child.style.left  = x + "px";
    child.style.top   = y + "px";
    child.style.color = color;
    child.style.font  = font;
    child.innerHTML   = str;
    child.name = parentId + ".text"

    canvasParent.appendChild(child);
  }

  /**
   * Clear the canvas and all text.
   */
  this.clearCanvas = function(g, width, height, id)
  {
    var canvas = document.getElementById(id);
    var canvasParent = canvas.parentNode;

    g.clearRect(0, 0, width, height);
    
    var childNodes = canvasParent.childNodes;
    for(var i=childNodes.length-1; i>0; i--)
     if(childNodes[i].tagName=='DIV')
      canvasParent.removeChild(childNodes[i]);
  }

////////////////////////////////////////////////////////////////
// Dialog
////////////////////////////////////////////////////////////////

  /**
   * Display a dialog with the given body.
   */
  this.showDialog = function(body)
  {
   this.closeMenu();
  
    var dlg = document.createElement("div");
    var i = hx.dialogCounter;
    while(document.getElementById(hx.dialogId + i) != null)
      i++;
      
    dlg.id=hx.dialogId+i;        
    hx.dialogCounter=i;
    if (hx.ie)
    {
      dlg.style.position = "absolute";
      dlg.style.left = document.body.scrollLeft + "px";
      dlg.style.top  = document.body.scrollTop + "px";
      dlg.style.width  = hx.getScreenWidth() + "px";
      dlg.style.height = hx.getScreenHeight() + "px";
    }
    else
    {
      dlg.style.position = "fixed";
      dlg.style.top  = "0px";
      dlg.style.left = "0px";
      dlg.style.width  = "100%";
      dlg.style.height = "100%";
    }
    

    if (hx.ie)
    {
      // If we are in IE, we need to shim the background
      // with an iframe, so that controls don't show thru
      var shim = document.createElement("iframe");
      shim.src = "javascript: '';";
      shim.frameBorder = "0";
      shim.style.position = "absolute";
      shim.style.left = "-" + document.body.scrollLeft + "px";
      shim.style.top  = "-" + document.body.scrollTop + "px";
      shim.style.width  = document.body.scrollWidth + "px";
      shim.style.height = document.body.scrollHeight + "px";
      shim.className = "dialog-background";
      shim.style.filter = 'alpha(opacity=50)';
      dlg.appendChild(shim);
    }
    else
    {
      // Else, just use a simple div to create the
      // transparent background layer
      var bg = document.createElement("div");
      bg.className = "dialog-background";
      dlg.appendChild(bg);
    }

    // Create dialog
    var content = document.createElement("div");
    content.className = "dialog";
    content.innerHTML = body;
    dlg.appendChild(content);

    // Add content
    document.forms[0].appendChild(dlg);

    //fix MaxHeight
    hx.dialogMaxHeight();
    
    // Try to focus first control in dialog
    hx.focus(dlg.childNodes);
    if(hx.dialogOnload !=null)
      eval(hx.dialogOnload);    
  }
  
  this.dialogMaxHeight = function()
  {
    var elem = $('dialog-maxHeight');
    if(elem == null)
      return;
    //replaceId do there is no collision
    elem.id='dialog-maxHeight' + hx.dialogCounter;
    
    var height = (parseInt(hx.getScreenHeight())*8/10) - 100;
    hx.maxHeight(elem, height);
    
    var width = (parseInt(hx.getScreenWidth())*8/10) - 30;  
    hx.maxWidth(elem, width);
  }
  
  this.resizeCurrentDialog = function()
  {
    var elem = $('dialog-maxHeight' + hx.dialogCounter);
    if(elem == null)
      return;
    
    elem.style.height="";  
    
    var height = (parseInt(hx.getScreenHeight())*8/10) - 100;
    hx.maxHeight(elem, height);
    
    elem.style.width="";
    var width = (parseInt(hx.getScreenWidth())*8/10) - 30;  
    hx.maxWidth(elem, width, true);    
  }
  
  this.maxHeight = function(elem, height)
  {
    if(elem == null)
      return;
    
    if(elem.offsetHeight > parseInt(height))
    {
      elem.style.height=height + "px";
      elem.style.overflow="auto";
    }
  }
  
  this.maxWidth = function(elem, width, ieFix)
  {
    if(elem == null)
      return;    
    
    if(elem.offsetWidth > parseInt(width))
    {
      elem.style.width=width + "px";
      elem.style.overflow="auto";      
    }              
    else if(hx.ie && ieFix)
    {
      elem.style.width=elem.offsetWidth + "px";
      elem.style.overflow="auto";      
    }
  }

  /**
   * Recurse through nodes to focus first input control.
   */
  this.focus = function(nodes)
  {
    for (var i=0; i<nodes.length; i++)
    {
      var name = nodes[i].nodeName;
      try
      {
        if ( (name == "SELECT" || name == "TEXTAREA" ||(name == "INPUT" && nodes[i].type != "hidden"))
        && nodes[i].disabled != true && nodes[i].readOnly != true)
        {
          nodes[i].focus();
          return true;
        }
      }
      catch(err)
      {
        
      }  
      if (hx.focus(nodes[i].childNodes)) return true;
    }
    return false;
  }

  /**
   * Close any open dialog. If path and eventId is not
   * null, then fire an event with that id.
   */
  this.closeDialog = function(path, eventId, event)
  {
    var eventSrc = null;
    if (event != null)
      eventSrc = (!event.target) ? window.event.srcElement : event.target;

    var form = null;
    var dlg = document.getElementById(hx.dialogId + hx.dialogCounter);
    if (dlg != null)
    {
      form = hx.encodeForm(document.body, eventSrc, null);
      dlg.parentNode.removeChild(dlg);      
      hx.dialogCounter--;
      if(hx.dialogCounter < 0)
        hx.dialogCounter=0;
    }

    if (path != null && eventId != null)
    {
      // Set path to something if empty to force the
      // header to be set.
      if (path.length == 0) path = "*";

      var msg = new Message();
      msg.setHeader("x-niagara-hx-path", path);
      msg.setHeader("x-niagara-hx-eventId", eventId);
      msg.setHeader("Content-Type", "application/x-niagara-hx-event");
      msg.send(window.location, form, hx.doFireEvent);
    }
  }

////////////////////////////////////////////////////////////////
// Action
////////////////////////////////////////////////////////////////

  this.invokeAction = function(actionOrd, actionArg)
  {
 if (actionArg == null) actionArg = "null";
 
 form = "action="+this.encodeString(actionOrd)+"&parameter="+this.encodeString(actionArg);
 this.setFormValue("action",actionOrd); 
  
 var msg = new Message();
 msg.setHeader("x-niagara-hx-path", "*");
 msg.setHeader("x-niagara-hx-eventId", "action");
 msg.setHeader("Content-Type", "application/x-niagara-hx-event");
 msg.send(window.location, form, hx.doFireEvent); 
  }

////////////////////////////////////////////////////////////////
// Menu
////////////////////////////////////////////////////////////////

  this.doShowMenu = function(body)
  {
    this.closeMenu();
    
    var dlg = document.createElement("div");
     
    dlg.id=hx.menuId;
    dlg.style.zIndex = "99";
    dlg.style.position = "absolute";
    dlg.style.left = hx.mx + "px";
    dlg.style.top  = hx.my + "px";
    
    // Create dialog
    dlg.innerHTML = body;
    
    // Add content
    document.forms[0].appendChild(dlg);
    
    // Make sure it doesn't hang below the bottom of the 
    // visible display window
    var screenHeight = this.getScreenHeight();
    if(dlg.offsetHeight+hx.my>screenHeight)
     dlg.style.top=(hx.my-((dlg.offsetHeight+hx.my)-screenHeight)-10) + "px";
    
    // Try to focus first control in dialog
    hx.focus(dlg.childNodes);
  }  

  this.closeMenu = function()
  {
    var form = null;
    var dlg = document.getElementById(hx.menuId);
    if (dlg != null)
    {
      dlg.parentNode.removeChild(dlg);      
    }
  }

////////////////////////////////////////////////////////////////
// Forms
////////////////////////////////////////////////////////////////

  /**
   * Encode the form elements that exist under the given
   * element into a string.
   */
  this.encodeForm = function(elem, submit, elemNameArray)
  { 
    var controls = new Array();
    
    // Recursively search for Elements
    var find = function(currentElem)
    {
      // Is the current node of type Node.ELEMENT_TYPE?
      if (currentElem.nodeType == 1)
      {
        var n = currentElem.tagName.toLowerCase();
                
        if (n == "input" || n == "select" || n == "textarea")
        {
          if (elemNameArray == null)
            controls.push(currentElem); 
          else
          {             
            for (var i=0; i<elemNameArray.length; i++)
            {
              if (currentElem.name == elemNameArray[i])
              {
                controls.push(currentElem);
                break;
              }
            }
          }  
        }
        
        for (var i=0; i<currentElem.childNodes.length; i++)
          find(currentElem.childNodes[i]);
      }
    }
    
    find(elem);
    
    var encoding = "";
  
    for (var i=0; i<controls.length; i++)
    {
      var control = controls[i];
      var type = control.type.toLowerCase();

      // if the input is a submit and it's not the button
      // that was pressed, skip it
      if ((submit != null) &&
          (control.type == "submit") &&
          (control != submit))
        continue;

      // Determine if control is "successful"
      if (control.disabled) continue;
      if (control.name == null || control.name.length == 0) continue;
      if ((type == "radio" || type == "checkbox") && !control.checked) continue;
   
      // Escape illegal charaters
      var value = this.encodeString(control.value);

      // Append name/value pair
      if (encoding.length > 0) encoding += "&";
      encoding += control.name + "=" + value;
    }

    return encoding;
  }
  
  /**
   * Encode illegal characters.
   */
  this.encodeString = function(s)
  {
    var e = "";
    for (var i=0; i<s.length; i++)
    {
      var ch = s.charAt(i);
      var code = s.charCodeAt(i);

      var isChar = false;
      if (code >= 48 && code <= 57) isChar = true;
      else if (code >= 65 && code <= 90)  isChar = true;
      else if (code >= 97 && code <= 122) isChar = true;

      if (ch == " ")  e += "+";
      else if (!isChar && code <= 127)
      {
        e += "%" + hx.toHex(code);
      }
      else if (code > 127 && code < 0x0800) // utf-8 two bytes
      {
        var high = 0xc0 | ((code >> 6) & 0x001F);
        var low  = 0x80 | (code & 0x003f);

        e += "%" + hx.toHex(high);
        e += "%" + hx.toHex(low);
      }
      else if (code >= 0x0800) // utf-8 three bytes
      {
        var high = 0xe0 | ((code >> 12) & 0x000f);
        var mid  = 0x80 | ((code >> 6) & 0x003f);
        var low  = 0x80 | (code & 0x003f);

        e += "%" + hx.toHex(high);
        e += "%" + hx.toHex(mid);
        e += "%" + hx.toHex(low);
      }
      // TODO: Handle four bytes
      else e += ch;
    }
    return e;
  }

  this.toHex = function(val)
  {
    var s = val.toString(16).toUpperCase();
    if (s.length == 1) s = "0" + s;
    return s;
  }

  /**
   * Set the value of the form element with this name. If this
   * element does not exist, add it as a hidden control type.
   */
  this.setFormValue = function(key, value)
  { 
    // TODO - should work for textarea?

    var control = null;
    var inputs = document.body.getElementsByTagName("input");

    for (var i=0; i<inputs.length; i++)
    {
      if (inputs[i].name == key)
      {
        control = inputs[i];
        break;
      }
    }
    
    var selects = document.body.getElementsByTagName("select");

    for (var i=0; i<selects.length; i++)
    {
      if (selects[i].name == key)
      {
        control = selects[i];
        break;
      }
    }
    
    var textarea = document.body.getElementsByTagName("textarea");

    for (var i=0; i<textarea.length; i++)
    {
      if (textarea[i].name == key)
      {
        control = textarea[i];
        break;
      }
    }

    if (control == null)
    {      
      control = document.createElement("input");
      control.type = "hidden";
      control.name = key;
      control.id   = key;
      document.forms[0].appendChild(control);
    }
 
    control.value = value;
  }
    
  /** 
   * Add a poll element to include in the update
   */
  this.addFormElementToPoll = function(name)
  {
    this.pollElementArray.push(name);
  }
  
  /**
   * Remove a poll lement from the update
   */
  this.removeFormElementFromPoll = function(name)
  {  
    for (var i = 0; i < this.pollElementArray.length; i++)
    {
      if (this.pollElementArray[i] == name)
      {
        this.pollElementArray.splice(i, 1);
        break;
      }
    }
  }
 
 this.setMouseEvent = function (event) 
 {
  // the hidden input fields used here are also
  // defined statically in BHxPxView.  When used
  // elsewhere the fields will be created dynamically.
 
  // set mouse button
  if(event.button == 2)
   hx.setFormValue("button", "right");
  else
   hx.setFormValue("button", "left");
 
  // set mouse position
  {
   var posx = 0;
   var posy = 0;
   if (event.pageX || event.pageY)  
   {
    posx = event.pageX;
    posy = event.pageY;
   }
   else if (event.clientX || event.clientY)  
   {
    posx = event.clientX + document.body.scrollLeft
     + document.documentElement.scrollLeft;
    posy = event.clientY + document.body.scrollTop
     + document.documentElement.scrollTop;
   }
   hx.setFormValue("x", posx);
   hx.setFormValue("y", posy);
  }

  hx.setFormValue("shiftModifier", event.shiftKey);
  hx.setFormValue("ctlModifier", event.ctrlKey);
  hx.setFormValue("altModifier", event.altKey);
  hx.setFormValue("metaModifier", event.metaKey);
 }
 
 this.stopEventPropagation = function (event)
 {
     if(event.stopPropagation) {event.stopPropagation();}
     event.cancelBubble = true;
     return false;
 }

////////////////////////////////////////////////////////////////
// Busy
////////////////////////////////////////////////////////////////

  /**
   * Enter the busy state.  This blocks input and displays
   * a busy indicator to the user.  You must call exitBusy()
   * to leave this state.
   */
  this.enterBusy = function()
  {
    // TODO - do something cooler
    var body = "<div class='busy'>Loading...</div>";
    hx.showDialog(body);
  }

  /**
   * Exit the busy state.
   */
  this.exitBusy = function()
  {
    hx.closeDialog(null, null, null);
  }

////////////////////////////////////////////////////////////////
// Error
////////////////////////////////////////////////////////////////

  /**
   * Handle window error.
   */
  this.doWindowError = function(msg, url, line)
  {
    // Create an excpetion to pass to doError
    var ex = new Object();
    ex.name    = "window.onerror";
    ex.message = msg;
    ex.fileName   = url;
    ex.lineNumber = line;
    ex.stack = "";

    hx.doError("window.onerror", "", ex);
    return true;
  }

  /**
   * Handle error.
   */
  this.doError = function(msg, details, exception)
  {

 // Make sure exception is valid
    if (exception == null) exception = new Object();
    
 // ONLY EFFECTS MOZILLA BROWSERS 
 //   If an ajax request is currently being processed and the user
 //   attempts to navigate else where, then this error is thrown
 //   (Mozilla).
    if (exception.name == 'NS_ERROR_NOT_AVAILABLE') return;
    
    // Default message if not set
    if (msg == null)
      msg = exception.name + ": " + exception.message;

    //Issue 13517 - hook for profile to try and handle the error more gracefully
    try
    {
      if(hx.errorInvokeCode != null)
      {
        hx.lastException=exception;
        hx.lastExceptionDetails=details;
        hx.lastExceptionMessage=msg;
        if(eval(hx.errorInvokeCode))
          return;
      }
    }  
    catch(err)
    {
    
    }
    finally
    {
      hx.lastException=null;          
      hx.lastExceptionDetails=null;
      hx.lastExceptionMessage=null;
    }
    
    var fileName = "undefined";
    var lineNumber = "undefined";
    var stack = "undefined";
    try
    {
      fileName = exception.fileName;
      lineNumber = exception.lineNumber;
      stack = exception.stack;
    }
    catch (err)
    {
      // If this throws an exception, we got one of those
      // internal exception things in Mozilla
      stack = exception;
    }

    while (document.body.childNodes.length > 0)
      document.body.removeChild(document.body.firstChild);

    // Force style so we don't get anything weird
    var style = document.body.style;
    style.color = "black";
    style.background = "white";
    style.font = "normal 11px Tahoma";
    style.padding = "10px";

    var html = "<div style='font:18px Tahoma; padding-bottom:5px;'>";
    html += "Cannot display page</div>";
    html += msg + "<br/><br/>";
    html += "<div style='color:blue; cursor:pointer; text-decoration:underline;'";
    html += "onclick='document.getElementById(\"details\").";
    html += "style.visibility=\"visible\";'>";
    html += "Show Details</div>";
    html += "<div id='details' style='visibility:hidden; margin-top:10px;'>";

    // Exception information
    html += "<table width='100%' cellspacing='0' cellpadding='3'";
    html += " style='border:1px solid #666;'>";
    html += "<tr>";
    html += " <td colspan='2' style='background:#ccc;'>";
    html += "  <b>" + exception.name + ": " + exception.message + "</b>";
    html += " </td>";
    html += "</tr>";

    html += "<tr>";
    html += " <td style='background:#ddd;'><b>File:</b></td>";
    html += " <td width='100%' style='background:#eee;'>" + fileName + "</td>";
    html += "</tr>";

    html += "<tr>";
    html += " <td style='background:#ddd;'><b>Line:</b></td>";
    html += " <td style='background:#eee;'>" + lineNumber + "</td>";
    html += "</tr>";

    html += "<tr>";
    html += " <td valign='top' style='background:#ddd;'><b>Stack:</b></td>";
    html += " <td style='background:#eee;'>" + stack + "</td>";
    html += "</tr>";
    html += "</table>";

    // Details if they exist
    if (details != null && details.length > 0)
    {
      html += "<div style='margin-top:10px; padding:5px; background:#eee; ";
      html += "border:1px solid #666;'>";
      html += details;
      html += "</div>";
    }

    html += "</div>";
    document.body.innerHTML = html;
    hx.failure = true;
  }
  
  /**
   * POST resource loading
   */
  this.addJavaScript = function(resource)
  {
    var head      = document.getElementsByTagName("head")[0];
    var tag       = "script";
    var type       = "text/javascript";
    var links     = head.getElementsByTagName(tag);
    var r         = "/ord?" + resource;
    var hasLink   = false;
    for( var i=0; i<links.length; i++)
    {
      if(links[i].getAttribute("src") == r) 
        hasLink=true;
    }
        
    if(!hasLink)
    {
      var resource  = document.createElement(tag);
      resource.type = type;
      resource.setAttribute("src", r);
      head.appendChild(resource);
    }
  }
  
  this.addStyleSheet = function(resource)
  {
    var head      = document.getElementsByTagName('head')[0];
    var tag       = 'link';
    var type       = 'text/css';
    var links     = head.getElementsByTagName(tag);
    var r         = "/ord?" + resource;
    var hasLink   = false;
    for( var i =0; i<links.length; i++)
    {
      if(links[i].getAttribute("href") == r) 
        hasLink=true;      
    }
    
    if(!hasLink)
    {
      var resource  = document.createElement(tag);
      resource.type = type;
      resource.rel    = 'stylesheet';
      resource.setAttribute("href", r);
      head.appendChild(resource);
    }      
  }
  
  this.setAlphaImageLoader = function(elem)
  {
    elem.style.backgroundColor = "transparent";
    elem.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/ord?" + elem.ord + "')";        
  }  
}
////////////////////////////////////////////////////////////////
// XmlHttp
////////////////////////////////////////////////////////////////

function Message()
{
  var headers = new Array();
  var responseHandler = null;
  var xmlhttp = (hx.ie)
    ? new ActiveXObject("Msxml2.XMLHTTP")
    : new XMLHttpRequest();

  /**
   * Set the specified HTTP header.
   */
  this.setHeader = function(name, value)
  {
    headers.push({name:name, value:value });
  }

  /**
   * Send a message.
   */
  this.send = function(url, body, handler)
  {
    try
    {
      responseHandler = handler;
      xmlhttp.open("post", url, true);
      for (var i=0; i<headers.length; i++)
      {
        var header = headers[i];
        xmlhttp.setRequestHeader(header.name, header.value);
      }
      xmlhttp.setRequestHeader("Content-Length", body.length);

      // 24 Aug 07 - AndyF: Added for IE cache issues (when using a proxy server).
      if (hx.ie)
        xmlhttp.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");

      xmlhttp.onreadystatechange = this.handleResponse;
      xmlhttp.send(body);
    }
    catch (err)
    {
      hx.doError("Message error", null, err);
    }
  }

  /**
   * Handle request response.
   */
  this.handleResponse = function()
  {
    try
    {
      if (xmlhttp.readyState == 4)
      {
        if (xmlhttp.status != "200")
        {
          var text = xmlhttp.responseText;
          if (text.length == 0) text = hx.err_LostConnection;
          hx.doError(text, null, null);
        }
        else if (responseHandler != null)
          responseHandler(xmlhttp);

        // 23 Oct 07 - AndyF: make sure we remove circular
        // dependancies to prevent memory leaks in IE
        xmlhttp.onreadystatechange = new function() {};
        xmlhttp = null;
      }
    }
    catch (err)
    {
      // 23 Oct 07 - AndyF: make sure we remove circular
      // dependancies to prevent memory leaks in IE
      xmlhttp.onreadystatechange = new function() {};
      xmlhttp = null;

      hx.doError(hx.err_LostConnection, null, err);
    }
  }
}

////////////////////////////////////////////////////////////////
// StringUtil
////////////////////////////////////////////////////////////////

var StringUtil = new _StringUtil();
function _StringUtil()
{
  /**
   * Return true if str starts with prefix.
   */
  this.startsWith = function(str, prefix)
  {
    // If prefix longer than str, not possible
    if (str.length < prefix.length) return false;

    // Cut off and compare
    var sub = str.substring(0, prefix.length);
    return (sub == prefix);
  }

  /**
   * Return true if str ends with suffix.
   */
  this.endsWith = function(str, suffix)
  {
    // If suffix is longer, not possible
    if (str.length < suffix.length) return false;

    // Cut off and compare
    var sub = str.substring(str.length - suffix.length);
    return (sub == suffix);
  }

  /**
   * Trim leading and trailing whitespace from the given
   * string.  Return a new string as result. The original
   * string is not modified.
   */
  this.trim = function(str)
  {
    var s = str;
    return s;
  }
}
