/* 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, Cm, Cr, components } = require("chrome");
const ChromeUtils = require("ChromeUtils");
const { ComponentUtils } = require("resource://gre/modules/ComponentUtils.jsm");
const Services = require("Services");

/**
 * This is a nsIChannelEventSink implementation that monitors channel redirects and
 * informs the registered StackTraceCollector about the old and new channels.
 */
const SINK_CLASS_DESCRIPTION = "NetworkMonitor Channel Event Sink";
const SINK_CLASS_ID = components.ID("{e89fa076-c845-48a8-8c45-2604729eba1d}");
const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
const SINK_CATEGORY_NAME = "net-channel-event-sinks";

function ChannelEventSink() {
  this.wrappedJSObject = this;
  this.collectors = new Set();
}

ChannelEventSink.prototype = {
  QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink"]),

  registerCollector(collector) {
    this.collectors.add(collector);
  },

  unregisterCollector(collector) {
    this.collectors.delete(collector);

    if (this.collectors.size == 0) {
      ChannelEventSinkFactory.unregister();
    }
  },

  // eslint-disable-next-line no-shadow
  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
    for (const collector of this.collectors) {
      try {
        collector.onChannelRedirect(oldChannel, newChannel, flags);
      } catch (ex) {
        console.error(
          "StackTraceCollector.onChannelRedirect threw an exception",
          ex
        );
      }
    }
    callback.onRedirectVerifyCallback(Cr.NS_OK);
  },
};

const ChannelEventSinkFactory = ComponentUtils.generateSingletonFactory(
  ChannelEventSink
);

ChannelEventSinkFactory.register = function() {
  const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
  if (registrar.isCIDRegistered(SINK_CLASS_ID)) {
    return;
  }

  registrar.registerFactory(
    SINK_CLASS_ID,
    SINK_CLASS_DESCRIPTION,
    SINK_CONTRACT_ID,
    ChannelEventSinkFactory
  );

  Services.catMan.addCategoryEntry(
    SINK_CATEGORY_NAME,
    SINK_CONTRACT_ID,
    SINK_CONTRACT_ID,
    false,
    true
  );
};

ChannelEventSinkFactory.unregister = function() {
  const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
  registrar.unregisterFactory(SINK_CLASS_ID, ChannelEventSinkFactory);

  Services.catMan.deleteCategoryEntry(
    SINK_CATEGORY_NAME,
    SINK_CONTRACT_ID,
    false
  );
};

ChannelEventSinkFactory.getService = function() {
  // Make sure the ChannelEventSink service is registered before accessing it
  ChannelEventSinkFactory.register();

  return Cc[SINK_CONTRACT_ID].getService(Ci.nsIChannelEventSink)
    .wrappedJSObject;
};
exports.ChannelEventSinkFactory = ChannelEventSinkFactory;
