/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

const { classes: Cc, interfaces: Ci, utils: Cu } = Components;

const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "self-hosted", "XStringBundle"];
const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms
const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
const FRAME_STEP_CLEAR_DELAY = 100; // ms
const CALL_STACK_PAGE_SIZE = 25; // frames

// The panel's window global is an EventEmitter firing the following events:
const EVENTS = {
  // When the debugger's source editor instance finishes loading or unloading.
  EDITOR_LOADED: "Debugger:EditorLoaded",
  EDITOR_UNLOADED: "Debugger:EditorUnoaded",

  // When new sources are received from the debugger server.
  NEW_SOURCE: "Debugger:NewSource",
  SOURCES_ADDED: "Debugger:SourcesAdded",

  // When a source is shown in the source editor.
  SOURCE_SHOWN: "Debugger:EditorSourceShown",
  SOURCE_ERROR_SHOWN: "Debugger:EditorSourceErrorShown",

  // When the editor has shown a source and set the line / column position
  EDITOR_LOCATION_SET: "Debugger:EditorLocationSet",

  // When scopes, variables, properties and watch expressions are fetched and
  // displayed in the variables view.
  FETCHED_SCOPES: "Debugger:FetchedScopes",
  FETCHED_VARIABLES: "Debugger:FetchedVariables",
  FETCHED_PROPERTIES: "Debugger:FetchedProperties",
  FETCHED_BUBBLE_PROPERTIES: "Debugger:FetchedBubbleProperties",
  FETCHED_WATCH_EXPRESSIONS: "Debugger:FetchedWatchExpressions",

  // When a breakpoint has been added or removed on the debugger server.
  BREAKPOINT_ADDED: "Debugger:BreakpointAdded",
  BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved",

  // When a breakpoint has been shown or hidden in the source editor.
  BREAKPOINT_SHOWN: "Debugger:BreakpointShown",
  BREAKPOINT_HIDDEN: "Debugger:BreakpointHidden",

  // When a conditional breakpoint's popup is showing or hiding.
  CONDITIONAL_BREAKPOINT_POPUP_SHOWING: "Debugger:ConditionalBreakpointPopupShowing",
  CONDITIONAL_BREAKPOINT_POPUP_HIDING: "Debugger:ConditionalBreakpointPopupHiding",

  // When event listeners are fetched or event breakpoints are updated.
  EVENT_LISTENERS_FETCHED: "Debugger:EventListenersFetched",
  EVENT_BREAKPOINTS_UPDATED: "Debugger:EventBreakpointsUpdated",

  // When a file search was performed.
  FILE_SEARCH_MATCH_FOUND: "Debugger:FileSearch:MatchFound",
  FILE_SEARCH_MATCH_NOT_FOUND: "Debugger:FileSearch:MatchNotFound",

  // When a function search was performed.
  FUNCTION_SEARCH_MATCH_FOUND: "Debugger:FunctionSearch:MatchFound",
  FUNCTION_SEARCH_MATCH_NOT_FOUND: "Debugger:FunctionSearch:MatchNotFound",

  // When a global text search was performed.
  GLOBAL_SEARCH_MATCH_FOUND: "Debugger:GlobalSearch:MatchFound",
  GLOBAL_SEARCH_MATCH_NOT_FOUND: "Debugger:GlobalSearch:MatchNotFound",

  // After the stackframes are cleared and debugger won't pause anymore.
  AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared",

  // When the options popup is showing or hiding.
  OPTIONS_POPUP_SHOWING: "Debugger:OptionsPopupShowing",
  OPTIONS_POPUP_HIDDEN: "Debugger:OptionsPopupHidden",

  // When the widgets layout has been changed.
  LAYOUT_CHANGED: "Debugger:LayoutChanged"
};

// Descriptions for what a stack frame represents after the debugger pauses.
const FRAME_TYPE = {
  NORMAL: 0,
  CONDITIONAL_BREAKPOINT_EVAL: 1,
  WATCH_EXPRESSIONS_EVAL: 2,
  PUBLIC_CLIENT_EVAL: 3
};

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/devtools/event-emitter.js");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource:///modules/devtools/SimpleListWidget.jsm");
Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
Cu.import("resource:///modules/devtools/VariablesView.jsm");
Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");

const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
const promise = require("devtools/toolkit/deprecated-sync-thenables");
const Editor = require("devtools/sourceeditor/editor");
const DebuggerEditor = require("devtools/sourceeditor/debugger.js");
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
const FastListWidget = require("devtools/shared/widgets/FastListWidget");

XPCOMUtils.defineLazyModuleGetter(this, "Parser",
  "resource:///modules/devtools/Parser.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "devtools",
  "resource://gre/modules/devtools/Loader.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
  "resource://gre/modules/devtools/DevToolsUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
  "resource://gre/modules/ShortcutUtils.jsm");

Object.defineProperty(this, "NetworkHelper", {
  get: function() {
    return devtools.require("devtools/toolkit/webconsole/network-helper");
  },
  configurable: true,
  enumerable: true
});

/**
 * Object defining the debugger controller components.
 */
let DebuggerController = {
  /**
   * Initializes the debugger controller.
   */
  initialize: function() {
    dumpn("Initializing the DebuggerController");

    this.startupDebugger = this.startupDebugger.bind(this);
    this.shutdownDebugger = this.shutdownDebugger.bind(this);
    this._onTabNavigated = this._onTabNavigated.bind(this);
    this._onTabDetached = this._onTabDetached.bind(this);
  },

  /**
   * Initializes the view.
   *
   * @return object
   *         A promise that is resolved when the debugger finishes startup.
   */
  startupDebugger: function() {
    if (this._startup) {
      return this._startup;
    }

    return this._startup = DebuggerView.initialize();
  },

  /**
   * Destroys the view and disconnects the debugger client from the server.
   *
   * @return object
   *         A promise that is resolved when the debugger finishes shutdown.
   */
  shutdownDebugger: function() {
    if (this._shutdown) {
      return this._shutdown;
    }

    return this._shutdown = DebuggerView.destroy().then(() => {
      DebuggerView.destroy();
      this.SourceScripts.disconnect();
      this.StackFrames.disconnect();
      this.ThreadState.disconnect();
      this.Tracer.disconnect();
      this.disconnect();
    });
  },

  /**
   * Initiates remote debugging based on the current target, wiring event
   * handlers as necessary.
   *
   * @return object
   *         A promise that is resolved when the debugger finishes connecting.
   */
  connect: function() {
    if (this._connection) {
      return this._connection;
    }

    let startedDebugging = promise.defer();
    this._connection = startedDebugging.promise;

    let target = this._target;
    let { client, form: { chromeDebugger, traceActor, addonActor } } = target;
    target.on("close", this._onTabDetached);
    target.on("navigate", this._onTabNavigated);
    target.on("will-navigate", this._onTabNavigated);
    this.client = client;

    if (addonActor) {
      this._startAddonDebugging(addonActor, startedDebugging.resolve);
    } else if (target.chrome) {
      this._startChromeDebugging(chromeDebugger, startedDebugging.resolve);
    } else {
      this._startDebuggingTab(startedDebugging.resolve);
      const startedTracing = promise.defer();
      if (Prefs.tracerEnabled && traceActor) {
        this._startTracingTab(traceActor, startedTracing.resolve);
      } else {
        startedTracing.resolve();
      }

      return promise.all([startedDebugging.promise, startedTracing.promise]);
    }

    return startedDebugging.promise;
  },

  /**
   * Disconnects the debugger client and removes event handlers as necessary.
   */
  disconnect: function() {
    // Return early if the client didn't even have a chance to instantiate.
    if (!this.client) {
      return;
    }

    this._connection = null;
    this.client = null;
    this.activeThread = null;
  },

  /**
   * Called for each location change in the debugged tab.
   *
   * @param string aType
   *        Packet type.
   * @param object aPacket
   *        Packet received from the server.
   */
  _onTabNavigated: function(aType, aPacket) {
    switch (aType) {
      case "will-navigate": {
        // Reset UI.
        DebuggerView.handleTabNavigation();

        // Discard all the cached sources *before* the target starts navigating.
        // Sources may be fetched during navigation, in which case we don't
        // want to hang on to the old source contents.
        DebuggerController.SourceScripts.clearCache();
        DebuggerController.Parser.clearCache();
        SourceUtils.clearCache();

        // Prevent performing any actions that were scheduled before navigation.
        clearNamedTimeout("new-source");
        clearNamedTimeout("event-breakpoints-update");
        clearNamedTimeout("event-listeners-fetch");
        break;
      }
      case "navigate": {
        this.ThreadState.handleTabNavigation();
        this.StackFrames.handleTabNavigation();
        this.SourceScripts.handleTabNavigation();
        break;
      }
    }
  },

  /**
   * Called when the debugged tab is closed.
   */
  _onTabDetached: function() {
    this.shutdownDebugger();
  },

  /**
   * Warn if resuming execution produced a wrongOrder error.
   */
  _ensureResumptionOrder: function(aResponse) {
    if (aResponse.error == "wrongOrder") {
      DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl);
    }
  },

  /**
   * Sets up a debugging session.
   *
   * @param function aCallback
   *        A function to invoke once the client attaches to the active thread.
   */
  _startDebuggingTab: function(aCallback) {
    this._target.activeTab.attachThread({
      useSourceMaps: Prefs.sourceMapsEnabled
    }, (aResponse, aThreadClient) => {
      if (!aThreadClient) {
        Cu.reportError("Couldn't attach to thread: " + aResponse.error);
        return;
      }
      this.activeThread = aThreadClient;

      this.ThreadState.connect();
      this.StackFrames.connect();
      this.SourceScripts.connect();
      if (aThreadClient.paused) {
        aThreadClient.resume(this._ensureResumptionOrder);
      }

      if (aCallback) {
        aCallback();
      }
    });
  },

  /**
   * Sets up an addon debugging session.
   *
   * @param object aAddonActor
   *        The actor for the addon that is being debugged.
   * @param function aCallback
   *        A function to invoke once the client attaches to the active thread.
   */
  _startAddonDebugging: function(aAddonActor, aCallback) {
    this.client.attachAddon(aAddonActor, (aResponse) => {
      return this._startChromeDebugging(aResponse.threadActor, aCallback);
    });
  },

  /**
   * Sets up a chrome debugging session.
   *
   * @param object aChromeDebugger
   *        The remote protocol grip of the chrome debugger.
   * @param function aCallback
   *        A function to invoke once the client attaches to the active thread.
   */
  _startChromeDebugging: function(aChromeDebugger, aCallback) {
    this.client.attachThread(aChromeDebugger, (aResponse, aThreadClient) => {
      if (!aThreadClient) {
        Cu.reportError("Couldn't attach to thread: " + aResponse.error);
        return;
      }
      this.activeThread = aThreadClient;

      this.ThreadState.connect();
      this.StackFrames.connect();
      this.SourceScripts.connect();
      if (aThreadClient.paused) {
        aThreadClient.resume(this._ensureResumptionOrder);
      }

      if (aCallback) {
        aCallback();
      }
    }, { useSourceMaps: Prefs.sourceMapsEnabled });
  },

  /**
   * Sets up an execution tracing session.
   *
   * @param object aTraceActor
   *        The remote protocol grip of the trace actor.
   * @param function aCallback
   *        A function to invoke once the client attaches to the tracer.
   */
  _startTracingTab: function(aTraceActor, aCallback) {
    this.client.attachTracer(aTraceActor, (response, traceClient) => {
      if (!traceClient) {
        DevToolsUtils.reportException("DebuggerController._startTracingTab",
                                      new Error("Failed to attach to tracing actor."));
        return;
      }

      this.traceClient = traceClient;
      this.Tracer.connect();

      if (aCallback) {
        aCallback();
      }
    });
  },

  /**
   * Detach and reattach to the thread actor with useSourceMaps true, blow
   * away old sources and get them again.
   */
  reconfigureThread: function(aUseSourceMaps) {
    this.activeThread.reconfigure({ useSourceMaps: aUseSourceMaps }, aResponse => {
      if (aResponse.error) {
        let msg = "Couldn't reconfigure thread: " + aResponse.message;
        Cu.reportError(msg);
        dumpn(msg);
        return;
      }

      // Reset the view and fetch all the sources again.
      DebuggerView.handleTabNavigation();
      this.SourceScripts.handleTabNavigation();

      // Update the stack frame list.
      if (this.activeThread.paused) {
        this.activeThread._clearFrames();
        this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
      }
    });
  },

  _startup: null,
  _shutdown: null,
  _connection: null,
  client: null,
  activeThread: null
};

/**
 * ThreadState keeps the UI up to date with the state of the
 * thread (paused/attached/etc.).
 */
function ThreadState() {
  this._update = this._update.bind(this);
}

ThreadState.prototype = {
  get activeThread() DebuggerController.activeThread,

  /**
   * Connect to the current thread client.
   */
  connect: function() {
    dumpn("ThreadState is connecting...");
    this.activeThread.addListener("paused", this._update);
    this.activeThread.addListener("resumed", this._update);
    this.activeThread.pauseOnExceptions(Prefs.pauseOnExceptions,
                                        Prefs.ignoreCaughtExceptions);
    this.handleTabNavigation();
  },

  /**
   * Disconnect from the client.
   */
  disconnect: function() {
    if (!this.activeThread) {
      return;
    }
    dumpn("ThreadState is disconnecting...");
    this.activeThread.removeListener("paused", this._update);
    this.activeThread.removeListener("resumed", this._update);
  },

  /**
   * Handles any initialization on a tab navigation event issued by the client.
   */
  handleTabNavigation: function() {
    if (!this.activeThread) {
      return;
    }
    dumpn("Handling tab navigation in the ThreadState");
    this._update();
  },

  /**
   * Update the UI after a thread state change.
   */
  _update: function(aEvent) {
    DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state);

    if (gTarget && (aEvent == "paused" || aEvent == "resumed")) {
      gTarget.emit("thread-" + aEvent);
    }
  }
};

/**
 * Keeps the stack frame list up-to-date, using the thread client's
 * stack frame cache.
 */
function StackFrames() {
  this._onPaused = this._onPaused.bind(this);
  this._onResumed = this._onResumed.bind(this);
  this._onFrames = this._onFrames.bind(this);
  this._onFramesCleared = this._onFramesCleared.bind(this);
  this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
  this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this);
  this._afterFramesCleared = this._afterFramesCleared.bind(this);
  this.evaluate = this.evaluate.bind(this);
}

StackFrames.prototype = {
  get activeThread() DebuggerController.activeThread,
  currentFrameDepth: -1,
  _currentFrameDescription: FRAME_TYPE.NORMAL,
  _syncedWatchExpressions: null,
  _currentWatchExpressions: null,
  _currentBreakpointLocation: null,
  _currentEvaluation: null,
  _currentException: null,
  _currentReturnedValue: null,

  /**
   * Connect to the current thread client.
   */
  connect: function() {
    dumpn("StackFrames is connecting...");
    this.activeThread.addListener("paused", this._onPaused);
    this.activeThread.addListener("resumed", this._onResumed);
    this.activeThread.addListener("framesadded", this._onFrames);
    this.activeThread.addListener("framescleared", this._onFramesCleared);
    this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
    this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
    this.handleTabNavigation();
  },

  /**
   * Disconnect from the client.
   */
  disconnect: function() {
    if (!this.activeThread) {
      return;
    }
    dumpn("StackFrames is disconnecting...");
    this.activeThread.removeListener("paused", this._onPaused);
    this.activeThread.removeListener("resumed", this._onResumed);
    this.activeThread.removeListener("framesadded", this._onFrames);
    this.activeThread.removeListener("framescleared", this._onFramesCleared);
    this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
    this.activeThread.removeListener("prettyprintchange", this._onPrettyPrintChange);
    clearNamedTimeout("frames-cleared");
  },

  /**
   * Handles any initialization on a tab navigation event issued by the client.
   */
  handleTabNavigation: function() {
    dumpn("Handling tab navigation in the StackFrames");
    // Nothing to do here yet.
  },

  /**
   * Handler for the thread client's paused notification.
   *
   * @param string aEvent
   *        The name of the notification ("paused" in this case).
   * @param object aPacket
   *        The response packet.
   */
  _onPaused: function(aEvent, aPacket) {
    switch (aPacket.why.type) {
      // If paused by a breakpoint, store the breakpoint location.
      case "breakpoint":
        this._currentBreakpointLocation = aPacket.frame.where;
        break;
      // If paused by a client evaluation, store the evaluated value.
      case "clientEvaluated":
        this._currentEvaluation = aPacket.why.frameFinished;
        break;
      // If paused by an exception, store the exception value.
      case "exception":
        this._currentException = aPacket.why.exception;
        break;
      // If paused while stepping out of a frame, store the returned value or
      // thrown exception.
      case "resumeLimit":
        if (!aPacket.why.frameFinished) {
          break;
        } else if (aPacket.why.frameFinished.throw) {
          this._currentException = aPacket.why.frameFinished.throw;
        } else if (aPacket.why.frameFinished.return) {
          this._currentReturnedValue = aPacket.why.frameFinished.return;
        }
        break;
    }

    this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
    DebuggerView.editor.focus();
  },

  /**
   * Handler for the thread client's resumed notification.
   */
  _onResumed: function() {
    // Prepare the watch expression evaluation string for the next pause.
    if (this._currentFrameDescription != FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
      this._currentWatchExpressions = this._syncedWatchExpressions;
    }
  },

  /**
   * Handler for the thread client's framesadded notification.
   */
  _onFrames: function() {
    // Ignore useless notifications.
    if (!this.activeThread || !this.activeThread.cachedFrames.length) {
      return;
    }

    let waitForNextPause = false;
    let breakLocation = this._currentBreakpointLocation;
    let watchExpressions = this._currentWatchExpressions;
    let client = DebuggerController.activeThread.client;

    // We moved conditional breakpoint handling to the server, but
    // need to support it in the client for a while until most of the
    // server code in production is updated with it. bug 990137 is
    // filed to mark this code to be removed.
    if (!client.mainRoot.traits.conditionalBreakpoints) {
      // Conditional breakpoints are { breakpoint, expression } tuples. The
      // boolean evaluation of the expression decides if the active thread
      // automatically resumes execution or not.
      if (breakLocation) {
        // Make sure a breakpoint actually exists at the specified url and line.
        let breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation);
        if (breakpointPromise) {
          breakpointPromise.then(({ conditionalExpression: e }) => { if (e) {
            // Evaluating the current breakpoint's conditional expression will
            // cause the stack frames to be cleared and active thread to pause,
            // sending a 'clientEvaluated' packed and adding the frames again.
            this.evaluate(e, { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL });
            waitForNextPause = true;
          }});
        }
      }
      // We'll get our evaluation of the current breakpoint's conditional
      // expression the next time the thread client pauses...
      if (waitForNextPause) {
        return;
      }
      if (this._currentFrameDescription == FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL) {
        this._currentFrameDescription = FRAME_TYPE.NORMAL;
        // If the breakpoint's conditional expression evaluation is falsy,
        // automatically resume execution.
        if (VariablesView.isFalsy({ value: this._currentEvaluation.return })) {
          this.activeThread.resume(DebuggerController._ensureResumptionOrder);
          return;
        }
      }
    }

    // Watch expressions are evaluated in the context of the topmost frame,
    // and the results are displayed in the variables view.
    // TODO: handle all of this server-side: Bug 832470, comment 14.
    if (watchExpressions) {
      // Evaluation causes the stack frames to be cleared and active thread to
      // pause, sending a 'clientEvaluated' packet and adding the frames again.
      this.evaluate(watchExpressions, { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL });
      waitForNextPause = true;
    }
    // We'll get our evaluation of the current watch expressions the next time
    // the thread client pauses...
    if (waitForNextPause) {
      return;
    }
    if (this._currentFrameDescription == FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
      this._currentFrameDescription = FRAME_TYPE.NORMAL;
      // If an error was thrown during the evaluation of the watch expressions,
      // then at least one expression evaluation could not be performed. So
      // remove the most recent watch expression and try again.
      if (this._currentEvaluation.throw) {
        DebuggerView.WatchExpressions.removeAt(0);
        DebuggerController.StackFrames.syncWatchExpressions();
        return;
      }
    }

    // Make sure the debugger view panes are visible, then refill the frames.
    DebuggerView.showInstrumentsPane();
    this._refillFrames();

    // No additional processing is necessary for this stack frame.
    if (this._currentFrameDescription != FRAME_TYPE.NORMAL) {
      this._currentFrameDescription = FRAME_TYPE.NORMAL;
    }
  },

  /**
   * Fill the StackFrames view with the frames we have in the cache, compressing
   * frames which have black boxed sources into single frames.
   */
  _refillFrames: function() {
    // Make sure all the previous stackframes are removed before re-adding them.
    DebuggerView.StackFrames.empty();

    for (let frame of this.activeThread.cachedFrames) {
      let { depth, where: { url, line }, source } = frame;
      let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false;
      let location = NetworkHelper.convertToUnicode(unescape(url));
      let title = StackFrameUtils.getFrameTitle(frame);
      DebuggerView.StackFrames.addFrame(title, location, line, depth, isBlackBoxed);
    }

    DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0);
    DebuggerView.StackFrames.dirty = this.activeThread.moreFrames;
  },

  /**
   * Handler for the thread client's framescleared notification.
   */
  _onFramesCleared: function() {
    switch (this._currentFrameDescription) {
      case FRAME_TYPE.NORMAL:
        this._currentEvaluation = null;
        this._currentException = null;
        this._currentReturnedValue = null;
        break;
      case FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL:
        this._currentBreakpointLocation = null;
        break;
      case FRAME_TYPE.WATCH_EXPRESSIONS_EVAL:
        this._currentWatchExpressions = null;
        break;
    }

    // After each frame step (in, over, out), framescleared is fired, which
    // forces the UI to be emptied and rebuilt on framesadded. Most of the times
    // this is not necessary, and will result in a brief redraw flicker.
    // To avoid it, invalidate the UI only after a short time if necessary.
    setNamedTimeout("frames-cleared", FRAME_STEP_CLEAR_DELAY, this._afterFramesCleared);
  },

  /**
   * Handler for the debugger's blackboxchange notification.
   */
  _onBlackBoxChange: function() {
    if (this.activeThread.state == "paused") {
      // Hack to avoid selecting the topmost frame after blackboxing a source.
      this.currentFrameDepth = NaN;
      this._refillFrames();
    }
  },

  /**
   * Handler for the debugger's prettyprintchange notification.
   */
  _onPrettyPrintChange: function() {
    // Makes sure the selected source remains selected
    // after the fillFrames is called.
    const source = DebuggerView.Sources.selectedValue;
    if (this.activeThread.state == "paused") {
      this.activeThread.fillFrames(
         CALL_STACK_PAGE_SIZE,
         () => DebuggerView.Sources.selectedValue = source);
    }
  },

  /**
   * Called soon after the thread client's framescleared notification.
   */
  _afterFramesCleared: function() {
    // Ignore useless notifications.
    if (this.activeThread.cachedFrames.length) {
      return;
    }
    DebuggerView.editor.clearDebugLocation();
    DebuggerView.StackFrames.empty();
    DebuggerView.Sources.unhighlightBreakpoint();
    DebuggerView.WatchExpressions.toggleContents(true);
    DebuggerView.Variables.empty(0);

    window.emit(EVENTS.AFTER_FRAMES_CLEARED);
  },

  /**
   * Marks the stack frame at the specified depth as selected and updates the
   * properties view with the stack frame's data.
   *
   * @param number aDepth
   *        The depth of the frame in the stack.
   */
  selectFrame: function(aDepth) {
    // Make sure the frame at the specified depth exists first.
    let frame = this.activeThread.cachedFrames[this.currentFrameDepth = aDepth];
    if (!frame) {
      return;
    }

    // Check if the frame does not represent the evaluation of debuggee code.
    let { environment, where } = frame;
    if (!environment) {
      return;
    }

    // Don't change the editor's location if the execution was paused by a
    // public client evaluation. This is useful for adding overlays on
    // top of the editor, like a variable inspection popup.
    let isClientEval = this._currentFrameDescription == FRAME_TYPE.PUBLIC_CLIENT_EVAL;
    let isPopupShown = DebuggerView.VariableBubble.contentsShown();
    if (!isClientEval && !isPopupShown) {
      // Move the editor's caret to the proper url and line.
      DebuggerView.setEditorLocation(where.url, where.line);
      // Highlight the breakpoint at the specified url and line if it exists.
      DebuggerView.Sources.highlightBreakpoint(where, { noEditorUpdate: true });
    }

    // Don't display the watch expressions textbox inputs in the pane.
    DebuggerView.WatchExpressions.toggleContents(false);

    // Start recording any added variables or properties in any scope and
    // clear existing scopes to create each one dynamically.
    DebuggerView.Variables.empty();

    // If watch expressions evaluation results are available, create a scope
    // to contain all the values.
    if (this._syncedWatchExpressions && aDepth == 0) {
      let label = L10N.getStr("watchExpressionsScopeLabel");
      let scope = DebuggerView.Variables.addScope(label);

      // Customize the scope for holding watch expressions evaluations.
      scope.descriptorTooltip = false;
      scope.contextMenuId = "debuggerWatchExpressionsContextMenu";
      scope.separatorStr = L10N.getStr("watchExpressionsSeparatorLabel");
      scope.switch = DebuggerView.WatchExpressions.switchExpression;
      scope.delete = DebuggerView.WatchExpressions.deleteExpression;

      // The evaluation hasn't thrown, so fetch and add the returned results.
      this._fetchWatchExpressions(scope, this._currentEvaluation.return);

      // The watch expressions scope is always automatically expanded.
      scope.expand();
    }

    do {
      // Create a scope to contain all the inspected variables in the
      // current environment.
      let label = StackFrameUtils.getScopeLabel(environment);
      let scope = DebuggerView.Variables.addScope(label);
      let innermost = environment == frame.environment;

      // Handle special additions to the innermost scope.
      if (innermost) {
        this._insertScopeFrameReferences(scope, frame);
      }

      // Handle the expansion of the scope, lazily populating it with the
      // variables in the current environment.
      DebuggerView.Variables.controller.addExpander(scope, environment);

      // The innermost scope is always automatically expanded, because it
      // contains the variables in the current stack frame which are likely to
      // be inspected.
      if (innermost) {
        scope.expand();
      }
    } while ((environment = environment.parent));

    // Signal that scope environments have been shown.
    window.emit(EVENTS.FETCHED_SCOPES);
  },

  /**
   * Loads more stack frames from the debugger server cache.
   */
  addMoreFrames: function() {
    this.activeThread.fillFrames(
      this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE);
  },

  /**
   * Evaluate an expression in the context of the selected frame.
   *
   * @param string aExpression
   *        The expression to evaluate.
   * @param object aOptions [optional]
   *        Additional options for this client evaluation:
   *          - depth: the frame depth used for evaluation, 0 being the topmost.
   *          - meta: some meta-description for what this evaluation represents.
   * @return object
   *         A promise that is resolved when the evaluation finishes,
   *         or rejected if there was no stack frame available or some
   *         other error occurred.
   */
  evaluate: function(aExpression, aOptions = {}) {
    let depth = "depth" in aOptions ? aOptions.depth : this.currentFrameDepth;
    let frame = this.activeThread.cachedFrames[depth];
    if (frame == null) {
      return promise.reject(new Error("No stack frame available."));
    }

    let deferred = promise.defer();

    this.activeThread.addOneTimeListener("paused", (aEvent, aPacket) => {
      let { type, frameFinished } = aPacket.why;
      if (type == "clientEvaluated") {
        if (!("terminated" in frameFinished)) {
          deferred.resolve(frameFinished);
        } else {
          deferred.reject(new Error("The execution was abruptly terminated."));
        }
      } else {
        deferred.reject(new Error("Active thread paused unexpectedly."));
      }
    });

    let meta = "meta" in aOptions ? aOptions.meta : FRAME_TYPE.PUBLIC_CLIENT_EVAL;
    this._currentFrameDescription = meta;
    this.activeThread.eval(frame.actor, aExpression);

    return deferred.promise;
  },

  /**
   * Add nodes for special frame references in the innermost scope.
   *
   * @param Scope aScope
   *        The scope where the references will be placed into.
   * @param object aFrame
   *        The frame to get some references from.
   */
  _insertScopeFrameReferences: function(aScope, aFrame) {
    // Add any thrown exception.
    if (this._currentException) {
      let excRef = aScope.addItem("<exception>", { value: this._currentException });
      DebuggerView.Variables.controller.addExpander(excRef, this._currentException);
    }
    // Add any returned value.
    if (this._currentReturnedValue) {
      let retRef = aScope.addItem("<return>", { value: this._currentReturnedValue });
      DebuggerView.Variables.controller.addExpander(retRef, this._currentReturnedValue);
    }
    // Add "this".
    if (aFrame.this) {
      let thisRef = aScope.addItem("this", { value: aFrame.this });
      DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this);
    }
  },

  /**
   * Adds the watch expressions evaluation results to a scope in the view.
   *
   * @param Scope aScope
   *        The scope where the watch expressions will be placed into.
   * @param object aExp
   *        The grip of the evaluation results.
   */
  _fetchWatchExpressions: function(aScope, aExp) {
    // Fetch the expressions only once.
    if (aScope._fetched) {
      return;
    }
    aScope._fetched = true;

    // Add nodes for every watch expression in scope.
    this.activeThread.pauseGrip(aExp).getPrototypeAndProperties(aResponse => {
      let ownProperties = aResponse.ownProperties;
      let totalExpressions = DebuggerView.WatchExpressions.itemCount;

      for (let i = 0; i < totalExpressions; i++) {
        let name = DebuggerView.WatchExpressions.getString(i);
        let expVal = ownProperties[i].value;
        let expRef = aScope.addItem(name, ownProperties[i]);
        DebuggerView.Variables.controller.addExpander(expRef, expVal);

        // Revert some of the custom watch expressions scope presentation flags,
        // so that they don't propagate to child items.
        expRef.switch = null;
        expRef.delete = null;
        expRef.descriptorTooltip = true;
        expRef.separatorStr = L10N.getStr("variablesSeparatorLabel");
      }

      // Signal that watch expressions have been fetched.
      window.emit(EVENTS.FETCHED_WATCH_EXPRESSIONS);
    });
  },

  /**
   * Updates a list of watch expressions to evaluate on each pause.
   * TODO: handle all of this server-side: Bug 832470, comment 14.
   */
  syncWatchExpressions: function() {
    let list = DebuggerView.WatchExpressions.getAllStrings();

    // Sanity check all watch expressions before syncing them. To avoid
    // having the whole watch expressions array throw because of a single
    // faulty expression, simply convert it to a string describing the error.
    // There's no other information necessary to be offered in such cases.
    let sanitizedExpressions = list.map(aString => {
      // Reflect.parse throws when it encounters a syntax error.
      try {
        Parser.reflectionAPI.parse(aString);
        return aString; // Watch expression can be executed safely.
      } catch (e) {
        return "\"" + e.name + ": " + e.message + "\""; // Syntax error.
      }
    });

    if (sanitizedExpressions.length) {
      this._syncedWatchExpressions =
        this._currentWatchExpressions =
          "[" +
            sanitizedExpressions.map(aString =>
              "eval(\"" +
                "try {" +
                  // Make sure all quotes are escaped in the expression's syntax,
                  // and add a newline after the statement to avoid comments
                  // breaking the code integrity inside the eval block.
                  aString.replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" +
                "} catch (e) {" +
                  "e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
                "}" +
              "\")"
            ).join(",") +
          "]";
    } else {
      this._syncedWatchExpressions =
        this._currentWatchExpressions = null;
    }

    this.currentFrameDepth = -1;
    this._onFrames();
  }
};

/**
 * Keeps the source script list up-to-date, using the thread client's
 * source script cache.
 */
function SourceScripts() {
  this._onNewGlobal = this._onNewGlobal.bind(this);
  this._onNewSource = this._onNewSource.bind(this);
  this._onSourcesAdded = this._onSourcesAdded.bind(this);
  this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
  this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this);
}

SourceScripts.prototype = {
  get activeThread() DebuggerController.activeThread,
  get debuggerClient() DebuggerController.client,
  _cache: new Map(),

  /**
   * Connect to the current thread client.
   */
  connect: function() {
    dumpn("SourceScripts is connecting...");
    this.debuggerClient.addListener("newGlobal", this._onNewGlobal);
    this.debuggerClient.addListener("newSource", this._onNewSource);
    this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
    this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
    this.handleTabNavigation();
  },

  /**
   * Disconnect from the client.
   */
  disconnect: function() {
    if (!this.activeThread) {
      return;
    }
    dumpn("SourceScripts is disconnecting...");
    this.debuggerClient.removeListener("newGlobal", this._onNewGlobal);
    this.debuggerClient.removeListener("newSource", this._onNewSource);
    this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
    this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
  },

  /**
   * Clears all the cached source contents.
   */
  clearCache: function() {
    this._cache.clear();
  },

  /**
   * Handles any initialization on a tab navigation event issued by the client.
   */
  handleTabNavigation: function() {
    if (!this.activeThread) {
      return;
    }
    dumpn("Handling tab navigation in the SourceScripts");

    // Retrieve the list of script sources known to the server from before
    // the client was ready to handle "newSource" notifications.
    this.activeThread.getSources(this._onSourcesAdded);
  },

  /**
   * Handler for the debugger client's unsolicited newGlobal notification.
   */
  _onNewGlobal: function(aNotification, aPacket) {
    // TODO: bug 806775, update the globals list using aPacket.hostAnnotations
    // from bug 801084.
  },

  /**
   * Handler for the debugger client's unsolicited newSource notification.
   */
  _onNewSource: function(aNotification, aPacket) {
    // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
    if (NEW_SOURCE_IGNORED_URLS.indexOf(aPacket.source.url) != -1) {
      return;
    }

    // Add the source in the debugger view sources container.
    DebuggerView.Sources.addSource(aPacket.source, { staged: false });

    // Select this source if it's the preferred one.
    let preferredValue = DebuggerView.Sources.preferredValue;
    if (aPacket.source.url == preferredValue) {
      DebuggerView.Sources.selectedValue = preferredValue;
    }
    // ..or the first entry if there's none selected yet after a while
    else {
      setNamedTimeout("new-source", NEW_SOURCE_DISPLAY_DELAY, () => {
        // If after a certain delay the preferred source still wasn't received,
        // just give up on waiting and display the first entry.
        if (!DebuggerView.Sources.selectedValue) {
          DebuggerView.Sources.selectedIndex = 0;
        }
      });
    }

    // If there are any stored breakpoints for this source, display them again,
    // both in the editor and the breakpoints pane.
    DebuggerController.Breakpoints.updateEditorBreakpoints();
    DebuggerController.Breakpoints.updatePaneBreakpoints();

    // Make sure the events listeners are up to date.
    if (DebuggerView.instrumentsPaneTab == "events-tab") {
      DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch();
    }

    // Signal that a new source has been added.
    window.emit(EVENTS.NEW_SOURCE);
  },

  /**
   * Callback for the debugger's active thread getSources() method.
   */
  _onSourcesAdded: function(aResponse) {
    if (aResponse.error) {
      let msg = "Error getting sources: " + aResponse.message;
      Cu.reportError(msg);
      dumpn(msg);
      return;
    }

    if (aResponse.sources.length === 0) {
      DebuggerView.Sources.emptyText = L10N.getStr("noSourcesText");
      window.emit(EVENTS.SOURCES_ADDED);
      return;
    }

    // Add all the sources in the debugger view sources container.
    for (let source of aResponse.sources) {
      // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
      if (NEW_SOURCE_IGNORED_URLS.indexOf(source.url) == -1) {
        DebuggerView.Sources.addSource(source, { staged: true });
      }
    }

    // Flushes all the prepared sources into the sources container.
    DebuggerView.Sources.commit({ sorted: true });

    // Select the preferred source if it exists and was part of the response.
    let preferredValue = DebuggerView.Sources.preferredValue;
    if (DebuggerView.Sources.containsValue(preferredValue)) {
      DebuggerView.Sources.selectedValue = preferredValue;
    }
    // ..or the first entry if there's no one selected yet.
    else if (!DebuggerView.Sources.selectedValue) {
      DebuggerView.Sources.selectedIndex = 0;
    }

    // If there are any stored breakpoints for the sources, display them again,
    // both in the editor and the breakpoints pane.
    DebuggerController.Breakpoints.updateEditorBreakpoints();
    DebuggerController.Breakpoints.updatePaneBreakpoints();

    // Signal that sources have been added.
    window.emit(EVENTS.SOURCES_ADDED);
  },

  /**
   * Handler for the debugger client's 'blackboxchange' notification.
   */
  _onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) {
    const item = DebuggerView.Sources.getItemByValue(url);
    if (item) {
      if (isBlackBoxed) {
        item.target.classList.add("black-boxed");
      } else {
        item.target.classList.remove("black-boxed");
      }
    }
    DebuggerView.Sources.updateToolbarButtonsState();
    DebuggerView.maybeShowBlackBoxMessage();
  },

  /**
   * Set the black boxed status of the given source.
   *
   * @param Object aSource
   *        The source form.
   * @param bool aBlackBoxFlag
   *        True to black box the source, false to un-black box it.
   * @returns Promise
   *          A promize that resolves to [aSource, isBlackBoxed] or rejects to
   *          [aSource, error].
   */
  setBlackBoxing: function(aSource, aBlackBoxFlag) {
    const sourceClient = this.activeThread.source(aSource);
    const deferred = promise.defer();

    sourceClient[aBlackBoxFlag ? "blackBox" : "unblackBox"](aPacket => {
      const { error, message } = aPacket;
      if (error) {
        let msg = "Couldn't toggle black boxing for " + aSource.url + ": " + message;
        dumpn(msg);
        Cu.reportError(msg);
        deferred.reject([aSource, msg]);
      } else {
        deferred.resolve([aSource, sourceClient.isBlackBoxed]);
      }
    });

    return deferred.promise;
  },

  /**
   * Toggle the pretty printing of a source's text. All subsequent calls to
   * |getText| will return the pretty-toggled text. Nothing will happen for
   * non-javascript files.
   *
   * @param Object aSource
   *        The source form from the RDP.
   * @returns Promise
   *          A promise that resolves to [aSource, prettyText] or rejects to
   *          [aSource, error].
   */
  togglePrettyPrint: function(aSource) {
    // Only attempt to pretty print JavaScript sources.
    if (!SourceUtils.isJavaScript(aSource.url, aSource.contentType)) {
      return promise.reject([aSource, "Can't prettify non-javascript files."]);
    }

    const sourceClient = this.activeThread.source(aSource);
    const wantPretty = !sourceClient.isPrettyPrinted;

    // Only use the existing promise if it is pretty printed.
    let textPromise = this._cache.get(aSource.url);
    if (textPromise && textPromise.pretty === wantPretty) {
      return textPromise;
    }

    const deferred = promise.defer();
    deferred.promise.pretty = wantPretty;
    this._cache.set(aSource.url, deferred.promise);

    const afterToggle = ({ error, message, source: text, contentType }) => {
      if (error) {
        // Revert the rejected promise from the cache, so that the original
        // source's text may be shown when the source is selected.
        this._cache.set(aSource.url, textPromise);
        deferred.reject([aSource, message || error]);
        return;
      }
      deferred.resolve([aSource, text, contentType]);
    };

    if (wantPretty) {
      sourceClient.prettyPrint(Prefs.editorTabSize, afterToggle);
    } else {
      sourceClient.disablePrettyPrint(afterToggle);
    }

    return deferred.promise;
  },

  /**
   * Handler for the debugger's prettyprintchange notification.
   */
  _onPrettyPrintChange: function(aEvent, { url }) {
    // Remove the cached source AST from the Parser, to avoid getting
    // wrong locations when searching for functions.
    DebuggerController.Parser.clearSource(url);
  },

  /**
   * Gets a specified source's text.
   *
   * @param object aSource
   *        The source object coming from the active thread.
   * @param function aOnTimeout [optional]
   *        Function called when the source text takes a long time to fetch,
   *        but not necessarily failing. Long fetch times don't cause the
   *        rejection of the returned promise.
   * @param number aDelay [optional]
   *        The amount of time it takes to consider a source slow to fetch.
   *        If unspecified, it defaults to a predefined value.
   * @return object
   *         A promise that is resolved after the source text has been fetched.
   */
  getText: function(aSource, aOnTimeout, aDelay = FETCH_SOURCE_RESPONSE_DELAY) {
    // Fetch the source text only once.
    let textPromise = this._cache.get(aSource.url);
    if (textPromise) {
      return textPromise;
    }

    let deferred = promise.defer();
    this._cache.set(aSource.url, deferred.promise);

    // If the source text takes a long time to fetch, invoke a callback.
    if (aOnTimeout) {
      var fetchTimeout = window.setTimeout(() => aOnTimeout(aSource), aDelay);
    }

    // Get the source text from the active thread.
    this.activeThread.source(aSource)
        .source(({ error, message, source: text, contentType }) => {
      if (aOnTimeout) {
        window.clearTimeout(fetchTimeout);
      }
      if (error) {
        deferred.reject([aSource, message || error]);
      } else {
        deferred.resolve([aSource, text, contentType]);
      }
    });

    return deferred.promise;
  },

  /**
   * Starts fetching all the sources, silently.
   *
   * @param array aUrls
   *        The urls for the sources to fetch. If fetching a source's text
   *        takes too long, it will be discarded.
   * @return object
   *         A promise that is resolved after source texts have been fetched.
   */
  getTextForSources: function(aUrls) {
    let deferred = promise.defer();
    let pending = new Set(aUrls);
    let fetched = [];

    // Can't use promise.all, because if one fetch operation is rejected, then
    // everything is considered rejected, thus no other subsequent source will
    // be getting fetched. We don't want that. Something like Q's allSettled
    // would work like a charm here.

    // Try to fetch as many sources as possible.
    for (let url of aUrls) {
      let sourceItem = DebuggerView.Sources.getItemByValue(url);
      let sourceForm = sourceItem.attachment.source;
      this.getText(sourceForm, onTimeout).then(onFetch, onError);
    }

    /* Called if fetching a source takes too long. */
    function onTimeout(aSource) {
      onError([aSource]);
    }

    /* Called if fetching a source finishes successfully. */
    function onFetch([aSource, aText, aContentType]) {
      // If fetching the source has previously timed out, discard it this time.
      if (!pending.has(aSource.url)) {
        return;
      }
      pending.delete(aSource.url);
      fetched.push([aSource.url, aText, aContentType]);
      maybeFinish();
    }

    /* Called if fetching a source failed because of an error. */
    function onError([aSource, aError]) {
      pending.delete(aSource.url);
      maybeFinish();
    }

    /* Called every time something interesting happens while fetching sources. */
    function maybeFinish() {
      if (pending.size == 0) {
        // Sort the fetched sources alphabetically by their url.
        deferred.resolve(fetched.sort(([aFirst], [aSecond]) => aFirst > aSecond));
      }
    }

    return deferred.promise;
  }
};

/**
 * Tracer update the UI according to the messages exchanged with the tracer
 * actor.
 */
function Tracer() {
  this._trace = null;
  this._idCounter = 0;
  this.onTraces = this.onTraces.bind(this);
}

Tracer.prototype = {
  get client() {
    return DebuggerController.client;
  },

  get traceClient() {
    return DebuggerController.traceClient;
  },

  get tracing() {
    return !!this._trace;
  },

  /**
   * Hooks up the debugger controller with the tracer client.
   */
  connect: function() {
    this._stack = [];
    this.client.addListener("traces", this.onTraces);
  },

  /**
   * Disconnects the debugger controller from the tracer client. Any further
   * communcation with the tracer actor will not have any effect on the UI.
   */
  disconnect: function() {
    this._stack = null;
    this.client.removeListener("traces", this.onTraces);
  },

  /**
   * Instructs the tracer actor to start tracing.
   */
  startTracing: function(aCallback = () => {}) {
    DebuggerView.Tracer.selectTab();
    if (this.tracing) {
      return;
    }
    this._trace = "dbg.trace" + Math.random();
    this.traceClient.startTrace([
      "name",
      "location",
      "parameterNames",
      "depth",
      "arguments",
      "return",
      "throw",
      "yield"
    ], this._trace, (aResponse) => {
      const { error } = aResponse;
      if (error) {
        DevToolsUtils.reportException("Tracer.prototype.startTracing", error);
        this._trace = null;
      }

      aCallback(aResponse);
    });
  },

  /**
   * Instructs the tracer actor to stop tracing.
   */
  stopTracing: function(aCallback = () => {}) {
    if (!this.tracing) {
      return;
    }
    this.traceClient.stopTrace(this._trace, aResponse => {
      const { error } = aResponse;
      if (error) {
        DevToolsUtils.reportException("Tracer.prototype.stopTracing", error);
      }

      this._trace = null;
      aCallback(aResponse);
    });
  },

  onTraces: function (aEvent, { traces }) {
    const tracesLength = traces.length;
    let tracesToShow;
    if (tracesLength > TracerView.MAX_TRACES) {
      tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES,
                                  tracesLength);
      DebuggerView.Tracer.empty();
      this._stack.splice(0, this._stack.length);
    } else {
      tracesToShow = traces;
    }

    for (let t of tracesToShow) {
      if (t.type == "enteredFrame") {
        this._onCall(t);
      } else {
        this._onReturn(t);
      }
    }

    DebuggerView.Tracer.commit();
  },

  /**
   * Callback for handling a new call frame.
   */
  _onCall: function({ name, location, parameterNames, depth, arguments: args }) {
    const item = {
      name: name,
      location: location,
      id: this._idCounter++
    };

    this._stack.push(item);
    DebuggerView.Tracer.addTrace({
      type: "call",
      name: name,
      location: location,
      depth: depth,
      parameterNames: parameterNames,
      arguments: args,
      frameId: item.id
    });
  },

  /**
   * Callback for handling an exited frame.
   */
  _onReturn: function(aPacket) {
    if (!this._stack.length) {
      return;
    }

    const { name, id, location } = this._stack.pop();
    DebuggerView.Tracer.addTrace({
      type: aPacket.why,
      name: name,
      location: location,
      depth: aPacket.depth,
      frameId: id,
      returnVal: aPacket.return || aPacket.throw || aPacket.yield
    });
  },

  /**
   * Create an object which has the same interface as a normal object client,
   * but since we already have all the information for an object that we will
   * ever get (the server doesn't create actors when tracing, just firehoses
   * data and forgets about it) just return the data immdiately.
   *
   * @param Object aObject
   *        The tracer object "grip" (more like a limited snapshot).
   * @returns Object
   *          The synchronous client object.
   */
  syncGripClient: function(aObject) {
    return {
      get isFrozen() { return aObject.frozen; },
      get isSealed() { return aObject.sealed; },
      get isExtensible() { return aObject.extensible; },

      get ownProperties() { return aObject.ownProperties; },
      get prototype() { return null; },

      getParameterNames: callback => callback(aObject),
      getPrototypeAndProperties: callback => callback(aObject),
      getPrototype: callback => callback(aObject),

      getOwnPropertyNames: (callback) => {
        callback({
          ownPropertyNames: aObject.ownProperties
            ? Object.keys(aObject.ownProperties)
            : []
        });
      },

      getProperty: (property, callback) => {
        callback({
          descriptor: aObject.ownProperties
            ? aObject.ownProperties[property]
            : null
        });
      },

      getDisplayString: callback => callback("[object " + aObject.class + "]"),

      getScope: callback => callback({
        error: "scopeNotAvailable",
        message: "Cannot get scopes for traced objects"
      })
    };
  },

  /**
   * Wraps object snapshots received from the tracer server so that we can
   * differentiate them from long living object grips from the debugger server
   * in the variables view.
   *
   * @param Object aObject
   *        The object snapshot from the tracer actor.
   */
  WrappedObject: function(aObject) {
    this.object = aObject;
  }
};

/**
 * Handles breaking on event listeners in the currently debugged target.
 */
function EventListeners() {
  this._onEventListeners = this._onEventListeners.bind(this);
}

EventListeners.prototype = {
  /**
   * A list of event names on which the debuggee will automatically pause
   * when invoked.
   */
  activeEventNames: [],

  /**
   * Updates the list of events types with listeners that, when invoked,
   * will automatically pause the debuggee. The respective events are
   * retrieved from the UI.
   */
  scheduleEventBreakpointsUpdate: function() {
    // Make sure we're not sending a batch of closely repeated requests.
    // This can easily happen when toggling all events of a certain type.
    setNamedTimeout("event-breakpoints-update", 0, () => {
      this.activeEventNames = DebuggerView.EventListeners.getCheckedEvents();
      gThreadClient.pauseOnDOMEvents(this.activeEventNames);

      // Notify that event breakpoints were added/removed on the server.
      window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED);
    });
  },

  /**
   * Fetches the currently attached event listeners from the debugee.
   */
  scheduleEventListenersFetch: function() {
    let getListeners = aCallback => gThreadClient.eventListeners(aResponse => {
      if (aResponse.error) {
        let msg = "Error getting event listeners: " + aResponse.message;
        DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
        return;
      }

      let outstandingListenersDefinitionSite = aResponse.listeners.map(aListener => {
        const deferred = promise.defer();

        gThreadClient.pauseGrip(aListener.function).getDefinitionSite(aResponse => {
          if (aResponse.error) {
            const msg = "Error getting function definition site: " + aResponse.message;
            DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
          } else {
            aListener.function.url = aResponse.url;
          }

          deferred.resolve(aListener);
        });

        return deferred.promise;
      });

      promise.all(outstandingListenersDefinitionSite).then(aListeners => {
        this._onEventListeners(aListeners);

        // Notify that event listeners were fetched and shown in the view,
        // and callback to resume the active thread if necessary.
        window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
        aCallback && aCallback();
      });
    });

    // Make sure we're not sending a batch of closely repeated requests.
    // This can easily happen whenever new sources are fetched.
    setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
      if (gThreadClient.state != "paused") {
        gThreadClient.interrupt(() => getListeners(() => gThreadClient.resume()));
      } else {
        getListeners();
      }
    });
  },

  /**
   * Callback for a debugger's successful active thread eventListeners() call.
   */
  _onEventListeners: function(aListeners) {
    // Add all the listeners in the debugger view event linsteners container.
    for (let listener of aListeners) {
      DebuggerView.EventListeners.addListener(listener, { staged: true });
    }

    // Flushes all the prepared events into the event listeners container.
    DebuggerView.EventListeners.commit();
  }
};

/**
 * Handles all the breakpoints in the current debugger.
 */
function Breakpoints() {
  this._onEditorBreakpointAdd = this._onEditorBreakpointAdd.bind(this);
  this._onEditorBreakpointRemove = this._onEditorBreakpointRemove.bind(this);
  this.addBreakpoint = this.addBreakpoint.bind(this);
  this.removeBreakpoint = this.removeBreakpoint.bind(this);
}

Breakpoints.prototype = {
  /**
   * A map of breakpoint promises as tracked by the debugger frontend.
   * The keys consist of a string representation of the breakpoint location.
   */
  _added: new Map(),
  _removing: new Map(),
  _disabled: new Map(),

  /**
   * Adds the source editor breakpoint handlers.
   *
   * @return object
   *         A promise that is resolved when the breakpoints finishes initializing.
   */
  initialize: function() {
    DebuggerView.editor.on("breakpointAdded", this._onEditorBreakpointAdd);
    DebuggerView.editor.on("breakpointRemoved", this._onEditorBreakpointRemove);

    // Initialization is synchronous, for now.
    return promise.resolve(null);
  },

  /**
   * Removes the source editor breakpoint handlers & all the added breakpoints.
   *
   * @return object
   *         A promise that is resolved when the breakpoints finishes destroying.
   */
  destroy: function() {
    DebuggerView.editor.off("breakpointAdded", this._onEditorBreakpointAdd);
    DebuggerView.editor.off("breakpointRemoved", this._onEditorBreakpointRemove);

    return this.removeAllBreakpoints();
  },

  /**
   * Event handler for new breakpoints that come from the editor.
   *
   * @param number aLine
   *        Line number where breakpoint was set.
   */
  _onEditorBreakpointAdd: function(_, aLine) {
    let url = DebuggerView.Sources.selectedValue;
    let location = { url: url, line: aLine + 1 };

    // Initialize the breakpoint, but don't update the editor, since this
    // callback is invoked because a breakpoint was added in the editor itself.
    this.addBreakpoint(location, { noEditorUpdate: true }).then(aBreakpointClient => {
      // If the breakpoint client has an "requestedLocation" attached, then
      // the original requested placement for the breakpoint wasn't accepted.
      // In this case, we need to update the editor with the new location.
      if (aBreakpointClient.requestedLocation) {
        DebuggerView.editor.removeBreakpoint(aBreakpointClient.requestedLocation.line - 1);
        DebuggerView.editor.addBreakpoint(aBreakpointClient.location.line - 1);
      }
      // Notify that we've shown a breakpoint in the source editor.
      window.emit(EVENTS.BREAKPOINT_SHOWN);
    });
  },

  /**
   * Event handler for breakpoints that are removed from the editor.
   *
   * @param number aLine
   *        Line number where breakpoint was removed.
   */
  _onEditorBreakpointRemove: function(_, aLine) {
    let url = DebuggerView.Sources.selectedValue;
    let location = { url: url, line: aLine + 1 };

    // Destroy the breakpoint, but don't update the editor, since this callback
    // is invoked because a breakpoint was removed from the editor itself.
    this.removeBreakpoint(location, { noEditorUpdate: true }).then(() => {
      // Notify that we've hidden a breakpoint in the source editor.
      window.emit(EVENTS.BREAKPOINT_HIDDEN);
    });
  },

  /**
   * Update the breakpoints in the editor view. This function takes the list of
   * breakpoints in the debugger and adds them back into the editor view.
   * This is invoked when the selected script is changed, or when new sources
   * are received via the _onNewSource and _onSourcesAdded event listeners.
   */
  updateEditorBreakpoints: function() {
    for (let breakpointPromise of this._addedOrDisabled) {
      breakpointPromise.then(aBreakpointClient => {
        let currentSourceUrl = DebuggerView.Sources.selectedValue;
        let breakpointUrl = aBreakpointClient.location.url;

        // Update the view only if the breakpoint is in the currently shown source.
        if (currentSourceUrl == breakpointUrl) {
          this._showBreakpoint(aBreakpointClient, { noPaneUpdate: true });
        }
      });
    }
  },

  /**
   * Update the breakpoints in the pane view. This function takes the list of
   * breakpoints in the debugger and adds them back into the breakpoints pane.
   * This is invoked when new sources are received via the _onNewSource and
   * _onSourcesAdded event listeners.
   */
  updatePaneBreakpoints: function() {
    for (let breakpointPromise of this._addedOrDisabled) {
      breakpointPromise.then(aBreakpointClient => {
        let container = DebuggerView.Sources;
        let breakpointUrl = aBreakpointClient.location.url;

        // Update the view only if the breakpoint exists in a known source.
        if (container.containsValue(breakpointUrl)) {
          this._showBreakpoint(aBreakpointClient, { noEditorUpdate: true });
        }
      });
    }
  },

  /**
   * Add a breakpoint.
   *
   * @param object aLocation
   *        The location where you want the breakpoint.
   *        This object must have two properties:
   *          - url: the breakpoint's source location.
   *          - line: the breakpoint's line number.
   *        It can also have the following optional properties:
   *          - condition: only pause if this condition evaluates truthy
   * @param object aOptions [optional]
   *        Additional options or flags supported by this operation:
   *          - openPopup: tells if the expression popup should be shown.
   *          - noEditorUpdate: tells if you want to skip editor updates.
   *          - noPaneUpdate: tells if you want to skip breakpoint pane updates.
   * @return object
   *         A promise that is resolved after the breakpoint is added, or
   *         rejected if there was an error.
   */
  addBreakpoint: Task.async(function*(aLocation, aOptions = {}) {
    // Make sure a proper location is available.
    if (!aLocation) {
      throw new Error("Invalid breakpoint location.");
    }
    let addedPromise, removingPromise;

    // If the breakpoint was already added, or is currently being added at the
    // specified location, then return that promise immediately.
    if ((addedPromise = this._getAdded(aLocation))) {
      return addedPromise;
    }

    // If the breakpoint is currently being removed from the specified location,
    // then wait for that to finish.
    if ((removingPromise = this._getRemoving(aLocation))) {
      yield removingPromise;
    }

    let deferred = promise.defer();

    // Remember the breakpoint initialization promise in the store.
    let identifier = this.getIdentifier(aLocation);
    this._added.set(identifier, deferred.promise);

    // Try adding the breakpoint.
    gThreadClient.setBreakpoint(aLocation, Task.async(function*(aResponse, aBreakpointClient) {
      // If the breakpoint response has an "actualLocation" attached, then
      // the original requested placement for the breakpoint wasn't accepted.
      if (aResponse.actualLocation) {
        // Remember the initialization promise for the new location instead.
        let oldIdentifier = identifier;
        let newIdentifier = identifier = this.getIdentifier(aResponse.actualLocation);
        this._added.delete(oldIdentifier);
        this._added.set(newIdentifier, deferred.promise);
      }

      // By default, new breakpoints are always enabled. Disabled breakpoints
      // are, in fact, removed from the server but preserved in the frontend,
      // so that they may not be forgotten across target navigations.
      let disabledPromise = this._disabled.get(identifier);
      if (disabledPromise) {
        let aPrevBreakpointClient = yield disabledPromise;
        let condition = aPrevBreakpointClient.getCondition();
        this._disabled.delete(identifier);

        if (condition) {
          aBreakpointClient = yield aBreakpointClient.setCondition(
            gThreadClient,
            condition
          );
        }
      }

      if (aResponse.actualLocation) {
        // Store the originally requested location in case it's ever needed
        // and update the breakpoint client with the actual location.
        aBreakpointClient.requestedLocation = aLocation;
        aBreakpointClient.location = aResponse.actualLocation;
      }

      // Preserve information about the breakpoint's line text, to display it
      // in the sources pane without requiring fetching the source (for example,
      // after the target navigated). Note that this will get out of sync
      // if the source text contents change.
      let line = aBreakpointClient.location.line - 1;
      aBreakpointClient.text = DebuggerView.editor.getText(line).trim();

      // Show the breakpoint in the editor and breakpoints pane, and resolve.
      this._showBreakpoint(aBreakpointClient, aOptions);

      // Notify that we've added a breakpoint.
      window.emit(EVENTS.BREAKPOINT_ADDED, aBreakpointClient);
      deferred.resolve(aBreakpointClient);
    }.bind(this)));

    return deferred.promise;
  }),

  /**
   * Remove a breakpoint.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   * @param object aOptions [optional]
   *        @see DebuggerController.Breakpoints.addBreakpoint
   * @return object
   *         A promise that is resolved after the breakpoint is removed, or
   *         rejected if there was an error.
   */
  removeBreakpoint: function(aLocation, aOptions = {}) {
    // Make sure a proper location is available.
    if (!aLocation) {
      return promise.reject(new Error("Invalid breakpoint location."));
    }

    // If the breakpoint was already removed, or has never even been added,
    // then return a resolved promise immediately.
    let addedPromise = this._getAdded(aLocation);
    if (!addedPromise) {
      return promise.resolve(aLocation);
    }

    // If the breakpoint is currently being removed from the specified location,
    // then return that promise immediately.
    let removingPromise = this._getRemoving(aLocation);
    if (removingPromise) {
      return removingPromise;
    }

    let deferred = promise.defer();

    // Remember the breakpoint removal promise in the store.
    let identifier = this.getIdentifier(aLocation);
    this._removing.set(identifier, deferred.promise);

    // Retrieve the corresponding breakpoint client first.
    addedPromise.then(aBreakpointClient => {
      // Try removing the breakpoint.
      aBreakpointClient.remove(aResponse => {
        // If there was an error removing the breakpoint, reject the promise
        // and forget about it that the breakpoint may be re-removed later.
        if (aResponse.error) {
          deferred.reject(aResponse);
          return void this._removing.delete(identifier);
        }

        // When a breakpoint is removed, the frontend may wish to preserve some
        // details about it, so that it can be easily re-added later. In such
        // cases, breakpoints are marked and stored as disabled, so that they
        // may not be forgotten across target navigations.
        if (aOptions.rememberDisabled) {
          aBreakpointClient.disabled = true;
          this._disabled.set(identifier, promise.resolve(aBreakpointClient));
        }

        // Forget both the initialization and removal promises from the store.
        this._added.delete(identifier);
        this._removing.delete(identifier);

        // Hide the breakpoint from the editor and breakpoints pane, and resolve.
        this._hideBreakpoint(aLocation, aOptions);

        // Notify that we've removed a breakpoint.
        window.emit(EVENTS.BREAKPOINT_REMOVED, aLocation);
        deferred.resolve(aLocation);
      });
    });

    return deferred.promise;
  },

  /**
   * Removes all the currently enabled breakpoints.
   *
   * @return object
   *         A promise that is resolved after all breakpoints are removed, or
   *         rejected if there was an error.
   */
  removeAllBreakpoints: function() {
    /* Gets an array of all the existing breakpoints promises. */
    let getActiveBreakpoints = (aPromises, aStore = []) => {
      for (let [, breakpointPromise] of aPromises) {
        aStore.push(breakpointPromise);
      }
      return aStore;
    }

    /* Gets an array of all the removed breakpoints promises. */
    let getRemovedBreakpoints = (aClients, aStore = []) => {
      for (let breakpointClient of aClients) {
        aStore.push(this.removeBreakpoint(breakpointClient.location));
      }
      return aStore;
    }

    // First, populate an array of all the currently added breakpoints promises.
    // Then, once all the breakpoints clients are retrieved, populate an array
    // of all the removed breakpoints promises and wait for their fulfillment.
    return promise.all(getActiveBreakpoints(this._added)).then(aBreakpointClients => {
      return promise.all(getRemovedBreakpoints(aBreakpointClients));
    });
  },

  /**
   * Update the condition of a breakpoint.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   * @param string aClients
   *        The condition to set on the breakpoint
   * @return object
   *         A promise that will be resolved with the breakpoint client
   */
  updateCondition: function(aLocation, aCondition) {
    let addedPromise = this._getAdded(aLocation);
    if (!addedPromise) {
      return promise.reject(new Error('breakpoint does not exist ' +
                                      'in specified location'));
    }

    var promise = addedPromise.then(aBreakpointClient => {
      return aBreakpointClient.setCondition(gThreadClient, aCondition);
    });

    // `setCondition` returns a new breakpoint that has the condition,
    // so we need to update the store
    this._added.set(this.getIdentifier(aLocation), promise);
    return promise;
  },

  /**
   * Update the editor and breakpoints pane to show a specified breakpoint.
   *
   * @param object aBreakpointData
   *        Information about the breakpoint to be shown.
   *        This object must have the following properties:
   *          - location: the breakpoint's source location and line number
   *          - disabled: the breakpoint's disabled state, boolean
   *          - text: the breakpoint's line text to be displayed
   * @param object aOptions [optional]
   *        @see DebuggerController.Breakpoints.addBreakpoint
   */
  _showBreakpoint: function(aBreakpointData, aOptions = {}) {
    let currentSourceUrl = DebuggerView.Sources.selectedValue;
    let location = aBreakpointData.location;

    // Update the editor if required.
    if (!aOptions.noEditorUpdate && !aBreakpointData.disabled) {
      if (location.url == currentSourceUrl) {
        DebuggerView.editor.addBreakpoint(location.line - 1);
      }
    }

    // Update the breakpoints pane if required.
    if (!aOptions.noPaneUpdate) {
      DebuggerView.Sources.addBreakpoint(aBreakpointData, aOptions);
    }
  },

  /**
   * Update the editor and breakpoints pane to hide a specified breakpoint.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   * @param object aOptions [optional]
   *        @see DebuggerController.Breakpoints.addBreakpoint
   */
  _hideBreakpoint: function(aLocation, aOptions = {}) {
    let currentSourceUrl = DebuggerView.Sources.selectedValue;

    // Update the editor if required.
    if (!aOptions.noEditorUpdate) {
      if (aLocation.url == currentSourceUrl) {
        DebuggerView.editor.removeBreakpoint(aLocation.line - 1);
      }
    }

    // Update the breakpoints pane if required.
    if (!aOptions.noPaneUpdate) {
      DebuggerView.Sources.removeBreakpoint(aLocation);
    }
  },

  /**
   * Get a Promise for the BreakpointActor client object which is already added
   * or currently being added at the given location.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   * @return object | null
   *         A promise that is resolved after the breakpoint is added, or
   *         null if no breakpoint was found.
   */
  _getAdded: function(aLocation) {
    return this._added.get(this.getIdentifier(aLocation));
  },

  /**
   * Get a Promise for the BreakpointActor client object which is currently
   * being removed from the given location.
   *
   * @param object aLocation
   *        @see DebuggerController.Breakpoints.addBreakpoint
   * @return object | null
   *         A promise that is resolved after the breakpoint is removed, or
   *         null if no breakpoint was found.
   */
  _getRemoving: function(aLocation) {
    return this._removing.get(this.getIdentifier(aLocation));
  },

  /**
   * Get an identifier string for a given location. Breakpoint promises are
   * identified in the store by a string representation of their location.
   *
   * @param object aLocation
   *        The location to serialize to a string.
   * @return string
   *         The identifier string.
   */
  getIdentifier: function(aLocation) {
    return aLocation.url + ":" + aLocation.line;
  }
};

/**
 * Gets all Promises for the BreakpointActor client objects that are
 * either enabled (added to the server) or disabled (removed from the server,
 * but for which some details are preserved).
 */
Object.defineProperty(Breakpoints.prototype, "_addedOrDisabled", {
  get: function* () {
    yield* this._added.values();
    yield* this._disabled.values();
  }
});

/**
 * Localization convenience methods.
 */
let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);

/**
 * Shortcuts for accessing various debugger preferences.
 */
let Prefs = new ViewHelpers.Prefs("devtools", {
  sourcesWidth: ["Int", "debugger.ui.panes-sources-width"],
  instrumentsWidth: ["Int", "debugger.ui.panes-instruments-width"],
  panesVisibleOnStartup: ["Bool", "debugger.ui.panes-visible-on-startup"],
  variablesSortingEnabled: ["Bool", "debugger.ui.variables-sorting-enabled"],
  variablesOnlyEnumVisible: ["Bool", "debugger.ui.variables-only-enum-visible"],
  variablesSearchboxVisible: ["Bool", "debugger.ui.variables-searchbox-visible"],
  pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"],
  ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"],
  sourceMapsEnabled: ["Bool", "debugger.source-maps-enabled"],
  prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"],
  autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"],
  tracerEnabled: ["Bool", "debugger.tracer"],
  editorTabSize: ["Int", "editor.tabsize"]
});

/**
 * Convenient way of emitting events from the panel window.
 */
EventEmitter.decorate(this);

/**
 * Preliminary setup for the DebuggerController object.
 */
DebuggerController.initialize();
DebuggerController.Parser = new Parser();
DebuggerController.ThreadState = new ThreadState();
DebuggerController.StackFrames = new StackFrames();
DebuggerController.SourceScripts = new SourceScripts();
DebuggerController.Breakpoints = new Breakpoints();
DebuggerController.Breakpoints.DOM = new EventListeners();
DebuggerController.Tracer = new Tracer();

/**
 * Export some properties to the global scope for easier access.
 */
Object.defineProperties(window, {
  "gTarget": {
    get: function() DebuggerController._target
  },
  "gHostType": {
    get: function() DebuggerView._hostType
  },
  "gClient": {
    get: function() DebuggerController.client
  },
  "gThreadClient": {
    get: function() DebuggerController.activeThread
  },
  "gCallStackPageSize": {
    get: function() CALL_STACK_PAGE_SIZE
  }
});

/**
 * Helper method for debugging.
 * @param string
 */
function dumpn(str) {
  if (wantLogging) {
    dump("DBG-FRONTEND: " + str + "\n");
  }
}

let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
