/* 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;

this.EXPORTED_SYMBOLS = ["DownloadsIPC"];

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

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

/**
  * This module lives in the child process and receives the ipc messages
  * from the parent. It saves the download's state and redispatch changes
  * to DOM objects using an observer notification.
  *
  * This module needs to be loaded once and only once per process.
  */

function debug(aStr) {
#ifdef MOZ_DEBUG
  dump("-*- DownloadsIPC.jsm : " + aStr + "\n");
#endif
}

const ipcMessages = ["Downloads:Added",
                     "Downloads:Removed",
                     "Downloads:Changed",
                     "Downloads:GetList:Return",
                     "Downloads:Remove:Return",
                     "Downloads:Pause:Return",
                     "Downloads:Resume:Return",
                     "Downloads:Adopt:Return"];

this.DownloadsIPC = {
  downloads: {},

  init: function() {
    debug("init");
    Services.obs.addObserver(this, "xpcom-shutdown", false);
    ipcMessages.forEach((aMessage) => {
      cpmm.addMessageListener(aMessage, this);
    });

    // We need to get the list of current downloads.
    this.ready = false;
    this.getListPromises = [];
    this.downloadPromises = {};
    cpmm.sendAsyncMessage("Downloads:GetList", {});
    this._promiseId = 0;
  },

  notifyChanges: function(aId) {
    // TODO: use the subject instead of stringifying.
    if (this.downloads[aId]) {
      debug("notifyChanges notifying changes for " + aId);
      Services.obs.notifyObservers(null, "downloads-state-change-" + aId,
                                   JSON.stringify(this.downloads[aId]));
    } else {
      debug("notifyChanges failed for " + aId)
    }
  },

  _updateDownloadsArray: function(aDownloads) {
    this.downloads = [];
    // We actually have an array of downloads.
    aDownloads.forEach((aDownload) => {
      this.downloads[aDownload.id] = aDownload;
    });
  },

  receiveMessage: function(aMessage) {
    let download = aMessage.data;
    debug("message: " + aMessage.name);
    switch(aMessage.name) {
      case "Downloads:GetList:Return":
        this._updateDownloadsArray(download);

        if (!this.ready) {
          this.getListPromises.forEach(aPromise =>
                                       aPromise.resolve(this.downloads));
          this.getListPromises.length = 0;
        }
        this.ready = true;
        break;
      case "Downloads:Added":
        this.downloads[download.id] = download;
        this.notifyChanges(download.id);
        break;
      case "Downloads:Removed":
        if (this.downloads[download.id]) {
          this.downloads[download.id] = download;
          this.notifyChanges(download.id);
          delete this.downloads[download.id];
        }
        break;
      case "Downloads:Changed":
        // Only update properties that actually changed.
        let cached = this.downloads[download.id];
        if (!cached) {
          debug("No download found for " + download.id);
          return;
        }
        let props = ["totalBytes", "currentBytes", "url", "path", "state",
                     "contentType", "startTime"];
        let changed = false;

        props.forEach((aProp) => {
          if (download[aProp] && (download[aProp] != cached[aProp])) {
            cached[aProp] = download[aProp];
            changed = true;
          }
        });

        // Updating the error property. We always get a 'state' change as
        // well.
        cached.error = download.error;

        if (changed) {
          this.notifyChanges(download.id);
        }
        break;
      case "Downloads:Remove:Return":
      case "Downloads:Pause:Return":
      case "Downloads:Resume:Return":
      case "Downloads:Adopt:Return":
        if (this.downloadPromises[download.promiseId]) {
          if (!download.error) {
            this.downloadPromises[download.promiseId].resolve(download);
          } else {
            this.downloadPromises[download.promiseId].reject(download);
          }
          delete this.downloadPromises[download.promiseId];
        }
        break;
    }
  },

  /**
    * Returns a promise that is resolved with the list of current downloads.
    */
  getDownloads: function() {
    debug("getDownloads()");
    let deferred = Promise.defer();
    if (this.ready) {
      debug("Returning existing list.");
      deferred.resolve(this.downloads);
    } else {
      this.getListPromises.push(deferred);
    }
    return deferred.promise;
  },

  /**
   * Void function to trigger removal of completed downloads.
   */
  clearAllDone: function() {
    debug("clearAllDone");
    cpmm.sendAsyncMessage("Downloads:ClearAllDone", {});
  },

  promiseId: function() {
    return this._promiseId++;
  },

  remove: function(aId) {
    debug("remove " + aId);
    let deferred = Promise.defer();
    let pId = this.promiseId();
    this.downloadPromises[pId] = deferred;
    cpmm.sendAsyncMessage("Downloads:Remove",
                          { id: aId, promiseId: pId });
    return deferred.promise;
  },

  pause: function(aId) {
    debug("pause " + aId);
    let deferred = Promise.defer();
    let pId = this.promiseId();
    this.downloadPromises[pId] = deferred;
    cpmm.sendAsyncMessage("Downloads:Pause",
                          { id: aId, promiseId: pId });
    return deferred.promise;
  },

  resume: function(aId) {
    debug("resume " + aId);
    let deferred = Promise.defer();
    let pId = this.promiseId();
    this.downloadPromises[pId] = deferred;
    cpmm.sendAsyncMessage("Downloads:Resume",
                          { id: aId, promiseId: pId });
    return deferred.promise;
  },

  adoptDownload: function(aJsonDownload) {
    debug("adoptDownload");
    let deferred = Promise.defer();
    let pId = this.promiseId();
    this.downloadPromises[pId] = deferred;
    cpmm.sendAsyncMessage("Downloads:Adopt",
                          { jsonDownload: aJsonDownload, promiseId: pId });
    return deferred.promise;
  },

  observe: function(aSubject, aTopic, aData) {
    if (aTopic == "xpcom-shutdown") {
      ipcMessages.forEach((aMessage) => {
        cpmm.removeMessageListener(aMessage, this);
      });
    }
  }
};

DownloadsIPC.init();