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

/**
 * A service for adding, removing and notifying observers of notifications.
 * Wraps the nsIObserverService interface.
 *
 * @version 0.2
 */
export var Observers = {
  /**
   * Register the given callback as an observer of the given topic.
   *
   * @param   {string}  topic
   *          the topic to observe
   *
   * @param   {object}  callback
   *          the callback; an Object that implements nsIObserver or a Function
   *          that gets called when the notification occurs
   *
   * @param   {object}  [thisObject]
   *          the object to use as |this| when calling a Function callback
   *
   * @returns the observer
   */
  add(topic, callback, thisObject) {
    let observer = new Observer(topic, callback, thisObject);
    this._cache.push(observer);
    Services.obs.addObserver(observer, topic, true);

    return observer;
  },

  /**
   * Unregister the given callback as an observer of the given topic.
   *
   * @param {string}  topic
   *        the topic being observed
   *
   * @param {object}  callback
   *        the callback doing the observing
   *
   * @param {object}  [thisObject]
   *        the object being used as |this| when calling a Function callback
   */
  remove(topic, callback, thisObject) {
    // This seems fairly inefficient, but I'm not sure how much better
    // we can make it.  We could index by topic, but we can't index by callback
    // or thisObject, as far as I know, since the keys to JavaScript hashes
    // (a.k.a. objects) can apparently only be primitive values.
    let [observer] = this._cache.filter(
      v =>
        v.topic == topic && v.callback == callback && v.thisObject == thisObject
    );
    if (observer) {
      Services.obs.removeObserver(observer, topic);
      this._cache.splice(this._cache.indexOf(observer), 1);
    } else {
      throw new Error("Attempt to remove non-existing observer");
    }
  },

  /**
   * Notify observers about something.
   *
   * @param {string}  topic
   *        the topic to notify observers about
   *
   * @param {object}  [subject]
   *        some information about the topic; can be any JS object or primitive
   *
   * @param {string}  [data] [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 })
   */
  notify(topic, subject, data) {
    subject = typeof subject == "undefined" ? null : new Subject(subject);
    data = typeof data == "undefined" ? null : data;
    Services.obs.notifyObservers(subject, topic, data);
  },

  /**
   * A cache of observers that have been added.
   *
   * We use this to remove observers when a caller calls |remove|.
   *
   * XXX This might result in reference cycles, causing memory leaks,
   * if we hold a reference to an observer that holds a reference to us.
   * Could we fix that by making this an independent top-level object
   * rather than a property of this object?
   */
  _cache: [],
};

function Observer(topic, callback, thisObject) {
  this.topic = topic;
  this.callback = callback;
  this.thisObject = thisObject;
}

Observer.prototype = {
  QueryInterface: ChromeUtils.generateQI([
    "nsIObserver",
    "nsISupportsWeakReference",
  ]),
  observe(subject, topic, data) {
    // Extract the wrapped object for subjects that are one of our wrappers
    // around a JS object.  This way we support both wrapped subjects created
    // using this module and those that are real XPCOM components.
    if (
      subject &&
      typeof subject == "object" &&
      "wrappedJSObject" in subject &&
      "observersModuleSubjectWrapper" in subject.wrappedJSObject
    ) {
      subject = subject.wrappedJSObject.object;
    }

    if (typeof this.callback == "function") {
      if (this.thisObject) {
        this.callback.call(this.thisObject, subject, data);
      } else {
        this.callback(subject, data);
      }
    } else {
      // typeof this.callback == "object" (nsIObserver)
      this.callback.observe(subject, topic, data);
    }
  },
};

function Subject(object) {
  // Double-wrap the object and set a property identifying the wrappedJSObject
  // as one of our wrappers to distinguish between subjects that are one of our
  // wrappers (which we should unwrap when notifying our observers) and those
  // that are real JS XPCOM components (which we should pass through unaltered).
  this.wrappedJSObject = { observersModuleSubjectWrapper: true, object };
}

Subject.prototype = {
  QueryInterface: ChromeUtils.generateQI([]),
  getScriptableHelper() {},
  getInterfaces() {},
};
