/* 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, Ci } = require("chrome");
const { when: unload } = require("./unload");
const { ns } = require("./namespace");
const { on, off, emit, once } = require("./system/events");
const { id } = require("self");

const subscribers = ns();
const cache = [];

/**
 * Topics specifically available to Jetpack-generated extensions.
 *
 * Using these predefined consts instead of the platform strings is good:
 *   - allows us to scope topics specifically for Jetpacks
 *   - addons aren't dependent on strings nor behavior of core platform topics
 *   - the core platform topics are not clearly named
 *
 */
exports.topics = {
  /**
   * A topic indicating that the application is in a state usable
   * by add-ons.
   */
  APPLICATION_READY: id + "_APPLICATION_READY"
};

function Listener(callback, target) {
  return function listener({ subject, data }) {
    callback.call(target || callback, subject, data);
  }
}

/**
 * Register the given callback as an observer of the given topic.
 *
 * @param   topic       {String}
 *          the topic to observe
 *
 * @param   callback    {Object}
 *          the callback; an Object that implements nsIObserver or a Function
 *          that gets called when the notification occurs
 *
 * @param   target  {Object}  [optional]
 *          the object to use as |this| when calling a Function callback
 *
 * @returns the observer
 */
function add(topic, callback, target) {
  let listeners = subscribers(callback);
  if (!(topic in listeners)) {
    let listener = Listener(callback, target);
    listeners[topic] = listener;

    // Cache callback unless it's already cached.
    if (!~cache.indexOf(callback))
      cache.push(callback);

    on(topic, listener);
  }
};
exports.add = add;

/**
 * Unregister the given callback as an observer of the given topic.
 *
 * @param   topic       {String}
 *          the topic being observed
 *
 * @param   callback    {Object}
 *          the callback doing the observing
 *
 * @param   target  {Object}  [optional]
 *          the object being used as |this| when calling a Function callback
 */
function remove(topic, callback, target) {
  let listeners = subscribers(callback);
  if (topic in listeners) {
    let listener = listeners[topic];
    delete listeners[topic];

    // If no more observers are registered and callback is still in cache
    // then remove it.
    let index = cache.indexOf(callback);
    if (~index && !Object.keys(listeners).length)
      cache.splice(index, 1)

    off(topic, listener);
  }
};
exports.remove = remove;

/**
 * Notify observers about something.
 *
 * @param topic   {String}
 *        the topic to notify observers about
 *
 * @param subject {Object}  [optional]
 *        some information about the topic; can be any JS object or primitive
 *
 * @param data    {String}  [optional] [deprecated]
 *        some more information about the topic; deprecated as the subject
 *        is sufficient to pass all needed information to the JS observers
 *        that this module targets; if you have multiple values to pass to
 *        the observer, wrap them in an object and pass them via the subject
 *        parameter (i.e.: { foo: 1, bar: "some string", baz: myObject })
 */
function notify(topic, subject, data) {
  emit(topic, {
    subject: subject === undefined ? null : subject,
    data: data === undefined ? null : data
  });
}
exports.notify = notify;

unload(function() {
  // Make a copy of cache first, since cache will be changing as we
  // iterate through it.
  cache.slice().forEach(function(callback) {
    Object.keys(subscribers(callback)).forEach(function(topic) {
      remove(topic, callback);
    });
  });
})
