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

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  BrowsingContextListener:
    "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs",
  TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
});

/**
 * Enum of possible network cache behaviors.
 *
 * @readonly
 * @enum {CacheBehavior}
 */
export const CacheBehavior = {
  Default: "default",
  Bypass: "bypass",
};

/**
 * The NetworkCacheManager is responsible for managing the cache status (enabling/disabling cache)
 * for navigables. It's meant to be a singleton, and the consumers can use the exported
 * methods to change the cache status or perform the state cleanup.
 *
 * @class NetworkCacheManager
 */
class NetworkCacheManager {
  #contextListener;
  #defaultCacheBehavior;
  #navigableCacheBehaviorMap;

  constructor() {
    this.#contextListener = new lazy.BrowsingContextListener();
    this.#contextListener.on("attached", this.#onContextAttached);

    this.#defaultCacheBehavior = CacheBehavior.Default;
    // WeakMap from navigables to cache behavior settings (CacheBehavior).
    this.#navigableCacheBehaviorMap = new WeakMap();
  }

  destroy() {
    this.#contextListener.off("attached", this.#onContextAttached);
    this.#contextListener.destroy();

    this.cleanup();
  }

  #getLoadFlags(behavior) {
    return behavior === CacheBehavior.Bypass
      ? Ci.nsIRequest.LOAD_BYPASS_CACHE
      : Ci.nsIRequest.LOAD_NORMAL;
  }

  #getWeakMapSize(weakMap) {
    return ChromeUtils.nondeterministicGetWeakMapKeys(weakMap).length;
  }

  #onContextAttached = (eventName, data = {}) => {
    if (this.#defaultCacheBehavior === CacheBehavior.Bypass) {
      this.#setLoadFlagsForBrowsingContext(
        data.browsingContext,
        this.#getLoadFlags(CacheBehavior.Bypass)
      );
    }
  };

  #setDefaultCacheBehavior(behavior) {
    this.#defaultCacheBehavior = behavior;
    this.#navigableCacheBehaviorMap = new WeakMap();

    const loadFlags = this.#getLoadFlags(behavior);

    // Update cache settings for all existing navigables.
    for (const browser of lazy.TabManager.getBrowsers()) {
      this.#setLoadFlagsForBrowsingContext(browser.browsingContext, loadFlags);
    }

    // In case the cache is globally disabled we have to listen to all
    // newly attached contexts and update the cache behavior for them.
    if (this.#defaultCacheBehavior === CacheBehavior.Bypass) {
      this.#contextListener.startListening();
    } else {
      this.#contextListener.stopListening();
    }
  }

  #setLoadFlagsForBrowsingContext(browsingContext, loadFlags) {
    if (browsingContext.defaultLoadFlags !== loadFlags) {
      browsingContext.defaultLoadFlags = loadFlags;
    }
  }

  /**
   * Reset network cache behavior to the default.
   */
  cleanup() {
    this.#setDefaultCacheBehavior(CacheBehavior.Default);

    if (this.#getWeakMapSize(this.#navigableCacheBehaviorMap) === 0) {
      return;
    }

    const loadFlags = this.#getLoadFlags(CacheBehavior.Default);

    for (const browser of lazy.TabManager.getBrowsers()) {
      if (this.#navigableCacheBehaviorMap.has(browser.browsingContext)) {
        this.#setLoadFlagsForBrowsingContext(
          browser.browsingContext,
          loadFlags
        );
      }
    }

    this.#navigableCacheBehaviorMap = new WeakMap();
  }

  /**
   * Update network cache behavior to a provided value
   * and optionally specified contexts.
   *
   * @param {CacheBehavior} behavior
   *     An enum value to set the network cache behavior.
   * @param {Array<BrowsingContext>=} contexts
   *     The list of browsing contexts where the network cache
   *     behaviour should be updated.
   */
  updateCacheBehavior(behavior, contexts = null) {
    if (contexts === null) {
      this.#setDefaultCacheBehavior(behavior);
      return;
    }

    const loadFlags = this.#getLoadFlags(behavior);

    for (const context of contexts) {
      if (this.#navigableCacheBehaviorMap.get(context) === behavior) {
        continue;
      }

      this.#setLoadFlagsForBrowsingContext(context, loadFlags);

      if (behavior === CacheBehavior.Default) {
        this.#navigableCacheBehaviorMap.delete(context);
      } else {
        this.#navigableCacheBehaviorMap.set(context, behavior);
      }
    }
  }
}

// Create a private NetworkCacheManager singleton.
const networkCacheManager = new NetworkCacheManager();

export function updateCacheBehavior(behavior, contexts) {
  return networkCacheManager.updateCacheBehavior(behavior, contexts);
}

export function cleanupCacheBypassState() {
  return networkCacheManager.cleanup();
}
