/* 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} = Components;

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

const MSG_PROMISE_REQUEST  = "WebAPIPromiseRequest";
const MSG_PROMISE_RESULT   = "WebAPIPromiseResult";
const MSG_INSTALL_EVENT    = "WebAPIInstallEvent";
const MSG_INSTALL_CLEANUP  = "WebAPICleanup";
const MSG_ADDON_EVENT_REQ  = "WebAPIAddonEventRequest";
const MSG_ADDON_EVENT      = "WebAPIAddonEvent";

const APIBroker = {
  _nextID: 0,

  init() {
    this._promises = new Map();

    // _installMap maps integer ids to DOM AddonInstall instances
    this._installMap = new Map();

    Services.cpmm.addMessageListener(MSG_PROMISE_RESULT, this);
    Services.cpmm.addMessageListener(MSG_INSTALL_EVENT, this);

    this._eventListener = null;
  },

  receiveMessage(message) {
    let payload = message.data;

    switch (message.name) {
      case MSG_PROMISE_RESULT: {
        if (!this._promises.has(payload.callbackID)) {
          return;
        }

        let resolve = this._promises.get(payload.callbackID);
        this._promises.delete(payload.callbackID);
        resolve(payload);
        break;
      }

      case MSG_INSTALL_EVENT: {
        let install = this._installMap.get(payload.id);
        if (!install) {
          let err = new Error(`Got install event for unknown install ${payload.id}`);
          Cu.reportError(err);
          return;
        }
        install._dispatch(payload);
        break;
      }

      case MSG_ADDON_EVENT: {
        if (this._eventListener) {
          this._eventListener(payload);
        }
      }
    }
  },

  sendRequest: function(type, ...args) {
    return new Promise(resolve => {
      let callbackID = this._nextID++;

      this._promises.set(callbackID, resolve);
      Services.cpmm.sendAsyncMessage(MSG_PROMISE_REQUEST, { type, callbackID, args });
    });
  },

  setAddonListener(callback) {
    this._eventListener = callback;
    if (callback) {
      Services.cpmm.addMessageListener(MSG_ADDON_EVENT, this);
      Services.cpmm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: true});
    } else {
      Services.cpmm.removeMessageListener(MSG_ADDON_EVENT, this);
      Services.cpmm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: false});
    }
  },

  sendCleanup: function(ids) {
    this.setAddonListener(null);
    Services.cpmm.sendAsyncMessage(MSG_INSTALL_CLEANUP, { ids });
  },
};

APIBroker.init();

function Addon(window, properties) {
  this.window = window;

  // We trust the webidl binding to broker access to our properties.
  for (let key of Object.keys(properties)) {
    this[key] = properties[key];
  }
}

function AddonInstall(window, properties) {
  let id = properties.id;
  APIBroker._installMap.set(id, this);

  this.window = window;
  this.handlers = new Map();

  for (let key of Object.keys(properties)) {
    this[key] = properties[key];
  }
}

/**
 * API methods all return promises from content.  They also follow a
 * similar pattern of sending a request to the parent process, then
 * wrapping the returned object or error appropriately for the page.
 * We must take care only to wrap and reject with errors that are meant
 * to be visible to content, and not internal errors.
 * This function is a wrapper to handle the common bits.
 *
 *   apiRequest is the name of the command to invoke in the parent process
 *   apiArgs is a callable that takes the content-provided args for the
 *           method and returns the arguments to send in the request to
 *           the parent process.
 *   if processor is non-null, it is called on the returned object to
 *           convert the result from the parent process back to an
 *           object appropriate for content.
 *
 * Both apiArgs and processor are called with "this" bound to the value
 * that is held when the actual api method was called.
 */
function WebAPITask(apiRequest, apiArgs, processor) {
  return function(...args) {
    let win = this.window;
    let boundApiArgs = apiArgs.bind(this);
    let boundProcessor = processor ? processor.bind(this) : null;

    return new win.Promise((resolve, reject) => {
      Task.spawn(function* () {
        let sendArgs = boundApiArgs(...args);
        let result = yield APIBroker.sendRequest(apiRequest, ...sendArgs);
        if ("reject" in result) {
          let err = new win.Error(result.reject.message);
          // We don't currently put any other properties onto Errors
          // generated by mozAddonManager.  If/when we do, they will
          // need to get copied here.
          reject(err);
          return;
        }

        let obj = result.resolve;
        if (boundProcessor) {
          obj = boundProcessor(obj);
        }
        resolve(obj);
      }).catch(err => {
        Cu.reportError(err);
        reject(new win.Error("Unexpected internal error"));
      });
    });
  }
}

Addon.prototype = {
  uninstall: WebAPITask("addonUninstall", function() { return [this.id]; }),
  setEnabled: WebAPITask("addonSetEnabled", function(value) {return [this.id, value]; }),
};

const INSTALL_EVENTS = [
  "onDownloadStarted",
  "onDownloadProgress",
  "onDownloadEnded",
  "onDownloadCancelled",
  "onDownloadFailed",
  "onInstallStarted",
  "onInstallEnded",
  "onInstallCancelled",
  "onInstallFailed",
];

AddonInstall.prototype = {
  _dispatch(data) {
    // The message for the event includes updated copies of all install
    // properties.  Use the usual "let webidl filter visible properties" trick.
    for (let key of Object.keys(data)) {
      this[key] = data[key];
    }

    let event = new this.window.Event(data.event);
    this.__DOM_IMPL__.dispatchEvent(event);
  },

  install: WebAPITask("addonInstallDoInstall", function() { return  [this.id]; }),
  cancel: WebAPITask("addonInstallCancel", function() { return  [this.id]; }),
};

function WebAPI() {
}

WebAPI.prototype = {
  init(window) {
    this.window = window;
    this.allInstalls = [];
    this.listenerCount = 0;

    window.addEventListener("unload", event => {
      APIBroker.sendCleanup(this.allInstalls);
    });
  },

  getAddonByID: WebAPITask("getAddonByID", id => [id], function(addonInfo) {
    if (!addonInfo) {
      return null;
    }
    let addon = new Addon(this.window, addonInfo);
    return this.window.Addon._create(this.window, addon);
  }),

  createInstall: WebAPITask("createInstall", options => [options], function(installInfo) {
    if (!installInfo) {
      return null;
    }
    let install = new AddonInstall(this.window, installInfo);
    this.allInstalls.push(installInfo.id);
    return this.window.AddonInstall._create(this.window, install);
  }),

  eventListenerWasAdded(type) {
    if (this.listenerCount == 0) {
      APIBroker.setAddonListener(data => {
        let event = new this.window.AddonEvent(data.event, data);
        this.__DOM_IMPL__.dispatchEvent(event);
      });
    }
    this.listenerCount++;
  },

  eventListenerWasRemoved(type) {
    this.listenerCount--;
    if (this.listenerCount == 0) {
      APIBroker.setAddonListener(null);
    }
  },

  classID: Components.ID("{8866d8e3-4ea5-48b7-a891-13ba0ac15235}"),
  contractID: "@mozilla.org/addon-web-api/manager;1",
  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIDOMGlobalPropertyInitializer])
};

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