/* 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 Cr = Components.results;

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

const kSystemMessageInternalReady = "system-message-internal-ready";

XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                   "@mozilla.org/childprocessmessagemanager;1",
                                   "nsISyncMessageSender");

function debug(aMsg) {
   // dump("-- SystemMessageManager " + Date.now() + " : " + aMsg + "\n");
}

// Implementation of the DOM API for system messages

function SystemMessageManager() {
  // If we have a system message handler registered for messages of type
  // |type|, this._dispatchers[type] equals {handler, messages, isHandling},
  // where
  //  - |handler| is the message handler that the page registered,
  //  - |messages| is a list of messages which we've received while
  //    dispatching messages to the handler, but haven't yet sent, and
  //  - |isHandling| indicates whether we're currently dispatching messages
  //    to this handler.
  this._dispatchers = {};

  // Pending messages for this page, keyed by message type.
  this._pendings = {};

  // Flag to specify if this process has already registered the manifest URL.
  this._registerManifestURLReady = false;

  // Used to know if the promise has to be accepted or not.
  this._isHandling = false;
  this._promise = null;

  // Flag to determine this process is a parent or child process.
  let appInfo = Cc["@mozilla.org/xre/app-info;1"];
  this._isParentProcess =
    !appInfo || appInfo.getService(Ci.nsIXULRuntime)
                  .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;

  // An oberver to listen to whether the |SystemMessageInternal| is ready.
  if (this._isParentProcess) {
    Services.obs.addObserver(this, kSystemMessageInternalReady, false);
  }
}

SystemMessageManager.prototype = {
  __proto__: DOMRequestIpcHelper.prototype,

  _dispatchMessage: function(aType, aDispatcher, aMessage, aMessageID) {
    if (aDispatcher.isHandling) {
      // Queue up the incomming message if we're currently dispatching a
      // message; we'll send the message once we finish with the current one.
      //
      // _dispatchMethod is reentrant because a page can spin up a nested
      // event loop from within a system message handler (e.g. via alert()),
      // and we can then try to send the page another message while it's
      // inside this nested event loop.
      aDispatcher.messages.push({ message: aMessage, messageID: aMessageID });
      return;
    }

    aDispatcher.isHandling = true;
    this._isHandling = true;

    // We get a json blob, but in some cases we want another kind of object
    // to be dispatched. To do so, we check if we have a valid contract ID of
    // "@mozilla.org/dom/system-messages/wrapper/TYPE;1" component implementing
    // nsISystemMessageWrapper.
    debug("Dispatching " + JSON.stringify(aMessage) + "\n");
    let contractID = "@mozilla.org/dom/system-messages/wrapper/" + aType + ";1";
    let wrapped = false;

    if (contractID in Cc) {
      debug(contractID + " is registered, creating an instance");
      let wrapper = Cc[contractID].createInstance(Ci.nsISystemMessagesWrapper);
      if (wrapper) {
        aMessage = wrapper.wrapMessage(aMessage, this._window);
        wrapped = true;
        debug("wrapped = " + aMessage);
      }
    }

    let message = wrapped ? aMessage : Cu.cloneInto(aMessage, this._window);

    let rejectPromise = false;
    try {
      aDispatcher.handler.handleMessage(message);
    } catch(e) {
      rejectPromise = true;
    }

    this._isHandling = false;
    aDispatcher.isHandling = false;

    let self = this;
    function sendResponse() {
      // We need to notify the parent one of the system messages has been
      // handled, so the parent can release the CPU wake lock it took on our
      // behalf.
      cpmm.sendAsyncMessage("SystemMessageManager:HandleMessageDone",
                            { type: aType,
                              manifestURL: self._manifestURL,
                              pageURL: self._pageURL,
                              msgID: aMessageID,
                              rejected: rejectPromise });
    }

    if (!this._promise) {
      debug("No promise set, sending the response immediately.");
      sendResponse();
    } else {
      debug("Using the promise to postpone the response.");
      this._promise.then(sendResponse, function() {
        rejectPromise = true;
        sendResponse();
      });
      this._promise = null;
    }

    if (aDispatcher.messages.length > 0) {
      let msg = aDispatcher.messages.shift();
      this._dispatchMessage(aType, aDispatcher, msg.message, msg.messageID);
    } else {
      // No more messages that need to be handled, we can notify the
      // ContentChild to release the CPU wake lock grabbed by the ContentParent
      // (i.e. NewWakeLockOnBehalfOfProcess()) and reset the process's priority.
      //
      // TODO: Bug 874353 - Remove SystemMessageHandledListener in ContentParent
      Services.obs.notifyObservers(/* aSubject */ null,
                                   "handle-system-messages-done",
                                   /* aData */ null);
    }
  },

  mozSetMessageHandler: function(aType, aHandler) {
    debug("set message handler for [" + aType + "] " + aHandler);

    if (this._isInBrowserElement) {
      debug("the app loaded in the browser cannot set message handler");
      // Don't throw there, but ignore the registration.
      return;
    }

    if (!aType) {
      // Just bail out if we have no type.
      return;
    }

    let dispatchers = this._dispatchers;
    if (!aHandler) {
      // Setting the dispatcher to null means we don't want to handle messages
      // for this type anymore.
      delete dispatchers[aType];
      return;
    }

    // Last registered handler wins.
    dispatchers[aType] = { handler: aHandler, messages: [], isHandling: false };

    // Ask for the list of currently pending messages.
    cpmm.sendAsyncMessage("SystemMessageManager:GetPendingMessages",
                          { type: aType,
                            pageURL: this._pageURL,
                            manifestURL: this._manifestURL });
  },

  mozHasPendingMessage: function(aType) {
    debug("asking pending message for [" + aType + "]");

    if (this._isInBrowserElement) {
      debug("the app loaded in the browser cannot ask pending message");
      // Don't throw there, but pretend to have no messages available.
      return false;
    }

    // If we have a handler for this type, we can't have any pending message.
    if (aType in this._dispatchers) {
      return false;
    }


    // Use SystemMessageManager directly when we are in the same process.
    if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) {
      return cpmm.sendSyncMessage("SystemMessageManager:HasPendingMessages",
                                  { type: aType,
                                    pageURL: this._pageURL,
                                    manifestURL: this._manifestURL })[0];
    }

    /*
     * NB: If the system message is fired after we received the cache
     *     and before we registered the pageURL we will get false
     *     negative however this is unlikely and will do no harm.
     */
    let cache = Cc["@mozilla.org/system-message-cache;1"]
                  .getService(Ci.nsISystemMessageCache);

    return cache.hasPendingMessage(aType, this._pageURL, this._manifestURL);
  },

  mozIsHandlingMessage: function() {
    debug("is handling message: " + this._isHandling);
    return this._isHandling;
  },

  mozSetMessageHandlerPromise: function(aPromise) {
    debug("setting a promise");

    if (!this._isHandling) {
      throw "Not in a handleMessage method";
    }

    if (this._promise) {
      throw "Promise already set";
    }

    this._promise = aPromise;
  },

  uninit: function()  {
    this._dispatchers = null;
    this._pendings = null;
    this._promise = null;

    if (this._isParentProcess) {
      Services.obs.removeObserver(this, kSystemMessageInternalReady);
    }

    if (this._isInBrowserElement) {
      debug("the app loaded in the browser doesn't need to unregister " +
            "the manifest URL for listening to the system messages");
      return;
    }

    cpmm.sendAsyncMessage("SystemMessageManager:Unregister",
                          { manifestURL: this._manifestURL,
                            pageURL: this._pageURL,
                            innerWindowID: this.innerWindowID });
  },

  // Possible messages:
  //
  //   - SystemMessageManager:Message
  //     This one will only be received when the child process is alive when
  //     the message is initially sent.
  //
  //   - SystemMessageManager:GetPendingMessages:Return
  //     This one will be received when the starting child process wants to
  //     retrieve the pending system messages from the parent (i.e. after
  //     sending SystemMessageManager:GetPendingMessages).
  receiveMessage: function(aMessage) {
    let msg = aMessage.data;
    debug("receiveMessage " + aMessage.name + " for [" + msg.type + "] " +
          "with manifest URL = " + msg.manifestURL +
          " and page URL = " + msg.pageURL);

    // Multiple windows can share the same target (process), the content
    // window needs to check if the manifest/page URL is matched. Only
    // *one* window should handle the system message.
    if (msg.manifestURL !== this._manifestURL ||
        msg.pageURL !== this._pageURL) {
      debug("This page shouldn't handle the messages because its " +
            "manifest URL = " + this._manifestURL +
            " and page URL = " + this._pageURL);
      return;
    }

    let messages = (aMessage.name == "SystemMessageManager:Message")
                   ? [{ msg: msg.msg, msgID: msg.msgID }]
                   : msg.msgQueue;

    // We only dispatch messages when a handler is registered.
    let dispatcher = this._dispatchers[msg.type];
    if (dispatcher) {
      if (aMessage.name == "SystemMessageManager:Message") {
        // Send an acknowledgement to parent to clean up the pending message
        // before we dispatch the message to apps, so a re-launched app won't
        // handle it again, which is redundant.
        cpmm.sendAsyncMessage("SystemMessageManager:Message:Return:OK",
                              { type: msg.type,
                                manifestURL: this._manifestURL,
                                pageURL: this._pageURL,
                                msgID: msg.msgID });
      }

      messages.forEach(function(aMsg) {
        this._dispatchMessage(msg.type, dispatcher, aMsg.msg, aMsg.msgID);
      }, this);

    } else {
      // Since no handlers are registered, we need to notify the parent as if
      // all the queued system messages have been handled (notice |handledCount:
      // messages.length|), so the parent can release the CPU wake lock it took
      // on our behalf.
      cpmm.sendAsyncMessage("SystemMessageManager:HandleMessagesDone",
                            { type: msg.type,
                              manifestURL: this._manifestURL,
                              pageURL: this._pageURL,
                              handledCount: messages.length });

      // We also need to notify the ContentChild to release the CPU wake lock
      // grabbed by the ContentParent (i.e. NewWakeLockOnBehalfOfProcess()) and
      // reset the process's priority.
      //
      // TODO: Bug 874353 - Remove SystemMessageHandledListener in ContentParent
      Services.obs.notifyObservers(/* aSubject */ null,
                                   "handle-system-messages-done",
                                   /* aData */ null);
    }
  },

  // nsIDOMGlobalPropertyInitializer implementation.
  init: function(aWindow) {
    debug("init");
    this.initDOMRequestHelper(aWindow,
                              ["SystemMessageManager:Message",
                               "SystemMessageManager:GetPendingMessages:Return"]);

    let principal = aWindow.document.nodePrincipal;

    // After bug 1238160, the principal no longer knows how to answer "is this a
    // browser element", which is really what this code path wants. Currently,
    // desktop is the only platform where we intend to disable isolation on a
    // browser frame, so non-desktop should be able to assume that
    // inIsolatedMozBrowser is true for all mozbrowser frames.  Additionally,
    // this system message API is disabled on desktop behind the pref
    // "dom.sysmsg.enabled".   We use a release assertion in
    // nsFrameLoader::OwnerIsIsolatedMozBrowserFrame so that platforms with apps
    // can assume inIsolatedMozBrowser is true for all mozbrowser frames.
    this._isInBrowserElement = principal.isInIsolatedMozBrowserElement;
    this._pageURL = principal.URI.spec;

    let appsService = Cc["@mozilla.org/AppsService;1"]
                        .getService(Ci.nsIAppsService);
    this._manifestURL = appsService.getManifestURLByLocalId(principal.appId);

    // Two cases are valid to register the manifest URL for the current process:
    // 1. This is asked by a child process (parent process must be ready).
    // 2. Parent process has already constructed the |SystemMessageInternal|.
    // Otherwise, delay to do it when the |SystemMessageInternal| is ready.
    let readyToRegister = true;
    if (this._isParentProcess) {
      let ready = cpmm.sendSyncMessage(
        "SystemMessageManager:AskReadyToRegister", null);
      if (ready.length == 0 || !ready[0]) {
        readyToRegister = false;
      }
    }
    if (readyToRegister) {
      this._registerManifestURL();
    }

    debug("done");
  },

  observe: function(aSubject, aTopic, aData) {
    if (aTopic === kSystemMessageInternalReady) {
      this._registerManifestURL();
    }

    // Call the DOMRequestIpcHelper.observe method.
    this.__proto__.__proto__.observe.call(this, aSubject, aTopic, aData);
  },

  _registerManifestURL: function() {
    if (this._isInBrowserElement) {
      debug("the app loaded in the browser doesn't need to register " +
            "the manifest URL for listening to the system messages");
      return;
    }

    if (!this._registerManifestURLReady) {
      cpmm.sendAsyncMessage("SystemMessageManager:Register",
                            { manifestURL: this._manifestURL,
                              pageURL: this._pageURL,
                              innerWindowID: this.innerWindowID });

      this._registerManifestURLReady = true;
    }
  },

  classID: Components.ID("{bc076ea0-609b-4d8f-83d7-5af7cbdc3bb2}"),

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMNavigatorSystemMessages,
                                         Ci.nsIDOMGlobalPropertyInitializer,
                                         Ci.nsIObserver,
                                         Ci.nsISupportsWeakReference])
}

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SystemMessageManager]);
