/* 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, results: Cr, utils: Cu} = Components;

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

this.EXPORTED_SYMBOLS = ["frame"];

this.frame = {};

const FRAME_SCRIPT = "chrome://marionette/content/listener.js";

// list of OOP frames that has the frame script loaded
var remoteFrames = [];

/**
 * An object representing a frame that Marionette has loaded a
 * frame script in.
 */
frame.RemoteFrame = function (windowId, frameId) {
  // outerWindowId relative to main process
  this.windowId = windowId;
  // actual frame relative to the windowId's frames list
  this.frameId = frameId;
  // assigned frame ID, used for messaging
  this.targetFrameId = this.frameId;
  // list of OOP frames that has the frame script loaded
  this.remoteFrames = [];
};

/**
 * The FrameManager will maintain the list of Out Of Process (OOP)
 * frames and will handle frame switching between them.
 *
 * It handles explicit frame switching (switchToFrame), and implicit
 * frame switching, which occurs when a modal dialog is triggered in B2G.
 *
 * @param {GeckoDriver} driver
 *     Reference to the driver instance.
 */
frame.Manager = class {
  constructor(driver) {
    // messageManager maintains the messageManager
    // for the current process' chrome frame or the global message manager

    // holds a member of the remoteFrames (for an OOP frame)
    // or null (for the main process)
    this.currentRemoteFrame = null;
    // frame we'll need to restore once interrupt is gone
    this.previousRemoteFrame = null;
    // set to true when we have been interrupted by a modal
    this.handledModal = false;
    this.driver = driver;
  }

  /**
   * Receives all messages from content messageManager.
   */
  receiveMessage(message) {
    switch (message.name) {
      case "MarionetteFrame:getInterruptedState":
        // this will return true if the calling frame was interrupted by a modal dialog
        if (this.previousRemoteFrame) {
          // get the frame window of the interrupted frame
          let interruptedFrame = Services.wm.getOuterWindowWithId(
              this.previousRemoteFrame.windowId);

          if (this.previousRemoteFrame.frameId !== null) {
            // find OOP frame
            let iframes = interruptedFrame.document.getElementsByTagName("iframe");
            interruptedFrame = iframes[this.previousRemoteFrame.frameId];
          }

          // check if the interrupted frame is the same as the calling frame
          if (interruptedFrame.src == message.target.src) {
            return {value: this.handledModal};
          }

        // we get here if previousRemoteFrame and currentRemoteFrame are null,
        // i.e. if we're in a non-OOP process, or we haven't switched into an OOP frame,
        // in which case, handledModal can't be set to true
        } else if (this.currentRemoteFrame === null) {
          return {value: this.handledModal};
        }
        return {value: false};

      // handleModal is called when we need to switch frames to the main
      // process due to a modal dialog interrupt
      case "MarionetteFrame:handleModal":
        // If previousRemoteFrame was set, that means we switched into a
        // remote frame.  If this is the case, then we want to switch back
        // into the system frame.  If it isn't the case, then we're in a
        // non-OOP environment, so we don't need to handle remote frames.
        let isLocal = true;
        if (this.currentRemoteFrame !== null) {
          isLocal = false;
          this.removeMessageManagerListeners(
              this.currentRemoteFrame.messageManager.get());

          // store the previous frame so we can switch back to it when
          // the modal is dismissed
          this.previousRemoteFrame = this.currentRemoteFrame;

          // by setting currentRemoteFrame to null,
          // it signifies we're in the main process
          this.currentRemoteFrame = null;
          this.driver.messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
              .getService(Ci.nsIMessageBroadcaster);
        }

        this.handledModal = true;
        this.driver.sendOk(this.driver.command_id);
        return {value: isLocal};

      case "MarionetteFrame:getCurrentFrameId":
        if (this.currentRemoteFrame !== null) {
          return this.currentRemoteFrame.frameId;
        }
    }
  }

  getOopFrame(winId, frameId) {
    // get original frame window
    let outerWin = Services.wm.getOuterWindowWithId(winId);
    // find the OOP frame
    let f = outerWin.document.getElementsByTagName("iframe")[frameId];
    return f;
  }

  getFrameMM(winId, frameId) {
    let oopFrame = this.getOopFrame(winId, frameId);
    let mm = oopFrame.QueryInterface(Ci.nsIFrameLoaderOwner)
        .frameLoader.messageManager;
    return mm;
  }

  /**
   * Switch to OOP frame.  We're handling this here so we can maintain
   * a list of remote frames.
   */
  switchToFrame(winId, frameId) {
    let oopFrame = this.getOopFrame(winId, frameId);
    let mm = this.getFrameMM(winId, frameId);

    // see if this frame already has our frame script loaded in it;
    // if so, just wake it up
    for (let i = 0; i < remoteFrames.length; i++) {
      let f = remoteFrames[i];
      let fmm = f.messageManager.get();
      try {
        fmm.sendAsyncMessage("aliveCheck", {});
      } catch (e) {
        if (e.result == Cr.NS_ERROR_NOT_INITIALIZED) {
          remoteFrames.splice(i--, 1);
          continue;
        }
      }

      if (fmm == mm) {
        this.currentRemoteFrame = f;
        this.addMessageManagerListeners(mm);

        mm.sendAsyncMessage("Marionette:restart");
        return oopFrame.id;
      }
    }

    // if we get here, then we need to load the frame script in this frame,
    // and set the frame's ChromeMessageSender as the active message manager
    // the driver will listen to.
    this.addMessageManagerListeners(mm);
    let f = new frame.RemoteFrame(winId, frameId);
    f.messageManager = Cu.getWeakReference(mm);
    remoteFrames.push(f);
    this.currentRemoteFrame = f;

    mm.loadFrameScript(FRAME_SCRIPT, true, true);

    return oopFrame.id;
  }

  /*
   * This function handles switching back to the frame that was
   * interrupted by the modal dialog.  It gets called by the interrupted
   * frame once the dialog is dismissed and the frame resumes its process.
   */
  switchToModalOrigin() {
    // only handle this if we indeed switched out of the modal's
    // originating frame
    if (this.previousRemoteFrame !== null) {
      this.currentRemoteFrame = this.previousRemoteFrame;
      let mm = this.currentRemoteFrame.messageManager.get();
      this.addMessageManagerListeners(mm);
    }
    this.handledModal = false;
  }

  /**
   * Adds message listeners to the driver,  listening for
   * messages from content frame scripts.  It also adds a
   * MarionetteFrame:getInterruptedState message listener to the
   * FrameManager, so the frame manager's state can be checked by the frame.
   *
   * @param {nsIMessageListenerManager} mm
   *     The message manager object, typically
   *     ChromeMessageBroadcaster or ChromeMessageSender.
   */
  addMessageManagerListeners(mm) {
    mm.addWeakMessageListener("Marionette:ok", this.driver);
    mm.addWeakMessageListener("Marionette:done", this.driver);
    mm.addWeakMessageListener("Marionette:error", this.driver);
    mm.addWeakMessageListener("Marionette:emitTouchEvent", this.driver);
    mm.addWeakMessageListener("Marionette:log", this.driver);
    mm.addWeakMessageListener("Marionette:shareData", this.driver);
    mm.addWeakMessageListener("Marionette:switchToModalOrigin", this.driver);
    mm.addWeakMessageListener("Marionette:switchedToFrame", this.driver);
    mm.addWeakMessageListener("Marionette:getVisibleCookies", this.driver);
    mm.addWeakMessageListener("Marionette:getImportedScripts", this.driver.importedScripts);
    mm.addWeakMessageListener("Marionette:register", this.driver);
    mm.addWeakMessageListener("Marionette:listenersAttached", this.driver);
    mm.addWeakMessageListener("MarionetteFrame:handleModal", this);
    mm.addWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
    mm.addWeakMessageListener("MarionetteFrame:getInterruptedState", this);
  }

  /**
   * Removes listeners for messages from content frame scripts.
   * We do not remove the MarionetteFrame:getInterruptedState or
   * the Marionette:switchToModalOrigin message listener, because we
   * want to allow all known frames to contact the frame manager so
   * that it can check if it was interrupted, and if so, it will call
   * switchToModalOrigin when its process gets resumed.
   *
   * @param {nsIMessageListenerManager} mm
   *     The message manager object, typically
   *     ChromeMessageBroadcaster or ChromeMessageSender.
   */
  removeMessageManagerListeners(mm) {
    mm.removeWeakMessageListener("Marionette:ok", this.driver);
    mm.removeWeakMessageListener("Marionette:done", this.driver);
    mm.removeWeakMessageListener("Marionette:error", this.driver);
    mm.removeWeakMessageListener("Marionette:log", this.driver);
    mm.removeWeakMessageListener("Marionette:shareData", this.driver);
    mm.removeWeakMessageListener("Marionette:switchedToFrame", this.driver);
    mm.removeWeakMessageListener("Marionette:getVisibleCookies", this.driver);
    mm.removeWeakMessageListener("Marionette:getImportedScripts", this.driver.importedScripts);
    mm.removeWeakMessageListener("Marionette:listenersAttached", this.driver);
    mm.removeWeakMessageListener("Marionette:register", this.driver);
    mm.removeWeakMessageListener("MarionetteFrame:handleModal", this);
    mm.removeWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
  }
};

frame.Manager.prototype.QueryInterface = XPCOMUtils.generateQI(
    [Ci.nsIMessageListener, Ci.nsISupportsWeakReference]);
