/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is DevTools (HeadsUpDisplay) Console Code
 *
 * The Initial Developer of the Original Code is
 *   Mozilla Foundation
 * Portions created by the Initial Developer are Copyright (C) 2010
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   David Dahl <ddahl@mozilla.com> (original author)
 *   Rob Campbell <rcampbell@mozilla.com>
 *   Johnathan Nightingale <jnightingale@mozilla.com>
 *   Patrick Walton <pcwalton@mozilla.com>
 *   Julian Viereck <jviereck@mozilla.com>
 *   Mihai Șucan <mihai.sucan@gmail.com>
 *   Michael Ratcliffe <mratcliffe@mozilla.com>
 *   Joe Walker <jwalker@mozilla.com>
 *   Sonny Piers <sonny.piers@gmail.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;

const CONSOLEAPI_CLASS_ID = "{b49c18f8-3379-4fc0-8c90-d7772c1a9ff3}";

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/NetworkHelper.jsm");
Cu.import("resource:///modules/PropertyPanel.jsm");

var EXPORTED_SYMBOLS = ["HUDService", "ConsoleUtils"];

XPCOMUtils.defineLazyServiceGetter(this, "scriptError",
                                   "@mozilla.org/scripterror;1",
                                   "nsIScriptError");

XPCOMUtils.defineLazyServiceGetter(this, "activityDistributor",
                                   "@mozilla.org/network/http-activity-distributor;1",
                                   "nsIHttpActivityDistributor");

XPCOMUtils.defineLazyServiceGetter(this, "mimeService",
                                   "@mozilla.org/mime;1",
                                   "nsIMIMEService");

XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
                                   "@mozilla.org/widget/clipboardhelper;1",
                                   "nsIClipboardHelper");

XPCOMUtils.defineLazyGetter(this, "gcli", function () {
  var obj = {};
  Cu.import("resource:///modules/gcli.jsm", obj);
  return obj.gcli;
});

XPCOMUtils.defineLazyGetter(this, "CssRuleView", function() {
  let tmp = {};
  Cu.import("resource:///modules/devtools/CssRuleView.jsm", tmp);
  return tmp.CssRuleView;
});

XPCOMUtils.defineLazyGetter(this, "NetUtil", function () {
  var obj = {};
  Cu.import("resource://gre/modules/NetUtil.jsm", obj);
  return obj.NetUtil;
});

XPCOMUtils.defineLazyGetter(this, "template", function () {
  var obj = {};
  Cu.import("resource:///modules/devtools/Templater.jsm", obj);
  return obj.template;
});

XPCOMUtils.defineLazyGetter(this, "PropertyPanel", function () {
  var obj = {};
  try {
    Cu.import("resource:///modules/PropertyPanel.jsm", obj);
  } catch (err) {
    Cu.reportError(err);
  }
  return obj.PropertyPanel;
});

XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function () {
  var obj = {};
  try {
    Cu.import("resource:///modules/AutocompletePopup.jsm", obj);
  }
  catch (err) {
    Cu.reportError(err);
  }
  return obj.AutocompletePopup;
});

XPCOMUtils.defineLazyGetter(this, "ScratchpadManager", function () {
  var obj = {};
  try {
    Cu.import("resource:///modules/devtools/scratchpad-manager.jsm", obj);
  }
  catch (err) {
    Cu.reportError(err);
  }
  return obj.ScratchpadManager;
});

XPCOMUtils.defineLazyGetter(this, "namesAndValuesOf", function () {
  var obj = {};
  Cu.import("resource:///modules/PropertyPanel.jsm", obj);
  return obj.namesAndValuesOf;
});

XPCOMUtils.defineLazyGetter(this, "gConsoleStorage", function () {
  let obj = {};
  Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", obj);
  return obj.ConsoleAPIStorage;
});

function LogFactory(aMessagePrefix)
{
  function log(aMessage) {
    var _msg = aMessagePrefix + " " + aMessage + "\n";
    dump(_msg);
  }
  return log;
}

/**
 * Load the various Command JSMs.
 * Should be called when the console first opens.
 *
 * @return an object containing the EXPORTED_SYMBOLS from all the command
 * modules. In general there is no reason when JSMs need to export symbols
 * except when they need the host environment to inform them of things like the
 * current window/document/etc.
 */
function loadCommands() {
  let commandExports = {};

  Cu.import("resource:///modules/GcliCommands.jsm", commandExports);

  return commandExports;
}

let log = LogFactory("*** HUDService:");

const HUD_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";

XPCOMUtils.defineLazyGetter(this, "stringBundle", function () {
  return Services.strings.createBundle(HUD_STRINGS_URI);
});

// The amount of time in milliseconds that must pass between messages to
// trigger the display of a new group.
const NEW_GROUP_DELAY = 5000;

// The amount of time in milliseconds that we wait before performing a live
// search.
const SEARCH_DELAY = 200;

// The number of lines that are displayed in the console output by default, for
// each category. The user can change this number by adjusting the hidden
// "devtools.hud.loglimit.{network,cssparser,exception,console}" preferences.
const DEFAULT_LOG_LIMIT = 200;

// The various categories of messages. We start numbering at zero so we can
// use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below.
const CATEGORY_NETWORK = 0;
const CATEGORY_CSS = 1;
const CATEGORY_JS = 2;
const CATEGORY_WEBDEV = 3;
const CATEGORY_INPUT = 4;   // always on
const CATEGORY_OUTPUT = 5;  // always on

// The possible message severities. As before, we start at zero so we can use
// these as indexes into MESSAGE_PREFERENCE_KEYS.
const SEVERITY_ERROR = 0;
const SEVERITY_WARNING = 1;
const SEVERITY_INFO = 2;
const SEVERITY_LOG = 3;

// A mapping from the console API log event levels to the Web Console
// severities.
const LEVELS = {
  error: SEVERITY_ERROR,
  warn: SEVERITY_WARNING,
  info: SEVERITY_INFO,
  log: SEVERITY_LOG,
  trace: SEVERITY_LOG,
  dir: SEVERITY_LOG,
  group: SEVERITY_LOG,
  groupCollapsed: SEVERITY_LOG,
  groupEnd: SEVERITY_LOG,
  time: SEVERITY_LOG,
  timeEnd: SEVERITY_LOG
};

// The lowest HTTP response code (inclusive) that is considered an error.
const MIN_HTTP_ERROR_CODE = 400;
// The highest HTTP response code (exclusive) that is considered an error.
const MAX_HTTP_ERROR_CODE = 600;

// HTTP status codes.
const HTTP_MOVED_PERMANENTLY = 301;
const HTTP_FOUND = 302;
const HTTP_SEE_OTHER = 303;
const HTTP_TEMPORARY_REDIRECT = 307;

// The HTML namespace.
const HTML_NS = "http://www.w3.org/1999/xhtml";

// The XUL namespace.
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

// The fragment of a CSS class name that identifies each category.
const CATEGORY_CLASS_FRAGMENTS = [
  "network",
  "cssparser",
  "exception",
  "console",
  "input",
  "output",
];

// The fragment of a CSS class name that identifies each severity.
const SEVERITY_CLASS_FRAGMENTS = [
  "error",
  "warn",
  "info",
  "log",
];

// The preference keys to use for each category/severity combination, indexed
// first by category (rows) and then by severity (columns).
//
// Most of these rather idiosyncratic names are historical and predate the
// division of message type into "category" and "severity".
const MESSAGE_PREFERENCE_KEYS = [
//  Error         Warning   Info    Log
  [ "network",    null,         null,   "networkinfo", ],  // Network
  [ "csserror",   "cssparser",  null,   null,          ],  // CSS
  [ "exception",  "jswarn",     null,   null,          ],  // JS
  [ "error",      "warn",       "info", "log",         ],  // Web Developer
  [ null,         null,         null,   null,          ],  // Input
  [ null,         null,         null,   null,          ],  // Output
];

// Possible directions that can be passed to HUDService.animate().
const ANIMATE_OUT = 0;
const ANIMATE_IN = 1;

// Constants used for defining the direction of JSTerm input history navigation.
const HISTORY_BACK = -1;
const HISTORY_FORWARD = 1;

// The maximum number of bytes a Network ResponseListener can hold.
const RESPONSE_BODY_LIMIT = 1024*1024; // 1 MB

// The maximum uint32 value.
const PR_UINT32_MAX = 4294967295;

// Minimum console height, in pixels.
const MINIMUM_CONSOLE_HEIGHT = 150;

// Minimum page height, in pixels. This prevents the Web Console from
// remembering a height that covers the whole page.
const MINIMUM_PAGE_HEIGHT = 50;

// The default console height, as a ratio from the content window inner height.
const DEFAULT_CONSOLE_HEIGHT = 0.33;

// Constant used when checking the typeof objects.
const TYPEOF_FUNCTION = "function";

const ERRORS = { LOG_MESSAGE_MISSING_ARGS:
                 "Missing arguments: aMessage, aConsoleNode and aMessageNode are required.",
                 CANNOT_GET_HUD: "Cannot getHeads Up Display with provided ID",
                 MISSING_ARGS: "Missing arguments",
                 LOG_OUTPUT_FAILED: "Log Failure: Could not append messageNode to outputNode",
};

// The indent of a console group in pixels.
const GROUP_INDENT = 12;

// The pref prefix for webconsole filters
const PREFS_PREFIX = "devtools.webconsole.filter.";

/**
 * Implements the nsIStreamListener and nsIRequestObserver interface. Used
 * within the HS_httpObserverFactory function to get the response body of
 * requests.
 *
 * The code is mostly based on code listings from:
 *
 *   http://www.softwareishard.com/blog/firebug/
 *      nsitraceablechannel-intercept-http-traffic/
 *
 * @param object aHttpActivity
 *        HttpActivity object associated with this request (see
 *        HS_httpObserverFactory). As the response is done, the response header,
 *        body and status is stored on aHttpActivity.
 */
function ResponseListener(aHttpActivity) {
  this.receivedData = "";
  this.httpActivity = aHttpActivity;
}

ResponseListener.prototype =
{
  /**
   * The response will be written into the outputStream of this nsIPipe.
   * Both ends of the pipe must be blocking.
   */
  sink: null,

  /**
   * The HttpActivity object associated with this response.
   */
  httpActivity: null,

  /**
   * Stores the received data as a string.
   */
  receivedData: null,

  /**
   * The nsIRequest we are started for.
   */
  request: null,

  /**
   * Sets the httpActivity object's response header if it isn't set already.
   *
   * @param nsIRequest aRequest
   */
  setResponseHeader: function RL_setResponseHeader(aRequest)
  {
    let httpActivity = this.httpActivity;
    // Check if the header isn't set yet.
    if (!httpActivity.response.header) {
      if (aRequest instanceof Ci.nsIHttpChannel) {
      httpActivity.response.header = {};
        try {
        aRequest.visitResponseHeaders({
          visitHeader: function(aName, aValue) {
            httpActivity.response.header[aName] = aValue;
          }
        });
      }
        // Accessing the response header can throw an NS_ERROR_NOT_AVAILABLE
        // exception. Catch it and stop it to make it not show up in the.
        // This can happen if the response is not finished yet and the user
        // reloades the page.
        catch (ex) {
          delete httpActivity.response.header;
        }
      }
    }
  },

  /**
   * Set the async listener for the given nsIAsyncInputStream. This allows us to
   * wait asynchronously for any data coming from the stream.
   *
   * @param nsIAsyncInputStream aStream
   *        The input stream from where we are waiting for data to come in.
   *
   * @param nsIInputStreamCallback aListener
   *        The input stream callback you want. This is an object that must have
   *        the onInputStreamReady() method. If the argument is null, then the
   *        current callback is removed.
   *
   * @returns void
   */
  setAsyncListener: function RL_setAsyncListener(aStream, aListener)
  {
    // Asynchronously wait for the stream to be readable or closed.
    aStream.asyncWait(aListener, 0, 0, Services.tm.mainThread);
  },

  /**
   * Stores the received data, if request/response body logging is enabled. It
   * also does limit the number of stored bytes, based on the
   * RESPONSE_BODY_LIMIT constant.
   *
   * Learn more about nsIStreamListener at:
   * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener
   *
   * @param nsIRequest aRequest
   * @param nsISupports aContext
   * @param nsIInputStream aInputStream
   * @param unsigned long aOffset
   * @param unsigned long aCount
   */
  onDataAvailable: function RL_onDataAvailable(aRequest, aContext, aInputStream,
                                               aOffset, aCount)
  {
    this.setResponseHeader(aRequest);

    let data = NetUtil.readInputStreamToString(aInputStream, aCount);

    if (!this.httpActivity.response.bodyDiscarded &&
        this.receivedData.length < RESPONSE_BODY_LIMIT) {
      this.receivedData += NetworkHelper.
                           convertToUnicode(data, aRequest.contentCharset);
    }
  },

  /**
   * See documentation at
   * https://developer.mozilla.org/En/NsIRequestObserver
   *
   * @param nsIRequest aRequest
   * @param nsISupports aContext
   */
  onStartRequest: function RL_onStartRequest(aRequest, aContext)
  {
    this.request = aRequest;

    // Always discard the response body if logging is not enabled in the Web
    // Console.
    this.httpActivity.response.bodyDiscarded =
      !HUDService.saveRequestAndResponseBodies;

    // Check response status and discard the body for redirects.
    if (!this.httpActivity.response.bodyDiscarded &&
        this.httpActivity.channel instanceof Ci.nsIHttpChannel) {
      switch (this.httpActivity.channel.responseStatus) {
        case HTTP_MOVED_PERMANENTLY:
        case HTTP_FOUND:
        case HTTP_SEE_OTHER:
        case HTTP_TEMPORARY_REDIRECT:
          this.httpActivity.response.bodyDiscarded = true;
          break;
      }
    }

    // Asynchronously wait for the data coming from the request.
    this.setAsyncListener(this.sink.inputStream, this);
  },

  /**
   * Handle the onStopRequest by storing the response header is stored on the
   * httpActivity object. The sink output stream is also closed.
   *
   * For more documentation about nsIRequestObserver go to:
   * https://developer.mozilla.org/En/NsIRequestObserver
   *
   * @param nsIRequest aRequest
   *        The request we are observing.
   * @param nsISupports aContext
   * @param nsresult aStatusCode
   */
  onStopRequest: function RL_onStopRequest(aRequest, aContext, aStatusCode)
  {
    // Retrieve the response headers, as they are, from the server.
    let response = null;
    for each (let item in HUDService.openResponseHeaders) {
      if (item.channel === this.httpActivity.channel) {
        response = item;
        break;
      }
    }

    if (response) {
      this.httpActivity.response.header = response.headers;
      delete HUDService.openResponseHeaders[response.id];
    }
    else {
      this.setResponseHeader(aRequest);
    }

    this.sink.outputStream.close();
  },

  /**
   * Clean up the response listener once the response input stream is closed.
   * This is called from onStopRequest() or from onInputStreamReady() when the
   * stream is closed.
   *
   * @returns void
   */
  onStreamClose: function RL_onStreamClose()
  {
    if (!this.httpActivity) {
      return;
    }

    // Remove our listener from the request input stream.
    this.setAsyncListener(this.sink.inputStream, null);

    if (!this.httpActivity.response.bodyDiscarded &&
        HUDService.saveRequestAndResponseBodies) {
      this.httpActivity.response.body = this.receivedData;
    }

    if (HUDService.lastFinishedRequestCallback) {
      HUDService.lastFinishedRequestCallback(this.httpActivity);
    }

    // Call update on all panels.
    this.httpActivity.panels.forEach(function(weakRef) {
      let panel = weakRef.get();
      if (panel) {
        panel.update();
      }
    });
    this.httpActivity.response.isDone = true;
    this.httpActivity = null;
    this.receivedData = "";
    this.request = null;
    this.sink = null;
    this.inputStream = null;
  },

  /**
   * The nsIInputStreamCallback for when the request input stream is ready -
   * either it has more data or it is closed.
   *
   * @param nsIAsyncInputStream aStream
   *        The sink input stream from which data is coming.
   *
   * @returns void
   */
  onInputStreamReady: function RL_onInputStreamReady(aStream)
  {
    if (!(aStream instanceof Ci.nsIAsyncInputStream) || !this.httpActivity) {
      return;
    }

    let available = -1;
    try {
      // This may throw if the stream is closed normally or due to an error.
      available = aStream.available();
    }
    catch (ex) { }

    if (available != -1) {
      if (available != 0) {
        // Note that passing 0 as the offset here is wrong, but the
        // onDataAvailable() method does not use the offset, so it does not
        // matter.
        this.onDataAvailable(this.request, null, aStream, 0, available);
      }
      this.setAsyncListener(aStream, this);
    }
    else {
      this.onStreamClose();
    }
  },

  QueryInterface: XPCOMUtils.generateQI([
    Ci.nsIStreamListener,
    Ci.nsIInputStreamCallback,
    Ci.nsIRequestObserver,
    Ci.nsISupports,
  ])
}

///////////////////////////////////////////////////////////////////////////
//// Helper for creating the network panel.

/**
 * Creates a DOMNode and sets all the attributes of aAttributes on the created
 * element.
 *
 * @param nsIDOMDocument aDocument
 *        Document to create the new DOMNode.
 * @param string aTag
 *        Name of the tag for the DOMNode.
 * @param object aAttributes
 *        Attributes set on the created DOMNode.
 *
 * @returns nsIDOMNode
 */
function createElement(aDocument, aTag, aAttributes)
{
  let node = aDocument.createElement(aTag);
  if (aAttributes) {
    for (let attr in aAttributes) {
      node.setAttribute(attr, aAttributes[attr]);
    }
  }
  return node;
}

/**
 * Creates a new DOMNode and appends it to aParent.
 *
 * @param nsIDOMNode aParent
 *        A parent node to append the created element.
 * @param string aTag
 *        Name of the tag for the DOMNode.
 * @param object aAttributes
 *        Attributes set on the created DOMNode.
 *
 * @returns nsIDOMNode
 */
function createAndAppendElement(aParent, aTag, aAttributes)
{
  let node = createElement(aParent.ownerDocument, aTag, aAttributes);
  aParent.appendChild(node);
  return node;
}

/**
 * Convenience function to unwrap a wrapped object.
 *
 * @param aObject the object to unwrap
 */

function unwrap(aObject)
{
  try {
    return XPCNativeWrapper.unwrap(aObject);
  } catch(e) {
    return aObject;
  }
}

///////////////////////////////////////////////////////////////////////////
//// NetworkPanel

/**
 * Creates a new NetworkPanel.
 *
 * @param nsIDOMNode aParent
 *        Parent node to append the created panel to.
 * @param object aHttpActivity
 *        HttpActivity to display in the panel.
 */
function NetworkPanel(aParent, aHttpActivity)
{
  let doc = aParent.ownerDocument;
  this.httpActivity = aHttpActivity;

  // Create the underlaying panel
  this.panel = createElement(doc, "panel", {
    label: HUDService.getStr("NetworkPanel.label"),
    titlebar: "normal",
    noautofocus: "true",
    noautohide: "true",
    close: "true"
  });

  // Create the iframe that displays the NetworkPanel XHTML.
  this.iframe = createAndAppendElement(this.panel, "iframe", {
    src: "chrome://browser/content/NetworkPanel.xhtml",
    type: "content",
    flex: "1"
  });

  let self = this;

  // Destroy the panel when it's closed.
  this.panel.addEventListener("popuphidden", function onPopupHide() {
    self.panel.removeEventListener("popuphidden", onPopupHide, false);
    self.panel.parentNode.removeChild(self.panel);
    self.panel = null;
    self.iframe = null;
    self.document = null;
    self.httpActivity = null;

    if (self.linkNode) {
      self.linkNode._panelOpen = false;
      self.linkNode = null;
    }
  }, false);

  // Set the document object and update the content once the panel is loaded.
  this.panel.addEventListener("load", function onLoad() {
    self.panel.removeEventListener("load", onLoad, true);
    self.document = self.iframe.contentWindow.document;
    self.update();
  }, true);

  // Create the footer.
  let footer = createElement(doc, "hbox", { align: "end" });
  createAndAppendElement(footer, "spacer", { flex: 1 });

  createAndAppendElement(footer, "resizer", { dir: "bottomend" });
  this.panel.appendChild(footer);

  aParent.appendChild(this.panel);
}

NetworkPanel.prototype =
{
  /**
   * Callback is called once the NetworkPanel is processed completly. Used by
   * unit tests.
   */
  isDoneCallback: null,

  /**
   * The current state of the output.
   */
  _state: 0,

  /**
   * State variables.
   */
  _INIT: 0,
  _DISPLAYED_REQUEST_HEADER: 1,
  _DISPLAYED_REQUEST_BODY: 2,
  _DISPLAYED_RESPONSE_HEADER: 3,
  _TRANSITION_CLOSED: 4,

  _fromDataRegExp: /Content-Type\:\s*application\/x-www-form-urlencoded/,

  /**
   * Small helper function that is nearly equal to  HUDService.getFormatStr
   * except that it prefixes aName with "NetworkPanel.".
   *
   * @param string aName
   *        The name of an i10n string to format. This string is prefixed with
   *        "NetworkPanel." before calling the HUDService.getFormatStr function.
   * @param array aArray
   *        Values used as placeholder for the i10n string.
   * @returns string
   *          The i10n formated string.
   */
  _format: function NP_format(aName, aArray)
  {
    return HUDService.getFormatStr("NetworkPanel." + aName, aArray);
  },

  /**
   * Returns the content type of the response body. This is based on the
   * response.header["Content-Type"] info. If this value is not available, then
   * the content type is tried to be estimated by the url file ending.
   *
   * @returns string or null
   *          Content type or null if no content type could be figured out.
   */
  get _contentType()
  {
    let response = this.httpActivity.response;
    let contentTypeValue = null;

    if (response.header && response.header["Content-Type"]) {
      let types = response.header["Content-Type"].split(/,|;/);
      for (let i = 0; i < types.length; i++) {
        let type = NetworkHelper.mimeCategoryMap[types[i]];
        if (type) {
          return types[i];
        }
      }
    }

    // Try to get the content type from the request file extension.
    let uri = NetUtil.newURI(this.httpActivity.url);
    let mimeType = null;
    if ((uri instanceof Ci.nsIURL) && uri.fileExtension) {
      try {
        mimeType = mimeService.getTypeFromExtension(uri.fileExtension);
      } catch(e) {
        // Added to prevent failures on OS X 64. No Flash?
        Cu.reportError(e);
        // Return empty string to pass unittests.
        return "";
      }
    }
    return mimeType;
  },

  /**
   *
   * @returns boolean
   *          True if the response is an image, false otherwise.
   */
  get _responseIsImage()
  {
    return NetworkHelper.mimeCategoryMap[this._contentType] == "image";
  },

  /**
   *
   * @returns boolean
   *          True if the response body contains text, false otherwise.
   */
  get _isResponseBodyTextData()
  {
    let contentType = this._contentType;

    if (!contentType)
      return false;

    if (contentType.indexOf("text/") == 0) {
      return true;
    }

    switch (NetworkHelper.mimeCategoryMap[contentType]) {
      case "txt":
      case "js":
      case "json":
      case "css":
      case "html":
      case "svg":
      case "xml":
        return true;

      default:
        return false;
    }
  },

  /**
   *
   * @returns boolean
   *          Returns true if the server responded that the request is already
   *          in the browser's cache, false otherwise.
   */
  get _isResponseCached()
  {
    return this.httpActivity.response.status.indexOf("304") != -1;
  },

  /**
   *
   * @returns boolean
   *          Returns true if the posted body contains form data.
   */
  get _isRequestBodyFormData()
  {
    let requestBody = this.httpActivity.request.body;
    return this._fromDataRegExp.test(requestBody);
  },

  /**
   * Appends the node with id=aId by the text aValue.
   *
   * @param string aId
   * @param string aValue
   * @returns void
   */
  _appendTextNode: function NP_appendTextNode(aId, aValue)
  {
    let textNode = this.document.createTextNode(aValue);
    this.document.getElementById(aId).appendChild(textNode);
  },

  /**
   * Generates some HTML to display the key-value pair of the aList data. The
   * generated HTML is added to node with id=aParentId.
   *
   * @param string aParentId
   *        Id of the parent node to append the list to.
   * @oaram object aList
   *        Object that holds the key-value information to display in aParentId.
   * @param boolean aIgnoreCookie
   *        If true, the key-value named "Cookie" is not added to the list.
   * @returns void
   */
  _appendList: function NP_appendList(aParentId, aList, aIgnoreCookie)
  {
    let parent = this.document.getElementById(aParentId);
    let doc = this.document;

    let sortedList = {};
    Object.keys(aList).sort().forEach(function(aKey) {
      sortedList[aKey] = aList[aKey];
    });

    for (let key in sortedList) {
      if (aIgnoreCookie && key == "Cookie") {
        continue;
      }

      /**
       * The following code creates the HTML:
       * <tr>
       * <th scope="row" class="property-name">${line}:</th>
       * <td class="property-value">${aList[line]}</td>
       * </tr>
       * and adds it to parent.
       */
      let row = doc.createElement("tr");
      let textNode = doc.createTextNode(key + ":");
      let th = doc.createElement("th");
      th.setAttribute("scope", "row");
      th.setAttribute("class", "property-name");
      th.appendChild(textNode);
      row.appendChild(th);

      textNode = doc.createTextNode(sortedList[key]);
      let td = doc.createElement("td");
      td.setAttribute("class", "property-value");
      td.appendChild(textNode);
      row.appendChild(td);

      parent.appendChild(row);
    }
  },

  /**
   * Displays the node with id=aId.
   *
   * @param string aId
   * @returns void
   */
  _displayNode: function NP_displayNode(aId)
  {
    this.document.getElementById(aId).style.display = "block";
  },

  /**
   * Sets the request URL, request method, the timing information when the
   * request started and the request header content on the NetworkPanel.
   * If the request header contains cookie data, a list of sent cookies is
   * generated and a special sent cookie section is displayed + the cookie list
   * added to it.
   *
   * @returns void
   */
  _displayRequestHeader: function NP_displayRequestHeader()
  {
    let timing = this.httpActivity.timing;
    let request = this.httpActivity.request;

    this._appendTextNode("headUrl", this.httpActivity.url);
    this._appendTextNode("headMethod", this.httpActivity.method);

    this._appendTextNode("requestHeadersInfo",
      ConsoleUtils.timestampString(timing.REQUEST_HEADER/1000));

    this._appendList("requestHeadersContent", request.header, true);

    if ("Cookie" in request.header) {
      this._displayNode("requestCookie");

      let cookies = request.header.Cookie.split(";");
      let cookieList = {};
      let cookieListSorted = {};
      cookies.forEach(function(cookie) {
        let name, value;
        [name, value] = cookie.trim().split("=");
        cookieList[name] = value;
      });
      this._appendList("requestCookieContent", cookieList);
    }
  },

  /**
   * Displays the request body section of the NetworkPanel and set the request
   * body content on the NetworkPanel.
   *
   * @returns void
   */
  _displayRequestBody: function NP_displayRequestBody() {
    this._displayNode("requestBody");
    this._appendTextNode("requestBodyContent", this.httpActivity.request.body);
  },

  /*
   * Displays the `sent form data` section. Parses the request header for the
   * submitted form data displays it inside of the `sent form data` section.
   *
   * @returns void
   */
  _displayRequestForm: function NP_processRequestForm() {
    let requestBodyLines = this.httpActivity.request.body.split("\n");
    let formData = requestBodyLines[requestBodyLines.length - 1].
                      replace(/\+/g, " ").split("&");

    function unescapeText(aText)
    {
      try {
        return decodeURIComponent(aText);
      }
      catch (ex) {
        return decodeURIComponent(unescape(aText));
      }
    }

    let formDataObj = {};
    for (let i = 0; i < formData.length; i++) {
      let data = formData[i];
      let idx = data.indexOf("=");
      let key = data.substring(0, idx);
      let value = data.substring(idx + 1);
      formDataObj[unescapeText(key)] = unescapeText(value);
    }

    this._appendList("requestFormDataContent", formDataObj);
    this._displayNode("requestFormData");
  },

  /**
   * Displays the response section of the NetworkPanel, sets the response status,
   * the duration between the start of the request and the receiving of the
   * response header as well as the response header content on the the NetworkPanel.
   *
   * @returns void
   */
  _displayResponseHeader: function NP_displayResponseHeader()
  {
    let timing = this.httpActivity.timing;
    let response = this.httpActivity.response;

    this._appendTextNode("headStatus", response.status);

    let deltaDuration =
      Math.round((timing.RESPONSE_HEADER - timing.REQUEST_HEADER) / 1000);
    this._appendTextNode("responseHeadersInfo",
      this._format("durationMS", [deltaDuration]));

    this._displayNode("responseContainer");
    this._appendList("responseHeadersContent", response.header);
  },

  /**
   * Displays the respones image section, sets the source of the image displayed
   * in the image response section to the request URL and the duration between
   * the receiving of the response header and the end of the request. Once the
   * image is loaded, the size of the requested image is set.
   *
   * @returns void
   */
  _displayResponseImage: function NP_displayResponseImage()
  {
    let self = this;
    let timing = this.httpActivity.timing;
    let response = this.httpActivity.response;
    let cached = "";

    if (this._isResponseCached) {
      cached = "Cached";
    }

    let imageNode = this.document.getElementById("responseImage" + cached +"Node");
    imageNode.setAttribute("src", this.httpActivity.url);

    // This function is called to set the imageInfo.
    function setImageInfo() {
      let deltaDuration =
        Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000);
      self._appendTextNode("responseImage" + cached + "Info",
        self._format("imageSizeDeltaDurationMS", [
          imageNode.width, imageNode.height, deltaDuration
        ]
      ));
    }

    // Check if the image is already loaded.
    if (imageNode.width != 0) {
      setImageInfo();
    }
    else {
      // Image is not loaded yet therefore add a load event.
      imageNode.addEventListener("load", function imageNodeLoad() {
        imageNode.removeEventListener("load", imageNodeLoad, false);
        setImageInfo();
      }, false);
    }

    this._displayNode("responseImage" + cached);
  },

  /**
   * Displays the response body section, sets the the duration between
   * the receiving of the response header and the end of the request as well as
   * the content of the response body on the NetworkPanel.
   *
   * @param [optional] string aCachedContent
   *        Cached content for this request. If this argument is set, the
   *        responseBodyCached section is displayed.
   * @returns void
   */
  _displayResponseBody: function NP_displayResponseBody(aCachedContent)
  {
    let timing = this.httpActivity.timing;
    let response = this.httpActivity.response;
    let cached =  "";
    if (aCachedContent) {
      cached = "Cached";
    }

    let deltaDuration =
      Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000);
    this._appendTextNode("responseBody" + cached + "Info",
      this._format("durationMS", [deltaDuration]));

    this._displayNode("responseBody" + cached);
    this._appendTextNode("responseBody" + cached + "Content",
                            aCachedContent || response.body);
  },

  /**
   * Displays the `Unknown Content-Type hint` and sets the duration between the
   * receiving of the response header on the NetworkPanel.
   *
   * @returns void
   */
  _displayResponseBodyUnknownType: function NP_displayResponseBodyUnknownType()
  {
    let timing = this.httpActivity.timing;

    this._displayNode("responseBodyUnknownType");
    let deltaDuration =
      Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000);
    this._appendTextNode("responseBodyUnknownTypeInfo",
      this._format("durationMS", [deltaDuration]));

    this._appendTextNode("responseBodyUnknownTypeContent",
      this._format("responseBodyUnableToDisplay.content", [this._contentType]));
  },

  /**
   * Displays the `no response body` section and sets the the duration between
   * the receiving of the response header and the end of the request.
   *
   * @returns void
   */
  _displayNoResponseBody: function NP_displayNoResponseBody()
  {
    let timing = this.httpActivity.timing;

    this._displayNode("responseNoBody");
    let deltaDuration =
      Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000);
    this._appendTextNode("responseNoBodyInfo",
      this._format("durationMS", [deltaDuration]));
  },

  /*
   * Calls the isDoneCallback function if one is specified.
   */
  _callIsDone: function() {
    if (this.isDoneCallback) {
      this.isDoneCallback();
    }
  },

  /**
   * Updates the content of the NetworkPanel's iframe.
   *
   * @returns void
   */
  update: function NP_update()
  {
    /**
     * After the iframe's contentWindow is ready, the document object is set.
     * If the document object isn't set yet, then the page is loaded and nothing
     * can be updated.
     */
    if (!this.document) {
      return;
    }

    let timing = this.httpActivity.timing;
    let request = this.httpActivity.request;
    let response = this.httpActivity.response;

    switch (this._state) {
      case this._INIT:
        this._displayRequestHeader();
        this._state = this._DISPLAYED_REQUEST_HEADER;
        // FALL THROUGH

      case this._DISPLAYED_REQUEST_HEADER:
        // Process the request body if there is one.
        if (!request.bodyDiscarded && request.body) {
          // Check if we send some form data. If so, display the form data special.
          if (this._isRequestBodyFormData) {
            this._displayRequestForm();
          }
          else {
            this._displayRequestBody();
          }
          this._state = this._DISPLAYED_REQUEST_BODY;
        }
        // FALL THROUGH

      case this._DISPLAYED_REQUEST_BODY:
        // There is always a response header. Therefore we can skip here if
        // we don't have a response header yet and don't have to try updating
        // anything else in the NetworkPanel.
        if (!response.header) {
          break
        }
        this._displayResponseHeader();
        this._state = this._DISPLAYED_RESPONSE_HEADER;
        // FALL THROUGH

      case this._DISPLAYED_RESPONSE_HEADER:
        // Check if the transition is done.
        if (timing.TRANSACTION_CLOSE && response.isDone) {
          if (response.bodyDiscarded) {
            this._callIsDone();
          }
          else if (this._responseIsImage) {
            this._displayResponseImage();
            this._callIsDone();
          }
          else if (!this._isResponseBodyTextData) {
            this._displayResponseBodyUnknownType();
            this._callIsDone();
          }
          else if (response.body) {
            this._displayResponseBody();
            this._callIsDone();
          }
          else if (this._isResponseCached) {
            let self = this;
            NetworkHelper.loadFromCache(this.httpActivity.url,
                                        this.httpActivity.charset,
                                        function(aContent) {
              // If some content could be loaded from the cache, then display
              // the body.
              if (aContent) {
                self._displayResponseBody(aContent);
                self._callIsDone();
              }
              // Otherwise, show the "There is no response body" hint.
              else {
                self._displayNoResponseBody();
                self._callIsDone();
              }
            });
          }
          else {
            this._displayNoResponseBody();
            this._callIsDone();
          }
          this._state = this._TRANSITION_CLOSED;
        }
        break;
    }
  }
}

///////////////////////////////////////////////////////////////////////////
//// Private utility functions for the HUD service

/**
 * Ensures that the number of message nodes of type aCategory don't exceed that
 * category's line limit by removing old messages as needed.
 *
 * @param aHUDId aHUDId
 *        The HeadsUpDisplay ID.
 * @param integer aCategory
 *        The category of message nodes to limit.
 * @return number
 *         The current user-selected log limit.
 */
function pruneConsoleOutputIfNecessary(aHUDId, aCategory)
{
  // Get the log limit, either from the pref or from the constant.
  let logLimit;
  try {
    let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory];
    logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName);
  } catch (e) {
    logLimit = DEFAULT_LOG_LIMIT;
  }

  let hudRef = HUDService.getHudReferenceById(aHUDId);
  let outputNode = hudRef.outputNode;

  let scrollBox = outputNode.scrollBoxObject.element;
  let oldScrollHeight = scrollBox.scrollHeight;
  let scrolledToBottom = ConsoleUtils.isOutputScrolledToBottom(outputNode);

  // Prune the nodes.
  let messageNodes = outputNode.querySelectorAll(".webconsole-msg-" +
      CATEGORY_CLASS_FRAGMENTS[aCategory]);
  let removeNodes = messageNodes.length - logLimit;
  for (let i = 0; i < removeNodes; i++) {
    if (messageNodes[i].classList.contains("webconsole-msg-cssparser")) {
      let desc = messageNodes[i].childNodes[2].textContent;
      let location = "";
      if (messageNodes[i].childNodes[4]) {
        location = messageNodes[i].childNodes[4].getAttribute("title");
      }
      delete hudRef.cssNodes[desc + location];
    }
    else if (messageNodes[i].classList.contains("webconsole-msg-inspector")) {
      hudRef.pruneConsoleDirNode(messageNodes[i]);
      continue;
    }
    messageNodes[i].parentNode.removeChild(messageNodes[i]);
  }

  if (!scrolledToBottom && removeNodes > 0 &&
      oldScrollHeight != scrollBox.scrollHeight) {
    scrollBox.scrollTop -= oldScrollHeight - scrollBox.scrollHeight;
  }

  return logLimit;
}

///////////////////////////////////////////////////////////////////////////
//// The HUD service

function HUD_SERVICE()
{
  // TODO: provide mixins for FENNEC: bug 568621
  if (appName() == "FIREFOX") {
    var mixins = new FirefoxApplicationHooks();
  }
  else {
    throw new Error("Unsupported Application");
  }

  this.mixins = mixins;

  // These methods access the "this" object, but they're registered as
  // event listeners. So we hammer in the "this" binding.
  this.onTabClose = this.onTabClose.bind(this);
  this.onWindowUnload = this.onWindowUnload.bind(this);

  // Remembers the last console height, in pixels.
  this.lastConsoleHeight = Services.prefs.getIntPref("devtools.hud.height");

  // Network response bodies are piped through a buffer of the given size (in
  // bytes).
  this.responsePipeSegmentSize =
    Services.prefs.getIntPref("network.buffer.cache.size");

  /**
   * Collection of HUDIds that map to the tabs/windows/contexts
   * that a HeadsUpDisplay can be activated for.
   */
  this.activatedContexts = [];

  /**
   * Collection of outer window IDs mapping to HUD IDs.
   */
  this.windowIds = {};

  /**
   * Each HeadsUpDisplay has a set of filter preferences
   */
  this.filterPrefs = {};

  /**
   * Keeps a reference for each HeadsUpDisplay that is created
   */
  this.hudReferences = {};

  /**
   * Requests that haven't finished yet.
   */
  this.openRequests = {};

  /**
   * Response headers for requests that haven't finished yet.
   */
  this.openResponseHeaders = {};
};

HUD_SERVICE.prototype =
{
  /**
   * Last value entered
   */
  lastInputValue: "",

  /**
   * L10N shortcut function
   *
   * @param string aName
   * @returns string
   */
  getStr: function HS_getStr(aName)
  {
    return stringBundle.GetStringFromName(aName);
  },

  /**
   * L10N shortcut function
   *
   * @param string aName
   * @returns (format) string
   */
  getFormatStr: function HS_getFormatStr(aName, aArray)
  {
    return stringBundle.formatStringFromName(aName, aArray, aArray.length);
  },

  /**
   * getter for UI commands to be used by the frontend
   *
   * @returns object
   */
  get consoleUI() {
    return HeadsUpDisplayUICommands;
  },

  /**
   * The sequencer is a generator (after initialization) that returns unique
   * integers
   */
  sequencer: null,

  /**
   * Gets the ID of the outer window of this DOM window
   *
   * @param nsIDOMWindow aWindow
   * @returns integer
   */
  getWindowId: function HS_getWindowId(aWindow)
  {
    return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
  },

  /**
   * Gets the ID of the inner window of this DOM window
   *
   * @param nsIDOMWindow aWindow
   * @returns integer
   */
  getInnerWindowId: function HS_getInnerWindowId(aWindow)
  {
    return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
           getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
  },

  /**
   * Gets the top level content window that has an outer window with
   * the given ID or returns null if no such content window exists
   *
   * @param integer aId
   * @returns nsIDOMWindow
   */
  getWindowByWindowId: function HS_getWindowByWindowId(aId)
  {
    // In the future (post-Electrolysis), getOuterWindowWithId() could
    // return null, because the originating window could have gone away
    // while we were in the process of receiving and/or processing a
    // message. For future-proofing purposes, we do a null check here.

    let someWindow = Services.wm.getMostRecentWindow(null);
    let content = null;

    if (someWindow) {
      let windowUtils = someWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                                  .getInterface(Ci.nsIDOMWindowUtils);
      content = windowUtils.getOuterWindowWithId(aId);
    }

    return content;
  },

  /**
   * Whether to save the bodies of network requests and responses. Disabled by
   * default to save memory.
   */
  saveRequestAndResponseBodies: false,

  /**
   * Tell the HUDService that a HeadsUpDisplay can be activated
   * for the window or context that has 'aContextDOMId' node id
   *
   * @param string aContextDOMId
   * @return void
   */
  registerActiveContext: function HS_registerActiveContext(aContextDOMId)
  {
    this.activatedContexts.push(aContextDOMId);
  },

  /**
   * Firefox-specific current tab getter
   *
   * @returns nsIDOMWindow
   */
  currentContext: function HS_currentContext() {
    return this.mixins.getCurrentContext();
  },

  /**
   * Tell the HUDService that a HeadsUpDisplay should be deactivated
   *
   * @param string aContextDOMId
   * @return void
   */
  unregisterActiveContext: function HS_deregisterActiveContext(aContextDOMId)
  {
    var domId = aContextDOMId.split("_")[1];
    var idx = this.activatedContexts.indexOf(domId);
    if (idx > -1) {
      this.activatedContexts.splice(idx, 1);
    }
  },

  /**
   * Tells callers that a HeadsUpDisplay can be activated for the context
   *
   * @param string aContextDOMId
   * @return boolean
   */
  canActivateContext: function HS_canActivateContext(aContextDOMId)
  {
    var domId = aContextDOMId.split("_")[1];
    for (var idx in this.activatedContexts) {
      if (this.activatedContexts[idx] == domId){
        return true;
      }
    }
    return false;
  },

  /**
   * Activate a HeadsUpDisplay for the given tab context.
   *
   * @param Element aContext the tab element.
   * @param boolean aAnimated animate opening the Web Console?
   * @returns void
   */
  activateHUDForContext: function HS_activateHUDForContext(aContext, aAnimated)
  {
    this.wakeup();

    let window = aContext.linkedBrowser.contentWindow;
    let chromeDocument = aContext.ownerDocument;
    let nBox = chromeDocument.defaultView.getNotificationBox(window);
    this.registerActiveContext(nBox.id);
    this.windowInitializer(window);

    let hudId = "hud_" + nBox.id;
    let hudRef = this.hudReferences[hudId];

    if (!aAnimated || hudRef.consolePanel) {
      this.disableAnimation(hudId);
    }

    // Create a processing instruction for GCLIs CSS stylesheet, but only if
    // we don't have one for this document. Also record the context we're
    // adding this for so we know when to remove it.
    let procInstr = aContext.ownerDocument.gcliCssProcInstr;
    if (!procInstr) {
      procInstr = aContext.ownerDocument.createProcessingInstruction(
              "xml-stylesheet",
              "href='chrome://browser/skin/devtools/gcli.css' type='text/css'");
      procInstr.contexts = [];

      let root = aContext.ownerDocument.getElementsByTagName('window')[0];
      root.parentNode.insertBefore(procInstr, root);
      aContext.ownerDocument.gcliCssProcInstr = procInstr;
    }
    if (procInstr.contexts.indexOf(hudId) == -1) {
      procInstr.contexts.push(hudId);
    }
  },

  /**
   * Deactivate a HeadsUpDisplay for the given tab context.
   *
   * @param nsIDOMWindow aContext
   * @param aAnimated animate closing the web console?
   * @returns void
   */
  deactivateHUDForContext: function HS_deactivateHUDForContext(aContext, aAnimated)
  {
    let browser = aContext.linkedBrowser;
    let window = browser.contentWindow;
    let chromeDocument = aContext.ownerDocument;
    let nBox = chromeDocument.defaultView.getNotificationBox(window);
    let hudId = "hud_" + nBox.id;
    let displayNode = chromeDocument.getElementById(hudId);

    if (hudId in this.hudReferences && displayNode) {
      if (!aAnimated) {
        this.storeHeight(hudId);
      }

      let hud = this.hudReferences[hudId];
      browser.webProgress.removeProgressListener(hud.progressListener);
      delete hud.progressListener;

      this.unregisterDisplay(hudId);

      window.focus();
    }

    // Remove this context from the list of contexts that need the GCLI CSS
    // processing instruction and then remove the processing instruction if it
    // isn't needed any more.
    let procInstr = aContext.ownerDocument.gcliCssProcInstr;
    if (procInstr) {
      procInstr.contexts = procInstr.contexts.filter(function(id) {
        return id !== hudId;
      });
      if (procInstr.contexts.length == 0 && procInstr.parentNode) {
        procInstr.parentNode.removeChild(procInstr);
        delete aContext.ownerDocument.gcliCssProcInstr;
      }
    }
  },

  /**
   * get a unique ID from the sequence generator
   *
   * @returns integer
   */
  sequenceId: function HS_sequencerId()
  {
    if (!this.sequencer) {
      this.sequencer = this.createSequencer(-1);
    }
    return this.sequencer.next();
  },

  /**
   * get the default filter prefs
   *
   * @param string aHUDId
   * @returns JS Object
   */
  getDefaultFilterPrefs: function HS_getDefaultFilterPrefs(aHUDId) {
    return this.filterPrefs[aHUDId];
  },

  /**
   * get the current filter prefs
   *
   * @param string aHUDId
   * @returns JS Object
   */
  getFilterPrefs: function HS_getFilterPrefs(aHUDId) {
    return this.filterPrefs[aHUDId];
  },

  /**
   * get the filter state for a specific toggle button on a heads up display
   *
   * @param string aHUDId
   * @param string aToggleType
   * @returns boolean
   */
  getFilterState: function HS_getFilterState(aHUDId, aToggleType)
  {
    if (!aHUDId) {
      return false;
    }
    try {
      var bool = this.filterPrefs[aHUDId][aToggleType];
      return bool;
    }
    catch (ex) {
      return false;
    }
  },

  /**
   * set the filter state for a specific toggle button on a heads up display
   *
   * @param string aHUDId
   * @param string aToggleType
   * @param boolean aState
   * @returns void
   */
  setFilterState: function HS_setFilterState(aHUDId, aToggleType, aState)
  {
    this.filterPrefs[aHUDId][aToggleType] = aState;
    this.adjustVisibilityForMessageType(aHUDId, aToggleType, aState);
    Services.prefs.setBoolPref(PREFS_PREFIX + aToggleType, aState);
  },

  /**
   * Splits the given console messages into groups based on their timestamps.
   *
   * @param nsIDOMNode aOutputNode
   *        The output node to alter.
   * @returns void
   */
  regroupOutput: function HS_regroupOutput(aOutputNode)
  {
    // Go through the nodes and adjust the placement of "webconsole-new-group"
    // classes.

    let nodes = aOutputNode.querySelectorAll(".hud-msg-node" +
      ":not(.hud-filtered-by-string):not(.hud-filtered-by-type)");
    let lastTimestamp;
    for (let i = 0; i < nodes.length; i++) {
      let thisTimestamp = nodes[i].timestamp;
      if (lastTimestamp != null &&
          thisTimestamp >= lastTimestamp + NEW_GROUP_DELAY) {
        nodes[i].classList.add("webconsole-new-group");
      }
      else {
        nodes[i].classList.remove("webconsole-new-group");
      }
      lastTimestamp = thisTimestamp;
    }
  },

  /**
   * Turns the display of log nodes on and off appropriately to reflect the
   * adjustment of the message type filter named by @aPrefKey.
   *
   * @param string aHUDId
   *        The ID of the HUD to alter.
   * @param string aPrefKey
   *        The preference key for the message type being filtered: one of the
   *        values in the MESSAGE_PREFERENCE_KEYS table.
   * @param boolean aState
   *        True if the filter named by @aMessageType is being turned on; false
   *        otherwise.
   * @returns void
   */
  adjustVisibilityForMessageType:
  function HS_adjustVisibilityForMessageType(aHUDId, aPrefKey, aState)
  {
    let outputNode = this.getHudReferenceById(aHUDId).outputNode;
    let doc = outputNode.ownerDocument;

    // Look for message nodes ("hud-msg-node") with the given preference key
    // ("hud-msg-error", "hud-msg-cssparser", etc.) and add or remove the
    // "hud-filtered-by-type" class, which turns on or off the display.

    let xpath = ".//*[contains(@class, 'hud-msg-node') and " +
      "contains(concat(@class, ' '), 'hud-" + aPrefKey + " ')]";
    let result = doc.evaluate(xpath, outputNode, null,
      Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
    for (let i = 0; i < result.snapshotLength; i++) {
      let node = result.snapshotItem(i);
      if (aState) {
        node.classList.remove("hud-filtered-by-type");
      }
      else {
        node.classList.add("hud-filtered-by-type");
      }
    }

    this.regroupOutput(outputNode);
  },

  /**
   * Check that the passed string matches the filter arguments.
   *
   * @param String aString
   *        to search for filter words in.
   * @param String aFilter
   *        is a string containing all of the words to filter on.
   * @returns boolean
   */
  stringMatchesFilters: function stringMatchesFilters(aString, aFilter)
  {
    if (!aFilter || !aString) {
      return true;
    }

    let searchStr = aString.toLowerCase();
    let filterStrings = aFilter.toLowerCase().split(/\s+/);
    return !filterStrings.some(function (f) {
      return searchStr.indexOf(f) == -1;
    });
  },

  /**
   * Turns the display of log nodes on and off appropriately to reflect the
   * adjustment of the search string.
   *
   * @param string aHUDId
   *        The ID of the HUD to alter.
   * @param string aSearchString
   *        The new search string.
   * @returns void
   */
  adjustVisibilityOnSearchStringChange:
  function HS_adjustVisibilityOnSearchStringChange(aHUDId, aSearchString)
  {
    let outputNode = this.getHudReferenceById(aHUDId).outputNode;

    let nodes = outputNode.querySelectorAll(".hud-msg-node");

    for (let i = 0; i < nodes.length; ++i) {
      let node = nodes[i];

      // hide nodes that match the strings
      let text = node.clipboardText;

      // if the text matches the words in aSearchString...
      if (this.stringMatchesFilters(text, aSearchString)) {
        node.classList.remove("hud-filtered-by-string");
      }
      else {
        node.classList.add("hud-filtered-by-string");
      }
    }

    this.regroupOutput(outputNode);
  },

  /**
   * Register a reference of each HeadsUpDisplay that is created
   */
  registerHUDReference:
  function HS_registerHUDReference(aHUD)
  {
    this.hudReferences[aHUD.hudId] = aHUD;

    let id = ConsoleUtils.supString(aHUD.hudId);
    Services.obs.notifyObservers(id, "web-console-created", null);
  },

  /**
   * Register a new Heads Up Display
   *
   * @returns void
   */
  registerDisplay: function HS_registerDisplay(aHUDId)
  {
    // register a display DOM node Id with the service.
    if (!aHUDId){
      throw new Error(ERRORS.MISSING_ARGS);
    }
    this.filterPrefs[aHUDId] = {
      network: Services.prefs.getBoolPref(PREFS_PREFIX + "network"),
      networkinfo: Services.prefs.getBoolPref(PREFS_PREFIX + "networkinfo"),
      csserror: Services.prefs.getBoolPref(PREFS_PREFIX + "csserror"),
      cssparser: Services.prefs.getBoolPref(PREFS_PREFIX + "cssparser"),
      exception: Services.prefs.getBoolPref(PREFS_PREFIX + "exception"),
      jswarn: Services.prefs.getBoolPref(PREFS_PREFIX + "jswarn"),
      error: Services.prefs.getBoolPref(PREFS_PREFIX + "error"),
      info: Services.prefs.getBoolPref(PREFS_PREFIX + "info"),
      warn: Services.prefs.getBoolPref(PREFS_PREFIX + "warn"),
      log: Services.prefs.getBoolPref(PREFS_PREFIX + "log"),
    };
  },

  /**
   * When a display is being destroyed, unregister it first
   *
   * @param string aHUDId
   *        The ID of a HUD.
   * @returns void
   */
  unregisterDisplay: function HS_unregisterDisplay(aHUDId)
  {
    let hud = this.getHudReferenceById(aHUDId);

    // Remove children from the output. If the output is not cleared, there can
    // be leaks as some nodes has node.onclick = function; set and GC can't
    // remove the nodes then.
    if (hud.jsterm) {
      hud.jsterm.clearOutput();
    }
    if (hud.gcliterm) {
      hud.gcliterm.clearOutput();
    }

    hud.destroy();

    // Make sure that the console panel does not try to call
    // deactivateHUDForContext() again.
    hud.consoleWindowUnregisterOnHide = false;

    // Remove the HUDBox and the consolePanel if the Web Console is inside a
    // floating panel.
    if (hud.consolePanel && hud.consolePanel.parentNode) {
      hud.consolePanel.parentNode.removeChild(hud.consolePanel);
      hud.consolePanel.removeAttribute("hudId");
      hud.consolePanel = null;
    }

    hud.HUDBox.parentNode.removeChild(hud.HUDBox);

    if (hud.splitter.parentNode) {
      hud.splitter.parentNode.removeChild(hud.splitter);
    }

    if (hud.jsterm) {
      hud.jsterm.autocompletePopup.destroy();
    }

    delete this.hudReferences[aHUDId];

    for (let windowID in this.windowIds) {
      if (this.windowIds[windowID] == aHUDId) {
        delete this.windowIds[windowID];
      }
    }

    this.unregisterActiveContext(aHUDId);

    let popupset = hud.chromeDocument.getElementById("mainPopupSet");
    let panels = popupset.querySelectorAll("panel[hudId=" + aHUDId + "]");
    for (let i = 0; i < panels.length; i++) {
      panels[i].hidePopup();
    }

    let id = ConsoleUtils.supString(aHUDId);
    Services.obs.notifyObservers(id, "web-console-destroyed", null);

    if (Object.keys(this.hudReferences).length == 0) {
      let autocompletePopup = hud.chromeDocument.
                              getElementById("webConsole_autocompletePopup");
      if (autocompletePopup) {
        autocompletePopup.parentNode.removeChild(autocompletePopup);
      }

      this.suspend();
    }
  },

  /**
   * "Wake up" the Web Console activity. This is called when the first Web
   * Console is open. This method initializes the various observers we have.
   *
   * @returns void
   */
  wakeup: function HS_wakeup()
  {
    if (Object.keys(this.hudReferences).length > 0) {
      return;
    }

    // begin observing HTTP traffic
    this.startHTTPObservation();

    HUDWindowObserver.init();
    HUDConsoleObserver.init();
    ConsoleAPIObserver.init();
  },

  /**
   * Suspend Web Console activity. This is called when all Web Consoles are
   * closed.
   *
   * @returns void
   */
  suspend: function HS_suspend()
  {
    activityDistributor.removeObserver(this.httpObserver);
    delete this.httpObserver;

    Services.obs.removeObserver(this.httpResponseExaminer,
                                "http-on-examine-response");

    this.openRequests = {};
    this.openResponseHeaders = {};

    delete this.defaultFilterPrefs;

    delete this.lastFinishedRequestCallback;

    HUDWindowObserver.uninit();
    HUDConsoleObserver.uninit();
    ConsoleAPIObserver.shutdown();
  },

  /**
   * Shutdown all HeadsUpDisplays on xpcom-shutdown
   *
   * @returns void
   */
  shutdown: function HS_shutdown()
  {
    for (let hudId in this.hudReferences) {
      this.deactivateHUDForContext(this.hudReferences[hudId].tab, false);
    }
  },

  /**
   * Returns the HeadsUpDisplay object associated to a content window.
   *
   * @param nsIDOMWindow aContentWindow
   * @returns object
   */
  getHudByWindow: function HS_getHudByWindow(aContentWindow)
  {
    let hudId = this.getHudIdByWindow(aContentWindow);
    return hudId ? this.hudReferences[hudId] : null;
  },

  /**
   * Returns the hudId that is corresponding to the hud activated for the
   * passed aContentWindow. If there is no matching hudId null is returned.
   *
   * @param nsIDOMWindow aContentWindow
   * @returns string or null
   */
  getHudIdByWindow: function HS_getHudIdByWindow(aContentWindow)
  {
    let windowId = this.getWindowId(aContentWindow);
    return this.getHudIdByWindowId(windowId);
  },

  /**
   * Returns the hudReference for a given id.
   *
   * @param string aId
   * @returns Object
   */
  getHudReferenceById: function HS_getHudReferenceById(aId)
  {
    return aId in this.hudReferences ? this.hudReferences[aId] : null;
  },

  /**
   * Returns the hudId that is corresponding to the given outer window ID.
   *
   * @param number aWindowId
   *        the outer window ID
   * @returns string the hudId
   */
  getHudIdByWindowId: function HS_getHudIdByWindowId(aWindowId)
  {
    return this.windowIds[aWindowId];
  },

  /**
   * Get the current filter string for the HeadsUpDisplay
   *
   * @param string aHUDId
   * @returns string
   */
  getFilterStringByHUDId: function HS_getFilterStringbyHUDId(aHUDId) {
    return this.getHudReferenceById(aHUDId).filterBox.value;
  },

  /**
   * Update the filter text in the internal tracking object for all
   * filter strings
   *
   * @param nsIDOMNode aTextBoxNode
   * @returns void
   */
  updateFilterText: function HS_updateFiltertext(aTextBoxNode)
  {
    var hudId = aTextBoxNode.getAttribute("hudId");
    this.adjustVisibilityOnSearchStringChange(hudId, aTextBoxNode.value);
  },

  /**
   * Logs a message to the Web Console that originates from the window.console
   * service ("console-api-log-event" notifications).
   *
   * @param string aHUDId
   *        The ID of the Web Console to which to send the message.
   * @param object aMessage
   *        The message reported by the console service.
   * @return void
   */
  logConsoleAPIMessage: function HS_logConsoleAPIMessage(aHUDId, aMessage)
  {
    // Pipe the message to createMessageNode().
    let hud = HUDService.hudReferences[aHUDId];
    function formatResult(x) {
      if (typeof(x) == "string") {
        return x;
      }
      if (hud.gcliterm) {
        return hud.gcliterm.formatResult(x);
      }
      if (hud.jsterm) {
        return hud.jsterm.formatResult(x);
      }
      return x;
    }

    let body = null;
    let clipboardText = null;
    let sourceURL = null;
    let sourceLine = 0;
    let level = aMessage.level;
    let args = aMessage.arguments;

    switch (level) {
      case "log":
      case "info":
      case "warn":
      case "error":
      case "debug":
        let mappedArguments = Array.map(args, formatResult);
        body = Array.join(mappedArguments, " ");
        sourceURL = aMessage.filename;
        sourceLine = aMessage.lineNumber;
        break;

      case "trace":
        let filename = ConsoleUtils.abbreviateSourceURL(args[0].filename);
        let functionName = args[0].functionName ||
                           this.getStr("stacktrace.anonymousFunction");
        let lineNumber = args[0].lineNumber;

        body = this.getFormatStr("stacktrace.outputMessage",
                                 [filename, functionName, lineNumber]);

        sourceURL = args[0].filename;
        sourceLine = args[0].lineNumber;

        clipboardText = "";

        args.forEach(function(aFrame) {
          clipboardText += aFrame.filename + " :: " +
                           aFrame.functionName + " :: " +
                           aFrame.lineNumber + "\n";
        });

        clipboardText = clipboardText.trimRight();
        break;

      case "dir":
        body = unwrap(args[0]);
        clipboardText = body.toString();
        sourceURL = aMessage.filename;
        sourceLine = aMessage.lineNumber;
        break;

      case "group":
      case "groupCollapsed":
        clipboardText = body = formatResult(args);
        sourceURL = aMessage.filename;
        sourceLine = aMessage.lineNumber;
        hud.groupDepth++;
        break;

      case "groupEnd":
        if (hud.groupDepth > 0) {
          hud.groupDepth--;
        }
        return;

      case "time":
        if (!args) {
          return;
        }
        if (args.error) {
          Cu.reportError(this.getStr(args.error));
          return;
        }
        body = this.getFormatStr("timerStarted", [args.name]);
        clipboardText = body;
        sourceURL = aMessage.filename;
        sourceLine = aMessage.lineNumber;
        break;

      case "timeEnd":
        if (!args) {
          return;
        }
        body = this.getFormatStr("timeEnd", [args.name, args.duration]);
        clipboardText = body;
        sourceURL = aMessage.filename;
        sourceLine = aMessage.lineNumber;
        break;

      default:
        Cu.reportError("Unknown Console API log level: " + level);
        return;
    }

    let node = ConsoleUtils.createMessageNode(hud.outputNode.ownerDocument,
                                              CATEGORY_WEBDEV,
                                              LEVELS[level],
                                              body,
                                              aHUDId,
                                              sourceURL,
                                              sourceLine,
                                              clipboardText,
                                              level,
                                              aMessage.timeStamp);

    // Make the node bring up the property panel, to allow the user to inspect
    // the stack trace.
    if (level == "trace") {
      node._stacktrace = args;

      let linkNode = node.querySelector(".webconsole-msg-body");
      linkNode.classList.add("hud-clickable");
      linkNode.setAttribute("aria-haspopup", "true");

      node.addEventListener("mousedown", function(aEvent) {
        this._startX = aEvent.clientX;
        this._startY = aEvent.clientY;
      }, false);

      node.addEventListener("click", function(aEvent) {
        if (aEvent.detail != 1 || aEvent.button != 0 ||
            (this._startX != aEvent.clientX &&
             this._startY != aEvent.clientY)) {
          return;
        }

        if (!this._panelOpen) {
          let propPanel = hud.jsterm.openPropertyPanel(null,
                                                       node._stacktrace,
                                                       this);
          propPanel.panel.setAttribute("hudId", aHUDId);
          this._panelOpen = true;
        }
      }, false);
    }

    ConsoleUtils.outputMessageNode(node, aHUDId);

    if (level == "dir") {
      // Initialize the inspector message node, by setting the PropertyTreeView
      // object on the tree view. This has to be done *after* the node is
      // shown, because the tree binding must be attached first.
      let tree = node.querySelector("tree");
      tree.view = node.propertyTreeView;
    }
  },

  /**
   * Inform user that the Web Console API has been replaced by a script
   * in a content page.
   *
   * @param string aHUDId
   *        The ID of the Web Console to which to send the message.
   * @return void
   */
  logWarningAboutReplacedAPI:
  function HS_logWarningAboutReplacedAPI(aHUDId)
  {
    let hud = this.hudReferences[aHUDId];
    let chromeDocument = hud.HUDBox.ownerDocument;
    let message = stringBundle.GetStringFromName("ConsoleAPIDisabled");
    let node = ConsoleUtils.createMessageNode(chromeDocument, CATEGORY_JS,
                                              SEVERITY_WARNING, message,
                                              aHUDId);
    ConsoleUtils.outputMessageNode(node, aHUDId);
  },

  /**
   * Reports an error in the page source, either JavaScript or CSS.
   *
   * @param nsIScriptError aScriptError
   *        The error message to report.
   * @return void
   */
  reportPageError: function HS_reportPageError(aScriptError)
  {
    if (!aScriptError.outerWindowID) {
      return;
    }

    let category;

    switch (aScriptError.category) {
      // We ignore chrome-originating errors as we only care about content.
      case "XPConnect JavaScript":
      case "component javascript":
      case "chrome javascript":
      case "chrome registration":
      case "XBL":
      case "XBL Prototype Handler":
      case "XBL Content Sink":
      case "xbl javascript":
        return;

      case "CSS Parser":
      case "CSS Loader":
        category = CATEGORY_CSS;
        break;

      default:
        category = CATEGORY_JS;
        break;
    }

    // Warnings and legacy strict errors become warnings; other types become
    // errors.
    let severity = SEVERITY_ERROR;
    if ((aScriptError.flags & aScriptError.warningFlag) ||
        (aScriptError.flags & aScriptError.strictFlag)) {
      severity = SEVERITY_WARNING;
    }

    let window = HUDService.getWindowByWindowId(aScriptError.outerWindowID);
    if (window) {
      let hudId = HUDService.getHudIdByWindow(window.top);
      if (hudId) {
        let outputNode = this.hudReferences[hudId].outputNode;
        let chromeDocument = outputNode.ownerDocument;

        let node = ConsoleUtils.createMessageNode(chromeDocument,
                                                  category,
                                                  severity,
                                                  aScriptError.errorMessage,
                                                  hudId,
                                                  aScriptError.sourceName,
                                                  aScriptError.lineNumber,
                                                  null, null,
                                                  aScriptError.timeStamp);

        ConsoleUtils.outputMessageNode(node, hudId);
      }
    }
  },

  /**
   * Register a Gecko app's specialized ApplicationHooks object
   *
   * @returns void or throws "UNSUPPORTED APPLICATION" error
   */
  registerApplicationHooks:
  function HS_registerApplications(aAppName, aHooksObject)
  {
    switch(aAppName) {
      case "FIREFOX":
        this.applicationHooks = aHooksObject;
        return;
      default:
        throw new Error("MOZ APPLICATION UNSUPPORTED");
    }
  },

  /**
   * Registry of ApplicationHooks used by specified Gecko Apps
   *
   * @returns Specific Gecko 'ApplicationHooks' Object/Mixin
   */
  applicationHooks: null,

  /**
   * Assign a function to this property to listen for finished httpRequests.
   * Used by unit tests.
   */
  lastFinishedRequestCallback: null,

  /**
   * Opens a NetworkPanel.
   *
   * @param nsIDOMNode aNode
   *        DOMNode to display the panel next to.
   * @param object aHttpActivity
   *        httpActivity object. The data of this object is displayed in the
   *        NetworkPanel.
   * @returns NetworkPanel
   */
  openNetworkPanel: function HS_openNetworkPanel(aNode, aHttpActivity)
  {
    let doc = aNode.ownerDocument;
    let parent = doc.getElementById("mainPopupSet");
    let netPanel = new NetworkPanel(parent, aHttpActivity);
    netPanel.linkNode = aNode;

    let panel = netPanel.panel;
    panel.openPopup(aNode, "after_pointer", 0, 0, false, false);
    panel.sizeTo(450, 500);
    panel.setAttribute("hudId", aHttpActivity.hudId);
    aHttpActivity.panels.push(Cu.getWeakReference(netPanel));
    return netPanel;
  },

  /**
   * Begin observing HTTP traffic that we care about,
   * namely traffic that originates inside any context that a Heads Up Display
   * is active for.
   */
  startHTTPObservation: function HS_httpObserverFactory()
  {
    // creates an observer for http traffic
    var self = this;
    var httpObserver = {
      observeActivity :
      function HS_SHO_observeActivity(aChannel,
                                      aActivityType,
                                      aActivitySubtype,
                                      aTimestamp,
                                      aExtraSizeData,
                                      aExtraStringData)
      {
        if (aActivityType ==
              activityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION ||
            aActivityType ==
              activityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) {

          aChannel = aChannel.QueryInterface(Ci.nsIHttpChannel);

          let transCodes = this.httpTransactionCodes;
          let hudId;

          if (aActivitySubtype ==
              activityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER ) {
            // Try to get the source window of the request.
            let win = NetworkHelper.getWindowForRequest(aChannel);
            if (!win) {
              return;
            }

            // Try to get the hudId that is associated to the window.
            hudId = self.getHudIdByWindow(win.top);
            if (!hudId) {
              return;
            }

            // The httpActivity object will hold all information concerning
            // this request and later response.

            let httpActivity = {
              id: self.sequenceId(),
              hudId: hudId,
              url: aChannel.URI.spec,
              method: aChannel.requestMethod,
              channel: aChannel,
              charset: win.document.characterSet,

              panels: [],
              request: {
                header: { }
              },
              response: {
                header: null
              },
              timing: {
                "REQUEST_HEADER": aTimestamp
              }
            };

            // Add a new output entry.
            let loggedNode = self.logNetActivity(httpActivity);

            // In some cases loggedNode can be undefined (e.g. if an image was
            // requested). Don't continue in such a case.
            if (!loggedNode) {
              return;
            }

            aChannel.QueryInterface(Ci.nsITraceableChannel);

            // The response will be written into the outputStream of this pipe.
            // This allows us to buffer the data we are receiving and read it
            // asynchronously.
            // Both ends of the pipe must be blocking.
            let sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);

            // The streams need to be blocking because this is required by the
            // stream tee.
            sink.init(false, false, HUDService.responsePipeSegmentSize,
                      PR_UINT32_MAX, null);

            // Add listener for the response body.
            let newListener = new ResponseListener(httpActivity);

            // Remember the input stream, so it isn't released by GC.
            newListener.inputStream = sink.inputStream;
            newListener.sink = sink;

            let tee = Cc["@mozilla.org/network/stream-listener-tee;1"].
                      createInstance(Ci.nsIStreamListenerTee);

            let originalListener = aChannel.setNewListener(tee);

            tee.init(originalListener, sink.outputStream, newListener);

            // Copy the request header data.
            aChannel.visitRequestHeaders({
              visitHeader: function(aName, aValue) {
                httpActivity.request.header[aName] = aValue;
              }
            });

            // Store the loggedNode and the httpActivity object for later reuse.
            let linkNode = loggedNode.querySelector(".webconsole-msg-link");

            httpActivity.messageObject = {
              messageNode: loggedNode,
              linkNode:    linkNode
            };
            self.openRequests[httpActivity.id] = httpActivity;

            // Make the network span clickable.
            linkNode.setAttribute("aria-haspopup", "true");
            linkNode.addEventListener("mousedown", function(aEvent) {
              this._startX = aEvent.clientX;
              this._startY = aEvent.clientY;
            }, false);

            linkNode.addEventListener("click", function(aEvent) {
              if (aEvent.detail != 1 || aEvent.button != 0 ||
                  (this._startX != aEvent.clientX &&
                   this._startY != aEvent.clientY)) {
                return;
              }

              if (!this._panelOpen) {
                self.openNetworkPanel(this, httpActivity);
                this._panelOpen = true;
              }
            }, false);
          }
          else {
            // Iterate over all currently ongoing requests. If aChannel can't
            // be found within them, then exit this function.
            let httpActivity = null;
            for each (let item in self.openRequests) {
              if (item.channel !== aChannel) {
                continue;
              }
              httpActivity = item;
              break;
            }

            if (!httpActivity) {
              return;
            }

            hudId = httpActivity.hudId;
            let msgObject = httpActivity.messageObject;

            let updatePanel = false;
            let data;
            // Store the time information for this activity subtype.
            httpActivity.timing[transCodes[aActivitySubtype]] = aTimestamp;

            switch (aActivitySubtype) {
              case activityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: {
                if (!self.saveRequestAndResponseBodies) {
                  httpActivity.request.bodyDiscarded = true;
                  break;
                }

                let gBrowser = msgObject.messageNode.ownerDocument.
                               defaultView.gBrowser;
                let HUD = HUDService.hudReferences[hudId];
                let browser = gBrowser.
                              getBrowserForDocument(HUD.contentDocument);

                let sentBody = NetworkHelper.
                               readPostTextFromRequest(aChannel, browser);
                if (!sentBody) {
                  // If the request URL is the same as the current page url, then
                  // we can try to get the posted text from the page directly.
                  // This check is necessary as otherwise the
                  //   NetworkHelper.readPostTextFromPage
                  // function is called for image requests as well but these
                  // are not web pages and as such don't store the posted text
                  // in the cache of the webpage.
                  if (httpActivity.url == browser.contentWindow.location.href) {
                    sentBody = NetworkHelper.readPostTextFromPage(browser);
                  }
                  if (!sentBody) {
                    sentBody = "";
                  }
                }
                httpActivity.request.body = sentBody;
                break;
              }

              case activityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: {
                // aExtraStringData contains the response header. The first line
                // contains the response status (e.g. HTTP/1.1 200 OK).
                //
                // Note: The response header is not saved here. Calling the
                //       aChannel.visitResponseHeaders at this point sometimes
                //       causes an NS_ERROR_NOT_AVAILABLE exception. Therefore,
                //       the response header and response body is stored on the
                //       httpActivity object within the RL_onStopRequest function.
                httpActivity.response.status =
                  aExtraStringData.split(/\r\n|\n|\r/)[0];

                // Add the response status.
                let linkNode = msgObject.linkNode;
                let statusNode = linkNode.
                  querySelector(".webconsole-msg-status");
                let statusText = "[" + httpActivity.response.status + "]";
                statusNode.setAttribute("value", statusText);

                let clipboardTextPieces =
                  [ httpActivity.method, httpActivity.url, statusText ];
                msgObject.messageNode.clipboardText =
                  clipboardTextPieces.join(" ");

                let status = parseInt(httpActivity.response.status.
                  replace(/^HTTP\/\d\.\d (\d+).+$/, "$1"));

                if (status >= MIN_HTTP_ERROR_CODE &&
                    status < MAX_HTTP_ERROR_CODE) {
                  ConsoleUtils.setMessageType(msgObject.messageNode,
                                              CATEGORY_NETWORK,
                                              SEVERITY_ERROR);
                }

                break;
              }

              case activityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: {
                let timing = httpActivity.timing;
                let requestDuration =
                  Math.round((timing.RESPONSE_COMPLETE -
                                timing.REQUEST_HEADER) / 1000);

                // Add the request duration.
                let linkNode = msgObject.linkNode;
                let statusNode = linkNode.
                  querySelector(".webconsole-msg-status");

                let statusText = httpActivity.response.status;
                let timeText = self.getFormatStr("NetworkPanel.durationMS",
                                                 [ requestDuration ]);
                let fullStatusText = "[" + statusText + " " + timeText + "]";
                statusNode.setAttribute("value", fullStatusText);

                let clipboardTextPieces =
                  [ httpActivity.method, httpActivity.url, fullStatusText ];
                msgObject.messageNode.clipboardText =
                  clipboardTextPieces.join(" ");

                delete httpActivity.messageObject;
                delete self.openRequests[httpActivity.id];
                updatePanel = true;
                break;
              }
            }

            if (updatePanel) {
              httpActivity.panels.forEach(function(weakRef) {
                let panel = weakRef.get();
                if (panel) {
                  panel.update();
                }
              });
            }
          }
        }
      },

      httpTransactionCodes: {
        0x5001: "REQUEST_HEADER",
        0x5002: "REQUEST_BODY_SENT",
        0x5003: "RESPONSE_START",
        0x5004: "RESPONSE_HEADER",
        0x5005: "RESPONSE_COMPLETE",
        0x5006: "TRANSACTION_CLOSE",

        0x804b0003: "STATUS_RESOLVING",
        0x804b000b: "STATUS_RESOLVED",
        0x804b0007: "STATUS_CONNECTING_TO",
        0x804b0004: "STATUS_CONNECTED_TO",
        0x804b0005: "STATUS_SENDING_TO",
        0x804b000a: "STATUS_WAITING_FOR",
        0x804b0006: "STATUS_RECEIVING_FROM"
      }
    };

    this.httpObserver = httpObserver;

    activityDistributor.addObserver(httpObserver);

    // This is used to find the correct HTTP response headers.
    Services.obs.addObserver(this.httpResponseExaminer,
                             "http-on-examine-response", false);
  },

  /**
   * Observe notifications for the http-on-examine-response topic, coming from
   * the nsIObserver service.
   *
   * @param string aTopic
   * @param nsIHttpChannel aSubject
   * @returns void
   */
  httpResponseExaminer: function HS_httpResponseExaminer(aSubject, aTopic)
  {
    if (aTopic != "http-on-examine-response" ||
        !(aSubject instanceof Ci.nsIHttpChannel)) {
      return;
    }

    let channel = aSubject.QueryInterface(Ci.nsIHttpChannel);
    let win = NetworkHelper.getWindowForRequest(channel);
    if (!win) {
      return;
    }
    let hudId = HUDService.getHudIdByWindow(win);
    if (!hudId) {
      return;
    }

    let response = {
      id: HUDService.sequenceId(),
      hudId: hudId,
      channel: channel,
      headers: {},
    };

    try {
      channel.visitResponseHeaders({
        visitHeader: function(aName, aValue) {
          response.headers[aName] = aValue;
        }
      });
    }
    catch (ex) {
      delete response.headers;
    }

    if (response.headers) {
      HUDService.openResponseHeaders[response.id] = response;
    }
  },

  /**
   * Logs network activity.
   *
   * @param object aActivityObject
   *        The activity to log.
   * @returns void
   */
  logNetActivity: function HS_logNetActivity(aActivityObject)
  {
    let hudId = aActivityObject.hudId;
    let outputNode = this.hudReferences[hudId].outputNode;

    let chromeDocument = outputNode.ownerDocument;
    let msgNode = chromeDocument.createElementNS(XUL_NS, "hbox");

    let methodNode = chromeDocument.createElementNS(XUL_NS, "label");
    methodNode.setAttribute("value", aActivityObject.method);
    methodNode.classList.add("webconsole-msg-body-piece");
    msgNode.appendChild(methodNode);

    let linkNode = chromeDocument.createElementNS(XUL_NS, "hbox");
    linkNode.setAttribute("flex", "1");
    linkNode.classList.add("webconsole-msg-body-piece");
    linkNode.classList.add("webconsole-msg-link");
    msgNode.appendChild(linkNode);

    let urlNode = chromeDocument.createElementNS(XUL_NS, "label");
    urlNode.setAttribute("crop", "center");
    urlNode.setAttribute("flex", "1");
    urlNode.setAttribute("title", aActivityObject.url);
    urlNode.setAttribute("value", aActivityObject.url);
    urlNode.classList.add("hud-clickable");
    urlNode.classList.add("webconsole-msg-body-piece");
    urlNode.classList.add("webconsole-msg-url");
    linkNode.appendChild(urlNode);

    let statusNode = chromeDocument.createElementNS(XUL_NS, "label");
    statusNode.setAttribute("value", "");
    statusNode.classList.add("hud-clickable");
    statusNode.classList.add("webconsole-msg-body-piece");
    statusNode.classList.add("webconsole-msg-status");
    linkNode.appendChild(statusNode);

    let clipboardText = aActivityObject.method + " " + aActivityObject.url;

    let messageNode = ConsoleUtils.createMessageNode(chromeDocument,
                                                     CATEGORY_NETWORK,
                                                     SEVERITY_LOG,
                                                     msgNode,
                                                     hudId,
                                                     null,
                                                     null,
                                                     clipboardText);

    ConsoleUtils.outputMessageNode(messageNode, aActivityObject.hudId);
    return messageNode;
  },

  /**
   * Initialize the JSTerm object to create a JS Workspace by attaching the UI
   * into the given parent node, using the mixin.
   *
   * @param nsIDOMWindow aContext the context used for evaluating user input
   * @param nsIDOMNode aParentNode where to attach the JSTerm
   * @param object aConsole
   *        Console object used within the JSTerm instance to report errors
   *        and log data (by calling console.error(), console.log(), etc).
   */
  initializeJSTerm: function HS_initializeJSTerm(aContext, aParentNode, aConsole)
  {
    // create Initial JS Workspace:
    var context = Cu.getWeakReference(aContext);

    // Attach the UI into the target parent node using the mixin.
    var firefoxMixin = new JSTermFirefoxMixin(context, aParentNode);
    var jsTerm = new JSTerm(context, aParentNode, firefoxMixin, aConsole);

    // TODO: injection of additional functionality needs re-thinking/api
    // see bug 559748
  },

  /**
   * Creates a generator that always returns a unique number for use in the
   * indexes
   *
   * @returns Generator
   */
  createSequencer: function HS_createSequencer(aInt)
  {
    function sequencer(aInt)
    {
      while(1) {
        aInt++;
        yield aInt;
      }
    }
    return sequencer(aInt);
  },

  // See jsapi.h (JSErrorReport flags):
  // http://mxr.mozilla.org/mozilla-central/source/js/src/jsapi.h#3429
  scriptErrorFlags: {
    0: "error", // JSREPORT_ERROR
    1: "warn", // JSREPORT_WARNING
    2: "exception", // JSREPORT_EXCEPTION
    4: "error", // JSREPORT_STRICT | JSREPORT_ERROR
    5: "warn", // JSREPORT_STRICT | JSREPORT_WARNING
    8: "error", // JSREPORT_STRICT_MODE_ERROR
    13: "warn", // JSREPORT_STRICT_MODE_ERROR | JSREPORT_WARNING | JSREPORT_ERROR
  },

  /**
   * replacement strings (L10N)
   */
  scriptMsgLogLevel: {
    0: "typeError", // JSREPORT_ERROR
    1: "typeWarning", // JSREPORT_WARNING
    2: "typeException", // JSREPORT_EXCEPTION
    4: "typeError", // JSREPORT_STRICT | JSREPORT_ERROR
    5: "typeStrict", // JSREPORT_STRICT | JSREPORT_WARNING
    8: "typeError", // JSREPORT_STRICT_MODE_ERROR
    13: "typeWarning", // JSREPORT_STRICT_MODE_ERROR | JSREPORT_WARNING | JSREPORT_ERROR
  },

  /**
   * onTabClose event handler function
   *
   * @param aEvent
   * @returns void
   */
  onTabClose: function HS_onTabClose(aEvent)
  {
    this.deactivateHUDForContext(aEvent.target, false);
  },

  /**
   * Called whenever a browser window closes. Cleans up any consoles still
   * around.
   *
   * @param nsIDOMEvent aEvent
   *        The dispatched event.
   * @returns void
   */
  onWindowUnload: function HS_onWindowUnload(aEvent)
  {
    let window = aEvent.target.defaultView;

    window.removeEventListener("unload", this.onWindowUnload, false);

    let gBrowser = window.gBrowser;
    let tabContainer = gBrowser.tabContainer;

    tabContainer.removeEventListener("TabClose", this.onTabClose, false);

    let tab = tabContainer.firstChild;
    while (tab != null) {
      this.deactivateHUDForContext(tab, false);
      tab = tab.nextSibling;
    }

    if (window.webConsoleCommandController) {
      window.controllers.removeController(window.webConsoleCommandController);
      window.webConsoleCommandController = null;
    }
  },

  /**
   * windowInitializer - checks what Gecko app is running and inits the HUD
   *
   * @param nsIDOMWindow aContentWindow
   * @returns void
   */
  windowInitializer: function HS_WindowInitalizer(aContentWindow)
  {
    var xulWindow = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebNavigation)
                      .QueryInterface(Ci.nsIDocShell)
                      .chromeEventHandler.ownerDocument.defaultView;

    let xulWindow = unwrap(xulWindow);

    let docElem = xulWindow.document.documentElement;
    if (!docElem || docElem.getAttribute("windowtype") != "navigator:browser" ||
        !xulWindow.gBrowser) {
      // Do not do anything unless we have a browser window.
      // This may be a view-source window or other type of non-browser window.
      return;
    }

    let gBrowser = xulWindow.gBrowser;

    let _browser = gBrowser.
      getBrowserForDocument(aContentWindow.top.document);

    // ignore newly created documents that don't belong to a tab's browser
    if (!_browser) {
      return;
    }

    let nBox = gBrowser.getNotificationBox(_browser);
    let nBoxId = nBox.getAttribute("id");
    let hudId = "hud_" + nBoxId;
    let windowUI = nBox.ownerDocument.getElementById("console_window_" + hudId);
    if (windowUI) {
      // The Web Console popup is already open, no need to continue.
      if (aContentWindow == aContentWindow.top) {
        let hud = this.hudReferences[hudId];
        hud.reattachConsole(aContentWindow);
      }
      return;
    }

    if (!this.canActivateContext(hudId)) {
      return;
    }

    xulWindow.addEventListener("unload", this.onWindowUnload, false);
    gBrowser.tabContainer.addEventListener("TabClose", this.onTabClose, false);

    this.registerDisplay(hudId);

    let hudNode;
    let childNodes = nBox.childNodes;

    for (let i = 0; i < childNodes.length; i++) {
      let id = childNodes[i].getAttribute("id");
      // `id` is a string with the format "hud_<number>".
      if (id.split("_")[0] == "hud") {
        hudNode = childNodes[i];
        break;
      }
    }

    let hud;
    // If there is no HUD for this tab create a new one.
    if (!hudNode) {
      // get nBox object and call new HUD
      let config = { parentNode: nBox,
                     contentWindow: aContentWindow.top
                   };

      hud = new HeadsUpDisplay(config);

      HUDService.registerHUDReference(hud);
      let windowId = this.getWindowId(aContentWindow.top);
      this.windowIds[windowId] = hudId;

      hud.progressListener = new ConsoleProgressListener(hudId);

      _browser.webProgress.addProgressListener(hud.progressListener,
        Ci.nsIWebProgress.NOTIFY_STATE_ALL);

      hud.displayCachedConsoleMessages();
    }
    else {
      hud = this.hudReferences[hudId];
      if (aContentWindow == aContentWindow.top) {
        // TODO: name change?? doesn't actually re-attach the console
        hud.reattachConsole(aContentWindow);
      }
    }

    // Need to detect that the console component has been paved over.
    let consoleObject = unwrap(aContentWindow).console;
    if (!("__mozillaConsole__" in consoleObject))
      this.logWarningAboutReplacedAPI(hudId);

    // register the controller to handle "select all" properly
    this.createController(xulWindow);
  },

  /**
   * Adds the command controller to the XUL window if it's not already present.
   *
   * @param nsIDOMWindow aWindow
   *        The browser XUL window.
   * @returns void
   */
  createController: function HUD_createController(aWindow)
  {
    if (aWindow.webConsoleCommandController == null) {
      aWindow.webConsoleCommandController = new CommandController(aWindow);
      aWindow.controllers.insertControllerAt(0,
        aWindow.webConsoleCommandController);
    }
  },

  /**
   * Animates the Console appropriately.
   *
   * @param string aHUDId The ID of the console.
   * @param string aDirection Whether to animate the console appearing
   *        (ANIMATE_IN) or disappearing (ANIMATE_OUT).
   * @param function aCallback An optional callback, which will be called with
   *        the "transitionend" event passed as a parameter once the animation
   *        finishes.
   */
  animate: function HS_animate(aHUDId, aDirection, aCallback)
  {
    let hudBox = this.getHudReferenceById(aHUDId).HUDBox;
    if (!hudBox.classList.contains("animated")) {
      if (aCallback) {
        aCallback();
      }
      return;
    }

    switch (aDirection) {
      case ANIMATE_OUT:
        hudBox.style.height = 0;
        break;
      case ANIMATE_IN:
        this.resetHeight(aHUDId);
        break;
    }

    if (aCallback) {
      hudBox.addEventListener("transitionend", aCallback, false);
    }
  },

  /**
   * Disables all animation for a console, for unit testing. After this call,
   * the console will instantly take on a reasonable height, and the close
   * animation will not occur.
   *
   * @param string aHUDId The ID of the console.
   */
  disableAnimation: function HS_disableAnimation(aHUDId)
  {
    let hudBox = HUDService.hudReferences[aHUDId].HUDBox;
    if (hudBox.classList.contains("animated")) {
      hudBox.classList.remove("animated");
      this.resetHeight(aHUDId);
    }
  },

  /**
   * Reset the height of the Web Console.
   *
   * @param string aHUDId The ID of the Web Console.
   */
  resetHeight: function HS_resetHeight(aHUDId)
  {
    let HUD = this.hudReferences[aHUDId];
    let innerHeight = HUD.contentWindow.innerHeight;
    let chromeWindow = HUD.chromeDocument.defaultView;
    if (!HUD.consolePanel) {
      let splitterStyle = chromeWindow.getComputedStyle(HUD.splitter, null);
      innerHeight += parseInt(splitterStyle.height) +
                     parseInt(splitterStyle.borderTopWidth) +
                     parseInt(splitterStyle.borderBottomWidth);
    }

    let boxStyle = chromeWindow.getComputedStyle(HUD.HUDBox, null);
    innerHeight += parseInt(boxStyle.height) +
                   parseInt(boxStyle.borderTopWidth) +
                   parseInt(boxStyle.borderBottomWidth);

    let height = this.lastConsoleHeight > 0 ? this.lastConsoleHeight :
      Math.ceil(innerHeight * DEFAULT_CONSOLE_HEIGHT);

    if ((innerHeight - height) < MINIMUM_PAGE_HEIGHT) {
      height = innerHeight - MINIMUM_PAGE_HEIGHT;
    }
    else if (height < MINIMUM_CONSOLE_HEIGHT) {
      height = MINIMUM_CONSOLE_HEIGHT;
    }

    HUD.HUDBox.style.height = height + "px";
  },

  /**
   * Remember the height of the given Web Console, such that it can later be
   * reused when other Web Consoles are open.
   *
   * @param string aHUDId The ID of the Web Console.
   */
  storeHeight: function HS_storeHeight(aHUDId)
  {
    let hudBox = this.hudReferences[aHUDId].HUDBox;
    let window = hudBox.ownerDocument.defaultView;
    let style = window.getComputedStyle(hudBox, null);
    let height = parseInt(style.height);
    height += parseInt(style.borderTopWidth);
    height += parseInt(style.borderBottomWidth);
    this.lastConsoleHeight = height;

    let pref = Services.prefs.getIntPref("devtools.hud.height");
    if (pref > -1) {
      Services.prefs.setIntPref("devtools.hud.height", height);
    }
  },

  /**
   * Copies the selected items to the system clipboard.
   *
   * @param nsIDOMNode aOutputNode
   *        The output node.
   * @returns void
   */
  copySelectedItems: function HS_copySelectedItems(aOutputNode)
  {
    // Gather up the selected items and concatenate their clipboard text.

    let strings = [];
    let newGroup = false;

    let children = aOutputNode.children;

    for (let i = 0; i < children.length; i++) {
      let item = children[i];
      if (!item.selected) {
        continue;
      }

      // Add dashes between groups so that group boundaries show up in the
      // copied output.
      if (i > 0 && item.classList.contains("webconsole-new-group")) {
        newGroup = true;
      }

      // Ensure the selected item hasn't been filtered by type or string.
      if (!item.classList.contains("hud-filtered-by-type") &&
          !item.classList.contains("hud-filtered-by-string")) {
        let timestampString = ConsoleUtils.timestampString(item.timestamp);
        if (newGroup) {
          strings.push("--");
          newGroup = false;
        }
        strings.push("[" + timestampString + "] " + item.clipboardText);
      }
    }
    clipboardHelper.copyString(strings.join("\n"));
  }
};

//////////////////////////////////////////////////////////////////////////
// HeadsUpDisplay
//////////////////////////////////////////////////////////////////////////

/*
 * HeadsUpDisplay is an interactive console initialized *per tab*  that
 * displays console log data as well as provides an interactive terminal to
 * manipulate the current tab's document content.
 * */
function HeadsUpDisplay(aConfig)
{
  // sample config: { parentNode: aDOMNode,
  //                  // or
  //                  parentNodeId: "myHUDParent123",
  //
  //                  placement: "appendChild"
  //                  // or
  //                  placement: "insertBefore",
  //                  placementChildNodeIndex: 0,
  //                }

  this.HUDBox = null;

  if (aConfig.parentNode) {
    // TODO: need to replace these DOM calls with internal functions
    // that operate on each application's node structure
    // better yet, we keep these functions in a "bridgeModule" or the HUDService
    // to keep a registry of nodeGetters for each application
    // see bug 568647
    this.parentNode = aConfig.parentNode;
    this.notificationBox = aConfig.parentNode;
    this.chromeDocument = aConfig.parentNode.ownerDocument;
    this.contentWindow = aConfig.contentWindow;
    this.uriSpec = aConfig.contentWindow.location.href;
    this.hudId = "hud_" + aConfig.parentNode.getAttribute("id");
  }
  else {
    // parentNodeId is the node's id where we attach the HUD
    // TODO: is the "navigator:browser" below used in all Gecko Apps?
    // see bug 568647
    let windowEnum = Services.wm.getEnumerator("navigator:browser");
    let parentNode;
    let contentDocument;
    let contentWindow;
    let chromeDocument;

    // TODO: the following  part is still very Firefox specific
    // see bug 568647

    while (windowEnum.hasMoreElements()) {
      let window = windowEnum.getNext();
      try {
        let gBrowser = window.gBrowser;
        let _browsers = gBrowser.browsers;
        let browserLen = _browsers.length;

        for (var i = 0; i < browserLen; i++) {
          var _notificationBox = gBrowser.getNotificationBox(_browsers[i]);
          this.notificationBox = _notificationBox;

          if (_notificationBox.getAttribute("id") == aConfig.parentNodeId) {
            this.parentNodeId = _notificationBox.getAttribute("id");
            this.hudId = "hud_" + this.parentNodeId;

            parentNode = _notificationBox;

            this.contentDocument =
              _notificationBox.childNodes[0].contentDocument;
            this.contentWindow =
              _notificationBox.childNodes[0].contentWindow;
            this.uriSpec = aConfig.contentWindow.location.href;

            this.chromeDocument =
              _notificationBox.ownerDocument;

            break;
          }
        }
      }
      catch (ex) {
        Cu.reportError(ex);
      }

      if (parentNode) {
        break;
      }
    }
    if (!parentNode) {
      throw new Error(this.ERRORS.PARENTNODE_NOT_FOUND);
    }
    this.parentNode = parentNode;
    this.notificationBox = parentNode;
  }

  // create textNode Factory:
  this.textFactory = NodeFactory("text", "xul", this.chromeDocument);

  this.chromeWindow = this.chromeDocument.defaultView;

  // create a panel dynamically and attach to the parentNode
  this.createHUD();

  this.HUDBox.lastTimestamp = 0;
  // create the JSTerm input element
  try {
    this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode);
    if (this.jsterm) {
      this.jsterm.inputNode.focus();
    }
    if (this.gcliterm) {
      this.gcliterm.inputNode.focus();
    }
  }
  catch (ex) {
    Cu.reportError(ex);
  }

  // A cache for tracking repeated CSS Nodes.
  this.cssNodes = {};
}

HeadsUpDisplay.prototype = {

  consolePanel: null,

  /**
   * The nesting depth of the currently active console group.
   */
  groupDepth: 0,

  get mainPopupSet()
  {
    return this.chromeDocument.getElementById("mainPopupSet");
  },

  /**
   * Get the tab associated to the HeadsUpDisplay object.
   */
  get tab()
  {
    // TODO: we should only keep a reference to the tab object and use
    // getters to determine the rest of objects we need - the chrome window,
    // document, etc. We should simplify the entire code to use only a single
    // tab object ref. See bug 656231.
    let tab = null;
    let id = this.notificationBox.id;
    Array.some(this.chromeDocument.defaultView.gBrowser.tabs, function(aTab) {
      if (aTab.linkedPanel == id) {
        tab = aTab;
        return true;
      }
    });

    return tab;
  },

  /**
   * Create a panel to open the web console if it should float above
   * the content in its own window.
   */
  createOwnWindowPanel: function HUD_createOwnWindowPanel()
  {
    if (this.uiInOwnWindow) {
      return this.consolePanel;
    }

    let width = 0;
    try {
      width = Services.prefs.getIntPref("devtools.webconsole.width");
    }
    catch (ex) {}

    if (width < 1) {
      width = this.HUDBox.clientWidth || this.contentWindow.innerWidth;
    }

    let height = this.HUDBox.clientHeight;

    let top = 0;
    try {
      top = Services.prefs.getIntPref("devtools.webconsole.top");
    }
    catch (ex) {}

    let left = 0;
    try {
      left = Services.prefs.getIntPref("devtools.webconsole.left");
    }
    catch (ex) {}

    let panel = this.chromeDocument.createElementNS(XUL_NS, "panel");

    let config = { id: "console_window_" + this.hudId,
                   label: this.getPanelTitle(),
                   titlebar: "normal",
                   noautohide: "true",
                   norestorefocus: "true",
                   close: "true",
                   flex: "1",
                   hudId: this.hudId,
                   width: width,
                   position: "overlap",
                   top: top,
                   left: left,
                 };

    for (let attr in config) {
      panel.setAttribute(attr, config[attr]);
    }

    panel.classList.add("web-console-panel");

    let onPopupShown = (function HUD_onPopupShown() {
      panel.removeEventListener("popupshown", onPopupShown, false);

      // Make sure that the HUDBox size updates when the panel is resized.

      let height = panel.clientHeight;

      this.HUDBox.style.height = "auto";
      this.HUDBox.setAttribute("flex", "1");

      panel.setAttribute("height", height);

      // Scroll the outputNode back to the last location.
      if (lastIndex > -1 && lastIndex < this.outputNode.getRowCount()) {
        this.outputNode.ensureIndexIsVisible(lastIndex);
      }

      if (this.jsterm) {
        this.jsterm.inputNode.focus();
      }
      if (this.gcliterm) {
        this.gcliterm.inputNode.focus();
      }
    }).bind(this);

    panel.addEventListener("popupshown", onPopupShown,false);

    let onPopupHidden = (function HUD_onPopupHidden(aEvent) {
      if (aEvent.target != panel) {
        return;
      }

      panel.removeEventListener("popuphidden", onPopupHidden, false);

      let width = 0;
      try {
        width = Services.prefs.getIntPref("devtools.webconsole.width");
      }
      catch (ex) { }

      if (width > 0) {
        Services.prefs.setIntPref("devtools.webconsole.width", panel.clientWidth);
      }

      // Are we destroying the HUD or repositioning it?
      if (this.consoleWindowUnregisterOnHide) {
        HUDService.deactivateHUDForContext(this.tab, false);
      } else {
        this.consoleWindowUnregisterOnHide = true;
      }
    }).bind(this);

    panel.addEventListener("popuphidden", onPopupHidden, false);

    let lastIndex = -1;

    if (this.outputNode.getIndexOfFirstVisibleRow) {
      lastIndex = this.outputNode.getIndexOfFirstVisibleRow() +
                  this.outputNode.getNumberOfVisibleRows() - 1;
    }

    if (this.splitter.parentNode) {
      this.splitter.parentNode.removeChild(this.splitter);
    }
    panel.appendChild(this.HUDBox);

    let space = this.chromeDocument.createElement("spacer");
    space.setAttribute("flex", "1");

    let bottomBox = this.chromeDocument.createElement("hbox");
    space.setAttribute("flex", "1");

    let resizer = this.chromeDocument.createElement("resizer");
    resizer.setAttribute("dir", "bottomend");
    resizer.setAttribute("element", config.id);

    bottomBox.appendChild(space);
    bottomBox.appendChild(resizer);

    panel.appendChild(bottomBox);

    this.mainPopupSet.appendChild(panel);

    Services.prefs.setCharPref("devtools.webconsole.position", "window");

    panel.openPopup(null, "overlay", left, top, false, false);

    this.consolePanel = panel;
    this.consoleWindowUnregisterOnHide = true;

    return panel;
  },

  /**
   * Retrieve the Web Console panel title.
   *
   * @return string
   *         The Web Console panel title.
   */
  getPanelTitle: function HUD_getPanelTitle()
  {
    return this.getFormatStr("webConsoleWindowTitleAndURL", [this.uriSpec]);
  },

  positions: {
    above: 0, // the childNode index
    below: 2,
    window: null
  },

  consoleWindowUnregisterOnHide: true,

  /**
   * Re-position the console
   */
  positionConsole: function HUD_positionConsole(aPosition)
  {
    if (!(aPosition in this.positions)) {
      throw new Error("Incorrect argument: " + aPosition +
        ". Cannot position Web Console");
    }

    if (aPosition == "window") {
      let closeButton = this.consoleFilterToolbar.
        querySelector(".webconsole-close-button");
      closeButton.setAttribute("hidden", "true");
      this.createOwnWindowPanel();
      this.positionMenuitems.window.setAttribute("checked", true);
      if (this.positionMenuitems.last) {
        this.positionMenuitems.last.setAttribute("checked", false);
      }
      this.positionMenuitems.last = this.positionMenuitems[aPosition];
      this.uiInOwnWindow = true;
      return;
    }

    let height = this.HUDBox.clientHeight;

    // get the node position index
    let nodeIdx = this.positions[aPosition];
    let nBox = this.notificationBox;
    let node = nBox.childNodes[nodeIdx];

    // check to see if console is already positioned in aPosition
    if (node == this.HUDBox) {
      return;
    }

    let lastIndex = -1;

    if (this.outputNode.getIndexOfFirstVisibleRow) {
      lastIndex = this.outputNode.getIndexOfFirstVisibleRow() +
                  this.outputNode.getNumberOfVisibleRows() - 1;
    }

    // remove the console and splitter and reposition
    if (this.splitter.parentNode) {
      this.splitter.parentNode.removeChild(this.splitter);
    }

    if (aPosition == "below") {
      nBox.appendChild(this.splitter);
      nBox.appendChild(this.HUDBox);
    }
    else {
      nBox.insertBefore(this.splitter, node);
      nBox.insertBefore(this.HUDBox, this.splitter);
    }

    this.positionMenuitems[aPosition].setAttribute("checked", true);
    if (this.positionMenuitems.last) {
      this.positionMenuitems.last.setAttribute("checked", false);
    }
    this.positionMenuitems.last = this.positionMenuitems[aPosition];

    Services.prefs.setCharPref("devtools.webconsole.position", aPosition);

    if (lastIndex > -1 && lastIndex < this.outputNode.getRowCount()) {
      this.outputNode.ensureIndexIsVisible(lastIndex);
    }

    let closeButton = this.consoleFilterToolbar.
      getElementsByClassName("webconsole-close-button")[0];
    closeButton.removeAttribute("hidden");

    this.uiInOwnWindow = false;
    if (this.consolePanel) {
      // must destroy the consolePanel
      this.consoleWindowUnregisterOnHide = false;
      this.consolePanel.hidePopup();
      this.consolePanel.parentNode.removeChild(this.consolePanel);
      this.consolePanel = null;   // remove this as we're not in panel anymore
      this.HUDBox.removeAttribute("flex");
      this.HUDBox.removeAttribute("height");
      this.HUDBox.style.height = height + "px";
    }

    if (this.jsterm) {
      this.jsterm.inputNode.focus();
    }
    if (this.gcliterm) {
      this.gcliterm.inputNode.focus();
    }
  },

  /**
   * L10N shortcut function
   *
   * @param string aName
   * @returns string
   */
  getStr: function HUD_getStr(aName)
  {
    return stringBundle.GetStringFromName(aName);
  },

  /**
   * L10N shortcut function
   *
   * @param string aName
   * @param array aArray
   * @returns string
   */
  getFormatStr: function HUD_getFormatStr(aName, aArray)
  {
    return stringBundle.formatStringFromName(aName, aArray, aArray.length);
  },

  /**
   * The JSTerm object that contains the console's inputNode
   *
   */
  jsterm: null,

  /**
   * The GcliTerm object that contains the console's GCLI
   */
  gcliterm: null,

  /**
   * creates and attaches the console input node
   *
   * @param nsIDOMWindow aWindow
   * @returns void
   */
  createConsoleInput:
  function HUD_createConsoleInput(aWindow, aParentNode, aExistingConsole)
  {
    let usegcli = false;
    try {
      usegcli = Services.prefs.getBoolPref("devtools.gcli.enable");
    }
    catch (ex) {}

    if (appName() == "FIREFOX") {
      if (!usegcli) {
        let context = Cu.getWeakReference(aWindow);
        let mixin = new JSTermFirefoxMixin(context, aParentNode,
                                           aExistingConsole);
        this.jsterm = new JSTerm(context, aParentNode, mixin, this.console);
      }
      else {
        this.gcliterm = new GcliTerm(aWindow, this.hudId, this.chromeDocument,
                                     this.console, this.hintNode, this.consoleWrap);
        aParentNode.appendChild(this.gcliterm.element);
      }
    }
    else {
      throw new Error("Unsupported Gecko Application");
    }
  },

  /**
   * Display cached messages that may have been collected before the UI is
   * displayed.
   *
   * @returns void
   */
  displayCachedConsoleMessages: function HUD_displayCachedConsoleMessages()
  {
    let innerWindowId = HUDService.getInnerWindowId(this.contentWindow);

    let messages = gConsoleStorage.getEvents(innerWindowId);

    let errors = {};
    Services.console.getMessageArray(errors, {});

    // Filter the errors to find only those we should display.
    let filteredErrors = (errors.value || []).filter(function(aError) {
      return aError instanceof Ci.nsIScriptError &&
             aError.innerWindowID == innerWindowId;
    }, this);

    messages.push.apply(messages, filteredErrors);
    messages.sort(function(a, b) { return a.timeStamp - b.timeStamp; });

    // Turn off scrolling for the moment.
    ConsoleUtils.scroll = false;
    this.outputNode.hidden = true;

    // Display all messages.
    messages.forEach(function(aMessage) {
      if (aMessage instanceof Ci.nsIScriptError) {
        HUDService.reportPageError(aMessage);
      }
      else {
        // In this case the cached message is a console message generated
        // by the ConsoleAPI, not an nsIScriptError
        HUDService.logConsoleAPIMessage(this.hudId, aMessage);
      }
    }, this);

    this.outputNode.hidden = false;
    ConsoleUtils.scroll = true;

    // Scroll to bottom.
    let numChildren = this.outputNode.childNodes.length;
    if (numChildren && this.outputNode.clientHeight) {
      // We also check the clientHeight to force a reflow, otherwise
      // ensureIndexIsVisible() does not work after outputNode.hidden = false.
      this.outputNode.ensureIndexIsVisible(numChildren - 1);
    }
  },

  /**
   * Re-attaches a console when the contentWindow is recreated
   *
   * @param nsIDOMWindow aContentWindow
   * @returns void
   */
  reattachConsole: function HUD_reattachConsole(aContentWindow)
  {
    this.contentWindow = aContentWindow;
    this.contentDocument = this.contentWindow.document;
    this.uriSpec = this.contentWindow.location.href;

    if (this.consolePanel) {
      this.consolePanel.label = this.getPanelTitle();
    }

    if (this.jsterm) {
      this.jsterm.context = Cu.getWeakReference(this.contentWindow);
      this.jsterm.console = this.console;
      this.jsterm.createSandbox();
    }
    else if (this.gcliterm) {
      this.gcliterm.reattachConsole(this.contentWindow, this.console);
    }
    else {
      this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode);
    }
  },

  /**
   * Shortcut to make XUL nodes
   *
   * @param string aTag
   * @returns nsIDOMNode
   */
  makeXULNode:
  function HUD_makeXULNode(aTag)
  {
    return this.chromeDocument.createElement(aTag);
  },

  /**
   * Build the UI of each HeadsUpDisplay
   *
   * @returns nsIDOMNode
   */
  makeHUDNodes: function HUD_makeHUDNodes()
  {
    let self = this;

    this.splitter = this.makeXULNode("splitter");
    this.splitter.setAttribute("class", "hud-splitter");

    this.HUDBox = this.makeXULNode("vbox");
    this.HUDBox.setAttribute("id", this.hudId);
    this.HUDBox.setAttribute("class", "hud-box animated");
    this.HUDBox.style.height = 0;

    let outerWrap = this.makeXULNode("vbox");
    outerWrap.setAttribute("class", "hud-outer-wrapper");
    outerWrap.setAttribute("flex", "1");

    let consoleCommandSet = this.makeXULNode("commandset");
    outerWrap.appendChild(consoleCommandSet);

    let consoleWrap = this.makeXULNode("vbox");
    this.consoleWrap = consoleWrap;
    consoleWrap.setAttribute("class", "hud-console-wrapper");
    consoleWrap.setAttribute("flex", "1");

    this.filterSpacer = this.makeXULNode("spacer");
    this.filterSpacer.setAttribute("flex", "1");

    this.filterBox = this.makeXULNode("textbox");
    this.filterBox.setAttribute("class", "compact hud-filter-box");
    this.filterBox.setAttribute("hudId", this.hudId);
    this.filterBox.setAttribute("placeholder", this.getStr("stringFilter"));
    this.filterBox.setAttribute("type", "search");

    this.setFilterTextBoxEvents();

    this.createConsoleMenu(this.consoleWrap);

    this.filterPrefs = HUDService.getDefaultFilterPrefs(this.hudId);

    let consoleFilterToolbar = this.makeFilterToolbar();
    consoleFilterToolbar.setAttribute("id", "viewGroup");
    this.consoleFilterToolbar = consoleFilterToolbar;

    this.hintNode = this.makeXULNode("vbox");
    this.hintNode.setAttribute("class", "gcliterm-hint-node");

    let hintParentNode = this.makeXULNode("vbox");
    hintParentNode.setAttribute("flex", "0");
    hintParentNode.setAttribute("class", "gcliterm-hint-parent");
    hintParentNode.setAttribute("pack", "end");
    hintParentNode.appendChild(this.hintNode);
    hintParentNode.hidden = true;

    let hbox = this.makeXULNode("hbox");
    hbox.setAttribute("flex", "1");
    hbox.setAttribute("class", "gcliterm-display");

    this.outputNode = this.makeXULNode("richlistbox");
    this.outputNode.setAttribute("class", "hud-output-node");
    this.outputNode.setAttribute("flex", "1");
    this.outputNode.setAttribute("orient", "vertical");
    this.outputNode.setAttribute("context", this.hudId + "-output-contextmenu");
    this.outputNode.setAttribute("style", "direction: ltr;");
    this.outputNode.setAttribute("seltype", "multiple");

    hbox.appendChild(hintParentNode);
    hbox.appendChild(this.outputNode);

    consoleWrap.appendChild(consoleFilterToolbar);
    consoleWrap.appendChild(hbox);

    outerWrap.appendChild(consoleWrap);

    this.HUDBox.lastTimestamp = 0;

    this.jsTermParentNode = outerWrap;
    this.HUDBox.appendChild(outerWrap);

    return this.HUDBox;
  },

  /**
   * sets the click events for all binary toggle filter buttons
   *
   * @returns void
   */
  setFilterTextBoxEvents: function HUD_setFilterTextBoxEvents()
  {
    var filterBox = this.filterBox;
    function onChange()
    {
      // To improve responsiveness, we let the user finish typing before we
      // perform the search.

      if (this.timer == null) {
        let timerClass = Cc["@mozilla.org/timer;1"];
        this.timer = timerClass.createInstance(Ci.nsITimer);
      } else {
        this.timer.cancel();
      }

      let timerEvent = {
        notify: function setFilterTextBoxEvents_timerEvent_notify() {
          HUDService.updateFilterText(filterBox);
        }
      };

      this.timer.initWithCallback(timerEvent, SEARCH_DELAY,
        Ci.nsITimer.TYPE_ONE_SHOT);
    }

    filterBox.addEventListener("command", onChange, false);
    filterBox.addEventListener("input", onChange, false);
  },

  /**
   * Make the filter toolbar where we can toggle logging filters
   *
   * @returns nsIDOMNode
   */
  makeFilterToolbar: function HUD_makeFilterToolbar()
  {
    const BUTTONS = [
      {
        name: "PageNet",
        category: "net",
        severities: [
          { name: "ConsoleErrors", prefKey: "network" },
          { name: "ConsoleLog", prefKey: "networkinfo" }
        ]
      },
      {
        name: "PageCSS",
        category: "css",
        severities: [
          { name: "ConsoleErrors", prefKey: "csserror" },
          { name: "ConsoleWarnings", prefKey: "cssparser" }
        ]
      },
      {
        name: "PageJS",
        category: "js",
        severities: [
          { name: "ConsoleErrors", prefKey: "exception" },
          { name: "ConsoleWarnings", prefKey: "jswarn" }
        ]
      },
      {
        name: "PageLogging",
        category: "logging",
        severities: [
          { name: "ConsoleErrors", prefKey: "error" },
          { name: "ConsoleWarnings", prefKey: "warn" },
          { name: "ConsoleInfo", prefKey: "info" },
          { name: "ConsoleLog", prefKey: "log" }
        ]
      }
    ];

    let toolbar = this.makeXULNode("toolbar");
    toolbar.setAttribute("class", "hud-console-filter-toolbar");
    toolbar.setAttribute("mode", "full");


    for (let i = 0; i < BUTTONS.length; i++) {
      this.makeFilterButton(toolbar, BUTTONS[i]);
    }

    toolbar.appendChild(this.filterSpacer);

    let positionUI = this.createPositionUI();
    toolbar.appendChild(positionUI);

    toolbar.appendChild(this.filterBox);
    this.makeClearConsoleButton(toolbar);

    this.makeCloseButton(toolbar);

    return toolbar;
  },

  /**
   * Creates the UI for re-positioning the console
   *
   * @return nsIDOMNode
   *         The toolbarbutton which holds the menu that allows the user to
   *         change the console position.
   */
  createPositionUI: function HUD_createPositionUI()
  {
    this._positionConsoleAbove = (function HUD_positionAbove() {
      this.positionConsole("above");
    }).bind(this);

    this._positionConsoleBelow = (function HUD_positionBelow() {
      this.positionConsole("below");
    }).bind(this);
    this._positionConsoleWindow = (function HUD_positionWindow() {
      this.positionConsole("window");
    }).bind(this);

    let button = this.makeXULNode("toolbarbutton");
    button.setAttribute("type", "menu");
    button.setAttribute("label", this.getStr("webConsolePosition"));
    button.setAttribute("tooltip", this.getStr("webConsolePositionTooltip"));

    let menuPopup = this.makeXULNode("menupopup");
    button.appendChild(menuPopup);

    let itemAbove = this.makeXULNode("menuitem");
    itemAbove.setAttribute("label", this.getStr("webConsolePositionAbove"));
    itemAbove.setAttribute("type", "checkbox");
    itemAbove.setAttribute("autocheck", "false");
    itemAbove.addEventListener("command", this._positionConsoleAbove, false);
    menuPopup.appendChild(itemAbove);

    let itemBelow = this.makeXULNode("menuitem");
    itemBelow.setAttribute("label", this.getStr("webConsolePositionBelow"));
    itemBelow.setAttribute("type", "checkbox");
    itemBelow.setAttribute("autocheck", "false");
    itemBelow.addEventListener("command", this._positionConsoleBelow, false);
    menuPopup.appendChild(itemBelow);

    let itemWindow = this.makeXULNode("menuitem");
    itemWindow.setAttribute("label", this.getStr("webConsolePositionWindow"));
    itemWindow.setAttribute("type", "checkbox");
    itemWindow.setAttribute("autocheck", "false");
    itemWindow.addEventListener("command", this._positionConsoleWindow, false);
    menuPopup.appendChild(itemWindow);

    this.positionMenuitems = {
      last: null,
      above: itemAbove,
      below: itemBelow,
      window: itemWindow,
    };

    return button;
  },

  /**
   * Creates the context menu on the console.
   *
   * @param nsIDOMNode aOutputNode
   *        The console output DOM node.
   * @returns void
   */
  createConsoleMenu: function HUD_createConsoleMenu(aConsoleWrapper) {
    let menuPopup = this.makeXULNode("menupopup");
    let id = this.hudId + "-output-contextmenu";
    menuPopup.setAttribute("id", id);
    menuPopup.addEventListener("popupshowing", function() {
      saveBodiesItem.setAttribute("checked",
        HUDService.saveRequestAndResponseBodies);
    }, true);

    let saveBodiesItem = this.makeXULNode("menuitem");
    saveBodiesItem.setAttribute("label", this.getStr("saveBodies.label"));
    saveBodiesItem.setAttribute("accesskey",
                                 this.getStr("saveBodies.accesskey"));
    saveBodiesItem.setAttribute("type", "checkbox");
    saveBodiesItem.setAttribute("buttonType", "saveBodies");
    saveBodiesItem.setAttribute("oncommand", "HUDConsoleUI.command(this);");
    menuPopup.appendChild(saveBodiesItem);

    menuPopup.appendChild(this.makeXULNode("menuseparator"));

    let copyItem = this.makeXULNode("menuitem");
    copyItem.setAttribute("label", this.getStr("copyCmd.label"));
    copyItem.setAttribute("accesskey", this.getStr("copyCmd.accesskey"));
    copyItem.setAttribute("key", "key_copy");
    copyItem.setAttribute("command", "cmd_copy");
    copyItem.setAttribute("buttonType", "copy");
    menuPopup.appendChild(copyItem);

    let selectAllItem = this.makeXULNode("menuitem");
    selectAllItem.setAttribute("label", this.getStr("selectAllCmd.label"));
    selectAllItem.setAttribute("accesskey",
                               this.getStr("selectAllCmd.accesskey"));
    selectAllItem.setAttribute("hudId", this.hudId);
    selectAllItem.setAttribute("buttonType", "selectAll");
    selectAllItem.setAttribute("oncommand", "HUDConsoleUI.command(this);");
    menuPopup.appendChild(selectAllItem);

    aConsoleWrapper.appendChild(menuPopup);
    aConsoleWrapper.setAttribute("context", id);
  },

  /**
   * Creates one of the filter buttons on the toolbar.
   *
   * @param nsIDOMNode aParent
   *        The node to which the filter button should be appended.
   * @param object aDescriptor
   *        A descriptor that contains info about the button. Contains "name",
   *        "category", and "prefKey" properties, and optionally a "severities"
   *        property.
   * @return void
   */
  makeFilterButton: function HUD_makeFilterButton(aParent, aDescriptor)
  {
    let toolbarButton = this.makeXULNode("toolbarbutton");
    aParent.appendChild(toolbarButton);

    let toggleFilter = HeadsUpDisplayUICommands.toggleFilter;
    toolbarButton.addEventListener("click", toggleFilter, false);

    let name = aDescriptor.name;
    toolbarButton.setAttribute("type", "menu-button");
    toolbarButton.setAttribute("label", this.getStr("btn" + name));
    toolbarButton.setAttribute("tooltip", this.getStr("tip" + name));
    toolbarButton.setAttribute("category", aDescriptor.category);
    toolbarButton.setAttribute("hudId", this.hudId);
    toolbarButton.classList.add("webconsole-filter-button");

    let menuPopup = this.makeXULNode("menupopup");
    toolbarButton.appendChild(menuPopup);

    let someChecked = false;
    for (let i = 0; i < aDescriptor.severities.length; i++) {
      let severity = aDescriptor.severities[i];
      let menuItem = this.makeXULNode("menuitem");
      menuItem.setAttribute("label", this.getStr("btn" + severity.name));
      menuItem.setAttribute("type", "checkbox");
      menuItem.setAttribute("autocheck", "false");
      menuItem.setAttribute("hudId", this.hudId);

      let prefKey = severity.prefKey;
      menuItem.setAttribute("prefKey", prefKey);

      let checked = this.filterPrefs[prefKey];
      menuItem.setAttribute("checked", checked);
      if (checked) {
        someChecked = true;
      }

      menuItem.addEventListener("command", toggleFilter, false);

      menuPopup.appendChild(menuItem);
    }

    toolbarButton.setAttribute("checked", someChecked);
  },

  /**
   * Creates the close button on the toolbar.
   *
   * @param nsIDOMNode aParent
   *        The toolbar to attach the close button to.
   * @return void
   */
  makeCloseButton: function HUD_makeCloseButton(aToolbar)
  {
    this.closeButtonOnCommand = (function HUD_closeButton_onCommand() {
      HUDService.animate(this.hudId, ANIMATE_OUT, (function() {
        HUDService.deactivateHUDForContext(this.tab, true);
      }).bind(this));
    }).bind(this);

    this.closeButton = this.makeXULNode("toolbarbutton");
    this.closeButton.classList.add("webconsole-close-button");
    this.closeButton.addEventListener("command",
      this.closeButtonOnCommand, false);
    aToolbar.appendChild(this.closeButton);
  },

  /**
   * Creates the "Clear Console" button.
   *
   * @param nsIDOMNode aParent
   *        The toolbar to attach the "Clear Console" button to.
   * @param string aHUDId
   *        The ID of the console.
   * @return void
   */
  makeClearConsoleButton: function HUD_makeClearConsoleButton(aToolbar)
  {
    let hudId = this.hudId;
    function HUD_clearButton_onCommand() {
      let hud = HUDService.getHudReferenceById(hudId);
      if (hud.jsterm) {
        hud.jsterm.clearOutput(true);
      }
      if (hud.gcliterm) {
        hud.gcliterm.clearOutput();
      }
    }

    let clearButton = this.makeXULNode("toolbarbutton");
    clearButton.setAttribute("label", this.getStr("btnClear"));
    clearButton.classList.add("webconsole-clear-console-button");
    clearButton.addEventListener("command", HUD_clearButton_onCommand, false);

    aToolbar.appendChild(clearButton);
  },

  /**
   * Destroy the property inspector message node. This performs the necessary
   * cleanup for the tree widget and removes it from the DOM.
   *
   * @param nsIDOMNode aMessageNode
   *        The message node that contains the property inspector from a
   *        console.dir call.
   */
  pruneConsoleDirNode: function HUD_pruneConsoleDirNode(aMessageNode)
  {
    aMessageNode.parentNode.removeChild(aMessageNode);
    let tree = aMessageNode.querySelector("tree");
    tree.parentNode.removeChild(tree);
    aMessageNode.propertyTreeView = null;
    tree.view = null;
    tree = null;
  },

  /**
   * Create the Web Console UI.
   *
   * @return nsIDOMNode
   *         The Web Console container element (HUDBox).
   */
  createHUD: function HUD_createHUD()
  {
    if (!this.HUDBox) {
      this.makeHUDNodes();
      let positionPref = Services.prefs.getCharPref("devtools.webconsole.position");
      this.positionConsole(positionPref);
    }
    return this.HUDBox;
  },

  uiInOwnWindow: false,

  get console() { return this.contentWindow.wrappedJSObject.console; },

  getLogCount: function HUD_getLogCount()
  {
    return this.outputNode.childNodes.length;
  },

  getLogNodes: function HUD_getLogNodes()
  {
    return this.outputNode.childNodes;
  },

  ERRORS: {
    HUD_BOX_DOES_NOT_EXIST: "Heads Up Display does not exist",
    TAB_ID_REQUIRED: "Tab DOM ID is required",
    PARENTNODE_NOT_FOUND: "parentNode element not found"
  },

  /**
   * Destroy the HUD object. Call this method to avoid memory leaks when the Web
   * Console is closed.
   */
  destroy: function HUD_destroy()
  {
    if (this.jsterm) {
      this.jsterm.destroy();
    }
    if (this.gcliterm) {
      this.gcliterm.destroy();
    }

    this.positionMenuitems.above.removeEventListener("command",
      this._positionConsoleAbove, false);
    this.positionMenuitems.below.removeEventListener("command",
      this._positionConsoleBelow, false);
    this.positionMenuitems.window.removeEventListener("command",
      this._positionConsoleWindow, false);

    this.closeButton.removeEventListener("command",
      this.closeButtonOnCommand, false);
  },
};


//////////////////////////////////////////////////////////////////////////////
// ConsoleAPIObserver
//////////////////////////////////////////////////////////////////////////////

let ConsoleAPIObserver = {

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),

  init: function CAO_init()
  {
    Services.obs.addObserver(this, "quit-application-granted", false);
    Services.obs.addObserver(this, "console-api-log-event", false);
  },

  observe: function CAO_observe(aMessage, aTopic, aData)
  {
    if (aTopic == "console-api-log-event") {
      aMessage = aMessage.wrappedJSObject;
      let windowId = parseInt(aData);
      let win = HUDService.getWindowByWindowId(windowId);
      if (!win)
        return;

      // Find the HUD ID for the topmost window
      let hudId = HUDService.getHudIdByWindow(win.top);
      if (!hudId)
        return;

      HUDService.logConsoleAPIMessage(hudId, aMessage);
    }
    else if (aTopic == "quit-application-granted") {
      HUDService.shutdown();
    }
  },

  shutdown: function CAO_shutdown()
  {
    Services.obs.removeObserver(this, "quit-application-granted");
    Services.obs.removeObserver(this, "console-api-log-event");
  }
};

/**
 * Creates a DOM Node factory for XUL nodes - as well as textNodes
 * @param aFactoryType "xul" or "text"
 * @param ignored This parameter is currently ignored, and will be removed
 * See bug 594304
 * @param aDocument The document, the factory is to generate nodes from
 * @return DOM Node Factory function
 */
function NodeFactory(aFactoryType, ignored, aDocument)
{
  // aDocument is presumed to be a XULDocument
  if (aFactoryType == "text") {
    return function factory(aText)
    {
      return aDocument.createTextNode(aText);
    }
  }
  else if (aFactoryType == "xul") {
    return function factory(aTag)
    {
      return aDocument.createElement(aTag);
    }
  }
  else {
    throw new Error('NodeFactory: Unknown factory type: ' + aFactoryType);
  }
}

//////////////////////////////////////////////////////////////////////////
// JS Completer
//////////////////////////////////////////////////////////////////////////

const STATE_NORMAL = 0;
const STATE_QUOTE = 2;
const STATE_DQUOTE = 3;

const OPEN_BODY = '{[('.split('');
const CLOSE_BODY = '}])'.split('');
const OPEN_CLOSE_BODY = {
  '{': '}',
  '[': ']',
  '(': ')'
};

/**
 * Analyses a given string to find the last statement that is interesting for
 * later completion.
 *
 * @param   string aStr
 *          A string to analyse.
 *
 * @returns object
 *          If there was an error in the string detected, then a object like
 *
 *            { err: "ErrorMesssage" }
 *
 *          is returned, otherwise a object like
 *
 *            {
 *              state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE,
 *              startPos: index of where the last statement begins
 *            }
 */
function findCompletionBeginning(aStr)
{
  let bodyStack = [];

  let state = STATE_NORMAL;
  let start = 0;
  let c;
  for (let i = 0; i < aStr.length; i++) {
    c = aStr[i];

    switch (state) {
      // Normal JS state.
      case STATE_NORMAL:
        if (c == '"') {
          state = STATE_DQUOTE;
        }
        else if (c == '\'') {
          state = STATE_QUOTE;
        }
        else if (c == ';') {
          start = i + 1;
        }
        else if (c == ' ') {
          start = i + 1;
        }
        else if (OPEN_BODY.indexOf(c) != -1) {
          bodyStack.push({
            token: c,
            start: start
          });
          start = i + 1;
        }
        else if (CLOSE_BODY.indexOf(c) != -1) {
          var last = bodyStack.pop();
          if (!last || OPEN_CLOSE_BODY[last.token] != c) {
            return {
              err: "syntax error"
            };
          }
          if (c == '}') {
            start = i + 1;
          }
          else {
            start = last.start;
          }
        }
        break;

      // Double quote state > " <
      case STATE_DQUOTE:
        if (c == '\\') {
          i ++;
        }
        else if (c == '\n') {
          return {
            err: "unterminated string literal"
          };
        }
        else if (c == '"') {
          state = STATE_NORMAL;
        }
        break;

      // Single quoate state > ' <
      case STATE_QUOTE:
        if (c == '\\') {
          i ++;
        }
        else if (c == '\n') {
          return {
            err: "unterminated string literal"
          };
          return;
        }
        else if (c == '\'') {
          state = STATE_NORMAL;
        }
        break;
    }
  }

  return {
    state: state,
    startPos: start
  };
}

/**
 * Provides a list of properties, that are possible matches based on the passed
 * scope and inputValue.
 *
 * @param object aScope
 *        Scope to use for the completion.
 *
 * @param string aInputValue
 *        Value that should be completed.
 *
 * @returns null or object
 *          If no completion valued could be computed, null is returned,
 *          otherwise a object with the following form is returned:
 *            {
 *              matches: [ string, string, string ],
 *              matchProp: Last part of the inputValue that was used to find
 *                         the matches-strings.
 *            }
 */
function JSPropertyProvider(aScope, aInputValue)
{
  let obj = unwrap(aScope);

  // Analyse the aInputValue and find the beginning of the last part that
  // should be completed.
  let beginning = findCompletionBeginning(aInputValue);

  // There was an error analysing the string.
  if (beginning.err) {
    return null;
  }

  // If the current state is not STATE_NORMAL, then we are inside of an string
  // which means that no completion is possible.
  if (beginning.state != STATE_NORMAL) {
    return null;
  }

  let completionPart = aInputValue.substring(beginning.startPos);

  // Don't complete on just an empty string.
  if (completionPart.trim() == "") {
    return null;
  }

  let properties = completionPart.split('.');
  let matchProp;
  if (properties.length > 1) {
    matchProp = properties.pop().trimLeft();
    for (let i = 0; i < properties.length; i++) {
      let prop = properties[i].trim();

      // If obj is undefined or null, then there is no chance to run completion
      // on it. Exit here.
      if (typeof obj === "undefined" || obj === null) {
        return null;
      }

      // Check if prop is a getter function on obj. Functions can change other
      // stuff so we can't execute them to get the next object. Stop here.
      if (isNonNativeGetter(obj, prop)) {
        return null;
      }
      try {
        obj = obj[prop];
      }
      catch (ex) {
        return null;
      }
    }
  }
  else {
    matchProp = properties[0].trimLeft();
  }

  // If obj is undefined or null, then there is no chance to run
  // completion on it. Exit here.
  if (typeof obj === "undefined" || obj === null) {
    return null;
  }

  // Skip Iterators and Generators.
  if (isIteratorOrGenerator(obj)) {
    return null;
  }

  let matches = [];
  for (let prop in obj) {
    if (prop.indexOf(matchProp) == 0) {
      matches.push(prop);
    }
  }

  return {
    matchProp: matchProp,
    matches: matches.sort(),
  };
}

function isIteratorOrGenerator(aObject)
{
  if (aObject === null) {
    return false;
  }

  if (typeof aObject == "object") {
    if (typeof aObject.__iterator__ == "function" ||
        aObject.constructor && aObject.constructor.name == "Iterator") {
      return true;
    }

    try {
      let str = aObject.toString();
      if (typeof aObject.next == "function" &&
          str.indexOf("[object Generator") == 0) {
        return true;
      }
    }
    catch (ex) {
      // window.history.next throws in the typeof check above.
      return false;
    }
  }

  return false;
}

//////////////////////////////////////////////////////////////////////////
// JSTerm
//////////////////////////////////////////////////////////////////////////

/**
 * JSTermHelper
 *
 * Defines a set of functions ("helper functions") that are available from the
 * WebConsole but not from the webpage.
 * A list of helper functions used by Firebug can be found here:
 *   http://getfirebug.com/wiki/index.php/Command_Line_API
 */
function JSTermHelper(aJSTerm)
{
  /**
   * Returns the result of document.getElementById(aId).
   *
   * @param string aId
   *        A string that is passed to window.document.getElementById.
   * @returns nsIDOMNode or null
   */
  aJSTerm.sandbox.$ = function JSTH_$(aId)
  {
    try {
      return aJSTerm._window.document.getElementById(aId);
    }
    catch (ex) {
      aJSTerm.console.error(ex.message);
    }
  };

  /**
   * Returns the result of document.querySelectorAll(aSelector).
   *
   * @param string aSelector
   *        A string that is passed to window.document.querySelectorAll.
   * @returns array of nsIDOMNode
   */
  aJSTerm.sandbox.$$ = function JSTH_$$(aSelector)
  {
    try {
      return aJSTerm._window.document.querySelectorAll(aSelector);
    }
    catch (ex) {
      aJSTerm.console.error(ex.message);
    }
  };

  /**
   * Runs a xPath query and returns all matched nodes.
   *
   * @param string aXPath
   *        xPath search query to execute.
   * @param [optional] nsIDOMNode aContext
   *        Context to run the xPath query on. Uses window.document if not set.
   * @returns array of nsIDOMNode
   */
  aJSTerm.sandbox.$x = function JSTH_$x(aXPath, aContext)
  {
    let nodes = [];
    let doc = aJSTerm._window.document;
    let aContext = aContext || doc;

    try {
      let results = doc.evaluate(aXPath, aContext, null,
                                  Ci.nsIDOMXPathResult.ANY_TYPE, null);

      let node;
      while (node = results.iterateNext()) {
        nodes.push(node);
      }
    }
    catch (ex) {
      aJSTerm.console.error(ex.message);
    }

    return nodes;
  };

  /**
   * Returns the currently selected object in the highlighter.
   *
   * @returns nsIDOMNode or null
   */
  Object.defineProperty(aJSTerm.sandbox, "$0", {
    get: function() {
      let mw = HUDService.currentContext();
      try {
        return mw.InspectorUI.selection;
      }
      catch (ex) {
        aJSTerm.console.error(ex.message);
      }
    },
    enumerable: true,
    configurable: false
  });

  /**
   * Clears the output of the JSTerm.
   */
  aJSTerm.sandbox.clear = function JSTH_clear()
  {
    aJSTerm.helperEvaluated = true;
    aJSTerm.clearOutput(true);
  };

  /**
   * Returns the result of Object.keys(aObject).
   *
   * @param object aObject
   *        Object to return the property names from.
   * @returns array of string
   */
  aJSTerm.sandbox.keys = function JSTH_keys(aObject)
  {
    try {
      return Object.keys(unwrap(aObject));
    }
    catch (ex) {
      aJSTerm.console.error(ex.message);
    }
  };

  /**
   * Returns the values of all properties on aObject.
   *
   * @param object aObject
   *        Object to display the values from.
   * @returns array of string
   */
  aJSTerm.sandbox.values = function JSTH_values(aObject)
  {
    let arrValues = [];
    let obj = unwrap(aObject);

    try {
      for (let prop in obj) {
        arrValues.push(obj[prop]);
      }
    }
    catch (ex) {
      aJSTerm.console.error(ex.message);
    }
    return arrValues;
  };

  /**
   * Opens a help window in MDC
   */
  aJSTerm.sandbox.help = function JSTH_help()
  {
    aJSTerm.helperEvaluated = true;
    aJSTerm._window.open(
        "https://developer.mozilla.org/AppLinks/WebConsoleHelp?locale=" +
        aJSTerm._window.navigator.language, "help", "");
  };

  /**
   * Inspects the passed aObject. This is done by opening the PropertyPanel.
   *
   * @param object aObject
   *        Object to inspect.
   * @returns void
   */
  aJSTerm.sandbox.inspect = function JSTH_inspect(aObject)
  {
    aJSTerm.helperEvaluated = true;
    let propPanel = aJSTerm.openPropertyPanel(null, unwrap(aObject));
    propPanel.panel.setAttribute("hudId", aJSTerm.hudId);
  };

  aJSTerm.sandbox.inspectrules = function JSTH_inspectrules(aNode)
  {
    aJSTerm.helperEvaluated = true;
    let doc = aJSTerm.inputNode.ownerDocument;
    let win = doc.defaultView;
    let panel = createElement(doc, "panel", {
      label: "CSS Rules",
      titlebar: "normal",
      noautofocus: "true",
      noautohide: "true",
      close: "true",
      width: 350,
      height: (win.screen.height / 2)
    });

    let iframe = createAndAppendElement(panel, "iframe", {
      src: "chrome://browser/content/devtools/cssruleview.xul",
      flex: "1",
    });

    panel.addEventListener("load", function onLoad() {
      panel.removeEventListener("load", onLoad, true);
      let doc = iframe.contentDocument;
      let view = new CssRuleView(doc);
      doc.documentElement.appendChild(view.element);
      view.highlight(aNode);
    }, true);

    let parent = doc.getElementById("mainPopupSet");
    parent.appendChild(panel);

    panel.addEventListener("popuphidden", function onHide() {
      panel.removeEventListener("popuphidden", onHide);
      parent.removeChild(panel);
    });

    let footer = createElement(doc, "hbox", { align: "end" });
    createAndAppendElement(footer, "spacer", { flex: 1});
    createAndAppendElement(footer, "resizer", { dir: "bottomend" });
    panel.appendChild(footer);

    let anchor = win.gBrowser.selectedBrowser;
    panel.openPopup(anchor, "end_before", 0, 0, false, false);

  }

  /**
   * Prints aObject to the output.
   *
   * @param object aObject
   *        Object to print to the output.
   * @returns void
   */
  aJSTerm.sandbox.pprint = function JSTH_pprint(aObject)
  {
    aJSTerm.helperEvaluated = true;
    if (aObject === null || aObject === undefined || aObject === true || aObject === false) {
      aJSTerm.console.error(HUDService.getStr("helperFuncUnsupportedTypeError"));
      return;
    }
    else if (typeof aObject === TYPEOF_FUNCTION) {
      aJSTerm.writeOutput(aObject + "\n", CATEGORY_OUTPUT, SEVERITY_LOG);
      return;
    }

    let output = [];
    let pairs = namesAndValuesOf(unwrap(aObject));

    pairs.forEach(function(pair) {
      output.push("  " + pair.display);
    });

    aJSTerm.writeOutput(output.join("\n"), CATEGORY_OUTPUT, SEVERITY_LOG);
  };

  /**
   * Print a string to the output, as-is.
   *
   * @param string aString
   *        A string you want to output.
   * @returns void
   */
  aJSTerm.sandbox.print = function JSTH_print(aString)
  {
    aJSTerm.helperEvaluated = true;
    aJSTerm.writeOutput("" + aString, CATEGORY_OUTPUT, SEVERITY_LOG);
  };
}

/**
 * JSTerm
 *
 * JavaScript Terminal: creates input nodes for console code interpretation
 * and 'JS Workspaces'
 */

/**
 * Create a JSTerminal or attach a JSTerm input node to an existing output node,
 * given by the parent node.
 *
 * @param object aContext
 *        Usually nsIDOMWindow, but doesn't have to be
 * @param nsIDOMNode aParentNode where to attach the JSTerm
 * @param object aMixin
 *        Gecko-app (or Jetpack) specific utility object
 * @param object aConsole
 *        Console object to use within the JSTerm.
 */
function JSTerm(aContext, aParentNode, aMixin, aConsole)
{
  // set the context, attach the UI by appending to aParentNode

  this.application = appName();
  this.context = aContext;
  this.parentNode = aParentNode;
  this.mixins = aMixin;
  this.console = aConsole;
  this.document = aParentNode.ownerDocument

  this.setTimeout = aParentNode.ownerDocument.defaultView.setTimeout;

  let node = aParentNode;
  while (!node.hasAttribute("id")) {
    node = node.parentNode;
  }
  this.hudId = node.getAttribute("id");

  this.historyIndex = 0;
  this.historyPlaceHolder = 0;  // this.history.length;
  this.log = LogFactory("*** JSTerm:");
  this.autocompletePopup = new AutocompletePopup(aParentNode.ownerDocument);
  this.autocompletePopup.onSelect = this.onAutocompleteSelect.bind(this);
  this.autocompletePopup.onClick = this.acceptProposedCompletion.bind(this);
  this.init();
}

JSTerm.prototype = {

  propertyProvider: JSPropertyProvider,

  COMPLETE_FORWARD: 0,
  COMPLETE_BACKWARD: 1,
  COMPLETE_HINT_ONLY: 2,

  init: function JST_init()
  {
    this.createSandbox();

    this.inputNode = this.mixins.inputNode;
    this.outputNode = this.mixins.outputNode;
    this.completeNode = this.mixins.completeNode;

    this._keyPress = this.keyPress.bind(this);
    this._inputEventHandler = this.inputEventHandler.bind(this);

    this.inputNode.addEventListener("keypress",
      this._keyPress, false);
    this.inputNode.addEventListener("input",
      this._inputEventHandler, false);
    this.inputNode.addEventListener("keyup",
      this._inputEventHandler, false);
  },

  get codeInputString()
  {
    return this.inputNode.value;
  },

  generateUI: function JST_generateUI()
  {
    this.mixins.generateUI();
  },

  attachUI: function JST_attachUI()
  {
    this.mixins.attachUI();
  },

  createSandbox: function JST_setupSandbox()
  {
    // create a JS Sandbox out of this.context
    this.sandbox = new Cu.Sandbox(this._window,
      { sandboxPrototype: this._window, wantXrays: false });
    this.sandbox.console = this.console;
    JSTermHelper(this);
  },

  get _window()
  {
    return this.context.get().QueryInterface(Ci.nsIDOMWindow);
  },

  /**
   * Evaluates a string in the sandbox.
   *
   * @param string aString
   *        String to evaluate in the sandbox.
   * @returns something
   *          The result of the evaluation.
   */
  evalInSandbox: function JST_evalInSandbox(aString)
  {
    // The help function needs to be easy to guess, so we make the () optional
    if (aString.trim() === "help" || aString.trim() === "?") {
      aString = "help()";
    }

    let window = unwrap(this.sandbox.window);
    let $ = null, $$ = null;

    // We prefer to execute the page-provided implementations for the $() and
    // $$() functions.
    if (typeof window.$ == "function") {
      $ = this.sandbox.$;
      delete this.sandbox.$;
    }
    if (typeof window.$$ == "function") {
      $$ = this.sandbox.$$;
      delete this.sandbox.$$;
    }

    let result = Cu.evalInSandbox(aString, this.sandbox, "1.8", "Web Console", 1);

    if ($) {
      this.sandbox.$ = $;
    }
    if ($$) {
      this.sandbox.$$ = $$;
    }

    return result;
  },


  execute: function JST_execute(aExecuteString)
  {
    // attempt to execute the content of the inputNode
    aExecuteString = aExecuteString || this.inputNode.value;
    if (!aExecuteString) {
      this.writeOutput("no value to execute", CATEGORY_OUTPUT, SEVERITY_LOG);
      return;
    }

    this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG);

    try {
      this.helperEvaluated = false;
      let result = this.evalInSandbox(aExecuteString);

      // Hide undefined results coming from helpers.
      let shouldShow = !(result === undefined && this.helperEvaluated);
      if (shouldShow) {
        let inspectable = this.isResultInspectable(result);
        let resultString = this.formatResult(result);

        if (inspectable) {
          this.writeOutputJS(aExecuteString, result, resultString);
        }
        else {
          this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG);
        }
      }
    }
    catch (ex) {
      this.writeOutput("" + ex, CATEGORY_OUTPUT, SEVERITY_ERROR);
    }

    this.history.push(aExecuteString);
    this.historyIndex++;
    this.historyPlaceHolder = this.history.length;
    this.setInputValue("");
    this.clearCompletion();
  },

  /**
   * Opens a new PropertyPanel. The panel has two buttons: "Update" reexecutes
   * the passed aEvalString and places the result inside of the tree. The other
   * button closes the panel.
   *
   * @param string aEvalString
   *        String that was used to eval the aOutputObject. Used as title
   *        and to update the tree content.
   * @param object aOutputObject
   *        Object to display/inspect inside of the tree.
   * @param nsIDOMNode aAnchor
   *        A node to popup the panel next to (using "after_pointer").
   * @returns object the created and opened propertyPanel.
   */
  openPropertyPanel: function JST_openPropertyPanel(aEvalString, aOutputObject,
                                                    aAnchor)
  {
    let self = this;
    let propPanel;
    // The property panel has one button:
    //    `Update`: reexecutes the string executed on the command line. The
    //    result will be inspected by this panel.
    let buttons = [];

    // If there is a evalString passed to this function, then add a `Update`
    // button to the panel so that the evalString can be reexecuted to update
    // the content of the panel.
    if (aEvalString !== null) {
      buttons.push({
        label: HUDService.getStr("update.button"),
        accesskey: HUDService.getStr("update.accesskey"),
        oncommand: function () {
          try {
            var result = self.evalInSandbox(aEvalString);

            if (result !== undefined) {
              // TODO: This updates the value of the tree.
              // However, the states of opened nodes is not saved.
              // See bug 586246.
              propPanel.treeView.data = result;
            }
          }
          catch (ex) {
            self.console.error(ex);
          }
        }
      });
    }

    let doc = self.document;
    let parent = doc.getElementById("mainPopupSet");
    let title = (aEvalString
        ? HUDService.getFormatStr("jsPropertyInspectTitle", [aEvalString])
        : HUDService.getStr("jsPropertyTitle"));

    propPanel = new PropertyPanel(parent, doc, title, aOutputObject, buttons);
    propPanel.linkNode = aAnchor;

    let panel = propPanel.panel;
    panel.openPopup(aAnchor, "after_pointer", 0, 0, false, false);
    panel.sizeTo(350, 450);
    return propPanel;
  },

  /**
   * Writes a JS object to the JSTerm outputNode. If the user clicks on the
   * written object, openPropertyPanel is called to open up a panel to inspect
   * the object.
   *
   * @param string aEvalString
   *        String that was evaluated to get the aOutputObject.
   * @param object aResultObject
   *        The evaluation result object.
   * @param object aOutputString
   *        The output string to be written to the outputNode.
   */
  writeOutputJS: function JST_writeOutputJS(aEvalString, aOutputObject, aOutputString)
  {
    let node = ConsoleUtils.createMessageNode(this.document,
                                              CATEGORY_OUTPUT,
                                              SEVERITY_LOG,
                                              aOutputString,
                                              this.hudId);

    let linkNode = node.querySelector(".webconsole-msg-body");

    linkNode.classList.add("hud-clickable");
    linkNode.setAttribute("aria-haspopup", "true");

    // Make the object bring up the property panel.
    node.addEventListener("mousedown", function(aEvent) {
      this._startX = aEvent.clientX;
      this._startY = aEvent.clientY;
    }, false);

    let self = this;
    node.addEventListener("click", function(aEvent) {
      if (aEvent.detail != 1 || aEvent.button != 0 ||
          (this._startX != aEvent.clientX &&
           this._startY != aEvent.clientY)) {
        return;
      }

      if (!this._panelOpen) {
        let propPanel = self.openPropertyPanel(aEvalString, aOutputObject, this);
        propPanel.panel.setAttribute("hudId", self.hudId);
        this._panelOpen = true;
      }
    }, false);

    ConsoleUtils.outputMessageNode(node, this.hudId);
  },

  /**
   * Writes a message to the HUD that originates from the interactive
   * JavaScript console.
   *
   * @param string aOutputMessage
   *        The message to display.
   * @param number aCategory
   *        The category of message: one of the CATEGORY_ constants.
   * @param number aSeverity
   *        The severity of message: one of the SEVERITY_ constants.
   * @returns void
   */
  writeOutput: function JST_writeOutput(aOutputMessage, aCategory, aSeverity)
  {
    let node = ConsoleUtils.createMessageNode(this.document,
                                              aCategory, aSeverity,
                                              aOutputMessage, this.hudId);

    ConsoleUtils.outputMessageNode(node, this.hudId);
  },

  /**
   * Format the jsterm execution result based on its type.
   *
   * @param mixed aResult
   *        The evaluation result object you want displayed.
   * @returns string
   *          The string that can be displayed.
   */
  formatResult: function JST_formatResult(aResult)
  {
    let output = "";
    let type = this.getResultType(aResult);

    switch (type) {
      case "string":
        output = this.formatString(aResult);
        break;
      case "boolean":
      case "date":
      case "error":
      case "number":
      case "regexp":
        output = aResult.toString();
        break;
      case "null":
      case "undefined":
        output = type;
        break;
      default:
        if (aResult.toSource) {
          try {
            output = aResult.toSource();
          } catch (ex) { }
        }
        if (!output || output == "({})") {
          output = aResult.toString();
        }
        break;
    }

    return output;
  },

  /**
   * Format a string for output.
   *
   * @param string aString
   *        The string you want to display.
   * @returns string
   *          The string that can be displayed.
   */
  formatString: function JST_formatString(aString)
  {
    function isControlCode(c) {
      // See http://en.wikipedia.org/wiki/C0_and_C1_control_codes
      // C0 is 0x00-0x1F, C1 is 0x80-0x9F (inclusive).
      // We also include DEL (U+007F) and NBSP (U+00A0), which are not strictly
      // in C1 but border it.
      return (c <= 0x1F) || (0x7F <= c && c <= 0xA0);
    }

    function replaceFn(aMatch, aType, aHex) {
      // Leave control codes escaped, but unescape the rest of the characters.
      let c = parseInt(aHex, 16);
      return isControlCode(c) ? aMatch : String.fromCharCode(c);
    }

    let output = uneval(aString).replace(/\\(x)([0-9a-fA-F]{2})/g, replaceFn)
                 .replace(/\\(u)([0-9a-fA-F]{4})/g, replaceFn);

    return output;
  },

  /**
   * Determine if the jsterm execution result is inspectable or not.
   *
   * @param mixed aResult
   *        The evaluation result object you want to check if it is inspectable.
   * @returns boolean
   *          True if the object is inspectable or false otherwise.
   */
  isResultInspectable: function JST_isResultInspectable(aResult)
  {
    let isEnumerable = false;

    // Skip Iterators and Generators.
    if (isIteratorOrGenerator(aResult)) {
      return false;
    }

    for (let p in aResult) {
      isEnumerable = true;
      break;
    }

    return isEnumerable && typeof(aResult) != "string";
  },

  /**
   * Determine the type of the jsterm execution result.
   *
   * @param mixed aResult
   *        The evaluation result object you want to check.
   * @returns string
   *          Constructor name or type: string, number, boolean, regexp, date,
   *          function, object, null, undefined...
   */
  getResultType: function JST_getResultType(aResult)
  {
    let type = aResult === null ? "null" : typeof aResult;
    if (type == "object" && aResult.constructor && aResult.constructor.name) {
      type = aResult.constructor.name;
    }

    return type.toLowerCase();
  },

  /**
   * Clear the Web Console output.
   *
   * @param boolean aClearStorage
   *        True if you want to clear the console messages storage associated to
   *        this Web Console.
   */
  clearOutput: function JST_clearOutput(aClearStorage)
  {
    let hud = HUDService.getHudReferenceById(this.hudId);
    hud.cssNodes = {};

    let node = hud.outputNode;
    while (node.firstChild) {
      if (node.firstChild.classList &&
          node.firstChild.classList.contains("webconsole-msg-inspector")) {
        hud.pruneConsoleDirNode(node.firstChild);
      }
      else {
        hud.outputNode.removeChild(node.firstChild);
      }
    }

    hud.HUDBox.lastTimestamp = 0;
    hud.groupDepth = 0;

    if (aClearStorage) {
      let windowId = HUDService.getInnerWindowId(hud.contentWindow);
      gConsoleStorage.clearEvents(windowId);
    }
  },

  /**
   * Updates the size of the input field (command line) to fit its contents.
   *
   * @returns void
   */
  resizeInput: function JST_resizeInput()
  {
    let inputNode = this.inputNode;

    // Reset the height so that scrollHeight will reflect the natural height of
    // the contents of the input field.
    inputNode.style.height = "auto";

    // Now resize the input field to fit its contents.
    let scrollHeight = inputNode.inputField.scrollHeight;
    if (scrollHeight > 0) {
      inputNode.style.height = scrollHeight + "px";
    }
  },

  /**
   * Sets the value of the input field (command line), and resizes the field to
   * fit its contents. This method is preferred over setting "inputNode.value"
   * directly, because it correctly resizes the field.
   *
   * @param string aNewValue
   *        The new value to set.
   * @returns void
   */
  setInputValue: function JST_setInputValue(aNewValue)
  {
    this.inputNode.value = aNewValue;
    this.lastInputValue = aNewValue;
    this.completeNode.value = "";
    this.resizeInput();
  },

  /**
   * The inputNode "input" and "keyup" event handler.
   *
   * @param nsIDOMEvent aEvent
   */
  inputEventHandler: function JSTF_inputEventHandler(aEvent)
  {
    if (this.lastInputValue != this.inputNode.value) {
      this.resizeInput();
      this.complete(this.COMPLETE_HINT_ONLY);
      this.lastInputValue = this.inputNode.value;
    }
  },

  /**
   * The inputNode "keypress" event handler.
   *
   * @param nsIDOMEvent aEvent
   */
  keyPress: function JSTF_keyPress(aEvent)
  {
    if (aEvent.ctrlKey) {
      switch (aEvent.charCode) {
        case 97:
          // control-a
          this.inputNode.setSelectionRange(0, 0);
          aEvent.preventDefault();
          break;
        case 101:
          // control-e
          this.inputNode.setSelectionRange(this.inputNode.value.length,
                                           this.inputNode.value.length);
          aEvent.preventDefault();
          break;
        default:
          break;
      }
      return;
    }
    else if (aEvent.shiftKey &&
        aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
      // shift return
      // TODO: expand the inputNode height by one line
      return;
    }

    let inputUpdated = false;

    switch(aEvent.keyCode) {
      case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE:
        if (this.autocompletePopup.isOpen) {
          this.clearCompletion();
          aEvent.preventDefault();
        }
        break;

      case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
        if (this.autocompletePopup.isOpen && this.autocompletePopup.selectedIndex > -1) {
          this.acceptProposedCompletion();
        }
        else {
          this.execute();
        }
        aEvent.preventDefault();
        break;

      case Ci.nsIDOMKeyEvent.DOM_VK_UP:
        if (this.autocompletePopup.isOpen) {
          inputUpdated = this.complete(this.COMPLETE_BACKWARD);
        }
        else if (this.canCaretGoPrevious()) {
          inputUpdated = this.historyPeruse(HISTORY_BACK);
        }
        if (inputUpdated) {
          aEvent.preventDefault();
        }
        break;

      case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
        if (this.autocompletePopup.isOpen) {
          inputUpdated = this.complete(this.COMPLETE_FORWARD);
        }
        else if (this.canCaretGoNext()) {
          inputUpdated = this.historyPeruse(HISTORY_FORWARD);
        }
        if (inputUpdated) {
          aEvent.preventDefault();
        }
        break;

      case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
        // Generate a completion and accept the first proposed value.
        if (this.complete(this.COMPLETE_HINT_ONLY) &&
            this.lastCompletion &&
            this.acceptProposedCompletion()) {
          aEvent.preventDefault();
        }
        else {
          this.updateCompleteNode(HUDService.getStr("Autocomplete.blank"));
          aEvent.preventDefault();
        }
        break;

      default:
        break;
    }
  },

  /**
   * Go up/down the history stack of input values.
   *
   * @param number aDirection
   *        History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
   *
   * @returns boolean
   *          True if the input value changed, false otherwise.
   */
  historyPeruse: function JST_historyPeruse(aDirection)
  {
    if (!this.history.length) {
      return false;
    }

    // Up Arrow key
    if (aDirection == HISTORY_BACK) {
      if (this.historyPlaceHolder <= 0) {
        return false;
      }

      let inputVal = this.history[--this.historyPlaceHolder];
      if (inputVal){
        this.setInputValue(inputVal);
      }
    }
    // Down Arrow key
    else if (aDirection == HISTORY_FORWARD) {
      if (this.historyPlaceHolder == this.history.length - 1) {
        this.historyPlaceHolder ++;
        this.setInputValue("");
      }
      else if (this.historyPlaceHolder >= (this.history.length)) {
        return false;
      }
      else {
        let inputVal = this.history[++this.historyPlaceHolder];
        if (inputVal){
          this.setInputValue(inputVal);
        }
      }
    }
    else {
      throw new Error("Invalid argument 0");
    }

    return true;
  },

  refocus: function JSTF_refocus()
  {
    this.inputNode.blur();
    this.inputNode.focus();
  },

  /**
   * Check if the caret is at a location that allows selecting the previous item
   * in history when the user presses the Up arrow key.
   *
   * @return boolean
   *         True if the caret is at a location that allows selecting the
   *         previous item in history when the user presses the Up arrow key,
   *         otherwise false.
   */
  canCaretGoPrevious: function JST_canCaretGoPrevious()
  {
    let node = this.inputNode;
    if (node.selectionStart != node.selectionEnd) {
      return false;
    }

    let multiline = /[\r\n]/.test(node.value);
    return node.selectionStart == 0 ? true :
           node.selectionStart == node.value.length && !multiline;
  },

  /**
   * Check if the caret is at a location that allows selecting the next item in
   * history when the user presses the Down arrow key.
   *
   * @return boolean
   *         True if the caret is at a location that allows selecting the next
   *         item in history when the user presses the Down arrow key, otherwise
   *         false.
   */
  canCaretGoNext: function JST_canCaretGoNext()
  {
    let node = this.inputNode;
    if (node.selectionStart != node.selectionEnd) {
      return false;
    }

    let multiline = /[\r\n]/.test(node.value);
    return node.selectionStart == node.value.length ? true :
           node.selectionStart == 0 && !multiline;
  },

  history: [],

  // Stores the data for the last completion.
  lastCompletion: null,

  /**
   * Completes the current typed text in the inputNode. Completion is performed
   * only if the selection/cursor is at the end of the string. If no completion
   * is found, the current inputNode value and cursor/selection stay.
   *
   * @param int type possible values are
   *    - this.COMPLETE_FORWARD: If there is more than one possible completion
   *          and the input value stayed the same compared to the last time this
   *          function was called, then the next completion of all possible
   *          completions is used. If the value changed, then the first possible
   *          completion is used and the selection is set from the current
   *          cursor position to the end of the completed text.
   *          If there is only one possible completion, then this completion
   *          value is used and the cursor is put at the end of the completion.
   *    - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the
   *          value stayed the same as the last time the function was called,
   *          then the previous completion of all possible completions is used.
   *    - this.COMPLETE_HINT_ONLY: If there is more than one possible
   *          completion and the input value stayed the same compared to the
   *          last time this function was called, then the same completion is
   *          used again. If there is only one possible completion, then
   *          the inputNode.value is set to this value and the selection is set
   *          from the current cursor position to the end of the completed text.
   *
   * @returns boolean true if there existed a completion for the current input,
   *          or false otherwise.
   */
  complete: function JSTF_complete(type)
  {
    let inputNode = this.inputNode;
    let inputValue = inputNode.value;
    // If the inputNode has no value, then don't try to complete on it.
    if (!inputValue) {
      this.clearCompletion();
      return false;
    }

    // Only complete if the selection is empty and at the end of the input.
    if (inputNode.selectionStart == inputNode.selectionEnd &&
        inputNode.selectionEnd != inputValue.length) {
      this.clearCompletion();
      return false;
    }

    let popup = this.autocompletePopup;

    if (!this.lastCompletion || this.lastCompletion.value != inputValue) {
      let properties = this.propertyProvider(this.sandbox.window, inputValue);
      if (!properties || !properties.matches.length) {
        this.clearCompletion();
        return false;
      }

      let items = properties.matches.map(function(aMatch) {
        return {label: aMatch};
      });
      popup.setItems(items);
      this.lastCompletion = {value: inputValue,
                             matchProp: properties.matchProp};

      if (items.length > 1 && !popup.isOpen) {
        popup.openPopup(this.inputNode);
      }
      else if (items.length < 2 && popup.isOpen) {
        popup.hidePopup();
      }

      if (items.length == 1) {
          popup.selectedIndex = 0;
      }
      this.onAutocompleteSelect();
    }

    let accepted = false;

    if (type != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
      this.acceptProposedCompletion();
      accepted = true;
    }
    else if (type == this.COMPLETE_BACKWARD) {
      this.autocompletePopup.selectPreviousItem();
    }
    else if (type == this.COMPLETE_FORWARD) {
      this.autocompletePopup.selectNextItem();
    }

    return accepted || popup.itemCount > 0;
  },

  onAutocompleteSelect: function JSTF_onAutocompleteSelect()
  {
    let currentItem = this.autocompletePopup.selectedItem;
    if (currentItem && this.lastCompletion) {
      let suffix = currentItem.label.substring(this.lastCompletion.
                                               matchProp.length);
      this.updateCompleteNode(suffix);
    }
    else {
      this.updateCompleteNode("");
    }
  },

  /**
   * Clear the current completion information and close the autocomplete popup,
   * if needed.
   */
  clearCompletion: function JSTF_clearCompletion()
  {
    this.autocompletePopup.clearItems();
    this.lastCompletion = null;
    this.updateCompleteNode("");
    if (this.autocompletePopup.isOpen) {
      this.autocompletePopup.hidePopup();
    }
  },

  /**
   * Accept the proposed input completion.
   *
   * @return boolean
   *         True if there was a selected completion item and the input value
   *         was updated, false otherwise.
   */
  acceptProposedCompletion: function JSTF_acceptProposedCompletion()
  {
    let updated = false;

    let currentItem = this.autocompletePopup.selectedItem;
    if (currentItem && this.lastCompletion) {
      let suffix = currentItem.label.substring(this.lastCompletion.
                                               matchProp.length);
      this.setInputValue(this.inputNode.value + suffix);
      updated = true;
    }

    this.clearCompletion();

    return updated;
  },

  /**
   * Update the node that displays the currently selected autocomplete proposal.
   *
   * @param string aSuffix
   *        The proposed suffix for the inputNode value.
   */
  updateCompleteNode: function JSTF_updateCompleteNode(aSuffix)
  {
    // completion prefix = input, with non-control chars replaced by spaces
    let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : "";
    this.completeNode.value = prefix + aSuffix;
  },

  /**
   * Destroy the JSTerm object. Call this method to avoid memory leaks.
   */
  destroy: function JST_destroy()
  {
    this.inputNode.removeEventListener("keypress", this._keyPress, false);
    this.inputNode.removeEventListener("input", this._inputEventHandler, false);
    this.inputNode.removeEventListener("keyup", this._inputEventHandler, false);
  },
};

/**
 * Generates and attaches the JS Terminal part of the Web Console, which
 * essentially consists of the interactive JavaScript input facility.
 *
 * @param nsWeakPtr<nsIDOMWindow> aContext
 *        A weak pointer to the DOM window that contains the Web Console.
 * @param nsIDOMNode aParentNode
 *        The Web Console wrapper node.
 * @param nsIDOMNode aExistingConsole
 *        The Web Console output node.
 * @return void
 */
function
JSTermFirefoxMixin(aContext,
                   aParentNode,
                   aExistingConsole)
{
  // aExisting Console is the existing outputNode to use in favor of
  // creating a new outputNode - this is so we can just attach the inputNode to
  // a normal HeadsUpDisplay console output, and re-use code.
  this.context = aContext;
  this.parentNode = aParentNode;
  this.existingConsoleNode = aExistingConsole;
  this.setTimeout = aParentNode.ownerDocument.defaultView.setTimeout;

  if (aParentNode.ownerDocument) {
    this.xulElementFactory =
      NodeFactory("xul", "xul", aParentNode.ownerDocument);

    this.textFactory = NodeFactory("text", "xul", aParentNode.ownerDocument);
    this.generateUI();
    this.attachUI();
  }
  else {
    throw new Error("aParentNode should be a DOM node with an ownerDocument property ");
  }
}

JSTermFirefoxMixin.prototype = {
  /**
   * Generates and attaches the UI for an entire JS Workspace or
   * just the input node used under the console output
   *
   * @returns void
   */
  generateUI: function JSTF_generateUI()
  {
    this.completeNode = this.xulElementFactory("textbox");
    this.completeNode.setAttribute("class", "jsterm-complete-node");
    this.completeNode.setAttribute("multiline", "true");
    this.completeNode.setAttribute("rows", "1");
    this.completeNode.setAttribute("tabindex", "-1");

    this.inputNode = this.xulElementFactory("textbox");
    this.inputNode.setAttribute("class", "jsterm-input-node");
    this.inputNode.setAttribute("multiline", "true");
    this.inputNode.setAttribute("rows", "1");

    let inputStack = this.xulElementFactory("stack");
    inputStack.setAttribute("class", "jsterm-stack-node");
    inputStack.setAttribute("flex", "1");
    inputStack.appendChild(this.completeNode);
    inputStack.appendChild(this.inputNode);

    if (this.existingConsoleNode == undefined) {
      throw new Error("This can't happen");
    }

    this.outputNode = this.existingConsoleNode;

    this.term = this.xulElementFactory("hbox");
    this.term.setAttribute("class", "jsterm-input-container");
    this.term.setAttribute("style", "direction: ltr;");
    this.term.appendChild(inputStack);
  },

  get inputValue()
  {
    return this.inputNode.value;
  },

  attachUI: function JSTF_attachUI()
  {
    this.parentNode.appendChild(this.term);
  }
};

/**
 * Firefox-specific Application Hooks.
 * Each Gecko-based application will need an object like this in
 * order to use the Heads Up Display
 */
function FirefoxApplicationHooks()
{ }

FirefoxApplicationHooks.prototype = {
  /**
   * gets the current contentWindow (Firefox-specific)
   *
   * @returns nsIDOMWindow
   */
  getCurrentContext: function FAH_getCurrentContext()
  {
    return Services.wm.getMostRecentWindow("navigator:browser");
  },
};

//////////////////////////////////////////////////////////////////////////////
// Utility functions used by multiple callers
//////////////////////////////////////////////////////////////////////////////

/**
 * ConsoleUtils: a collection of globally used functions
 *
 */

ConsoleUtils = {
  /**
   * Flag to turn on and off scrolling.
   */
  scroll: true,

  supString: function ConsoleUtils_supString(aString)
  {
    let str = Cc["@mozilla.org/supports-string;1"].
      createInstance(Ci.nsISupportsString);
    str.data = aString;
    return str;
  },

  /**
   * Generates a millisecond resolution timestamp.
   *
   * @returns integer
   */
  timestamp: function ConsoleUtils_timestamp()
  {
    return Date.now();
  },

  /**
   * Generates a formatted timestamp string for displaying in console messages.
   *
   * @param integer [ms] Optional, allows you to specify the timestamp in
   * milliseconds since the UNIX epoch.
   * @returns string The timestamp formatted for display.
   */
  timestampString: function ConsoleUtils_timestampString(ms)
  {
    var d = new Date(ms ? ms : null);
    let hours = d.getHours(), minutes = d.getMinutes();
    let seconds = d.getSeconds(), milliseconds = d.getMilliseconds();
    let parameters = [ hours, minutes, seconds, milliseconds ];
    return HUDService.getFormatStr("timestampFormat", parameters);
  },

  /**
   * Scrolls a node so that it's visible in its containing XUL "scrollbox"
   * element.
   *
   * @param nsIDOMNode aNode
   *        The node to make visible.
   * @returns void
   */
  scrollToVisible: function ConsoleUtils_scrollToVisible(aNode) {
    if (!this.scroll) {
      return;
    }

    // Find the enclosing richlistbox node.
    let richListBoxNode = aNode.parentNode;
    while (richListBoxNode.tagName != "richlistbox") {
      richListBoxNode = richListBoxNode.parentNode;
    }

    // Use the scroll box object interface to ensure the element is visible.
    let boxObject = richListBoxNode.scrollBoxObject;
    let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject);
    nsIScrollBoxObject.ensureElementIsVisible(aNode);
  },

  /**
   * Given a category and message body, creates a DOM node to represent an
   * incoming message. The timestamp is automatically added.
   *
   * @param nsIDOMDocument aDocument
   *        The document in which to create the node.
   * @param number aCategory
   *        The category of the message: one of the CATEGORY_* constants.
   * @param number aSeverity
   *        The severity of the message: one of the SEVERITY_* constants;
   * @param string|nsIDOMNode aBody
   *        The body of the message, either a simple string or a DOM node.
   * @param number aHUDId
   *        The HeadsUpDisplay ID.
   * @param string aSourceURL [optional]
   *        The URL of the source file that emitted the error.
   * @param number aSourceLine [optional]
   *        The line number on which the error occurred. If zero or omitted,
   *        there is no line number associated with this message.
   * @param string aClipboardText [optional]
   *        The text that should be copied to the clipboard when this node is
   *        copied. If omitted, defaults to the body text. If `aBody` is not
   *        a string, then the clipboard text must be supplied.
   * @param number aLevel [optional]
   *        The level of the console API message.
   * @param number aTimeStamp [optional]
   *        The timestamp to use for this message node. If omitted, the current
   *        date and time is used.
   * @return nsIDOMNode
   *         The message node: a XUL richlistitem ready to be inserted into
   *         the Web Console output node.
   */
  createMessageNode:
  function ConsoleUtils_createMessageNode(aDocument, aCategory, aSeverity,
                                          aBody, aHUDId, aSourceURL,
                                          aSourceLine, aClipboardText, aLevel,
                                          aTimeStamp) {
    if (typeof aBody != "string" && aClipboardText == null && aBody.innerText) {
      aClipboardText = aBody.innerText;
    }

    // Make the icon container, which is a vertical box. Its purpose is to
    // ensure that the icon stays anchored at the top of the message even for
    // long multi-line messages.
    let iconContainer = aDocument.createElementNS(XUL_NS, "vbox");
    iconContainer.classList.add("webconsole-msg-icon-container");
    // Apply the curent group by indenting appropriately.
    let hud = HUDService.getHudReferenceById(aHUDId);
    iconContainer.style.marginLeft = hud.groupDepth * GROUP_INDENT + "px";

    // Make the icon node. It's sprited and the actual region of the image is
    // determined by CSS rules.
    let iconNode = aDocument.createElementNS(XUL_NS, "image");
    iconNode.classList.add("webconsole-msg-icon");
    iconContainer.appendChild(iconNode);

    // Make the spacer that positions the icon.
    let spacer = aDocument.createElementNS(XUL_NS, "spacer");
    spacer.setAttribute("flex", "1");
    iconContainer.appendChild(spacer);

    // Create the message body, which contains the actual text of the message.
    let bodyNode = aDocument.createElementNS(XUL_NS, "description");
    bodyNode.setAttribute("flex", "1");
    bodyNode.classList.add("webconsole-msg-body");

    // Store the body text, since it is needed later for the property tree
    // case.
    let body = aBody;
    // If a string was supplied for the body, turn it into a DOM node and an
    // associated clipboard string now.
    aClipboardText = aClipboardText ||
                     (aBody + (aSourceURL ? " @ " + aSourceURL : "") +
                              (aSourceLine ? ":" + aSourceLine : ""));
    aBody = aBody instanceof Ci.nsIDOMNode && !(aLevel == "dir") ?
            aBody : aDocument.createTextNode(aBody);

    if (!aBody.nodeType) {
      aBody = aDocument.createTextNode(aBody.toString());
    }
    if (typeof aBody == "string") {
      aBody = aDocument.createTextNode(aBody);
    }

    bodyNode.appendChild(aBody);

    let repeatContainer = aDocument.createElementNS(XUL_NS, "hbox");
    repeatContainer.setAttribute("align", "start");
    let repeatNode = aDocument.createElementNS(XUL_NS, "label");
    repeatNode.setAttribute("value", "1");
    repeatNode.classList.add("webconsole-msg-repeat");
    repeatContainer.appendChild(repeatNode);

    // Create the timestamp.
    let timestampNode = aDocument.createElementNS(XUL_NS, "label");
    timestampNode.classList.add("webconsole-timestamp");
    let timestamp = aTimeStamp || ConsoleUtils.timestamp();
    let timestampString = ConsoleUtils.timestampString(timestamp);
    timestampNode.setAttribute("value", timestampString);

    // Create the source location (e.g. www.example.com:6) that sits on the
    // right side of the message, if applicable.
    let locationNode;
    if (aSourceURL) {
      locationNode = this.createLocationNode(aDocument, aSourceURL,
                                             aSourceLine);
    }

    // Create the containing node and append all its elements to it.
    let node = aDocument.createElementNS(XUL_NS, "richlistitem");
    node.clipboardText = aClipboardText;
    node.classList.add("hud-msg-node");

    node.timestamp = timestamp;
    ConsoleUtils.setMessageType(node, aCategory, aSeverity);

    node.appendChild(timestampNode);
    node.appendChild(iconContainer);
    // Display the object tree after the message node.
    if (aLevel == "dir") {
      // Make the body container, which is a vertical box, for grouping the text
      // and tree widgets.
      let bodyContainer = aDocument.createElement("vbox");
      bodyContainer.setAttribute("flex", "1");
      bodyContainer.appendChild(bodyNode);
      // Create the tree.
      let tree = createElement(aDocument, "tree", {
        flex: 1,
        hidecolumnpicker: "true"
      });

      let treecols = aDocument.createElement("treecols");
      let treecol = createElement(aDocument, "treecol", {
        primary: "true",
        flex: 1,
        hideheader: "true",
        ignoreincolumnpicker: "true"
      });
      treecols.appendChild(treecol);
      tree.appendChild(treecols);

      tree.appendChild(aDocument.createElement("treechildren"));

      bodyContainer.appendChild(tree);
      node.appendChild(bodyContainer);
      node.classList.add("webconsole-msg-inspector");
      // Create the treeView object.
      let treeView = node.propertyTreeView = new PropertyTreeView();
      treeView.data = body;
      tree.setAttribute("rows", treeView.rowCount);
    }
    else {
      node.appendChild(bodyNode);
    }
    node.appendChild(repeatContainer);
    if (locationNode) {
      node.appendChild(locationNode);
    }

    node.setAttribute("id", "console-msg-" + HUDService.sequenceId());

    return node;
  },

  /**
   * Adjusts the category and severity of the given message, clearing the old
   * category and severity if present.
   *
   * @param nsIDOMNode aMessageNode
   *        The message node to alter.
   * @param number aNewCategory
   *        The new category for the message; one of the CATEGORY_ constants.
   * @param number aNewSeverity
   *        The new severity for the message; one of the SEVERITY_ constants.
   * @return void
   */
  setMessageType:
  function ConsoleUtils_setMessageType(aMessageNode, aNewCategory,
                                       aNewSeverity) {
    // Remove the old CSS classes, if applicable.
    if ("category" in aMessageNode) {
      let oldCategory = aMessageNode.category;
      let oldSeverity = aMessageNode.severity;
      aMessageNode.classList.remove("webconsole-msg-" +
                                    CATEGORY_CLASS_FRAGMENTS[oldCategory]);
      aMessageNode.classList.remove("webconsole-msg-" +
                                    SEVERITY_CLASS_FRAGMENTS[oldSeverity]);
      let key = "hud-" + MESSAGE_PREFERENCE_KEYS[oldCategory][oldSeverity];
      aMessageNode.classList.remove(key);
    }

    // Add in the new CSS classes.
    aMessageNode.category = aNewCategory;
    aMessageNode.severity = aNewSeverity;
    aMessageNode.classList.add("webconsole-msg-" +
                               CATEGORY_CLASS_FRAGMENTS[aNewCategory]);
    aMessageNode.classList.add("webconsole-msg-" +
                               SEVERITY_CLASS_FRAGMENTS[aNewSeverity]);
    let key = "hud-" + MESSAGE_PREFERENCE_KEYS[aNewCategory][aNewSeverity];
    aMessageNode.classList.add(key);
  },

  /**
   * Creates the XUL label that displays the textual location of an incoming
   * message.
   *
   * @param nsIDOMDocument aDocument
   *        The document in which to create the node.
   * @param string aSourceURL
   *        The URL of the source file responsible for the error.
   * @param number aSourceLine [optional]
   *        The line number on which the error occurred. If zero or omitted,
   *        there is no line number associated with this message.
   * @return nsIDOMNode
   *         The new XUL label node, ready to be added to the message node.
   */
  createLocationNode:
  function ConsoleUtils_createLocationNode(aDocument, aSourceURL,
                                           aSourceLine) {
    let locationNode = aDocument.createElementNS(XUL_NS, "label");

    // Create the text, which consists of an abbreviated version of the URL
    // plus an optional line number.
    let text = ConsoleUtils.abbreviateSourceURL(aSourceURL);
    if (aSourceLine) {
      text += ":" + aSourceLine;
    }
    locationNode.setAttribute("value", text);

    // Style appropriately.
    locationNode.setAttribute("crop", "center");
    locationNode.setAttribute("title", aSourceURL);
    locationNode.classList.add("webconsole-location");
    locationNode.classList.add("text-link");

    // Make the location clickable.
    locationNode.addEventListener("click", function() {
      if (aSourceURL == "Scratchpad") {
        let win = Services.wm.getMostRecentWindow("devtools:scratchpad");
        if (win) {
          win.focus();
        }
        return;
      }
      let viewSourceUtils = aDocument.defaultView.gViewSourceUtils;
      viewSourceUtils.viewSource(aSourceURL, null, aDocument, aSourceLine);
    }, true);

    return locationNode;
  },

  /**
   * Applies the user's filters to a newly-created message node via CSS
   * classes.
   *
   * @param nsIDOMNode aNode
   *        The newly-created message node.
   * @param string aHUDId
   *        The ID of the HUD which this node is to be inserted into.
   */
  filterMessageNode: function ConsoleUtils_filterMessageNode(aNode, aHUDId) {
    // Filter by the message type.
    let prefKey = MESSAGE_PREFERENCE_KEYS[aNode.category][aNode.severity];
    if (prefKey && !HUDService.getFilterState(aHUDId, prefKey)) {
      // The node is filtered by type.
      aNode.classList.add("hud-filtered-by-type");
    }

    // Filter on the search string.
    let search = HUDService.getFilterStringByHUDId(aHUDId);
    let text = aNode.clipboardText;

    // if string matches the filter text
    if (!HUDService.stringMatchesFilters(text, search)) {
      aNode.classList.add("hud-filtered-by-string");
    }
  },

  /**
   * Merge the attributes of the two nodes that are about to be filtered.
   * Increment the number of repeats of aOriginal.
   *
   * @param nsIDOMNode aOriginal
   *        The Original Node. The one being merged into.
   * @param nsIDOMNode aFiltered
   *        The node being filtered out because it is repeated.
   */
  mergeFilteredMessageNode:
  function ConsoleUtils_mergeFilteredMessageNode(aOriginal, aFiltered) {
    // childNodes[3].firstChild is the node containing the number of repetitions
    // of a node.
    let repeatNode = aOriginal.childNodes[3].firstChild;
    if (!repeatNode) {
      return aOriginal; // no repeat node, return early.
    }

    let occurrences = parseInt(repeatNode.getAttribute("value")) + 1;
    repeatNode.setAttribute("value", occurrences);
  },

  /**
   * Filter the css node from the output node if it is a repeat. CSS messages
   * are merged with previous messages if they occurred in the past.
   *
   * @param nsIDOMNode aNode
   *        The message node to be filtered or not.
   * @param nsIDOMNode aOutput
   *        The outputNode of the HUD.
   * @returns boolean
   *         true if the message is filtered, false otherwise.
   */
  filterRepeatedCSS:
  function ConsoleUtils_filterRepeatedCSS(aNode, aOutput, aHUDId) {
    let hud = HUDService.getHudReferenceById(aHUDId);

    // childNodes[2] is the description node containing the text of the message.
    let description = aNode.childNodes[2].textContent;
    let location;

    // childNodes[4] represents the location (source URL) of the error message.
    // The full source URL is stored in the title attribute.
    if (aNode.childNodes[4]) {
      // browser_webconsole_bug_595934_message_categories.js
      location = aNode.childNodes[4].getAttribute("title");
    }
    else {
      location = "";
    }

    let dupe = hud.cssNodes[description + location];
    if (!dupe) {
      // no matching nodes
      hud.cssNodes[description + location] = aNode;
      return false;
    }

    this.mergeFilteredMessageNode(dupe, aNode);

    return true;
  },

  /**
   * Filter the console node from the output node if it is a repeat. Console
   * messages are filtered from the output if and only if they match the
   * immediately preceding message. The output node's last occurrence should
   * have its timestamp updated.
   *
   * @param nsIDOMNode aNode
   *        The message node to be filtered or not.
   * @param nsIDOMNode aOutput
   *        The outputNode of the HUD.
   * @return boolean
   *         true if the message is filtered, false otherwise.
   */
  filterRepeatedConsole:
  function ConsoleUtils_filterRepeatedConsole(aNode, aOutput) {
    let lastMessage = aOutput.lastChild;

    // childNodes[2] is the description element
    if (lastMessage && !aNode.classList.contains("webconsole-msg-inspector") &&
        aNode.childNodes[2].textContent ==
        lastMessage.childNodes[2].textContent) {
      this.mergeFilteredMessageNode(lastMessage, aNode);
      return true;
    }

    return false;
  },

  /**
   * Filters a node appropriately, then sends it to the output, regrouping and
   * pruning output as necessary.
   *
   * @param nsIDOMNode aNode
   *        The message node to send to the output.
   * @param string aHUDId
   *        The ID of the HUD in which to insert this node.
   */
  outputMessageNode: function ConsoleUtils_outputMessageNode(aNode, aHUDId) {
    ConsoleUtils.filterMessageNode(aNode, aHUDId);
    let outputNode = HUDService.hudReferences[aHUDId].outputNode;

    let scrolledToBottom = ConsoleUtils.isOutputScrolledToBottom(outputNode);

    let isRepeated = false;
    if (aNode.classList.contains("webconsole-msg-cssparser")) {
      isRepeated = this.filterRepeatedCSS(aNode, outputNode, aHUDId);
    }

    if (!isRepeated &&
        (aNode.classList.contains("webconsole-msg-console") ||
         aNode.classList.contains("webconsole-msg-exception") ||
         aNode.classList.contains("webconsole-msg-error"))) {
      isRepeated = this.filterRepeatedConsole(aNode, outputNode);
    }

    if (!isRepeated) {
      outputNode.appendChild(aNode);
    }

    HUDService.regroupOutput(outputNode);

    if (pruneConsoleOutputIfNecessary(aHUDId, aNode.category) == 0) {
      // We can't very well scroll to make the message node visible if the log
      // limit is zero and the node was destroyed in the first place.
      return;
    }

    let isInputOutput = aNode.classList.contains("webconsole-msg-input") ||
                        aNode.classList.contains("webconsole-msg-output");
    let isFiltered = aNode.classList.contains("hud-filtered-by-string") ||
                     aNode.classList.contains("hud-filtered-by-type");

    // Scroll to the new node if it is not filtered, and if the output node is
    // scrolled at the bottom or if the new node is a jsterm input/output
    // message.
    if (!isFiltered && !isRepeated && (scrolledToBottom || isInputOutput)) {
      ConsoleUtils.scrollToVisible(aNode);
    }

    let id = ConsoleUtils.supString(aHUDId);
    let nodeID = aNode.getAttribute("id");
    Services.obs.notifyObservers(id, "web-console-message-created", nodeID);
  },

  /**
   * Check if the given output node is scrolled to the bottom.
   *
   * @param nsIDOMNode aOutputNode
   * @return boolean
   *         True if the output node is scrolled to the bottom, or false
   *         otherwise.
   */
  isOutputScrolledToBottom:
  function ConsoleUtils_isOutputScrolledToBottom(aOutputNode)
  {
    let lastNodeHeight = aOutputNode.lastChild ?
                         aOutputNode.lastChild.clientHeight : 0;
    let scrollBox = aOutputNode.scrollBoxObject.element;

    return scrollBox.scrollTop + scrollBox.clientHeight >=
           scrollBox.scrollHeight - lastNodeHeight / 2;
  },

  /**
   * Abbreviates the given source URL so that it can be displayed flush-right
   * without being too distracting.
   *
   * @param string aSourceURL
   *        The source URL to shorten.
   * @return string
   *         The abbreviated form of the source URL.
   */
  abbreviateSourceURL: function ConsoleUtils_abbreviateSourceURL(aSourceURL) {
    // Remove any query parameters.
    let hookIndex = aSourceURL.indexOf("?");
    if (hookIndex > -1) {
      aSourceURL = aSourceURL.substring(0, hookIndex);
    }

    // Remove a trailing "/".
    if (aSourceURL[aSourceURL.length - 1] == "/") {
      aSourceURL = aSourceURL.substring(0, aSourceURL.length - 1);
    }

    // Remove all but the last path component.
    let slashIndex = aSourceURL.lastIndexOf("/");
    if (slashIndex > -1) {
      aSourceURL = aSourceURL.substring(slashIndex + 1);
    }

    return aSourceURL;
  }
};

//////////////////////////////////////////////////////////////////////////
// HeadsUpDisplayUICommands
//////////////////////////////////////////////////////////////////////////

HeadsUpDisplayUICommands = {
  toggleHUD: function UIC_toggleHUD() {
    var window = HUDService.currentContext();
    var gBrowser = window.gBrowser;
    var linkedBrowser = gBrowser.selectedTab.linkedBrowser;
    var tabId = gBrowser.getNotificationBox(linkedBrowser).getAttribute("id");
    var hudId = "hud_" + tabId;
    var ownerDocument = gBrowser.selectedTab.ownerDocument;
    var hud = ownerDocument.getElementById(hudId);
    var hudRef = HUDService.hudReferences[hudId];

    if (hudRef && hud) {
      if (hudRef.consolePanel) {
        hudRef.consolePanel.hidePopup();
      }
      else {
        HUDService.storeHeight(hudId);

        HUDService.animate(hudId, ANIMATE_OUT, function() {
          // If the user closes the console while the console is animating away,
          // then these callbacks will queue up, but all the callbacks after the
          // first will have no console to operate on. This test handles this
          // case gracefully.
          if (ownerDocument.getElementById(hudId)) {
            HUDService.deactivateHUDForContext(gBrowser.selectedTab, true);
          }
        });
      }
    }
    else {
      HUDService.activateHUDForContext(gBrowser.selectedTab, true);
      HUDService.animate(hudId, ANIMATE_IN);
    }
  },

  /**
   * Find the hudId for the active chrome window.
   * @return string|null
   *         The hudId or null if the active chrome window has no open Web
   *         Console.
   */
  getOpenHUD: function UIC_getOpenHUD() {
    let chromeWindow = HUDService.currentContext();
    let contentWindow = chromeWindow.gBrowser.selectedBrowser.contentWindow;
    return HUDService.getHudIdByWindow(contentWindow);
  },

  /**
   * The event handler that is called whenever a user switches a filter on or
   * off.
   *
   * @param nsIDOMEvent aEvent
   *        The event that triggered the filter change.
   * @return boolean
   */
  toggleFilter: function UIC_toggleFilter(aEvent) {
    let hudId = this.getAttribute("hudId");
    switch (this.tagName) {
      case "toolbarbutton": {
        let originalTarget = aEvent.originalTarget;
        let classes = originalTarget.classList;

        if (originalTarget.localName !== "toolbarbutton") {
          // Oddly enough, the click event is sent to the menu button when
          // selecting a menu item with the mouse. Detect this case and bail
          // out.
          break;
        }

        if (!classes.contains("toolbarbutton-menubutton-button") &&
            originalTarget.getAttribute("type") === "menu-button") {
          // This is a filter button with a drop-down. The user clicked the
          // drop-down, so do nothing. (The menu will automatically appear
          // without our intervention.)
          break;
        }

        let state = this.getAttribute("checked") !== "true";
        this.setAttribute("checked", state);

        // This is a filter button with a drop-down, and the user clicked the
        // main part of the button. Go through all the severities and toggle
        // their associated filters.
        let menuItems = this.querySelectorAll("menuitem");
        for (let i = 0; i < menuItems.length; i++) {
          menuItems[i].setAttribute("checked", state);
          let prefKey = menuItems[i].getAttribute("prefKey");
          HUDService.setFilterState(hudId, prefKey, state);
        }
        break;
      }

      case "menuitem": {
        let state = this.getAttribute("checked") !== "true";
        this.setAttribute("checked", state);

        let prefKey = this.getAttribute("prefKey");
        HUDService.setFilterState(hudId, prefKey, state);

        // Adjust the state of the button appropriately.
        let menuPopup = this.parentNode;

        let someChecked = false;
        let menuItem = menuPopup.firstChild;
        while (menuItem) {
          if (menuItem.getAttribute("checked") === "true") {
            someChecked = true;
            break;
          }
          menuItem = menuItem.nextSibling;
        }
        let toolbarButton = menuPopup.parentNode;
        toolbarButton.setAttribute("checked", someChecked);
        break;
      }
    }

    return true;
  },

  command: function UIC_command(aButton) {
    var filter = aButton.getAttribute("buttonType");
    var hudId = aButton.getAttribute("hudId");
    switch (filter) {
      case "copy": {
        let outputNode = HUDService.hudReferences[hudId].outputNode;
        HUDService.copySelectedItems(outputNode);
        break;
      }
      case "selectAll": {
        HUDService.hudReferences[hudId].outputNode.selectAll();
        break;
      }
      case "saveBodies": {
        let checked = aButton.getAttribute("checked") === "true";
        HUDService.saveRequestAndResponseBodies = checked;
        break;
      }
    }
  },

};

/**
 * A Console log entry
 *
 * @param JSObject aConfig, object literal with ConsolEntry properties
 * @param integer aId
 * @returns void
 */

function ConsoleEntry(aConfig, id)
{
  if (!aConfig.logLevel && aConfig.message) {
    throw new Error("Missing Arguments when creating a console entry");
  }

  this.config = aConfig;
  this.id = id;
  for (var prop in aConfig) {
    if (!(typeof aConfig[prop] == "function")){
      this[prop] = aConfig[prop];
    }
  }

  if (aConfig.logLevel == "network") {
    this.transactions = { };
    if (aConfig.activity) {
      this.transactions[aConfig.activity.stage] = aConfig.activity;
    }
  }

}

ConsoleEntry.prototype = {

  updateTransaction: function CE_updateTransaction(aActivity) {
    this.transactions[aActivity.stage] = aActivity;
  }
};

//////////////////////////////////////////////////////////////////////////
// HUDWindowObserver
//////////////////////////////////////////////////////////////////////////

HUDWindowObserver = {
  QueryInterface: XPCOMUtils.generateQI(
    [Ci.nsIObserver,]
  ),

  init: function HWO_init()
  {
    Services.obs.addObserver(this, "xpcom-shutdown", false);
    Services.obs.addObserver(this, "content-document-global-created", false);
  },

  observe: function HWO_observe(aSubject, aTopic, aData)
  {
    if (aTopic == "content-document-global-created") {
      HUDService.windowInitializer(aSubject);
    }
    else if (aTopic == "xpcom-shutdown") {
      this.uninit();
    }
  },

  uninit: function HWO_uninit()
  {
    Services.obs.removeObserver(this, "content-document-global-created");
    Services.obs.removeObserver(this, "xpcom-shutdown");
    this.initialConsoleCreated = false;
  },

};

///////////////////////////////////////////////////////////////////////////////
// CommandController
///////////////////////////////////////////////////////////////////////////////

/**
 * A controller (an instance of nsIController) that makes editing actions
 * behave appropriately in the context of the Web Console.
 */
function CommandController(aWindow) {
  this.window = aWindow;
}

CommandController.prototype = {
  /**
   * Returns the HUD output node that currently has the focus, or null if the
   * currently-focused element isn't inside the output node.
   *
   * @returns nsIDOMNode
   *          The currently-focused output node.
   */
  _getFocusedOutputNode: function CommandController_getFocusedOutputNode()
  {
    let element = this.window.document.commandDispatcher.focusedElement;
    if (element && element.classList.contains("hud-output-node")) {
      return element;
    }
    return null;
  },

  /**
   * Copies the currently-selected entries in the Web Console output to the
   * clipboard.
   *
   * @param nsIDOMNode aOutputNode
   *        The Web Console output node.
   * @returns void
   */
  copy: function CommandController_copy(aOutputNode)
  {
    HUDService.copySelectedItems(aOutputNode);
  },

  /**
   * Selects all the text in the HUD output.
   *
   * @param nsIDOMNode aOutputNode
   *        The HUD output node.
   * @returns void
   */
  selectAll: function CommandController_selectAll(aOutputNode)
  {
    aOutputNode.selectAll();
  },

  supportsCommand: function CommandController_supportsCommand(aCommand)
  {
    return this.isCommandEnabled(aCommand);
  },

  isCommandEnabled: function CommandController_isCommandEnabled(aCommand)
  {
    let outputNode = this._getFocusedOutputNode();
    if (!outputNode) {
      return false;
    }

    switch (aCommand) {
      case "cmd_copy":
        // Only enable "copy" if nodes are selected.
        return outputNode.selectedCount > 0;
      case "cmd_selectAll":
        // "Select All" is always enabled.
        return true;
    }
  },

  doCommand: function CommandController_doCommand(aCommand)
  {
    let outputNode = this._getFocusedOutputNode();
    switch (aCommand) {
      case "cmd_copy":
        this.copy(outputNode);
        break;
      case "cmd_selectAll":
        this.selectAll(outputNode);
        break;
    }
  }
};

///////////////////////////////////////////////////////////////////////////////
// HUDConsoleObserver
///////////////////////////////////////////////////////////////////////////////

/**
 * HUDConsoleObserver: Observes nsIConsoleService for global consoleMessages,
 * if a message originates inside a contentWindow we are tracking,
 * then route that message to the HUDService for logging.
 */

HUDConsoleObserver = {
  QueryInterface: XPCOMUtils.generateQI(
    [Ci.nsIObserver]
  ),

  init: function HCO_init()
  {
    Services.console.registerListener(this);
    Services.obs.addObserver(this, "quit-application-granted", false);
  },

  uninit: function HCO_uninit()
  {
    Services.console.unregisterListener(this);
    Services.obs.removeObserver(this, "quit-application-granted");
  },

  observe: function HCO_observe(aSubject, aTopic, aData)
  {
    if (aTopic == "quit-application-granted") {
      this.uninit();
    }
    else if (aSubject instanceof Ci.nsIScriptError) {
      HUDService.reportPageError(aSubject);
    }
  }
};

/**
 * A WebProgressListener that listens for location changes, to update HUDService
 * state information on page navigation.
 *
 * @constructor
 * @param string aHudId
 *        The HeadsUpDisplay ID.
 */
function ConsoleProgressListener(aHudId)
{
  this.hudId = aHudId;
}

ConsoleProgressListener.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                                         Ci.nsISupportsWeakReference]),

  onStateChange: function CPL_onStateChange(aProgress, aRequest, aState,
                                            aStatus)
  {
    if (!(aState & Ci.nsIWebProgressListener.STATE_START)) {
      return;
    }

    let uri = null;
    if (aRequest instanceof Ci.imgIRequest) {
      let imgIRequest = aRequest.QueryInterface(Ci.imgIRequest);
      uri = imgIRequest.URI;
    }
    else if (aRequest instanceof Ci.nsIChannel) {
      let nsIChannel = aRequest.QueryInterface(Ci.nsIChannel);
      uri = nsIChannel.URI;
    }

    if (!uri || !uri.schemeIs("file") && !uri.schemeIs("ftp")) {
      return;
    }

    let outputNode = HUDService.hudReferences[this.hudId].outputNode;

    let chromeDocument = outputNode.ownerDocument;
    let msgNode = chromeDocument.createElementNS(HTML_NS, "html:span");

    // Create the clickable URL part of the message.
    let linkNode = chromeDocument.createElementNS(HTML_NS, "html:span");
    linkNode.appendChild(chromeDocument.createTextNode(uri.spec));
    linkNode.classList.add("hud-clickable");
    linkNode.classList.add("webconsole-msg-url");

    linkNode.addEventListener("mousedown", function(aEvent) {
      this._startX = aEvent.clientX;
      this._startY = aEvent.clientY;
    }, false);

    linkNode.addEventListener("click", function(aEvent) {
      if (aEvent.detail == 1 && aEvent.button == 0 &&
          this._startX == aEvent.clientX && this._startY == aEvent.clientY) {
        let viewSourceUtils = chromeDocument.defaultView.gViewSourceUtils;
        viewSourceUtils.viewSource(uri.spec, null, chromeDocument);
      }
    }, false);

    msgNode.appendChild(linkNode);

    let messageNode = ConsoleUtils.createMessageNode(chromeDocument,
                                                     CATEGORY_NETWORK,
                                                     SEVERITY_LOG,
                                                     msgNode,
                                                     this.hudId,
                                                     null,
                                                     null,
                                                     uri.spec);

    ConsoleUtils.outputMessageNode(messageNode, this.hudId);
  },

  onLocationChange: function() {},
  onStatusChange: function() {},
  onProgressChange: function() {},
  onSecurityChange: function() {},
};

///////////////////////////////////////////////////////////////////////////
// appName
///////////////////////////////////////////////////////////////////////////

/**
 * Get the app's name so we can properly dispatch app-specific
 * methods per API call
 * @returns Gecko application name
 */
function appName()
{
  let APP_ID = Services.appinfo.QueryInterface(Ci.nsIXULRuntime).ID;

  let APP_ID_TABLE = {
    "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "FIREFOX" ,
    "{3550f703-e582-4d05-9a08-453d09bdfdc6}": "THUNDERBIRD",
    "{a23983c0-fd0e-11dc-95ff-0800200c9a66}": "FENNEC" ,
    "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "SEAMONKEY",
  };

  let name = APP_ID_TABLE[APP_ID];

  if (name){
    return name;
  }
  throw new Error("appName: UNSUPPORTED APPLICATION UUID");
}

XPCOMUtils.defineLazyGetter(this, "HUDService", function () {
  try {
    return new HUD_SERVICE();
  }
  catch (ex) {
    Cu.reportError(ex);
  }
});

///////////////////////////////////////////////////////////////////////////
// GcliTerm
///////////////////////////////////////////////////////////////////////////

/**
 * Some commands need customization - this is how we get at them.
 */
let commandExports = undefined;

/**
 * GcliTerm
 *
 * Initialize GCLI by creating a set of startup options from the available
 * properties.
 *
 * @param nsIDOMWindow aContentWindow
 *        The content window that we're providing as the context to commands
 * @param string aHudId
 *        The HUD to which we should send console messages.
 * @param nsIDOMDocument aDocument
 *        The DOM document from which to create nodes.
 * @param object aConsole
 *        Console object to use within the GcliTerm.
 * @param nsIDOMElement aHintNode
 *        The node to which we add GCLI's hints.
 * @constructor
 */
function GcliTerm(aContentWindow, aHudId, aDocument, aConsole, aHintNode, aConsoleWrap)
{
  this.context = Cu.getWeakReference(aContentWindow);
  this.hudId = aHudId;
  this.document = aDocument;
  this.console = aConsole;
  this.hintNode = aHintNode;
  this._window = this.context.get().QueryInterface(Ci.nsIDOMWindow);

  this.createUI();
  this.createSandbox();

  this.show = this.show.bind(this);
  this.hide = this.hide.bind(this);

  // Allow GCLI:Inputter to decide how and when to open a scratchpad window
  let scratchpad = {
    shouldActivate: function Scratchpad_shouldActivate(aEvent) {
      return aEvent.shiftKey &&
          aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN;
    },
    activate: function Scratchpad_activate(aValue) {
      aValue = aValue.replace(/^\s*{\s*/, '');
      ScratchpadManager.openScratchpad({ text: aValue });
      return true;
    },
    linkText: stringBundle.GetStringFromName('scratchpad.linkText')
  };

  this.opts = {
    environment: {
      hudId: this.hudId,
      chromeDocument: this.document,
      contentDocument: aContentWindow.document
    },
    chromeDocument: this.document,
    contentDocument: aContentWindow.document,
    jsEnvironment: {
      globalObject: unwrap(aContentWindow),
      evalFunction: this.evalInSandbox.bind(this)
    },
    inputElement: this.inputNode,
    completeElement: this.completeNode,
    inputBackgroundElement: this.inputStack,
    hintElement: this.hintNode,
    consoleWrap: aConsoleWrap,
    scratchpad: scratchpad,
    gcliTerm: this
  };

  gcli._internal.commandOutputManager.addListener(this.onCommandOutput, this);
  gcli._internal.createView(this.opts);

  if (!commandExports) {
    commandExports = loadCommands();
  }
}

GcliTerm.prototype = {
  /**
   * Remove the hint column from the display.
   */
  hide: function GcliTerm_hide()
  {
    let permaHint = false;
    try {
      permaHint = Services.prefs.getBoolPref("devtools.gcli.permaHint");
    }
    catch (ex) {}

    if (!permaHint) {
      this.hintNode.parentNode.hidden = true;
    }
  },

  /**
   * Undo the effects of calling hide().
   */
  show: function GcliTerm_show()
  {
    this.hintNode.parentNode.hidden = false;
  },

  /**
   * Destroy the GcliTerm object. Call this method to avoid memory leaks.
   */
  destroy: function Gcli_destroy()
  {
    gcli._internal.removeView(this.opts);
    gcli._internal.commandOutputManager.removeListener(this.onCommandOutput, this);

    delete this.opts.chromeDocument;
    delete this.opts.inputElement;
    delete this.opts.completeElement;
    delete this.opts.inputBackgroundElement;
    delete this.opts.hintElement;
    delete this.opts.contentDocument;
    delete this.opts.jsEnvironment;
    delete this.opts.gcliTerm;

    delete this.context;
    delete this.document;
    delete this.console;
    delete this.hintNode;
    delete this._window;

    delete this.sandbox;
    delete this.element
    delete this.inputStack
    delete this.completeNode
    delete this.inputNode
  },

  /**
   * Re-attaches a console when the contentWindow is recreated.
   *
   * @param nsIDOMWindow aContentWindow
   *        The content window that we're providing as the context to commands
   * @param object aConsole
   *        Console object to use within the GcliTerm.
   */
  reattachConsole: function Gcli_reattachConsole(aContentWindow, aConsole)
  {
    this.context = Cu.getWeakReference(aContentWindow);
    this.console = aConsole;
    this.createSandbox();
  },

  /**
   * Generates and attaches the GCLI Terminal part of the Web Console, which
   * essentially consists of the interactive JavaScript input facility.
   */
  createUI: function Gcli_createUI()
  {
    this.element = this.document.createElement("vbox");
    this.element.setAttribute("class", "gcliterm-input-container");
    this.element.setAttribute("flex", "0");

    this.inputStack = this.document.createElement("stack");
    this.inputStack.setAttribute("class", "gcliterm-stack-node");
    this.element.appendChild(this.inputStack);

    this.completeNode = this.document.createElementNS(HTML_NS, "div");
    this.completeNode.setAttribute("class", "gcliterm-complete-node");
    this.completeNode.setAttribute("aria-live", "polite");
    this.inputStack.appendChild(this.completeNode);

    this.inputNode = this.document.createElement("textbox");
    this.inputNode.setAttribute("class", "gcliterm-input-node");
    this.inputNode.setAttribute("rows", "1");
    this.inputStack.appendChild(this.inputNode);
  },

  /**
   * Called by GCLI/canon when command line output changes.
   */
  onCommandOutput: function Gcli_onCommandOutput(aEvent)
  {
    // When we can update the history of the console, then we should stop
    // filtering incomplete reports.
    if (!aEvent.output.completed) {
      return;
    }

    this.writeOutput(aEvent.output.typed, CATEGORY_INPUT);

    // This is an experiment to see how much people yell when we stop reporting
    // undefined replies.
    if (aEvent.output.output === undefined) {
      return;
    }

    let output = aEvent.output.output;
    let declaredType = aEvent.output.command.returnType || "";

    if (declaredType == "object") {
      let actualType = typeof output;
      if (output === null) {
        output = "null";
      }
      else if (actualType == "string") {
        output = "\"" + output + "\"";
      }
      else if (actualType == "object" || actualType == "function") {
        let formatOpts = [ nameObject(output) ];
        output = stringBundle.formatStringFromName('gcliterm.instanceLabel',
                formatOpts, formatOpts.length);
        let linkNode = this.document.createElementNS(HTML_NS, 'html:span');
        linkNode.appendChild(this.document.createTextNode(output));
        linkNode.classList.add("hud-clickable");
        linkNode.setAttribute("aria-haspopup", "true");

        // Make the object bring up the property panel.
        linkNode.addEventListener("mousedown", function(aEv) {
          this._startX = aEv.clientX;
          this._startY = aEv.clientY;
        }.bind(this), false);

        linkNode.addEventListener("click", function(aEv) {
          if (aEv.detail != 1 || aEv.button != 0 ||
              (this._startX != aEv.clientX && this._startY != aEv.clientY)) {
            return;
          }

          if (!this._panelOpen) {
            let propPanel = this.openPropertyPanel(aEvent.output.typed, aEvent.output.output, this);
            propPanel.panel.setAttribute("hudId", this.hudId);
            this._panelOpen = true;
          }
        }.bind(this), false);

        output = linkNode;
      }
      // else if (actualType == number/boolean/undefined) do nothing
    }

    if (declaredType == "html" && typeof output == "string") {
      output = this.document.createRange().createContextualFragment(
          '<div xmlns="' + HTML_NS + '" xmlns:xul="' + XUL_NS + '">' +
          output + '</div>');
    }

    // See https://github.com/mozilla/domtemplate/blob/master/README.md
    // for docs on the template() function
    let element = this.document.createRange().createContextualFragment(
      '<richlistitem xmlns="' + XUL_NS + '" _clipboardText="${clipboardText}"' +
      '    _timestamp="${timestamp}" _id="${id}" class="hud-msg-node">' +
      '  <label class="webconsole-timestamp" _value="${timestampString}"/>' +
      '  <vbox class="webconsole-msg-icon-container" _style="${iconContainerStyle}">' +
      '    <image class="webconsole-msg-icon"/>' +
      '    <spacer flex="1"/>' +
      '  </vbox>' +
      '  <hbox flex="1" class="gcliterm-msg-body">${output}</hbox>' +
      '  <hbox align="start"><label value="1" class="webconsole-msg-repeat"/></hbox>' +
      '</richlistitem>').firstChild;

    let hud = HUDService.getHudReferenceById(this.hudId);
    let timestamp = ConsoleUtils.timestamp();
    template(element, {
      iconContainerStyle: "margin-left=" + (hud.groupDepth * GROUP_INDENT) + "px",
      output: output,
      timestamp: timestamp,
      timestampString: ConsoleUtils.timestampString(timestamp),
      clipboardText: output.innerText,
      id: "console-msg-" + HUDService.sequenceId()
    });

    ConsoleUtils.setMessageType(element, CATEGORY_OUTPUT, SEVERITY_LOG);
    ConsoleUtils.outputMessageNode(element, this.hudId);
  },

  /**
   * Setup the eval sandbox, should be called whenever we are attached.
   */
  createSandbox: function Gcli_createSandbox()
  {
    // create a JS Sandbox out of this.context
    this.sandbox = new Cu.Sandbox(this._window, {
      sandboxPrototype: this._window,
      wantXrays: false
    });
    this.sandbox.console = this.console;

    JSTermHelper(this);
  },

  /**
   * Evaluates a string in the sandbox.
   *
   * @param string aString
   *        String to evaluate in the sandbox
   * @return The result of the evaluation
   */
  evalInSandbox: function Gcli_evalInSandbox(aString)
  {
    let window = unwrap(this.sandbox.window);
    let temp$ = null;
    let temp$$ = null;

    // We prefer to execute the page-provided implementations for the $() and
    // $$() functions.
    if (typeof window.$ == "function") {
      temp$ = this.sandbox.$;
      delete this.sandbox.$;
    }
    if (typeof window.$$ == "function") {
      temp$$ = this.sandbox.$$;
      delete this.sandbox.$$;
    }

    let result = Cu.evalInSandbox(aString, this.sandbox, "1.8", "Web Console", 1);

    if (temp$) {
      this.sandbox.$ = temp$;
    }
    if (temp$$) {
      this.sandbox.$$ = temp$$;
    }

    return result;
  },

  /**
   * Writes a message to the HUD that originates from the interactive
   * JavaScript console.
   *
   * @param string aOutputMessage
   *        The message to display.
   * @param number aCategory
   *        One of the CATEGORY_ constants.
   * @param number aSeverity
   *        One of the SEVERITY_ constants.
   */
  writeOutput: function Gcli_writeOutput(aOutputMessage, aCategory, aSeverity, aOptions)
  {
    aOptions = aOptions || {};

    let node = ConsoleUtils.createMessageNode(
                    this.document,
                    aCategory || CATEGORY_OUTPUT,
                    aSeverity || SEVERITY_LOG,
                    aOutputMessage,
                    this.hudId,
                    aOptions.sourceUrl || undefined,
                    aOptions.sourceLine || undefined,
                    aOptions.clipboardText || undefined);

    ConsoleUtils.outputMessageNode(node, this.hudId);
  },

  clearOutput: JSTerm.prototype.clearOutput,
  openPropertyPanel: JSTerm.prototype.openPropertyPanel,

  formatResult: JSTerm.prototype.formatResult,
  getResultType: JSTerm.prototype.getResultType,
  formatString: JSTerm.prototype.formatString,
};

/**
 * A fancy version of toString()
 */
function nameObject(aObj) {
  if (aObj.constructor && aObj.constructor.name) {
    return aObj.constructor.name;
  }
  // If that fails, use Objects toString which sometimes gives something
  // better than 'Object', and at least defaults to Object if nothing better
  return Object.prototype.toString.call(aObj).slice(8, -1);
}
