/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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";
/**
 * Fennec-specific actors.
 */

var windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]
  .getService(Ci.nsIWindowMediator);

function createRootActor(aConnection) {
  return new BrowserRootActor(aConnection);
}

/**
 * Creates the root actor that client-server communications always start with.
 * The root actor is responsible for the initial 'hello' packet and for
 * responding to a 'listTabs' request that produces the list of currently open
 * tabs.
 *
 * @param aConnection DebuggerServerConnection
 *        The conection to the client.
 */
function BrowserRootActor(aConnection) {
  this.conn = aConnection;
  this._tabActors = new WeakMap();
  this._tabActorPool = null;
  this._actorFactories = null;

  this.onTabClosed = this.onTabClosed.bind(this);
  windowMediator.addListener(this);
}

BrowserRootActor.prototype = {
  /**
   * Return a 'hello' packet as specified by the Remote Debugging Protocol.
   */
  sayHello: function BRA_sayHello() {
    return { from: "root",
             applicationType: "browser",
             traits: [] };
  },

  /**
   * Disconnects the actor from the browser window.
   */
  disconnect: function BRA_disconnect() {
    windowMediator.removeListener(this);

    // We may have registered event listeners on browser windows to
    // watch for tab closes, remove those.
    let win = windowMediator.getMostRecentWindow("navigator:browser");
    this.unwatchWindow(win);

    // Signal our imminent shutdown.
    let evt = win.document.createEvent("Event");
    evt.initEvent("Debugger:Shutdown", true, false);
    win.document.documentElement.dispatchEvent(evt);
  },

  /**
   * Handles the listTabs request.  Builds a list of actors
   * for the tabs running in the process.  The actors will survive
   * until at least the next listTabs request.
   */
  onListTabs: function BRA_onListTabs() {
    // Get actors for all the currently-running tabs (reusing
    // existing actors where applicable), and store them in
    // an ActorPool.

    let actorPool = new ActorPool(this.conn);
    let actorList = [];

    let win = windowMediator.getMostRecentWindow("navigator:browser");
    this.browser = win.BrowserApp.selectedBrowser;

    // Watch the window for tab closes so we can invalidate
    // actors as needed.
    this.watchWindow(win);

    let tabs = win.BrowserApp.tabs;
    let selected;

    for each (let tab in tabs) {
      let browser = tab.browser;

      if (browser == this.browser) {
        selected = actorList.length;
      }

      let actor = this._tabActors.get(browser);
      if (!actor) {
        actor = new BrowserTabActor(this.conn, browser);
        actor.parentID = this.actorID;
        this._tabActors.set(browser, actor);
      }

      actorPool.addActor(actor);
      actorList.push(actor);
    }

    // Now drop the old actorID -> actor map.  Actors that still
    // mattered were added to the new map, others will go
    // away.
    if (this._tabActorPool) {
      this.conn.removeActorPool(this._tabActorPool);
    }

    this._tabActorPool = actorPool;
    this.conn.addActorPool(this._tabActorPool);

    return { "from": "root",
             "selected": selected,
             "tabs": [actor.grip()
                      for each (actor in actorList)] };
  },

  /**
   * Watch a window that was visited during onListTabs for
   * tab closures.
   */
  watchWindow: function BRA_watchWindow(aWindow) {
    let tabContainer = aWindow.document.getElementById("browsers");
    tabContainer.addEventListener("TabClose",
                                  this.onTabClosed,
                                  false);
  },

  /**
   * Stop watching a window for tab closes.
   */
  unwatchWindow: function BRA_unwatchWindow(aWindow) {
    let tabContainer = aWindow.document.getElementById("browsers");
    tabContainer.removeEventListener("TabClose", this.onTabClosed);
    this.exitTabActor(aWindow);
  },

  /**
   * When a tab is closed, exit its tab actor.  The actor
   * will be dropped at the next listTabs request.
   */
  onTabClosed: function BRA_onTabClosed(aEvent) {
    this.exitTabActor(aEvent.target.browser);
  },

  /**
   * Exit the tab actor of the specified tab.
   */
  exitTabActor: function BRA_exitTabActor(aWindow) {
    let actor = this._tabActors.get(aWindow);
    if (actor) {
      actor.exit();
    }
  },

  // nsIWindowMediatorListener
  onWindowTitleChange: function BRA_onWindowTitleChange(aWindow, aTitle) { },
  onOpenWindow: function BRA_onOpenWindow(aWindow) { },
  onCloseWindow: function BRA_onCloseWindow(aWindow) {
    if (aWindow.BrowserApp) {
      this.unwatchWindow(aWindow);
    }
  }
}

/**
 * The request types this actor can handle.
 */
BrowserRootActor.prototype.requestTypes = {
  "listTabs": BrowserRootActor.prototype.onListTabs
};

/**
 * Creates a tab actor for handling requests to a browser tab, like attaching
 * and detaching.
 *
 * @param aConnection DebuggerServerConnection
 *        The conection to the client.
 * @param aBrowser browser
 *        The browser instance that contains this tab.
 */
function BrowserTabActor(aConnection, aBrowser)
{
  this.conn = aConnection;
  this._browser = aBrowser;

  this._onWindowCreated = this.onWindowCreated.bind(this);
}

// XXX (bug 710213): BrowserTabActor attach/detach/exit/disconnect is a
// *complete* mess, needs to be rethought asap.

BrowserTabActor.prototype = {
  get browser() { return this._browser; },

  get exited() { return !this._browser; },
  get attached() { return !!this._attached },

  _tabPool: null,
  get tabActorPool() { return this._tabPool; },

  _contextPool: null,
  get contextActorPool() { return this._contextPool; },

  /**
   * Add the specified breakpoint to the default actor pool connection, in order
   * to be alive as long as the server is.
   *
   * @param BreakpointActor aActor
   *        The actor object.
   */
  addToBreakpointPool: function BTA_addToBreakpointPool(aActor) {
    this.conn.addActor(aActor);
  },

  /**
   * Remove the specified breakpint from the default actor pool.
   *
   * @param string aActor
   *        The actor ID.
   */
  removeFromBreakpointPool: function BTA_removeFromBreakpointPool(aActor) {
    this.conn.removeActor(aActor);
  },

  actorPrefix: "tab",

  grip: function BTA_grip() {
    dbg_assert(!this.exited,
               "grip() shouldn't be called on exited browser actor.");
    dbg_assert(this.actorID,
               "tab should have an actorID.");
    return { actor: this.actorID,
             title: this._browser.contentTitle,
             url: this._browser.currentURI.spec }
  },

  /**
   * Called when the actor is removed from the connection.
   */
  disconnect: function BTA_disconnect() {
    this._detach();
  },

  /**
   * Called by the root actor when the underlying tab is closed.
   */
  exit: function BTA_exit() {
    if (this.exited) {
      return;
    }

    if (this.attached) {
      this._detach();
      this.conn.send({ from: this.actorID,
                       type: "tabDetached" });
    }

    this._browser = null;
  },

  /**
   * Does the actual work of attching to a tab.
   */
  _attach: function BTA_attach() {
    if (this._attached) {
      return;
    }

    // Create a pool for tab-lifetime actors.
    dbg_assert(!this._tabPool, "Shouldn't have a tab pool if we weren't attached.");
    this._tabPool = new ActorPool(this.conn);
    this.conn.addActorPool(this._tabPool);

    // ... and a pool for context-lifetime actors.
    this._pushContext();

    // Watch for globals being created in this tab.
    this._browser.addEventListener("DOMWindowCreated", this._onWindowCreated, true);

    this._attached = true;
  },

  /**
   * Creates a thread actor and a pool for context-lifetime actors. It then sets
   * up the content window for debugging.
   */
  _pushContext: function BTA_pushContext() {
    dbg_assert(!this._contextPool, "Can't push multiple contexts");

    this._contextPool = new ActorPool(this.conn);
    this.conn.addActorPool(this._contextPool);

    this.threadActor = new ThreadActor(this);
    this._addDebuggees(this._browser.contentWindow.wrappedJSObject);
    this._contextPool.addActor(this.threadActor);
  },

  /**
   * Add the provided window and all windows in its frame tree as debuggees.
   */
  _addDebuggees: function BTA__addDebuggees(aWindow) {
    this.threadActor.addDebuggee(aWindow);
    let frames = aWindow.frames;
    for (let i = 0; i < frames.length; i++) {
      this._addDebuggees(frames[i]);
    }
  },

  /**
   * Exits the current thread actor and removes the context-lifetime actor pool.
   * The content window is no longer being debugged after this call.
   */
  _popContext: function BTA_popContext() {
    dbg_assert(!!this._contextPool, "No context to pop.");

    this.conn.removeActorPool(this._contextPool);
    this._contextPool = null;
    this.threadActor.exit();
    this.threadActor = null;
  },

  /**
   * Does the actual work of detaching from a tab.
   */
  _detach: function BTA_detach() {
    if (!this.attached) {
      return;
    }

    this._browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, true);

    this._popContext();

    // Shut down actors that belong to this tab's pool.
    this.conn.removeActorPool(this._tabPool);
    this._tabPool = null;

    this._attached = false;
  },

  // Protocol Request Handlers

  onAttach: function BTA_onAttach(aRequest) {
    if (this.exited) {
      return { type: "exited" };
    }

    this._attach();

    return { type: "tabAttached", threadActor: this.threadActor.actorID };
  },

  onDetach: function BTA_onDetach(aRequest) {
    if (!this.attached) {
      return { error: "wrongState" };
    }

    this._detach();

    return { type: "detached" };
  },

  /**
   * Prepare to enter a nested event loop by disabling debuggee events.
   */
  preNest: function BTA_preNest() {
    if (!this._browser) {
      // The tab is already closed.
      return;
    }
    let windowUtils = this._browser.contentWindow
                          .QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDOMWindowUtils);
    windowUtils.suppressEventHandling(true);
    windowUtils.suspendTimeouts();
  },

  /**
   * Prepare to exit a nested event loop by enabling debuggee events.
   */
  postNest: function BTA_postNest(aNestData) {
    if (!this._browser) {
      // The tab is already closed.
      return;
    }
    let windowUtils = this._browser.contentWindow
                          .QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDOMWindowUtils);
    windowUtils.resumeTimeouts();
    windowUtils.suppressEventHandling(false);
  },

  /**
   * Handle location changes, by sending a tabNavigated notification to the
   * client.
   */
  onWindowCreated: function BTA_onWindowCreated(evt) {
    if (evt.target === this._browser.contentDocument) {
      if (this._attached) {
        this.conn.send({ from: this.actorID, type: "tabNavigated",
                         url: this._browser.contentDocument.URL });
      }
    }
  }
};

/**
 * The request types this actor can handle.
 */
BrowserTabActor.prototype.requestTypes = {
  "attach": BrowserTabActor.prototype.onAttach,
  "detach": BrowserTabActor.prototype.onDetach
};

/**
 * Registers handlers for new request types defined dynamically. This is used
 * for example by add-ons to augment the functionality of the tab actor.
 *
 * @param aName string
 *        The name of the new request type.
 * @param aFunction function
 *        The handler for this request type.
 */
DebuggerServer.addTabRequest = function DS_addTabRequest(aName, aFunction) {
  BrowserTabActor.prototype.requestTypes[aName] = function(aRequest) {
    if (!this.attached) {
      return { error: "wrongState" };
    }
    return aFunction(this, aRequest);
  }
};
