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

// This module exposes a subset of the functionalities of the parent DOM
// Registry to content processes, to be used from the AppsService component.

this.EXPORTED_SYMBOLS = ["DOMApplicationRegistry", "WrappedManifestCache"];

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

function debug(s) {
  //dump("-*- AppsServiceChild.jsm: " + s + "\n");
}

const APPS_IPC_MSG_NAMES = [
  "Webapps:AddApp",
  "Webapps:RemoveApp",
  "Webapps:UpdateApp",
  "Webapps:CheckForUpdate:Return:KO",
  "Webapps:FireEvent",
  "Webapps:UpdateState"
];

// A simple cache for the wrapped manifests.
this.WrappedManifestCache = {
  _cache: { },

  // Gets an entry from the cache, and populates the cache if needed.
  get: function mcache_get(aManifestURL, aManifest, aWindow, aInnerWindowID) {
    if (!aManifest) {
      return;
    }

    if (!(aManifestURL in this._cache)) {
      this._cache[aManifestURL] = { };
    }

    let winObjs = this._cache[aManifestURL];
    if (!(aInnerWindowID in winObjs)) {
      winObjs[aInnerWindowID] = Cu.cloneInto(aManifest, aWindow);
    }

    return winObjs[aInnerWindowID];
  },

  // Invalidates an entry in the cache.
  evict: function mcache_evict(aManifestURL, aInnerWindowID) {
    debug("Evicting manifest " + aManifestURL + " window ID " +
          aInnerWindowID);
    if (aManifestURL in this._cache) {
      let winObjs = this._cache[aManifestURL];
      if (aInnerWindowID in winObjs) {
        delete winObjs[aInnerWindowID];
      }

      if (Object.keys(winObjs).length == 0) {
        delete this._cache[aManifestURL];
      }
    }
  },

  observe: function(aSubject, aTopic, aData) {
    // Clear the cache on memory pressure.
    this._cache = { };
    Cu.forceGC();
  },

  init: function() {
    Services.obs.addObserver(this, "memory-pressure", false);
  }
};

this.WrappedManifestCache.init();


// DOMApplicationRegistry keeps a cache containing a list of apps in the device.
// This information is updated with the data received from the main process and
// it is queried by the DOM objects to set their state.
// This module handle all the messages broadcasted from the parent process,
// including DOM events, which are dispatched to the corresponding DOM objects.

this.DOMApplicationRegistry = {
  // DOMApps will hold a list of arrays of weak references to
  // mozIDOMApplication objects indexed by manifest URL.
  DOMApps: {},

  ready: false,
  webapps: null,

  init: function init() {
    this.cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
                  .getService(Ci.nsISyncMessageSender);

    APPS_IPC_MSG_NAMES.forEach((function(aMsgName) {
      this.cpmm.addMessageListener(aMsgName, this);
    }).bind(this));

    this.resetList();

    Services.obs.addObserver(this, "xpcom-shutdown", false);
  },

  resetList: function() {
    this.cpmm.sendAsyncMessage("Webapps:RegisterForMessages", {
      messages: APPS_IPC_MSG_NAMES
    });

    // We need to prime the cache with the list of apps.
    let list = this.cpmm.sendSyncMessage("Webapps:GetList", { })[0];
    this.webapps = list.webapps;

    // We need a fast mapping from localId -> app, so we add an index.
    // We also add the manifest to the app object.
    this.localIdIndex = { };
    for (let id in this.webapps) {
      let app = this.webapps[id];
      this.localIdIndex[app.localId] = app;
      app.manifest = list.manifests[id];
    }
  },

  observe: function(aSubject, aTopic, aData) {
    // cpmm.addMessageListener causes the DOMApplicationRegistry object to
    // live forever if we don't clean up properly.
    this.webapps = null;
    this.DOMApps = null;

    APPS_IPC_MSG_NAMES.forEach((aMsgName) => {
      this.cpmm.removeMessageListener(aMsgName, this);
    });
  },

  receiveMessage: function receiveMessage(aMessage) {
    debug("Received " + aMessage.name + " message.");
    let msg = aMessage.data;
    switch (aMessage.name) {
      case "Webapps:AddApp":
        this.webapps[msg.id] = msg.app;
        this.localIdIndex[msg.app.localId] = msg.app;
        if (msg.manifest) {
          this.webapps[msg.id].manifest = msg.manifest;
        }
        break;
      case "Webapps:RemoveApp":
        delete this.DOMApps[this.webapps[msg.id].manifestURL];
        delete this.localIdIndex[this.webapps[msg.id].localId];
        delete this.webapps[msg.id];
        break;
      case "Webapps:UpdateApp":
        let app = this.webapps[msg.oldId];
        if (!app) {
          return;
        }

        if (msg.app) {
          for (let prop in msg.app) {
            app[prop] = msg.app[prop];
          }
        }

        this.webapps[msg.newId] = app;
        this.localIdIndex[app.localId] = app;
        delete this.webapps[msg.oldId];

        let apps = this.DOMApps[msg.app.manifestURL];
        if (!apps) {
          return;
        }
        for (let i = 0; i < apps.length; i++) {
          let domApp = apps[i].get();
          if (!domApp || domApp._window === null) {
            apps.splice(i, 1);
            continue;
          }
          domApp._proxy = new Proxy(domApp, {
            get: function(target, prop) {
              if (!DOMApplicationRegistry.webapps[msg.newId]) {
                return;
              }
              return DOMApplicationRegistry.webapps[msg.newId][prop];
            },
            set: function(target, prop, val) {
              if (!DOMApplicationRegistry.webapps[msg.newId]) {
                return;
              }
              DOMApplicationRegistry.webapps[msg.newId][prop] = val;
              return;
            },
          });
        }
        break;
      case "Webapps:FireEvent":
        this._fireEvent(aMessage);
        break;
      case "Webapps:UpdateState":
        this._updateState(msg);
        break;
      case "Webapps:CheckForUpdate:Return:KO":
        let DOMApps = this.DOMApps[msg.manifestURL];
        if (!DOMApps || !msg.requestID) {
          return;
        }
        DOMApps.forEach((DOMApp) => {
          let domApp = DOMApp.get();
          if (domApp && msg.requestID) {
            domApp._fireRequestResult(aMessage, true /* aIsError */);
          }
        });
        break;
    }
  },

  /**
   * mozIDOMApplication management
   */

  // Every time a DOM app is created, we save a weak reference to it that will
  // be used to dispatch events and fire request results.
  addDOMApp: function(aApp, aManifestURL, aId) {
    let weakRef = Cu.getWeakReference(aApp);

    if (!this.DOMApps[aManifestURL]) {
      this.DOMApps[aManifestURL] = [];
    }

    let apps = this.DOMApps[aManifestURL];

    // Get rid of dead weak references.
    for (let i = 0; i < apps.length; i++) {
      let app = apps[i].get();
      if (!app || app._window === null) {
        apps.splice(i, 1);
      }
    }

    apps.push(weakRef);

    // Each DOM app contains a proxy object used to build their state. We
    // return the handler for this proxy object with traps to get and set
    // app properties kept in the DOMApplicationRegistry app cache.
    return {
      get: function(target, prop) {
        if (!DOMApplicationRegistry.webapps[aId]) {
          return;
        }

        if (prop in DOMApplicationRegistry.webapps[aId]) {
          return DOMApplicationRegistry.webapps[aId][prop];
        }
        return null;
      },
      set: function(target, prop, val) {
        if (!DOMApplicationRegistry.webapps[aId]) {
          return;
        }
        DOMApplicationRegistry.webapps[aId][prop] = val;
        return;
      },
    };
  },

  _fireEvent: function(aMessage) {
    let msg = aMessage.data;
    debug("_fireEvent " + JSON.stringify(msg));
    if (!this.DOMApps || !msg.manifestURL || !msg.eventType) {
      return;
    }

    let DOMApps = this.DOMApps[msg.manifestURL];
    if (!DOMApps) {
      return;
    }

    // The parent might ask childs to trigger more than one event in one
    // shot, so in order to avoid needless IPC we allow an array for the
    // 'eventType' IPC message field.
    if (!Array.isArray(msg.eventType)) {
      msg.eventType = [msg.eventType];
    }

    DOMApps.forEach((DOMApp) => {
      let domApp = DOMApp.get();
      if (!domApp) {
        return;
      }
      msg.eventType.forEach((aEventType) => {
        if ('on' + aEventType in domApp) {
          domApp._fireEvent(aEventType);
        }
      });

      if (msg.requestID) {
        aMessage.data.result = msg.manifestURL;
        domApp._fireRequestResult(aMessage);
      }
    });
  },

  _updateState: function(aMessage) {
    if (!this.DOMApps || !aMessage.id) {
      return;
    }

    let app = this.webapps[aMessage.id];
    if (!app) {
      return;
    }

    if (aMessage.app) {
      for (let prop in aMessage.app) {
        app[prop] = aMessage.app[prop];
      }
    }

    if ("error" in aMessage) {
      app.downloadError = aMessage.error;
    }

    if (aMessage.manifest) {
      app.manifest = aMessage.manifest;
      // Evict the wrapped manifest cache for all the affected DOM objects.
      let DOMApps = this.DOMApps[app.manifestURL];
      if (!DOMApps) {
        return;
      }
      DOMApps.forEach((DOMApp) => {
        let domApp = DOMApp.get();
        if (!domApp) {
          return;
        }
        WrappedManifestCache.evict(app.manifestURL, domApp.innerWindowID);
      });
    }
  },

  getAll: function(aCallback) {
    debug("getAll()\n");
    if (!aCallback || typeof aCallback !== "function") {
      return;
    }

    let res = [];
    for (let id in this.webapps) {
      res.push(this.webapps[id]);
    }
    aCallback(res);
  },

  getAdditionalLanguages: function(aManifestURL) {
    for (let id in this.webapps) {
      if (this.webapps[id].manifestURL == aManifestURL) {
        return this.webapps[id].additionalLanguages || {};
      }
    }
    return {};
  },

  /**
   * nsIAppsService API
   */
  getAppByManifestURL: function getAppByManifestURL(aManifestURL) {
    debug("getAppByManifestURL " + aManifestURL);
    return AppsUtils.getAppByManifestURL(this.webapps, aManifestURL);
  },

  getAppLocalIdByManifestURL: function getAppLocalIdByManifestURL(aManifestURL) {
    debug("getAppLocalIdByManifestURL " + aManifestURL);
    return AppsUtils.getAppLocalIdByManifestURL(this.webapps, aManifestURL);
  },

  getManifestCSPByLocalId: function(aLocalId) {
    debug("getManifestCSPByLocalId:" + aLocalId);
    return AppsUtils.getManifestCSPByLocalId(this.webapps, aLocalId);
  },

  getDefaultCSPByLocalId: function(aLocalId) {
    debug("getDefaultCSPByLocalId:" + aLocalId);
    return AppsUtils.getDefaultCSPByLocalId(this.webapps, aLocalId);
  },

  getAppLocalIdByStoreId: function(aStoreId) {
    debug("getAppLocalIdByStoreId:" + aStoreId);
    return AppsUtils.getAppLocalIdByStoreId(this.webapps, aStoreId);
  },

  getAppByLocalId: function getAppByLocalId(aLocalId) {
    debug("getAppByLocalId " + aLocalId + " - ready: " + this.ready);
    let app = this.localIdIndex[aLocalId];
    if (!app) {
      debug("Ouch, No app!");
      return null;
    }

    return new mozIApplication(app);
  },

  getManifestURLByLocalId: function getManifestURLByLocalId(aLocalId) {
    debug("getManifestURLByLocalId " + aLocalId);
    return AppsUtils.getManifestURLByLocalId(this.webapps, aLocalId);
  },

  getCoreAppsBasePath: function getCoreAppsBasePath() {
    debug("getCoreAppsBasePath() not yet supported on child!");
    return null;
  },

  getWebAppsBasePath: function getWebAppsBasePath() {
    debug("getWebAppsBasePath() not yet supported on child!");
    return null;
  },

  getAppInfo: function getAppInfo(aAppId) {
    return AppsUtils.getAppInfo(this.webapps, aAppId);
  }
}

DOMApplicationRegistry.init();
