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

/*
 * This module enables consumers to register callbacks on every
 * current and future browser window.
 *
 * Usage: EveryWindow.registerCallback(id, init, uninit);
 *        EveryWindow.unregisterCallback(id);
 *
 * id is expected to be a unique value that identifies the
 * consumer, to be used for unregistration. If the id is already
 * in use, registerCallback returns false without doing anything.
 *
 * Each callback will receive the window for which it is presently
 * being called as the first argument.
 *
 * init is called on every existing window at the time of registration,
 * and on all future windows at browser-delayed-startup-finished.
 *
 * uninit is called on every existing window if requested at the time
 * of unregistration, and at the time of domwindowclosed.
 * If the window is closing, a second argument is passed with value `true`.
 */

var initialized = false;
var callbacks = new Map();

function callForEveryWindow(callback) {
  let windowList = Services.wm.getEnumerator("navigator:browser");
  for (let win of windowList) {
    win.delayedStartupPromise.then(() => {
      callback(win);
    });
  }
}

export const EveryWindow = {
  /**
   * The current list of all browser windows whose delayedStartupPromise has resolved
   */
  get readyWindows() {
    return Array.from(Services.wm.getEnumerator("navigator:browser")).filter(
      win => win.gBrowserInit?.delayedStartupFinished
    );
  },

  /**
   * Registers init and uninit functions to be called on every window.
   *
   * @param {string} id A unique identifier for the consumer, to be
   *   used for unregistration.
   * @param {function} init The function to be called on every currently
   *   existing window and every future window after delayed startup.
   * @param {function} uninit The function to be called on every window
   *   at the time of callback unregistration or after domwindowclosed.
   * @returns {boolean} Returns false if the id was taken, else true.
   */
  registerCallback: function EW_registerCallback(id, init, uninit) {
    if (callbacks.has(id)) {
      return false;
    }

    if (!initialized) {
      let addUnloadListener = win => {
        function observer(subject, topic) {
          if (topic == "domwindowclosed" && subject === win) {
            Services.ww.unregisterNotification(observer);
            for (let c of callbacks.values()) {
              c.uninit(win, true);
            }
          }
        }
        Services.ww.registerNotification(observer);
      };

      Services.obs.addObserver(win => {
        for (let c of callbacks.values()) {
          c.init(win);
        }
        addUnloadListener(win);
      }, "browser-delayed-startup-finished");

      callForEveryWindow(addUnloadListener);

      initialized = true;
    }

    callForEveryWindow(init);
    callbacks.set(id, { id, init, uninit });

    return true;
  },

  /**
   * Unregisters a previously registered consumer.
   *
   * @param {string} id The id to unregister.
   * @param {boolean} [callUninit=true] Whether to call the registered uninit
   *   function on every window.
   */
  unregisterCallback: function EW_unregisterCallback(id, callUninit = true) {
    if (!callbacks.has(id)) {
      return;
    }

    if (callUninit) {
      callForEveryWindow(callbacks.get(id).uninit);
    }

    callbacks.delete(id);
  },
};
