/* -*- 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 Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;

const FRAME_STEP_CACHE_DURATION = 100; // ms
const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
const SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes

Cu.import("resource:///modules/source-editor.jsm");
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import('resource://gre/modules/Services.jsm');

/**
 * Controls the debugger view by handling the source scripts, the current
 * thread state and thread stack frame cache.
 */
let DebuggerController = {

  /**
   * Makes a few preliminary changes and bindings to the controller.
   */
  init: function() {
    this._startupDebugger = this._startupDebugger.bind(this);
    this._shutdownDebugger = this._shutdownDebugger.bind(this);
    this._onTabNavigated = this._onTabNavigated.bind(this);
    this._onTabDetached = this._onTabDetached.bind(this);

    window.addEventListener("DOMContentLoaded", this._startupDebugger, true);
    window.addEventListener("unload", this._shutdownDebugger, true);
  },

  /**
   * Initializes the debugger view and connects a debugger client to the server.
   */
  _startupDebugger: function DC__startupDebugger() {
    if (this._isInitialized) {
      return;
    }
    this._isInitialized = true;
    window.removeEventListener("DOMContentLoaded", this._startupDebugger, true);

    DebuggerView.initializePanes();
    DebuggerView.initializeEditor();
    DebuggerView.StackFrames.initialize();
    DebuggerView.Properties.initialize();
    DebuggerView.Scripts.initialize();
    DebuggerView.showCloseButton(!this._isRemoteDebugger && !this._isChromeDebugger);

    this.dispatchEvent("Debugger:Loaded");
    this._connect();
  },

  /**
   * Destroys the debugger view, disconnects the debugger client and cleans up
   * any active listeners.
   */
  _shutdownDebugger: function DC__shutdownDebugger() {
    if (this._isDestroyed) {
      return;
    }
    this._isDestroyed = true;
    window.removeEventListener("unload", this._shutdownDebugger, true);

    DebuggerView.destroyPanes();
    DebuggerView.destroyEditor();
    DebuggerView.Scripts.destroy();
    DebuggerView.StackFrames.destroy();
    DebuggerView.Properties.destroy();

    DebuggerController.Breakpoints.destroy();
    DebuggerController.SourceScripts.disconnect();
    DebuggerController.StackFrames.disconnect();
    DebuggerController.ThreadState.disconnect();

    this.dispatchEvent("Debugger:Unloaded");
    this._disconnect();
    this._isChromeDebugger && this._quitApp();
  },

  /**
   * Prepares the hostname and port number for a remote debugger connection
   * and handles connection retries and timeouts.
   *
   * @return boolean true if connection should proceed normally
   */
  _prepareConnection: function DC__prepareConnection() {
    // If we exceeded the total number of connection retries, bail.
    if (this._remoteConnectionTry === Prefs.remoteConnectionRetries) {
      Services.prompt.alert(null,
        L10N.getStr("remoteDebuggerPromptTitle"),
        L10N.getStr("remoteDebuggerConnectionFailedMessage"));
      this.dispatchEvent("Debugger:Close");
      return false;
    }

    // TODO: This is ugly, need to rethink the design for the UI in #751677.
    if (!Prefs.remoteAutoConnect) {
      let prompt = new RemoteDebuggerPrompt();
      let result = prompt.show(!!this._remoteConnectionTimeout);
      // If the connection was not established before the user canceled the
      // prompt, close the remote debugger.
      if (!result && !DebuggerController.activeThread) {
        this.dispatchEvent("Debugger:Close");
        return false;
      }
      Prefs.remoteHost = prompt.uri.host;
      Prefs.remotePort = prompt.uri.port;
    }

    // If this debugger is connecting remotely to a server, we need to check
    // after a while if the connection actually succeeded.
    this._remoteConnectionTry = ++this._remoteConnectionTry || 1;
    this._remoteConnectionTimeout = window.setTimeout(function() {
      // If we couldn't connect to any server yet, try again...
      if (!DebuggerController.activeThread) {
        DebuggerController._onRemoteConnectionTimeout();
        DebuggerController._connect();
      }
    }, Prefs.remoteTimeout);

    return true;
  },

  /**
   * Called when a remote connection timeout occurs.
   */
  _onRemoteConnectionTimeout: function DC__onRemoteConnectionTimeout() {
    Cu.reportError("Couldn't connect to " +
      Prefs.remoteHost + ":" + Prefs.remotePort);
  },

  /**
   * Initializes a debugger client and connects it to the debugger server,
   * wiring event handlers as necessary.
   */
  _connect: function DC__connect() {
    if (this._isRemoteDebugger) {
      if (!this._prepareConnection()) {
        return;
      }
    }

    let transport = (this._isChromeDebugger || this._isRemoteDebugger)
      ? debuggerSocketConnect(Prefs.remoteHost, Prefs.remotePort)
      : DebuggerServer.connectPipe();

    let client = this.client = new DebuggerClient(transport);

    client.addListener("tabNavigated", this._onTabDetached);
    client.addListener("tabDetached", this._onTabDetached);

    client.connect(function(aType, aTraits) {
      client.listTabs(function(aResponse) {
        let tab = aResponse.tabs[aResponse.selected];
        this._startDebuggingTab(client, tab);
        this.dispatchEvent("Debugger:Connecting");
      }.bind(this));
    }.bind(this));
  },

  /**
   * Closes the debugger client and removes event handlers as necessary.
   */
  _disconnect: function DC__disconnect() {
    this.client.removeListener("tabNavigated", this._onTabDetached);
    this.client.removeListener("tabDetached", this._onTabDetached);
    this.client.close();

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

  /**
   * Starts debugging the current tab. This function is called on each location
   * change in this tab.
   */
  _onTabNavigated: function DC__onTabNavigated(aNotification, aPacket) {
    let client = this.client;

    client.activeThread.detach(function() {
      client.activeTab.detach(function() {
        client.listTabs(function(aResponse) {
          let tab = aResponse.tabs[aResponse.selected];
          this._startDebuggingTab(client, tab);
          this.dispatchEvent("Debugger:Connecting");
        }.bind(this));
      }.bind(this));
    }.bind(this));
  },

  /**
   * Stops debugging the current tab.
   */
  _onTabDetached: function DC__onTabDetached() {
    this.dispatchEvent("Debugger:Close");
  },

  /**
   * Sets up a debugging session.
   *
   * @param DebuggerClient aClient
   *        The debugger client.
   * @param object aTabGrip
   *        The remote protocol grip of the tab.
   */
  _startDebuggingTab: function DC__startDebuggingTab(aClient, aTabGrip) {
    if (!aClient) {
      Cu.reportError("No client found!");
      return;
    }
    this.client = aClient;

    aClient.attachTab(aTabGrip.actor, function(aResponse, aTabClient) {
      if (!aTabClient) {
        Cu.reportError("No tab client found!");
        return;
      }
      this.tabClient = aTabClient;

      aClient.attachThread(aResponse.threadActor, function(aResponse, aThreadClient) {
        if (!aThreadClient) {
          Cu.reportError("Couldn't attach to thread: " + aResponse.error);
          return;
        }
        this.activeThread = aThreadClient;

        DebuggerController.ThreadState.connect(function() {
          DebuggerController.StackFrames.connect(function() {
            DebuggerController.SourceScripts.connect(function() {
              aThreadClient.resume();
            });
          });
        });

      }.bind(this));
    }.bind(this));
  },

  /**
   * Returns true if this is a remote debugger instance.
   * @return boolean
   */
  get _isRemoteDebugger() {
    return window._remoteFlag;
  },

  /**
   * Returns true if this is a chrome debugger instance.
   * @return boolean
   */
  get _isChromeDebugger() {
    return !window.parent.content && !this._isRemoteDebugger;
  },

  /**
   * Attempts to quit the current process if allowed.
   */
  _quitApp: function DC__quitApp() {
    let canceled = Cc["@mozilla.org/supports-PRBool;1"]
      .createInstance(Ci.nsISupportsPRBool);

    Services.obs.notifyObservers(canceled, "quit-application-requested", null);

    // Somebody canceled our quit request.
    if (canceled.data) {
      return;
    }

    Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit);
  },

  /**
   * Convenience method, dispatching a custom event.
   *
   * @param string aType
   *        The name of the event.
   * @param string aDetail
   *        The data passed when initializing the event.
   */
  dispatchEvent: function DC_dispatchEvent(aType, aDetail) {
    let evt = document.createEvent("CustomEvent");
    evt.initCustomEvent(aType, true, false, aDetail);
    document.documentElement.dispatchEvent(evt);
  }
};

/**
 * 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 = {

  /**
   * Gets the current thread the client has connected to.
   */
  get activeThread() {
    return DebuggerController.activeThread;
  },

  /**
   * Connect to the current thread client.
   *
   * @param function aCallback
   *        The next function in the initialization sequence.
   */
  connect: function TS_connect(aCallback) {
    this.activeThread.addListener("paused", this._update);
    this.activeThread.addListener("resumed", this._update);
    this.activeThread.addListener("detached", this._update);

    this._update();

    aCallback && aCallback();
  },

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

  /**
   * Update the UI after a thread state change.
   */
  _update: function TS__update(aEvent) {
    DebuggerView.StackFrames.updateState(this.activeThread.state);
  }
};

/**
 * 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._onResume = this._onResume.bind(this);
  this._onFrames = this._onFrames.bind(this);
  this._onFramesCleared = this._onFramesCleared.bind(this);
  this._afterFramesCleared = this._afterFramesCleared.bind(this);
}

StackFrames.prototype = {

  /**
   * The maximum number of frames allowed to be loaded at a time.
   */
  pageSize: 25,

  /**
   * The currently selected frame depth.
   */
  selectedFrame: null,

  /**
   * A flag that defines whether the debuggee will pause whenever an exception
   * is thrown.
   */
  pauseOnExceptions: false,

  /**
   * Gets the current thread the client has connected to.
   */
  get activeThread() {
    return DebuggerController.activeThread;
  },

  /**
   * Watch the given thread client.
   *
   * @param function aCallback
   *        The next function in the initialization sequence.
   */
  connect: function SF_connect(aCallback) {
    window.addEventListener("Debugger:FetchedVariables", this._onFetchedVars, false);

    this._onFramesCleared();

    this.activeThread.addListener("paused", this._onPaused);
    this.activeThread.addListener("resumed", this._onResume);
    this.activeThread.addListener("framesadded", this._onFrames);
    this.activeThread.addListener("framescleared", this._onFramesCleared);

    this.updatePauseOnExceptions(this.pauseOnExceptions);

    aCallback && aCallback();
  },

  /**
   * Disconnect from the client.
   */
  disconnect: function SF_disconnect() {
    window.removeEventListener("Debugger:FetchedVariables", this._onFetchedVars, false);

    if (!this.activeThread) {
      return;
    }
    this.activeThread.removeListener("paused", this._onPaused);
    this.activeThread.removeListener("resumed", this._onResume);
    this.activeThread.removeListener("framesadded", this._onFrames);
    this.activeThread.removeListener("framescleared", this._onFramesCleared);
  },

  /**
   * 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 SF__onPaused(aEvent, aPacket) {
    // In case the pause was caused by an exception, store the exception value.
    if (aPacket.why.type == "exception") {
      this.exception = aPacket.why.exception;
    }
    this.activeThread.fillFrames(this.pageSize);
  },

  /**
   * Handler for the thread client's resumed notification.
   */
  _onResume: function SF__onResume() {
    DebuggerView.editor.setDebugLocation(-1);
  },

  /**
   * Handler for the thread client's framesadded notification.
   */
  _onFrames: function SF__onFrames() {
    if (!this.activeThread.cachedFrames.length) {
      DebuggerView.StackFrames.emptyText();
      DebuggerView.Properties.emptyText();
      return;
    }
    DebuggerView.StackFrames.empty();
    DebuggerView.Properties.empty();

    for each (let frame in this.activeThread.cachedFrames) {
      this._addFrame(frame);
    }
    if (!this.selectedFrame) {
      this.selectFrame(0);
    }
    if (this.activeThread.moreFrames) {
      DebuggerView.StackFrames.dirty = true;
    }
  },

  /**
   * Handler for the thread client's framescleared notification.
   */
  _onFramesCleared: function SF__onFramesCleared() {
    this.selectedFrame = null;
    this.exception = null;
    // 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.
    window.setTimeout(this._afterFramesCleared, FRAME_STEP_CACHE_DURATION);
  },

  /**
   * Called soon after the thread client's framescleared notification.
   */
  _afterFramesCleared: function SF__afterFramesCleared() {
    if (this.activeThread && !this.activeThread.cachedFrames.length) {
      DebuggerView.StackFrames.emptyText();
      DebuggerView.Properties.emptyText();
      DebuggerController.dispatchEvent("Debugger:AfterFramesCleared");
    }
  },

  /**
   * Update the source editor's current debug location based on the selected
   * frame and script.
   */
  updateEditorLocation: function SF_updateEditorLocation() {
    let frame = this.activeThread.cachedFrames[this.selectedFrame];
    if (!frame) {
      return;
    }

    let url = frame.where.url;
    let line = frame.where.line;
    let editor = DebuggerView.editor;

    // Move the editor's caret to the proper line.
    if (DebuggerView.Scripts.isSelected(url) && line) {
      editor.setDebugLocation(line - 1);
    } else {
      editor.setDebugLocation(-1);
    }
  },

  /**
   * Inform the debugger client whether the debuggee should be paused whenever
   * an exception is thrown.
   *
   * @param boolean aFlag
   *        The new value of the flag: true for pausing, false otherwise.
   */
  updatePauseOnExceptions: function SF_updatePauseOnExceptions(aFlag) {
    this.pauseOnExceptions = aFlag;
    this.activeThread.pauseOnExceptions(this.pauseOnExceptions);
  },

  /**
   * Marks the stack frame in 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 SF_selectFrame(aDepth) {
    // Deselect any previously highlighted frame.
    if (this.selectedFrame !== null) {
      DebuggerView.StackFrames.unhighlightFrame(this.selectedFrame);
    }

    // Highlight the current frame.
    this.selectedFrame = aDepth;
    DebuggerView.StackFrames.highlightFrame(this.selectedFrame);

    let frame = this.activeThread.cachedFrames[aDepth];
    if (!frame) {
      return;
    }

    let url = frame.where.url;
    let line = frame.where.line;
    let editor = DebuggerView.editor;

    // Move the editor's caret to the proper line.
    if (DebuggerView.Scripts.isSelected(url) && line) {
      editor.setCaretPosition(line - 1);
      editor.setDebugLocation(line - 1);
    }
    else if (DebuggerView.Scripts.contains(url)) {
      DebuggerView.Scripts.selectScript(url);
      editor.setCaretPosition(line - 1);
    }
    else {
      editor.setDebugLocation(-1);
    }

    // Start recording any added variables or properties in any scope.
    DebuggerView.Properties.createHierarchyStore();

    // Clear existing scopes and create each one dynamically.
    DebuggerView.Properties.empty();

    if (frame.environment) {
      let env = frame.environment;
      do {
        // Construct the scope name.
        let name = env.type.charAt(0).toUpperCase() + env.type.slice(1);
        // Call the outermost scope Global.
        if (!env.parent) {
          name = L10N.getStr("globalScopeLabel");
        }
        let label = L10N.getFormatStr("scopeLabel", [name]);
        switch (env.type) {
          case "with":
          case "object":
            label += " [" + env.object.class + "]";
            break;
          case "function":
            if (env.functionName) {
              label += " [" + env.functionName + "]";
            }
            break;
          default:
            break;
        }

        let scope = DebuggerView.Properties.addScope(label);

        // Special additions to the innermost scope.
        if (env == frame.environment) {
          // Add any thrown exception.
          if (aDepth == 0 && this.exception) {
            let excVar = scope.addVar("<exception>");
            if (typeof this.exception == "object") {
              excVar.setGrip({
                type: this.exception.type,
                class: this.exception.class
              });
              this._addExpander(excVar, this.exception);
            } else {
              excVar.setGrip(this.exception);
            }
          }

          // Add "this".
          if (frame.this) {
            let thisVar = scope.addVar("this");
            thisVar.setGrip({
              type: frame.this.type,
              class: frame.this.class
            });
            this._addExpander(thisVar, frame.this);
          }

          // Expand the innermost scope by default.
          scope.expand(true);
          scope.addToHierarchy();
        }

        switch (env.type) {
          case "with":
          case "object":
            let objClient = this.activeThread.pauseGrip(env.object);
            objClient.getPrototypeAndProperties(function SF_getProps(aResponse) {
              this._addScopeVariables(aResponse.ownProperties, scope);
              // Signal that variables have been fetched.
              DebuggerController.dispatchEvent("Debugger:FetchedVariables");
            }.bind(this));
            break;
          case "block":
          case "function":
            // Add nodes for every argument.
            let variables = env.bindings.arguments;
            for each (let variable in variables) {
              let name = Object.getOwnPropertyNames(variable)[0];
              let paramVar = scope.addVar(name, variable[name]);
              let paramVal = variable[name].value;
              paramVar.setGrip(paramVal);
              this._addExpander(paramVar, paramVal);
            }
            // Add nodes for every other variable in scope.
            this._addScopeVariables(env.bindings.variables, scope);
            break;
          default:
            Cu.reportError("Unknown Debugger.Environment type: " + env.type);
            break;
        }
      } while (env = env.parent);
    }

    // Signal that variables have been fetched.
    DebuggerController.dispatchEvent("Debugger:FetchedVariables");
  },

  /**
   * Called afters variables have been fetched after a frame was selected.
   */
  _onFetchedVars: function SF__onFetchedVars() {
    DebuggerView.Properties.commitHierarchy();
  },

  /**
   * Add nodes for every variable in scope.
   *
   * @param object aVariables
   *        The map of names to variables, as specified in the Remote
   *        Debugging Protocol.
   * @param object aScope
   *        The scope where the nodes will be placed into.
   */
  _addScopeVariables: function SF_addScopeVariables(aVariables, aScope) {
    // Sort all of the variables before adding them, for better UX.
    let variables = {};
    for each (let prop in Object.keys(aVariables).sort()) {
      variables[prop] = aVariables[prop];
    }

    // Add the sorted variables to the specified scope.
    for (let variable in variables) {
      let paramVar = aScope.addVar(variable, variables[variable]);
      let paramVal = variables[variable].value;
      paramVar.setGrip(paramVal);
      this._addExpander(paramVar, paramVal);
    }
  },

  /**
   * Adds an 'onexpand' callback for a variable, lazily handling the addition of
   * new properties.
   */
  _addExpander: function SF__addExpander(aVar, aObject) {
    // No need for expansion for null and undefined values.
    if (!aVar || !aObject || typeof aObject !== "object" ||
        aObject.type !== "object") {
      return;
    }

    // Force the twisty to show up.
    aVar.forceShowArrow();
    aVar.onexpand = this._addVarProperties.bind(this, aVar, aObject);
  },

  /**
   * Adds properties to a variable in the view. Triggered when a variable is
   * expanded.
   */
  _addVarProperties: function SF__addVarProperties(aVar, aObject) {
    // Retrieve the properties only once.
    if (aVar.fetched) {
      return;
    }

    let objClient = this.activeThread.pauseGrip(aObject);
    objClient.getPrototypeAndProperties(function SF_onProtoAndProps(aResponse) {
      // Sort all of the properties before adding them, for better UX.
      let properties = {};
      for each (let prop in Object.keys(aResponse.ownProperties).sort()) {
        properties[prop] = aResponse.ownProperties[prop];
      }
      aVar.addProperties(properties);

      // Expansion handlers must be set after the properties are added.
      for (let prop in aResponse.ownProperties) {
        this._addExpander(aVar[prop], aResponse.ownProperties[prop].value);
      }

      // Add __proto__.
      if (aResponse.prototype.type !== "null") {
        let properties = { "__proto__ ": { value: aResponse.prototype } };
        aVar.addProperties(properties);

        // Expansion handlers must be set after the properties are added.
        this._addExpander(aVar["__proto__ "], aResponse.prototype);
      }
      aVar.fetched = true;
    }.bind(this));
  },

  /**
   * Adds the specified stack frame to the list.
   *
   * @param Debugger.Frame aFrame
   *        The new frame to add.
   */
  _addFrame: function SF__addFrame(aFrame) {
    let depth = aFrame.depth;
    let label = DebuggerController.SourceScripts._getScriptLabel(aFrame.where.url);

    let startText = this._getFrameTitle(aFrame);
    let endText = label + ":" + aFrame.where.line;

    let frame = DebuggerView.StackFrames.addFrame(depth, startText, endText);
    if (frame) {
      frame.debuggerFrame = aFrame;
    }
  },

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

  /**
   * Create a textual representation for the stack frame specified, for
   * displaying in the stack frame list.
   *
   * @param Debugger.Frame aFrame
   *        The stack frame to label.
   */
  _getFrameTitle: function SF__getFrameTitle(aFrame) {
    if (aFrame.type == "call") {
      return aFrame["calleeName"] ? aFrame["calleeName"] : "(anonymous)";
    }
    return "(" + aFrame.type + ")";
  },

  /**
   * Evaluate an expression in the context of the selected frame. This is used
   * for modifying the value of variables in scope.
   *
   * @param string aExpression
   *        The expression to evaluate.
   */
  evaluate: function SF_evaluate(aExpression) {
    let frame = this.activeThread.cachedFrames[this.selectedFrame];
    this.activeThread.eval(frame.actor, aExpression);
  }
};

/**
 * Keeps the source script list up-to-date, using the thread client's
 * source script cache.
 */
function SourceScripts() {
  this._onNewScript = this._onNewScript.bind(this);
  this._onScriptsAdded = this._onScriptsAdded.bind(this);
  this._onScriptsCleared = this._onScriptsCleared.bind(this);
  this._onShowScript = this._onShowScript.bind(this);
  this._onLoadSource = this._onLoadSource.bind(this);
  this._onLoadSourceFinished = this._onLoadSourceFinished.bind(this);
}

SourceScripts.prototype = {

  /**
   * A cache containing simplified labels from script urls.
   */
  _labelsCache: {},

  /**
   * Gets the current thread the client has connected to.
   */
  get activeThread() {
    return DebuggerController.activeThread;
  },

  /**
   * Gets the current debugger client.
   */
  get debuggerClient() {
    return DebuggerController.client;
  },

  /**
   * Watch the given thread client.
   *
   * @param function aCallback
   *        The next function in the initialization sequence.
   */
  connect: function SS_connect(aCallback) {
    window.addEventListener("Debugger:LoadSource", this._onLoadSource, false);

    this.debuggerClient.addListener("newScript", this._onNewScript);
    this.activeThread.addListener("scriptsadded", this._onScriptsAdded);
    this.activeThread.addListener("scriptscleared", this._onScriptsCleared);

    this._clearLabelsCache();
    this._onScriptsCleared();

    // Retrieve the list of scripts known to the server from before the client
    // was ready to handle new script notifications.
    this.activeThread.fillScripts();

    aCallback && aCallback();
  },

  /**
   * Disconnect from the client.
   */
  disconnect: function TS_disconnect() {
    window.removeEventListener("Debugger:LoadSource", this._onLoadSource, false);

    if (!this.activeThread) {
      return;
    }
    this.debuggerClient.removeListener("newScript", this._onNewScript);
    this.activeThread.removeListener("scriptsadded", this._onScriptsAdded);
    this.activeThread.removeListener("scriptscleared", this._onScriptsCleared);
  },

  /**
   * Handler for the debugger client's unsolicited newScript notification.
   */
  _onNewScript: function SS__onNewScript(aNotification, aPacket) {
    // Ignore scripts generated from 'clientEvaluate' packets.
    if (aPacket.url == "debugger eval code") {
      return;
    }

    this._addScript({ url: aPacket.url, startLine: aPacket.startLine }, true);
    // If there are any stored breakpoints for this script, display them again.
    for each (let bp in DebuggerController.Breakpoints.store) {
      if (bp.location.url == aPacket.url) {
        DebuggerController.Breakpoints.displayBreakpoint(bp.location);
      }
    }
  },

  /**
   * Handler for the thread client's scriptsadded notification.
   */
  _onScriptsAdded: function SS__onScriptsAdded() {
    for each (let script in this.activeThread.cachedScripts) {
      this._addScript(script, false);
    }
    DebuggerView.Scripts.commitScripts();
  },

  /**
   * Handler for the thread client's scriptscleared notification.
   */
  _onScriptsCleared: function SS__onScriptsCleared() {
    DebuggerView.Scripts.empty();
  },

  /**
   * Sets the proper editor mode (JS or HTML) according to the specified
   * content type, or by determining the type from the URL.
   *
   * @param string aUrl
   *        The script URL.
   * @param string aContentType [optional]
   *        The script content type.
   */
  _setEditorMode: function SS__setEditorMode(aUrl, aContentType) {
    if (aContentType) {
      if (/javascript/.test(aContentType)) {
        DebuggerView.editor.setMode(SourceEditor.MODES.JAVASCRIPT);
      } else {
        DebuggerView.editor.setMode(SourceEditor.MODES.HTML);
      }
      return;
    }

    // Use JS mode for files with .js and .jsm extensions.
    if (/\.jsm?$/.test(this.trimUrlQuery(aUrl))) {
      DebuggerView.editor.setMode(SourceEditor.MODES.JAVASCRIPT);
    } else {
      DebuggerView.editor.setMode(SourceEditor.MODES.HTML);
    }
  },

  /**
   * Trims the query part or reference identifier of a url string, if necessary.
   *
   * @param string aUrl
   *        The script url.
   * @return string
   *         The url with the trimmed query.
   */
  trimUrlQuery: function SS_trimUrlQuery(aUrl) {
    let length = aUrl.length;
    let q1 = aUrl.indexOf('?');
    let q2 = aUrl.indexOf('&');
    let q3 = aUrl.indexOf('#');
    let q = Math.min(q1 !== -1 ? q1 : length,
                     q2 !== -1 ? q2 : length,
                     q3 !== -1 ? q3 : length);

    return aUrl.slice(0, q);
  },

  /**
   * Trims as much as possible from a URL, while keeping the result unique
   * in the Debugger View scripts container.
   *
   * @param string | nsIURL aUrl
   *        The script URL.
   * @param string aLabel [optional]
   *        The resulting label at each step.
   * @param number aSeq [optional]
   *        The current iteration step.
   * @return string
   *         The resulting label at the final step.
   */
  _trimURL: function SS__trimURL(aUrl, aLabel, aSeq) {
    if (!(aUrl instanceof Ci.nsIURL)) {
      try {
        // Use an nsIURL to parse all the url path parts.
        aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
      } catch (e) {
        // This doesn't look like a url, or nsIURL can't handle it.
        return aUrl;
      }
    }
    if (!aSeq) {
      let name = aUrl.fileName;
      if (name) {
        // This is a regular file url, get only the file name (contains the
        // base name and extension if available).

        // If this url contains an invalid query, unfortunately nsIURL thinks
        // it's part of the file extension. It must be removed.
        aLabel = aUrl.fileName.replace(/\&.*/, "");
      } else {
        // This is not a file url, hence there is no base name, nor extension.
        // Proceed using other available information.
        aLabel = "";
      }
      aSeq = 1;
    }

    // If we have a label and it doesn't start with a query...
    if (aLabel && aLabel.indexOf("?") !== 0) {

      if (DebuggerView.Scripts.containsIgnoringQuery(aUrl.spec)) {
        // A page may contain multiple requests to the same url but with different
        // queries. It would be redundant to show each one.
        return aLabel;
      }
      if (!DebuggerView.Scripts.containsLabel(aLabel)) {
        // We found the shortest unique label for the url.
        return aLabel;
      }
    }

    // Append the url query.
    if (aSeq === 1) {
      let query = aUrl.query;
      if (query) {
        return this._trimURL(aUrl, aLabel + "?" + query, aSeq + 1);
      }
      aSeq++;
    }
    // Append the url reference.
    if (aSeq === 2) {
      let ref = aUrl.ref;
      if (ref) {
        return this._trimURL(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1);
      }
      aSeq++;
    }
    // Prepend the url directory.
    if (aSeq === 3) {
      let dir = aUrl.directory;
      if (dir) {
        return this._trimURL(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1);
      }
      aSeq++;
    }
    // Prepend the hostname and port number.
    if (aSeq === 4) {
      let host = aUrl.hostPort;
      if (host) {
        return this._trimURL(aUrl, host + "/" + aLabel, aSeq + 1);
      }
      aSeq++;
    }
    // Use the whole url spec but ignoring the reference.
    if (aSeq === 5) {
      return this._trimURL(aUrl, aUrl.specIgnoringRef, aSeq + 1);
    }
    // Give up.
    return aUrl.spec;
  },

  /**
   * Gets a unique, simplified label from a script url.
   *
   * @param string aUrl
   *        The script url.
   * @param string aHref
   *        The content location href to be used. If unspecified, it will
   *        default to the script url prepath.
   * @return string
   *         The simplified label.
   */
  _getScriptLabel: function SS__getScriptLabel(aUrl, aHref) {
    return this._labelsCache[aUrl] || (this._labelsCache[aUrl] = this._trimURL(aUrl));
  },

  /**
   * Clears the labels cache, populated by SS_getScriptLabel.
   * This should be done every time the content location changes.
   */
  _clearLabelsCache: function SS__clearLabelsCache() {
    this._labelsCache = {};
  },

  /**
   * Add the specified script to the list.
   *
   * @param object aScript
   *        The script object coming from the active thread.
   * @param boolean aForceFlag
   *        True to force the script to be immediately added.
   */
  _addScript: function SS__addScript(aScript, aForceFlag) {
    DebuggerView.Scripts.addScript(
      this._getScriptLabel(aScript.url), aScript, aForceFlag);
  },

  /**
   * Load the editor with the script text if available, otherwise fire an event
   * to load and display the script text.
   *
   * @param object aScript
   *        The script object coming from the active thread.
   * @param object [aOptions]
   *        Additional options for showing the script. Supported options:
   *        - targetLine: place the editor at the given line number.
   */
  showScript: function SS_showScript(aScript, aOptions) {
    if (aScript.loaded) {
      this._onShowScript(aScript, aOptions);
      return;
    }

    let editor = DebuggerView.editor;
    editor.setMode(SourceEditor.MODES.TEXT);
    editor.setText(L10N.getStr("loadingText"));
    editor.resetUndo();

    // Notify that we need to load a script file.
    DebuggerController.dispatchEvent("Debugger:LoadSource", {
      url: aScript.url,
      options: aOptions
    });
  },

  /**
   * Display the script source once it loads.
   *
   * @private
   * @param object aScript
   *        The script object coming from the active thread.
   * @param object aOptions [optional]
   *        Additional options for showing the script. Supported options:
   *        - targetLine: place the editor at the given line number.
   */
  _onShowScript: function SS__onShowScript(aScript, aOptions) {
    aOptions = aOptions || {};

    if (aScript.text.length < SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
      this._setEditorMode(aScript.url, aScript.contentType);
    }

    let editor = DebuggerView.editor;
    editor.setText(aScript.text);
    editor.resetUndo();

    DebuggerController.Breakpoints.updateEditorBreakpoints();
    DebuggerController.StackFrames.updateEditorLocation();

    // Handle any additional options for showing the script.
    if (aOptions.targetLine) {
      editor.setCaretPosition(aOptions.targetLine - 1);
    }

    // Notify that we shown script file.
    DebuggerController.dispatchEvent("Debugger:ScriptShown", {
      url: aScript.url
    });
  },

  /**
   * Handles notifications to load a source script from the cache or from a
   * local file.
   *
   * XXX: It may be better to use nsITraceableChannel to get to the sources
   * without relying on caching when we can (not for eval, etc.):
   * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
   */
  _onLoadSource: function SS__onLoadSource(aEvent) {
    let url = aEvent.detail.url;
    let options = aEvent.detail.options;
    let self = this;

    switch (Services.io.extractScheme(url)) {
      case "file":
      case "chrome":
      case "resource":
        try {
          NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) {
            if (!Components.isSuccessCode(aStatus)) {
              return self._logError(url, aStatus);
            }
            let source = NetUtil.readInputStreamToString(aStream, aStream.available());
            self._onLoadSourceFinished(url, source, null, options);
            aStream.close();
          });
        } catch (ex) {
          return self._logError(url, ex.name);
        }
        break;

      default:
        let channel = Services.io.newChannel(url, null, null);
        let chunks = [];
        let streamListener = {
          onStartRequest: function(aRequest, aContext, aStatusCode) {
            if (!Components.isSuccessCode(aStatusCode)) {
              return self._logError(url, aStatusCode);
            }
          },
          onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
            chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
          },
          onStopRequest: function(aRequest, aContext, aStatusCode) {
            if (!Components.isSuccessCode(aStatusCode)) {
              return self._logError(url, aStatusCode);
            }
            self._onLoadSourceFinished(
              url, chunks.join(""), channel.contentType, options);
          }
        };

        channel.loadFlags = channel.LOAD_FROM_CACHE;
        channel.asyncOpen(streamListener, null);
        break;
    }
  },

  /**
   * Called when source has been loaded.
   *
   * @private
   * @param string aSourceUrl
   *        The URL of the source script.
   * @param string aSourceText
   *        The text of the source script.
   * @param string aContentType
   *        The content type of the source script.
   * @param object aOptions [optional]
   *        Additional options for showing the script. Supported options:
   *        - targetLine: place the editor at the given line number.
   */
  _onLoadSourceFinished:
  function SS__onLoadSourceFinished(aSourceUrl, aSourceText, aContentType, aOptions) {
    let scripts = document.getElementById("scripts");
    let element = scripts.getElementsByAttribute("value", aSourceUrl)[0];
    let script = element.getUserData("sourceScript");

    script.loaded = true;
    script.text = aSourceText;
    script.contentType = aContentType;
    element.setUserData("sourceScript", script, null);

    this.showScript(script, aOptions);
  },

  /**
   * Log an error message in the error console when a script fails to load.
   *
   * @param string aUrl
   *        The URL of the source script.
   * @param string aStatus
   *        The failure status code.
   */
  _logError: function SS__logError(aUrl, aStatus) {
    Cu.reportError(L10N.getFormatStr("loadingError", [aUrl, aStatus]));
  },
};

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

Breakpoints.prototype = {

  /**
   * Skip editor breakpoint change events.
   *
   * This property tells the source editor event handler to skip handling of
   * the BREAKPOINT_CHANGE events. This is used when the debugger adds/removes
   * breakpoints from the editor. Typically, the BREAKPOINT_CHANGE event handler
   * adds/removes events from the debugger, but when breakpoints are added from
   * the public debugger API, we need to do things in reverse.
   *
   * This implementation relies on the fact that the source editor fires the
   * BREAKPOINT_CHANGE events synchronously.
   *
   * @private
   * @type boolean
   */
  _skipEditorBreakpointChange: false,

  /**
   * The list of breakpoints in the debugger as tracked by the current
   * debugger instance. This is an object where the values are BreakpointActor
   * objects received from the client, while the keys are actor names, for
   * example "conn0.breakpoint3".
   *
   * @type object
   */
  store: {},

  /**
   * Gets the current thread the client has connected to.
   */
  get activeThread() {
    return DebuggerController.ThreadState.activeThread;
  },

  /**
   * Gets the source editor in the debugger view.
   */
  get editor() {
    return DebuggerView.editor;
  },

  /**
   * Sets up the source editor breakpoint handlers.
   */
  initialize: function BP_initialize() {
    this.editor.addEventListener(
      SourceEditor.EVENTS.BREAKPOINT_CHANGE, this._onEditorBreakpointChange);
  },

  /**
   * Removes all currently added breakpoints.
   */
  destroy: function BP_destroy() {
    for each (let breakpoint in this.store) {
      this.removeBreakpoint(breakpoint);
    }
  },

  /**
   * Event handler for breakpoint changes that happen in the editor. This
   * function syncs the breakpoint changes in the editor to those in the
   * debugger.
   *
   * @private
   * @param object aEvent
   *        The SourceEditor.EVENTS.BREAKPOINT_CHANGE event object.
   */
  _onEditorBreakpointChange: function BP__onEditorBreakpointChange(aEvent) {
    if (this._skipEditorBreakpointChange) {
      return;
    }

    aEvent.added.forEach(this._onEditorBreakpointAdd, this);
    aEvent.removed.forEach(this._onEditorBreakpointRemove, this);
  },

  /**
   * Event handler for new breakpoints that come from the editor.
   *
   * @private
   * @param object aBreakpoint
   *        The breakpoint object coming from the editor.
   */
  _onEditorBreakpointAdd: function BP__onEditorBreakpointAdd(aBreakpoint) {
    let url = DebuggerView.Scripts.selected;
    if (!url) {
      return;
    }

    let line = aBreakpoint.line + 1;

    this.addBreakpoint({ url: url, line: line }, null, true);
  },

  /**
   * Event handler for breakpoints that are removed from the editor.
   *
   * @private
   * @param object aBreakpoint
   *        The breakpoint object that was removed from the editor.
   */
  _onEditorBreakpointRemove: function BP__onEditorBreakpointRemove(aBreakpoint) {
    let url = DebuggerView.Scripts.selected;
    if (!url) {
      return;
    }

    let line = aBreakpoint.line + 1;

    let breakpoint = this.getBreakpoint(url, line);
    if (breakpoint) {
      this.removeBreakpoint(breakpoint, null, true);
    }
  },

  /**
   * 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.
   */
  updateEditorBreakpoints: function BP_updateEditorBreakpoints() {
    let url = DebuggerView.Scripts.selected;
    if (!url) {
      return;
    }

    this._skipEditorBreakpointChange = true;
    for each (let breakpoint in this.store) {
      if (breakpoint.location.url == url) {
        this.editor.addBreakpoint(breakpoint.location.line - 1);
      }
    }
    this._skipEditorBreakpointChange = false;
  },

  /**
   * Add a breakpoint.
   *
   * @param object aLocation
   *        The location where you want the breakpoint. This object must have
   *        two properties:
   *          - url - the URL of the script.
   *          - line - the line number (starting from 1).
   * @param function [aCallback]
   *        Optional function to invoke once the breakpoint is added. The
   *        callback is invoked with two arguments:
   *          - aBreakpointClient - the BreakpointActor client object, if the
   *          breakpoint has been added successfully.
   *          - aResponseError - if there was any error.
   * @param boolean [aNoEditorUpdate=false]
   *        Tells if you want to skip editor updates. Typically the editor is
   *        updated to visually indicate that a breakpoint has been added.
   */
  addBreakpoint:
  function BP_addBreakpoint(aLocation, aCallback, aNoEditorUpdate) {
    let breakpoint = this.getBreakpoint(aLocation.url, aLocation.line);
    if (breakpoint) {
      aCallback && aCallback(breakpoint);
      return;
    }

    this.activeThread.setBreakpoint(aLocation, function(aResponse, aBpClient) {
      this.store[aBpClient.actor] = aBpClient;
      this.displayBreakpoint(aLocation, aNoEditorUpdate);
      aCallback && aCallback(aBpClient, aResponse.error);
    }.bind(this));
  },

  /**
   * Update the editor to display the specified breakpoint in the gutter.
   *
   * @param object aLocation
   *        The location where you want the breakpoint. This object must have
   *        two properties:
   *          - url - the URL of the script.
   *          - line - the line number (starting from 1).
   * @param boolean [aNoEditorUpdate=false]
   *        Tells if you want to skip editor updates. Typically the editor is
   *        updated to visually indicate that a breakpoint has been added.
   */
  displayBreakpoint: function BP_displayBreakpoint(aLocation, aNoEditorUpdate) {
    if (!aNoEditorUpdate) {
      let url = DebuggerView.Scripts.selected;
      if (url == aLocation.url) {
        this._skipEditorBreakpointChange = true;
        this.editor.addBreakpoint(aLocation.line - 1);
        this._skipEditorBreakpointChange = false;
      }
    }
  },

  /**
   * Remove a breakpoint.
   *
   * @param object aBreakpoint
   *        The breakpoint you want to remove.
   * @param function [aCallback]
   *        Optional function to invoke once the breakpoint is removed. The
   *        callback is invoked with one argument: the breakpoint location
   *        object which holds the url and line properties.
   * @param boolean [aNoEditorUpdate=false]
   *        Tells if you want to skip editor updates. Typically the editor is
   *        updated to visually indicate that a breakpoint has been removed.
   */
  removeBreakpoint:
  function BP_removeBreakpoint(aBreakpoint, aCallback, aNoEditorUpdate) {
    if (!(aBreakpoint.actor in this.store)) {
      aCallback && aCallback(aBreakpoint.location);
      return;
    }

    aBreakpoint.remove(function() {
      delete this.store[aBreakpoint.actor];

      if (!aNoEditorUpdate) {
        let url = DebuggerView.Scripts.selected;
        if (url == aBreakpoint.location.url) {
          this._skipEditorBreakpointChange = true;
          this.editor.removeBreakpoint(aBreakpoint.location.line - 1);
          this._skipEditorBreakpointChange = false;
        }
      }

      aCallback && aCallback(aBreakpoint.location);
    }.bind(this));
  },

  /**
   * Get the breakpoint object at the given location.
   *
   * @param string aUrl
   *        The URL of where the breakpoint is.
   * @param number aLine
   *        The line number where the breakpoint is.
   * @return object
   *         The BreakpointActor object.
   */
  getBreakpoint: function BP_getBreakpoint(aUrl, aLine) {
    for each (let breakpoint in this.store) {
      if (breakpoint.location.url == aUrl && breakpoint.location.line == aLine) {
        return breakpoint;
      }
    }
    return null;
  }
};

/**
 * Localization convenience methods.
 */
let L10N = {

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

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

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

/**
 * Shortcuts for accessing various debugger preferences.
 */
let Prefs = {

  /**
   * Gets the preferred stackframes pane width.
   * @return number
   */
  get stackframesWidth() {
    if (this._sfrmWidth === undefined) {
      this._sfrmWidth = Services.prefs.getIntPref("devtools.debugger.ui.stackframes-width");
    }
    return this._sfrmWidth;
  },

  /**
   * Sets the preferred stackframes pane width.
   * @return number
   */
  set stackframesWidth(value) {
    Services.prefs.setIntPref("devtools.debugger.ui.stackframes-width", value);
    this._sfrmWidth = value;
  },

  /**
   * Gets the preferred variables pane width.
   * @return number
   */
  get variablesWidth() {
    if (this._varsWidth === undefined) {
      this._varsWidth = Services.prefs.getIntPref("devtools.debugger.ui.variables-width");
    }
    return this._varsWidth;
  },

  /**
   * Sets the preferred variables pane width.
   * @return number
   */
  set variablesWidth(value) {
    Services.prefs.setIntPref("devtools.debugger.ui.variables-width", value);
    this._varsWidth = value;
  },

  /**
   * Gets a flag specifying if the the debugger should automatically connect to
   * the default host and port number.
   * @return boolean
   */
  get remoteAutoConnect() {
    if (this._autoConn === undefined) {
      this._autoConn = Services.prefs.getBoolPref("devtools.debugger.remote-autoconnect");
    }
    return this._autoConn;
  },

  /**
   * Sets a flag specifying if the the debugger should automatically connect.
   * @param boolean value
   */
  set remoteAutoConnect(value) {
    Services.prefs.setBoolPref("devtools.debugger.remote-autoconnect", value);
    this._autoConn = value;
  }
};

/**
 * Gets the preferred default remote debugging host.
 * @return string
 */
XPCOMUtils.defineLazyGetter(Prefs, "remoteHost", function() {
  return Services.prefs.getCharPref("devtools.debugger.remote-host");
});

/**
 * Gets the preferred default remote debugging port.
 * @return number
 */
XPCOMUtils.defineLazyGetter(Prefs, "remotePort", function() {
  return Services.prefs.getIntPref("devtools.debugger.remote-port");
});

/**
 * Gets the max number of attempts to reconnect to a remote server.
 * @return number
 */
XPCOMUtils.defineLazyGetter(Prefs, "remoteConnectionRetries", function() {
  return Services.prefs.getIntPref("devtools.debugger.remote-connection-retries");
});

/**
 * Gets the remote debugging connection timeout (in milliseconds).
 * @return number
 */
XPCOMUtils.defineLazyGetter(Prefs, "remoteTimeout", function() {
  return Services.prefs.getIntPref("devtools.debugger.remote-timeout");
});

/**
 * Preliminary setup for the DebuggerController object.
 */
DebuggerController.init();
DebuggerController.ThreadState = new ThreadState();
DebuggerController.StackFrames = new StackFrames();
DebuggerController.SourceScripts = new SourceScripts();
DebuggerController.Breakpoints = new Breakpoints();

/**
 * Export some properties to the global scope for easier access in tests.
 */
Object.defineProperty(window, "gClient", {
  get: function() { return DebuggerController.client; }
});

Object.defineProperty(window, "gTabClient", {
  get: function() { return DebuggerController.tabClient; }
});

Object.defineProperty(window, "gThreadClient", {
  get: function() { return DebuggerController.activeThread; }
});
