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

this.EXPORTED_SYMBOLS = ["InterAppCommService"];

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

const DEBUG = false;
function debug(aMsg) {
  dump("-- InterAppCommService: " + Date.now() + ": " + aMsg + "\n");
}

XPCOMUtils.defineLazyServiceGetter(this, "appsService",
                                   "@mozilla.org/AppsService;1",
                                   "nsIAppsService");

XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
                                   "@mozilla.org/parentprocessmessagemanager;1",
                                   "nsIMessageBroadcaster");

XPCOMUtils.defineLazyServiceGetter(this, "UUIDGenerator",
                                   "@mozilla.org/uuid-generator;1",
                                   "nsIUUIDGenerator");

XPCOMUtils.defineLazyServiceGetter(this, "messenger",
                                   "@mozilla.org/system-message-internal;1",
                                   "nsISystemMessagesInternal");

const kMessages =["Webapps:Connect",
                  "Webapps:GetConnections",
                  "InterAppConnection:Cancel",
                  "InterAppMessagePort:PostMessage",
                  "InterAppMessagePort:Register",
                  "InterAppMessagePort:Unregister",
                  "child-process-shutdown"];

/**
 * This module contains helpers for Inter-App Communication API [1] related
 * purposes, which plays the role of the central service receiving messages
 * from and interacting with the content processes.
 *
 * [1] https://wiki.mozilla.org/WebAPI/Inter_App_Communication_Alt_proposal
 */

this.InterAppCommService = {
  init: function() {
    Services.obs.addObserver(this, "xpcom-shutdown", false);
    Services.obs.addObserver(this, "webapps-clear-data", false);

    kMessages.forEach(function(aMsg) {
      ppmm.addMessageListener(aMsg, this);
    }, this);

    // This matrix is used for saving the inter-app connection info registered in
    // the app manifest. The object literal is defined as below:
    //
    // {
    //   "keyword1": {
    //     "subAppManifestURL1": {
    //       /* subscribed info */
    //     },
    //     "subAppManifestURL2": {
    //       /* subscribed info */
    //     },
    //     ...
    //   },
    //   "keyword2": {
    //     "subAppManifestURL3": {
    //       /* subscribed info */
    //     },
    //     ...
    //   },
    //   ...
    // }
    //
    // For example:
    //
    // {
    //   "foo": {
    //     "app://subApp1.gaiamobile.org/manifest.webapp": {
    //       pageURL: "app://subApp1.gaiamobile.org/handler.html",
    //       description: "blah blah",
    //       rules: { ... }
    //     },
    //     "app://subApp2.gaiamobile.org/manifest.webapp": {
    //       pageURL: "app://subApp2.gaiamobile.org/handler.html",
    //       description: "blah blah",
    //       rules: { ... }
    //     }
    //   },
    //   "bar": {
    //     "app://subApp3.gaiamobile.org/manifest.webapp": {
    //       pageURL: "app://subApp3.gaiamobile.org/handler.html",
    //       description: "blah blah",
    //       rules: { ... }
    //     }
    //   }
    // }
    //
    // TODO Bug 908999 - Update registered connections when app gets uninstalled.
    this._registeredConnections = {};

    // This matrix is used for saving the permitted connections, which allows
    // the messaging between publishers and subscribers. The object literal is
    // defined as below:
    //
    // {
    //   "keyword1": {
    //     "pubAppManifestURL1": [
    //       "subAppManifestURL1",
    //       "subAppManifestURL2",
    //       ...
    //     ],
    //     "pubAppManifestURL2": [
    //       "subAppManifestURL3",
    //       "subAppManifestURL4",
    //       ...
    //     ],
    //     ...
    //   },
    //   "keyword2": {
    //     "pubAppManifestURL3": [
    //       "subAppManifestURL5",
    //       ...
    //     ],
    //     ...
    //   },
    //   ...
    // }
    //
    // For example:
    //
    // {
    //   "foo": {
    //     "app://pubApp1.gaiamobile.org/manifest.webapp": [
    //       "app://subApp1.gaiamobile.org/manifest.webapp",
    //       "app://subApp2.gaiamobile.org/manifest.webapp"
    //     ],
    //     "app://pubApp2.gaiamobile.org/manifest.webapp": [
    //       "app://subApp3.gaiamobile.org/manifest.webapp",
    //       "app://subApp4.gaiamobile.org/manifest.webapp"
    //     ]
    //   },
    //   "bar": {
    //     "app://pubApp3.gaiamobile.org/manifest.webapp": [
    //       "app://subApp5.gaiamobile.org/manifest.webapp",
    //     ]
    //   }
    // }
    //
    // TODO Bug 908999 - Update allowed connections when app gets uninstalled.
    this._allowedConnections = {};

    // This matrix is used for saving the caller info from the content process,
    // which is indexed by a random UUID, to know where to return the promise
    // resolvser's callback when the prompt UI for allowing connections returns.
    // An example of the object literal is shown as below:
    //
    // {
    //   "fooID": {
    //     outerWindowID: 12,
    //     requestID: 34,
    //     target: pubAppTarget1
    //   },
    //   "barID": {
    //     outerWindowID: 56,
    //     requestID: 78,
    //     target: pubAppTarget2
    //   }
    // }
    //
    // where |outerWindowID| is the ID of the window requesting the connection,
    //       |requestID| is the ID specifying the promise resolver to return,
    //       |target| is the target of the process requesting the connection.
    this._promptUICallers = {};

    // This matrix is used for saving the pair of message ports, which is indexed
    // by a random UUID, so that each port can know whom it should talk to.
    // An example of the object literal is shown as below:
    //
    // {
    //   "UUID1": {
    //     keyword: "keyword1",
    //     publisher: {
    //       manifestURL: "app://pubApp1.gaiamobile.org/manifest.webapp",
    //       target: pubAppTarget1,
    //       pageURL: "app://pubApp1.gaiamobile.org/caller.html",
    //       messageQueue: [...]
    //     },
    //     subscriber: {
    //       manifestURL: "app://subApp1.gaiamobile.org/manifest.webapp",
    //       target: subAppTarget1,
    //       pageURL: "app://pubApp1.gaiamobile.org/handler.html",
    //       messageQueue: [...]
    //     }
    //   },
    //   "UUID2": {
    //     keyword: "keyword2",
    //     publisher: {
    //       manifestURL: "app://pubApp2.gaiamobile.org/manifest.webapp",
    //       target: pubAppTarget2,
    //       pageURL: "app://pubApp2.gaiamobile.org/caller.html",
    //       messageQueue: [...]
    //     },
    //     subscriber: {
    //       manifestURL: "app://subApp2.gaiamobile.org/manifest.webapp",
    //       target: subAppTarget2,
    //       pageURL: "app://pubApp2.gaiamobile.org/handler.html",
    //       messageQueue: [...]
    //     }
    //   }
    // }
    this._messagePortPairs = {};
  },

  /* These attributes main use is to allow testing this in an isolated way
   * that doesn't depend on the app service, or the system messenger working on
   * the test environment
   */
  get appsService() {
    return this._appsService || appsService;
  },
  set appsService(aService) {
    this._appsService = aService;
  },
  get messenger() {
    return this._messenger || messenger;
  },
  set messenger(aMessenger) {
    this._messenger = aMessenger;
  },


  /**
   * Registration of a page that wants to be connected to other apps through
   * the Inter-App Communication API.
   *
   * @param aKeyword        The connection's keyword.
   * @param aHandlerPageURI The URI of the handler's page.
   * @param aManifestURI    The webapp's manifest URI.
   * @param aDescription    The connection's description.
   * @param aRules          The connection's rules.
   */
  registerConnection: function(aKeyword, aHandlerPageURI, aManifestURI,
                               aDescription, aRules) {
    let manifestURL = aManifestURI.spec;
    let pageURL = aHandlerPageURI.spec;

    if (DEBUG) {
      debug("registerConnection: aKeyword: " + aKeyword +
            " manifestURL: " + manifestURL + " pageURL: " + pageURL +
            " aDescription: " + aDescription +
            " aRules.minimumAccessLevel: " + aRules.minimumAccessLevel +
            " aRules.manifestURLs: " + aRules.manifestURLs +
            " aRules.pageURLs: " + aRules.pageURLs +
            " aRules.installOrigins: " + aRules.installOrigins);
    }

    let subAppManifestURLs = this._registeredConnections[aKeyword];
    if (!subAppManifestURLs) {
      subAppManifestURLs = this._registeredConnections[aKeyword] = {};
    }

    subAppManifestURLs[manifestURL] = {
      pageURL: pageURL,
      description: aDescription,
      rules: aRules,
      manifestURL: manifestURL
    };
  },

  _matchMinimumAccessLevel: function(aRules, aAppStatus) {
    if (!aRules || !aRules.minimumAccessLevel) {
      if (DEBUG) {
        debug("rules.minimumAccessLevel is not available. No need to match.");
      }
      return true;
    }

    let minAccessLevel = aRules.minimumAccessLevel;
    switch (minAccessLevel) {
      case "web":
        if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_INSTALLED ||
            aAppStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED ||
            aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
          return true;
        }
        break;
      case "privileged":
        if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED ||
            aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
          return true;
        }
        break;
      case "certified":
        if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
          return true;
        }
        break;
    }

    if (DEBUG) {
      debug("rules.minimumAccessLevel is not matched!" +
            " minAccessLevel: " + minAccessLevel +
            " aAppStatus : " + aAppStatus);
    }
    return false;
  },

  _matchManifestURLs: function(aRules, aManifestURL) {
    if (!aRules || !Array.isArray(aRules.manifestURLs)) {
      if (DEBUG) {
        debug("rules.manifestURLs is not available. No need to match.");
      }
      return true;
    }

    let manifestURLs = aRules.manifestURLs;
    if (manifestURLs.indexOf(aManifestURL) != -1) {
      return true;
    }

    if (DEBUG) {
      debug("rules.manifestURLs is not matched!" +
            " manifestURLs: " + manifestURLs +
            " aManifestURL : " + aManifestURL);
    }
    return false;
  },

  _matchPageURLs: function(aRules, aPageURL) {

    if (!aRules || !aRules.pageURLs) {
      if (DEBUG) {
        debug("rules.pageURLs is not available. No need to match.");
      }
      return true;
    }

    if (!Array.isArray(aRules.pageURLs)) {
      aRules.pageURLs = [aRules.pageURLs];
    }

    let pageURLs = aRules.pageURLs;
    let isAllowed = false;
    for (let i = 0, li = pageURLs.length; i < li && !isAllowed ; i++) {
      let regExpAllowedURL = new RegExp(pageURLs[i]);
      isAllowed = regExpAllowedURL.test(aPageURL);
    }

    if (DEBUG) {
      debug("rules.pageURLs is " + (isAllowed ? "" : "not") + " matched!" +
            " pageURLs: " + pageURLs +
            " aPageURL: " + aPageURL);
    }

    return isAllowed;
  },

  _matchInstallOrigins: function(aRules, aInstallOrigin) {
    if (!aRules || !Array.isArray(aRules.installOrigins)) {
      if (DEBUG) {
        debug("rules.installOrigins is not available. No need to match.");
      }
      return true;
    }

    let installOrigins = aRules.installOrigins;
    if (installOrigins.indexOf(aInstallOrigin) != -1) {
      return true;
    }

    if (DEBUG) {
      debug("rules.installOrigins is not matched!" +
            " installOrigins: " + installOrigins +
            " installOrigin : " + aInstallOrigin);
    }
    return false;
  },

  // A connection is allowed if all the rules are matched.
  // The publisher is matched against the rules defined by the subscriber on the
  // manifest, and the subscriber is matched against the rules defined by the
  // publisher on the call to connect.
  // The possible rules for both subscribers and publishers are:
  //  * minimumAccessLevel: "privileged"|"certified"|"web"|undefined
  //     The default (non existant or undefined value) is "certified".
  //     That means that if an explicit minimumAccessLevel rule does not
  //     exist then the peer of the connection *must* be a certified app.
  //  * pageURLs: Array of regExp of URLs. If the value exists, only the pages
  //     whose URLs are explicitly declared on the array (matched) can connect.
  //     Otherwise all pages can connect
  //  * installOrigins: Array of origin URLs. If the value exist, only the apps
  //    whose origins are on the array can connect. Otherwise, all origins are
  //    allowed. This is only checked for non certified apps!
  // The default value (empty or non existant rules) is:
  //   * Only certified apps can connect
  //   * Any originator/receiving page URLs are valid
  //   * Any origin is valid.
  _matchRules: function(aPubAppManifestURL, aPubRules,
                        aSubAppManifestURL, aSubRules,
                        aPubPageURL, aSubPageURL) {
    let pubApp = this.appsService.getAppByManifestURL(aPubAppManifestURL);
    let subApp = this.appsService.getAppByManifestURL(aSubAppManifestURL);

    let isPubAppCertified =
      (pubApp.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED);

    let isSubAppCertified =
      (subApp.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED);

//@line 428 "/build/bluegriffon/src/mozilla-central-f14898695ee0/dom/apps/InterAppCommService.jsm"

    let numSubRules =  (aSubRules && Object.keys(aSubRules).length) || 0;
    let numPubRules =  (aPubRules && Object.keys(aPubRules).length) || 0;

    if ((!isSubAppCertified && !numPubRules) ||
        (!isPubAppCertified && !numSubRules)) {
      if (DEBUG) {
        debug("If there aren't rules defined only certified apps are allowed " +
              "to do connections.");
      }
      return false;
    }

//@line 442 "/build/bluegriffon/src/mozilla-central-f14898695ee0/dom/apps/InterAppCommService.jsm"

    if (!aPubRules && !aSubRules) {
      if (DEBUG) {
        debug("No rules for publisher and subscriber. No need to match.");
      }
      return true;
    }

    // Check minimumAccessLevel.
    if (!this._matchMinimumAccessLevel(aPubRules, subApp.appStatus) ||
        !this._matchMinimumAccessLevel(aSubRules, pubApp.appStatus)) {
      return false;
    }

    // Check manifestURLs.
    if (!this._matchManifestURLs(aPubRules, aSubAppManifestURL) ||
        !this._matchManifestURLs(aSubRules, aPubAppManifestURL)) {
      return false;
    }

    // Check pageURLs.
    if (!this._matchPageURLs(aPubRules, aSubPageURL) ||
        !this._matchPageURLs(aSubRules, aPubPageURL)) {
      return false;
    }

    // Check installOrigins. Note that we only check the install origin for the
    // non-certified app, because the certified app doesn't have install origin.
    if ((!isSubAppCertified &&
         !this._matchInstallOrigins(aPubRules, subApp.installOrigin)) ||
        (!isPubAppCertified &&
         !this._matchInstallOrigins(aSubRules, pubApp.installOrigin))) {
      return false;
    }

    if (DEBUG) debug("All rules are matched.");
    return true;
  },

  _dispatchMessagePorts: function(aKeyword, aPubAppManifestURL,
                                  aAllowedSubAppManifestURLs,
                                  aTarget, aOuterWindowID, aRequestID,
                                  aPubPageURL) {
    if (DEBUG) {
      debug("_dispatchMessagePorts: aKeyword: " + aKeyword +
            " aPubAppManifestURL: " + aPubAppManifestURL +
            " aAllowedSubAppManifestURLs: " + aAllowedSubAppManifestURLs +
            " aPubPageURL: " + aPubPageURL);
    }

    if (aAllowedSubAppManifestURLs.length == 0) {
      if (DEBUG) debug("No apps are allowed to connect. Returning.");
      aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
                               { oid: aOuterWindowID, requestID: aRequestID });
      return;
    }

    let subAppManifestURLs = this._registeredConnections[aKeyword];
    if (!subAppManifestURLs) {
      if (DEBUG) debug("No apps are subscribed to connect. Returning.");
      aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
                               { oid: aOuterWindowID, requestID: aRequestID });
      return;
    }

    let messagePortIDs = [];
    aAllowedSubAppManifestURLs.forEach(function(aAllowedSubAppManifestURL) {
      let subscribedInfo = subAppManifestURLs[aAllowedSubAppManifestURL];
      if (!subscribedInfo) {
        if (DEBUG) {
          debug("The sunscribed info is not available. Skipping: " +
                aAllowedSubAppManifestURL);
        }
        return;
      }

      // The message port ID is aimed for identifying the coupling targets
      // to deliver messages with each other. This ID is centrally generated
      // by the parent and dispatched to both the sender and receiver ends
      // for creating their own message ports respectively.
      let messagePortID = UUIDGenerator.generateUUID().toString();
      this._messagePortPairs[messagePortID] = {
        keyword: aKeyword,
        publisher: {
          manifestURL: aPubAppManifestURL
        },
        subscriber: {
          manifestURL: aAllowedSubAppManifestURL
        }
      };

      // Fire system message to deliver the message port to the subscriber.
      this.messenger.sendMessage("connection",
        { keyword: aKeyword,
          messagePortID: messagePortID,
          pubPageURL: aPubPageURL},
        Services.io.newURI(subscribedInfo.pageURL, null, null),
        Services.io.newURI(subscribedInfo.manifestURL, null, null));

      messagePortIDs.push(messagePortID);
    }, this);

    if (messagePortIDs.length == 0) {
      if (DEBUG) debug("No apps are subscribed to connect. Returning.");
      aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
                               { oid: aOuterWindowID, requestID: aRequestID });
      return;
    }

    // Return the message port IDs to open the message ports for the publisher.
    if (DEBUG) debug("messagePortIDs: " + messagePortIDs);
    aTarget.sendAsyncMessage("Webapps:Connect:Return:OK",
                             { keyword: aKeyword,
                               messagePortIDs: messagePortIDs,
                               oid: aOuterWindowID, requestID: aRequestID });
  },

  /**
   * Fetch the subscribers that are currently allowed to connect.
   *
   * @param aKeyword           The connection's keyword.
   * @param aPubAppManifestURL The manifest URL of the publisher.
   *
   * @param return an array of manifest URLs of the subscribers.
   */
  _getAllowedSubAppManifestURLs: function(aKeyword, aPubAppManifestURL) {
    let allowedPubAppManifestURLs = this._allowedConnections[aKeyword];
    if (!allowedPubAppManifestURLs) {
      return [];
    }

    let allowedSubAppManifestURLs =
      allowedPubAppManifestURLs[aPubAppManifestURL];
    if (!allowedSubAppManifestURLs) {
      return [];
    }

    return allowedSubAppManifestURLs;
  },

  /**
   * Add the newly selected apps into the allowed connections and return the
   * aggregated allowed connections.
   *
   * @param aKeyword           The connection's keyword.
   * @param aPubAppManifestURL The manifest URL of the publisher.
   * @param aSelectedApps      An array of the subscribers' information.
   *
   * @param return an array of manifest URLs of the subscribers.
   */
  _addSelectedApps: function(aKeyword, aPubAppManifestURL, aSelectedApps) {
    let allowedPubAppManifestURLs = this._allowedConnections[aKeyword];

    // Add a new entry for |aKeyword|.
    if (!allowedPubAppManifestURLs) {
      allowedPubAppManifestURLs = this._allowedConnections[aKeyword] = {};
    }

    let allowedSubAppManifestURLs =
      allowedPubAppManifestURLs[aPubAppManifestURL];

    // Add a new entry for |aPubAppManifestURL|.
    if (!allowedSubAppManifestURLs) {
      allowedSubAppManifestURLs =
        allowedPubAppManifestURLs[aPubAppManifestURL] = [];
    }

    // Add the selected apps into the existing set of allowed connections.
    aSelectedApps.forEach(function(aSelectedApp) {
      let allowedSubAppManifestURL = aSelectedApp.manifestURL;
      if (allowedSubAppManifestURLs.indexOf(allowedSubAppManifestURL) == -1) {
        allowedSubAppManifestURLs.push(allowedSubAppManifestURL);
      }
    });

    return allowedSubAppManifestURLs;
  },

  _connect: function(aMessage, aTarget) {
    let keyword = aMessage.keyword;
    let pubRules = aMessage.rules;
    let pubPageURL = aMessage.pubPageURL;
    let pubAppManifestURL = aMessage.manifestURL;
    let outerWindowID = aMessage.outerWindowID;
    let requestID = aMessage.requestID;

    let subAppManifestURLs = this._registeredConnections[keyword];
    if (!subAppManifestURLs) {
      if (DEBUG) {
        debug("No apps are subscribed for this connection. Returning.");
      }
      this._dispatchMessagePorts(keyword, pubAppManifestURL, [],
                                 aTarget, outerWindowID, requestID, pubPageURL);
      return;
    }

    // Fetch the apps that are currently allowed to connect, so that users
    // don't need to select/allow them again, which means we only pop up the
    // prompt UI for the *new* connections.
    let allowedSubAppManifestURLs =
      this._getAllowedSubAppManifestURLs(keyword, pubAppManifestURL);

    // Check rules to see if a subscribed app is allowed to connect.
    let appsToSelect = [];
    for (let subAppManifestURL in subAppManifestURLs) {
      if (allowedSubAppManifestURLs.indexOf(subAppManifestURL) != -1) {
        if (DEBUG) {
          debug("Don't need to select again. Skipping: " + subAppManifestURL);
        }
        continue;
      }

      // Only rule-matched publishers/subscribers are allowed to connect.
      let subscribedInfo = subAppManifestURLs[subAppManifestURL];
      let subRules = subscribedInfo.rules;

      let matched =
        this._matchRules(pubAppManifestURL, pubRules,
                         subAppManifestURL, subRules,
                         pubPageURL, subscribedInfo.pageURL);
      if (!matched) {
        if (DEBUG) {
          debug("Rules are not matched. Skipping: " + subAppManifestURL);
        }
        continue;
      }

      appsToSelect.push({
        manifestURL: subAppManifestURL,
        description: subscribedInfo.description
      });
    }

    if (appsToSelect.length == 0) {
      if (DEBUG) {
        debug("No additional apps need to be selected for this connection. " +
              "Just dispatch message ports for the existing connections.");
      }

      this._dispatchMessagePorts(keyword, pubAppManifestURL,
                                 allowedSubAppManifestURLs,
                                 aTarget, outerWindowID, requestID, pubPageURL);
      return;
    }

    // Remember the caller info with an UUID so that we can know where to
    // return the promise resolver's callback when the prompt UI returns.
    let callerID = UUIDGenerator.generateUUID().toString();
    this._promptUICallers[callerID] = {
      outerWindowID: outerWindowID,
      requestID: requestID,
      target: aTarget
    };

    let glue = Cc["@mozilla.org/dom/apps/inter-app-comm-ui-glue;1"]
        .createInstance(Ci.nsIInterAppCommUIGlue);
    if (glue) {
      glue.selectApps(callerID, pubAppManifestURL, keyword, appsToSelect).then(
        function(aData) {
          aData.pubPageURL = pubPageURL;
          this._handleSelectedApps(aData);
        }.bind(this),
        function(aError) {
          if (DEBUG) {
            debug("Error occurred in the UI glue component. " + aError);
          }

          // Resolve the caller as if there were no selected apps.
          this._handleSelectedApps({ callerID: callerID,
                                     keyword: keyword,
                                     manifestURL: pubAppManifestURL,
                                     pubPageURL: pubPageURL,
                                     selectedApps: [] });
        }.bind(this)
      );
    } else {
      if (DEBUG) {
        debug("Error! The UI glue component is not implemented.");
      }

      // Resolve the caller as if there were no selected apps.
      this._handleSelectedApps({ callerID: callerID,
                                 keyword: keyword,
                                 manifestURL: pubAppManifestURL,
                                 pubPageURL: pubPageURL,
                                 selectedApps: [] });
    }
  },

  _getConnections: function(aMessage, aTarget) {
    let outerWindowID = aMessage.outerWindowID;
    let requestID = aMessage.requestID;

    let connections = [];
    for (let keyword in this._allowedConnections) {
      let allowedPubAppManifestURLs = this._allowedConnections[keyword];
      for (let allowedPubAppManifestURL in allowedPubAppManifestURLs) {
        let allowedSubAppManifestURLs =
          allowedPubAppManifestURLs[allowedPubAppManifestURL];
        allowedSubAppManifestURLs.forEach(function(allowedSubAppManifestURL) {
          connections.push({ keyword: keyword,
                             pubAppManifestURL: allowedPubAppManifestURL,
                             subAppManifestURL: allowedSubAppManifestURL });
        });
      }
    }

    aTarget.sendAsyncMessage("Webapps:GetConnections:Return:OK",
                             { connections: connections,
                               oid: outerWindowID, requestID: requestID });
  },

  _cancelConnection: function(aMessage) {
    let keyword = aMessage.keyword;
    let pubAppManifestURL = aMessage.pubAppManifestURL;
    let subAppManifestURL = aMessage.subAppManifestURL;

    let allowedPubAppManifestURLs = this._allowedConnections[keyword];
    if (!allowedPubAppManifestURLs) {
      if (DEBUG) debug("keyword is not found: " + keyword);
      return;
    }

    let allowedSubAppManifestURLs =
      allowedPubAppManifestURLs[pubAppManifestURL];
    if (!allowedSubAppManifestURLs) {
      if (DEBUG) debug("publisher is not found: " + pubAppManifestURL);
      return;
    }

    let index = allowedSubAppManifestURLs.indexOf(subAppManifestURL);
    if (index == -1) {
      if (DEBUG) debug("subscriber is not found: " + subAppManifestURL);
      return;
    }

    if (DEBUG) debug("Cancelling the connection.");
    allowedSubAppManifestURLs.splice(index, 1);

    // Clean up the parent entries if needed.
    if (allowedSubAppManifestURLs.length == 0) {
      delete allowedPubAppManifestURLs[pubAppManifestURL];
      if (Object.keys(allowedPubAppManifestURLs).length == 0) {
        delete this._allowedConnections[keyword];
      }
    }

    if (DEBUG) debug("Unregistering message ports based on this connection.");
    let messagePortIDs = [];
    for (let messagePortID in this._messagePortPairs) {
      let pair = this._messagePortPairs[messagePortID];
      if (pair.keyword == keyword &&
          pair.publisher.manifestURL == pubAppManifestURL &&
          pair.subscriber.manifestURL == subAppManifestURL) {
        messagePortIDs.push(messagePortID);
      }
    }
    messagePortIDs.forEach(function(aMessagePortID) {
      delete this._messagePortPairs[aMessagePortID];
    }, this);
  },

  _identifyMessagePort: function(aMessagePortID, aManifestURL) {
    let pair = this._messagePortPairs[aMessagePortID];
    if (!pair) {
      if (DEBUG) {
        debug("Error! The message port ID is invalid: " + aMessagePortID +
              ", which should have been generated by parent.");
      }
      return null;
    }

    // Check it the message port is for publisher.
    if (pair.publisher.manifestURL == aManifestURL) {
      return { pair: pair, isPublisher: true };
    }

    // Check it the message port is for subscriber.
    if (pair.subscriber.manifestURL == aManifestURL) {
      return { pair: pair, isPublisher: false };
    }

    if (DEBUG) {
      debug("Error! The manifest URL is invalid: " + aManifestURL +
            ", which might be a hacked app.");
    }
    return null;
  },

  _registerMessagePort: function(aMessage, aTarget) {
    let messagePortID = aMessage.messagePortID;
    let manifestURL = aMessage.manifestURL;
    let pageURL = aMessage.pageURL;

    let identity = this._identifyMessagePort(messagePortID, manifestURL);
    if (!identity) {
      if (DEBUG) {
        debug("Cannot identify the message port. Failed to register.");
      }
      return;
    }

    if (DEBUG) debug("Registering message port for " + manifestURL);
    let pair = identity.pair;
    let isPublisher = identity.isPublisher;

    let sender = isPublisher ? pair.publisher : pair.subscriber;
    sender.target = aTarget;
    sender.pageURL = pageURL;
    sender.messageQueue = [];

    // Check if the other port has queued messages. Deliver them if needed.
    if (DEBUG) {
      debug("Checking if the other port used to send messages but queued.");
    }
    let receiver = isPublisher ? pair.subscriber : pair.publisher;
    if (receiver.messageQueue) {
      while (receiver.messageQueue.length) {
        let message = receiver.messageQueue.shift();
        if (DEBUG) debug("Delivering message: " + JSON.stringify(message));
        sender.target.sendAsyncMessage("InterAppMessagePort:OnMessage",
                                       { message: message,
                                         manifestURL: sender.manifestURL,
                                         pageURL: sender.pageURL,
                                         messagePortID: messagePortID });
      }
    }
  },

  _unregisterMessagePort: function(aMessage) {
    let messagePortID = aMessage.messagePortID;
    let manifestURL = aMessage.manifestURL;

    let identity = this._identifyMessagePort(messagePortID, manifestURL);
    if (!identity) {
      if (DEBUG) {
        debug("Cannot identify the message port. Failed to unregister.");
      }
      return;
    }

    if (DEBUG) {
      debug("Unregistering message port for " + manifestURL);
    }

    let receiver = identity.isPublisher ? identity.pair.subscriber
                                        : identity.pair.publisher;
    receiver.target.sendAsyncMessage("InterAppMessagePort:OnClose",
                                     { manifestURL: receiver.manifestURL,
                                       pageURL: receiver.pageURL,
                                       messagePortID: messagePortID });

    delete this._messagePortPairs[messagePortID];
  },

  _removeTarget: function(aTarget) {
    if (!aTarget) {
      if (DEBUG) debug("Error! aTarget cannot be null/undefined in any way.");
      return
    }

    if (DEBUG) debug("Unregistering message ports based on this target.");
    let messagePortIDs = [];
    for (let messagePortID in this._messagePortPairs) {
      let pair = this._messagePortPairs[messagePortID];
      if (pair.publisher.target === aTarget ||
          pair.subscriber.target === aTarget) {
        messagePortIDs.push(messagePortID);
        // Send a shutdown message to the part of the pair that is still alive.
        let actor = pair.publisher.target === aTarget ? pair.subscriber
                                                      : pair.publisher;
        actor.target.sendAsyncMessage("InterAppMessagePort:Shutdown",
          { manifestURL: actor.manifestURL,
            pageURL: actor.pageURL,
            messagePortID: messagePortID });
      }
    }
    messagePortIDs.forEach(function(aMessagePortID) {
      delete this._messagePortPairs[aMessagePortID];
    }, this);
  },

  _postMessage: function(aMessage) {
    let messagePortID = aMessage.messagePortID;
    let manifestURL = aMessage.manifestURL;
    let message = aMessage.message;

    let identity = this._identifyMessagePort(messagePortID, manifestURL);
    if (!identity) {
      if (DEBUG) debug("Cannot identify the message port. Failed to post.");
      return;
    }

    let pair = identity.pair;
    let isPublisher = identity.isPublisher;

    let receiver = isPublisher ? pair.subscriber : pair.publisher;
    if (!receiver.target) {
      if (DEBUG) {
        debug("The receiver's target is not ready yet. Queuing the message.");
      }
      let sender = isPublisher ? pair.publisher : pair.subscriber;
      sender.messageQueue.push(message);
      return;
    }

    if (DEBUG) debug("Delivering message: " + JSON.stringify(message));
    receiver.target.sendAsyncMessage("InterAppMessagePort:OnMessage",
                                     { manifestURL: receiver.manifestURL,
                                       pageURL: receiver.pageURL,
                                       messagePortID: messagePortID,
                                       message: message });
  },

  _handleSelectedApps: function(aData) {
    let callerID = aData.callerID;
    let caller = this._promptUICallers[callerID];
    if (!caller) {
      if (DEBUG) debug("Error! Cannot find the caller.");
      return;
    }

    delete this._promptUICallers[callerID];

    let outerWindowID = caller.outerWindowID;
    let requestID = caller.requestID;
    let target = caller.target;

    let pubAppManifestURL = aData.manifestURL;
    let pubPageURL = aData.pubPageURL;
    let keyword = aData.keyword;
    let selectedApps = aData.selectedApps;

    let allowedSubAppManifestURLs;
    if (selectedApps.length == 0) {
      // Only do the connections for the existing allowed subscribers because
      // no new apps are selected to connect.
      if (DEBUG) debug("No new apps are selected to connect.");

      allowedSubAppManifestURLs =
        this._getAllowedSubAppManifestURLs(keyword, pubAppManifestURL);
    } else {
      // Do connections for for the existing allowed subscribers and the newly
      // selected subscribers.
      if (DEBUG) debug("Some new apps are selected to connect.");

      allowedSubAppManifestURLs =
        this._addSelectedApps(keyword, pubAppManifestURL, selectedApps);
    }

    // Finally, dispatch the message ports for the allowed connections,
    // including the old connections and the newly selected connection.
    this._dispatchMessagePorts(keyword, pubAppManifestURL,
                               allowedSubAppManifestURLs,
                               target, outerWindowID, requestID, pubPageURL);
  },

  receiveMessage: function(aMessage) {
    if (DEBUG) debug("receiveMessage: name: " + aMessage.name);
    let message = aMessage.json;
    let target = aMessage.target;

    // To prevent the hacked child process from sending commands to parent
    // to do illegal connections, we need to check its manifest URL.
    if (aMessage.name !== "child-process-shutdown" &&
        // TODO: fix bug 988142 to re-enable "InterAppMessagePort:Unregister".
        aMessage.name !== "InterAppMessagePort:Unregister" &&
        kMessages.indexOf(aMessage.name) != -1) {
      if (!target.assertContainApp(message.manifestURL)) {
        if (DEBUG) {
          debug("Got message from a process carrying illegal manifest URL.");
        }
        return null;
      }
    }

    switch (aMessage.name) {
      case "Webapps:Connect":
        this._connect(message, target);
        break;
      case "Webapps:GetConnections":
        this._getConnections(message, target);
        break;
      case "InterAppConnection:Cancel":
        this._cancelConnection(message);
        break;
      case "InterAppMessagePort:PostMessage":
        this._postMessage(message);
        break;
      case "InterAppMessagePort:Register":
        this._registerMessagePort(message, target);
        break;
      case "InterAppMessagePort:Unregister":
        this._unregisterMessagePort(message);
        break;
      case "child-process-shutdown":
        this._removeTarget(target);
        break;
    }
  },

  observe: function(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "xpcom-shutdown":
        Services.obs.removeObserver(this, "xpcom-shutdown");
        Services.obs.removeObserver(this, "webapps-clear-data");
        kMessages.forEach(function(aMsg) {
          ppmm.removeMessageListener(aMsg, this);
        }, this);
        ppmm = null;
        break;
      case "webapps-clear-data":
        let params =
          aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
        if (!params) {
          if (DEBUG) {
            debug("Error updating registered/allowed connections for an " +
                  "uninstalled app.");
          }
          return;
        }

        // Only update registered/allowed connections for apps.
        if (params.browserOnly) {
          if (DEBUG) {
            debug("Only update registered/allowed connections for apps.");
          }
          return;
        }

        let manifestURL =
                     this.appsService.getManifestURLByLocalId(params.appId);
        if (!manifestURL) {
          if (DEBUG) {
            debug("Error updating registered/allowed connections for an " +
                  "uninstalled app.");
          }
          return;
        }

        // Update registered connections.
        for (let keyword in this._registeredConnections) {
          let subAppManifestURLs = this._registeredConnections[keyword];
          if (subAppManifestURLs[manifestURL]) {
            delete subAppManifestURLs[manifestURL];
            if (DEBUG) {
              debug("Remove " + manifestURL + " from registered connections " +
                    "due to app uninstallation.");
            }
          }
        }

        // Update allowed connections.
        for (let keyword in this._allowedConnections) {
          let allowedPubAppManifestURLs = this._allowedConnections[keyword];
          if (allowedPubAppManifestURLs[manifestURL]) {
            delete allowedPubAppManifestURLs[manifestURL];
            if (DEBUG) {
              debug("Remove " + manifestURL + " (as a pub app) from allowed " +
                    "connections due to app uninstallation.");
            }
          }

          for (let pubAppManifestURL in allowedPubAppManifestURLs) {
            let subAppManifestURLs = allowedPubAppManifestURLs[pubAppManifestURL];
            for (let i = subAppManifestURLs.length - 1; i >= 0; i--) {
              if (subAppManifestURLs[i] === manifestURL) {
                subAppManifestURLs.splice(i, 1);
                if (DEBUG) {
                  debug("Remove " + manifestURL + " (as a sub app to pub " +
                        pubAppManifestURL + ") from allowed connections " +
                        "due to app uninstallation.");
                }
              }
            }
          }
        }
        debug("Finish updating registered/allowed connections for an " +
              "uninstalled app.");
        break;
    }
  }
};

InterAppCommService.init();
