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

let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
  HomePage: "resource:///modules/HomePage.sys.mjs",
  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

/**
 * AboutHomeStartupCache is responsible for reading and writing the
 * initial about:home document from the HTTP cache as a startup
 * performance optimization. It only works when the "privileged about
 * content process" is enabled and when ENABLED_PREF is set to true.
 *
 * See https://firefox-source-docs.mozilla.org/browser/extensions/newtab/docs/v2-system-addon/about_home_startup_cache.html
 * for further details.
 */
export var AboutHomeStartupCache = {
  ABOUT_HOME_URI_STRING: "about:home",
  SCRIPT_EXTENSION: "script",
  ENABLED_PREF: "browser.startup.homepage.abouthome_cache.enabled",
  PRELOADED_NEWTAB_PREF: "browser.newtab.preload",
  LOG_LEVEL_PREF: "browser.startup.homepage.abouthome_cache.loglevel",

  // It's possible that the layout of about:home will change such that
  // we want to invalidate any pre-existing caches. We do this by setting
  // this meta key in the nsICacheEntry for the page.
  //
  // The version is currently set to the build ID, meaning that the cache
  // is invalidated after every upgrade (like the main startup cache).
  CACHE_VERSION_META_KEY: "version",

  LOG_NAME: "AboutHomeStartupCache",

  // These messages are used to request the "privileged about content process"
  // to create the cached document, and then to receive that document.
  CACHE_REQUEST_MESSAGE: "AboutHomeStartupCache:CacheRequest",
  CACHE_RESPONSE_MESSAGE: "AboutHomeStartupCache:CacheResponse",
  CACHE_USAGE_RESULT_MESSAGE: "AboutHomeStartupCache:UsageResult",

  // When a "privileged about content process" is launched, this message is
  // sent to give it some nsIInputStream's for the about:home document they
  // should load.
  SEND_STREAMS_MESSAGE: "AboutHomeStartupCache:InputStreams",

  // This time in ms is used to debounce messages that are broadcast to
  // all about:newtab's, or the preloaded about:newtab. We use those
  // messages as a signal that it's likely time to refresh the cache.
  CACHE_DEBOUNCE_RATE_MS: 5000,

  // This is how long we'll block the AsyncShutdown while waiting for
  // the cache to write. If we fail to write within that time, we will
  // allow the shutdown to proceed.
  SHUTDOWN_CACHE_WRITE_TIMEOUT_MS: 1000,

  // The following values are as possible values for the
  // browser.startup.abouthome_cache_result scalar. Keep these in sync with the
  // scalar definition in Scalars.yaml and the matching Glean metric in
  // browser/components/metrics.yaml. See setDeferredResult for more
  // information.
  CACHE_RESULT_SCALARS: {
    UNSET: 0,
    DOES_NOT_EXIST: 1,
    CORRUPT_PAGE: 2,
    CORRUPT_SCRIPT: 3,
    INVALIDATED: 4,
    LATE: 5,
    VALID_AND_USED: 6,
    DISABLED: 7,
    NOT_LOADING_ABOUTHOME: 8,
    PRELOADING_DISABLED: 9,
  },

  // This will be set to one of the values of CACHE_RESULT_SCALARS
  // once it is determined which result best suits what occurred.
  _cacheDeferredResultScalar: -1,

  // A reference to the nsICacheEntry to read from and write to.
  _cacheEntry: null,

  // These nsIPipe's are sent down to the "privileged about content process"
  // immediately after the process launches. This allows us to race the loading
  // of the cache entry in the parent process with the load of the about:home
  // page in the content process, since we'll connect the InputStream's to
  // the pipes as soon as the nsICacheEntry is available.
  //
  // The page pipe is for the HTML markup for the page.
  _pagePipe: null,
  // The script pipe is for the JavaScript that the HTML markup loads
  // to set its internal state.
  _scriptPipe: null,
  _cacheDeferred: null,

  _enabled: false,
  _initted: false,
  _hasWrittenThisSession: false,
  _finalized: false,
  _firstPrivilegedProcessCreated: false,

  init() {
    if (this._initted) {
      throw new Error("AboutHomeStartupCache already initted.");
    }

    if (
      Services.startup.isInOrBeyondShutdownPhase(
        Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
      )
    ) {
      // Stay not initted, such that using us will reject or be a no-op.
      return;
    }

    this.setDeferredResult(this.CACHE_RESULT_SCALARS.UNSET);

    this._enabled = Services.prefs.getBoolPref(
      "browser.startup.homepage.abouthome_cache.enabled"
    );

    if (!this._enabled) {
      this.recordResult(this.CACHE_RESULT_SCALARS.DISABLED);
      return;
    }

    this.log = console.createInstance({
      prefix: this.LOG_NAME,
      maxLogLevelPref: this.LOG_LEVEL_PREF,
    });

    this.log.trace("Initting.");

    // If the user is not configured to load about:home at startup, then
    // let's not bother with the cache - loading it needlessly is more likely
    // to hinder what we're actually trying to load.
    let willLoadAboutHome =
      !lazy.HomePage.overridden &&
      Services.prefs.getIntPref("browser.startup.page") === 1;

    if (!willLoadAboutHome) {
      this.log.trace("Not configured to load about:home by default.");
      this.recordResult(this.CACHE_RESULT_SCALARS.NOT_LOADING_ABOUTHOME);
      return;
    }

    if (!Services.prefs.getBoolPref(this.PRELOADED_NEWTAB_PREF, false)) {
      this.log.trace("Preloaded about:newtab disabled.");
      this.recordResult(this.CACHE_RESULT_SCALARS.PRELOADING_DISABLED);
      return;
    }

    Services.obs.addObserver(this, "ipc:content-created");
    Services.obs.addObserver(this, "process-type-set");
    Services.obs.addObserver(this, "ipc:content-shutdown");
    Services.obs.addObserver(this, "intl:app-locales-changed");

    this.log.trace("Constructing pipes.");
    this._pagePipe = this.makePipe();
    this._scriptPipe = this.makePipe();

    this._cacheEntryPromise = new Promise(resolve => {
      this._cacheEntryResolver = resolve;
    });

    let lci = Services.loadContextInfo.default;
    let storage = Services.cache2.diskCacheStorage(lci);
    try {
      storage.asyncOpenURI(
        this.aboutHomeURI,
        "",
        Ci.nsICacheStorage.OPEN_PRIORITY,
        this
      );
    } catch (e) {
      this.log.error("Failed to open about:home cache entry", e);
    }

    this._cacheTask = new lazy.DeferredTask(async () => {
      await this.cacheNow();
    }, this.CACHE_DEBOUNCE_RATE_MS);

    this._shutdownBlocker = async () => {
      await this.onShutdown();
    };

    lazy.AsyncShutdown.appShutdownConfirmed.addBlocker(
      "AboutHomeStartupCache: Writing cache",
      this._shutdownBlocker,
      () => this._cacheProgress
    );

    this._cacheDeferred = null;
    this._initted = true;
    this.log.trace("Initialized.");
  },

  get initted() {
    return this._initted;
  },

  uninit() {
    if (!this._enabled) {
      return;
    }

    try {
      Services.obs.removeObserver(this, "ipc:content-created");
      Services.obs.removeObserver(this, "process-type-set");
      Services.obs.removeObserver(this, "ipc:content-shutdown");
      Services.obs.removeObserver(this, "intl:app-locales-changed");
    } catch (e) {
      // If we failed to initialize and register for these observer
      // notifications, then attempting to remove them will throw.
      // It's fine to ignore that case on shutdown.
    }

    if (this._cacheTask) {
      this._cacheTask.disarm();
      this._cacheTask = null;
    }

    this._pagePipe = null;
    this._scriptPipe = null;
    this._initted = false;
    this._cacheEntry = null;
    this._hasWrittenThisSession = false;
    this._cacheEntryPromise = null;
    this._cacheEntryResolver = null;
    this._cacheDeferredResultScalar = -1;

    if (this.log) {
      this.log.trace("Uninitialized.");
      this.log = null;
    }

    this._procManager = null;
    this._procManagerID = null;
    this._appender = null;
    this._cacheDeferred = null;
    this._finalized = false;
    this._firstPrivilegedProcessCreated = false;

    lazy.AsyncShutdown.appShutdownConfirmed.removeBlocker(
      this._shutdownBlocker
    );
    this._shutdownBlocker = null;
  },

  _aboutHomeURI: null,

  get aboutHomeURI() {
    if (this._aboutHomeURI) {
      return this._aboutHomeURI;
    }

    this._aboutHomeURI = Services.io.newURI(this.ABOUT_HOME_URI_STRING);
    return this._aboutHomeURI;
  },

  // For the AsyncShutdown blocker, this is used to populate the progress
  // value.
  _cacheProgress: "Not yet begun",

  /**
   * Called by the AsyncShutdown blocker on quit-application
   * to potentially flush the most recent cache to disk. If one was
   * never written during the session, one is generated and written
   * before the async function resolves.
   *
   * @param {boolean} withTimeout
   *   Whether or not the timeout mechanism should be used. Defaults
   *   to true.
   * @returns {Promise<boolean>}
   *   If a cache has never been written, or a cache write is in
   *   progress, resolves true when the cache has been written. Also
   *   resolves to true if a cache didn't need to be written.
   *
   *   Resolves to false if a cache write unexpectedly timed out.
   */
  async onShutdown(withTimeout = true) {
    // If we never wrote this session, arm the task so that the next
    // step can finalize.
    if (!this._hasWrittenThisSession) {
      this.log.trace("Never wrote a cache this session. Arming cache task.");
      this._cacheTask.arm();
    }

    Glean.browserStartup.abouthomeCacheShutdownwrite.set(
      this._cacheTask.isArmed
    );

    if (this._cacheTask.isArmed) {
      this.log.trace("Finalizing cache task on shutdown");
      this._finalized = true;

      // To avoid hanging shutdowns, we'll ensure that we wait a maximum of
      // SHUTDOWN_CACHE_WRITE_TIMEOUT_MS millseconds before giving up.
      const TIMED_OUT = Symbol();
      let timeoutID = 0;

      let timeoutPromise = new Promise(resolve => {
        timeoutID = lazy.setTimeout(
          () => resolve(TIMED_OUT),
          this.SHUTDOWN_CACHE_WRITE_TIMEOUT_MS
        );
      });

      let promises = [this._cacheTask.finalize()];
      if (withTimeout) {
        this.log.trace("Using timeout mechanism.");
        promises.push(timeoutPromise);
      } else {
        this.log.trace("Skipping timeout mechanism.");
      }

      let result = await Promise.race(promises);
      this.log.trace("Done blocking shutdown.");
      lazy.clearTimeout(timeoutID);
      if (result === TIMED_OUT) {
        this.log.error("Timed out getting cache streams. Skipping cache task.");
        return false;
      }
    }
    this.log.trace("onShutdown is exiting");
    return true;
  },

  /**
   * Called by the _cacheTask DeferredTask to actually do the work of
   * caching the about:home document.
   *
   * @returns {Promise<undefined>}
   *   Resolves when a fresh version of the cache has been written.
   */
  async cacheNow() {
    this.log.trace("Caching now.");
    this._cacheProgress = "Getting cache streams";

    let { pageInputStream, scriptInputStream } = await this.requestCache();

    if (!pageInputStream || !scriptInputStream) {
      this.log.trace("Failed to get cache streams.");
      this._cacheProgress = "Failed to get streams";
      return;
    }

    this.log.trace("Got cache streams.");

    this._cacheProgress = "Writing to cache";

    try {
      this.log.trace("Populating cache.");
      await this.populateCache(pageInputStream, scriptInputStream);
    } catch (e) {
      this._cacheProgress = "Failed to populate cache";
      this.log.error("Populating the cache failed: ", e);
      return;
    }

    this._cacheProgress = "Done";
    this.log.trace("Done writing to cache.");
    this._hasWrittenThisSession = true;
  },

  /**
   * Requests the cached document streams from the "privileged about content
   * process".
   *
   * @returns {Promise<object>}
   *   Resolves with an Object with the following properties:
   *
   *   pageInputStream (nsIInputStream)
   *     The page content to write to the cache, or null if request the streams
   *     failed.
   *
   *   scriptInputStream (nsIInputStream)
   *     The script content to write to the cache, or null if request the streams
   *     failed.
   */
  requestCache() {
    this.log.trace("Parent is requesting Activity Stream state object.");

    if (!this._initted) {
      this.log.error("requestCache called despite not initted!");
      return { pageInputStream: null, scriptInputStream: null };
    }

    if (!this._procManager) {
      this.log.error("requestCache called with no _procManager!");
      return { pageInputStream: null, scriptInputStream: null };
    }

    if (
      this._procManager.remoteType != lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
    ) {
      this.log.error("Somehow got the wrong process type.");
      return { pageInputStream: null, scriptInputStream: null };
    }

    let state = lazy.AboutNewTab.activityStream.store.getState();
    return new Promise(resolve => {
      this._cacheDeferred = resolve;
      this.log.trace("Parent is requesting cache streams.");
      this._procManager.sendAsyncMessage(this.CACHE_REQUEST_MESSAGE, { state });
    });
  },

  /**
   * Helper function that returns a newly constructed nsIPipe instance.
   *
   * @returns {nsIPipe}
   */
  makePipe() {
    let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
    pipe.init(
      true /* non-blocking input */,
      true /* non-blocking output */,
      0 /* segment size */,
      0 /* max segments */
    );
    return pipe;
  },

  get pagePipe() {
    return this._pagePipe;
  },

  get scriptPipe() {
    return this._scriptPipe;
  },

  /**
   * Called when the nsICacheEntry has been accessed. If the nsICacheEntry
   * has content that we want to send down to the "privileged about content
   * process", then we connect that content to the nsIPipe's that may or
   * may not have already been sent down to the process.
   *
   * In the event that the nsICacheEntry doesn't contain anything usable,
   * the nsInputStreams on the nsIPipe's are closed.
   */
  connectToPipes() {
    this.log.trace(`Connecting nsICacheEntry to pipes.`);

    // If the cache doesn't yet exist, we'll know because the version metadata
    // won't exist yet.
    let version;
    try {
      this.log.trace("");
      version = this._cacheEntry.getMetaDataElement(
        this.CACHE_VERSION_META_KEY
      );
    } catch (e) {
      if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
        this.log.debug("Cache meta data does not exist. Closing streams.");
        this.pagePipe.outputStream.close();
        this.scriptPipe.outputStream.close();
        this.setDeferredResult(this.CACHE_RESULT_SCALARS.DOES_NOT_EXIST);
        return;
      }

      throw e;
    }

    this.log.info("Version retrieved is", version);

    if (version != Services.appinfo.appBuildID) {
      this.log.info("Version does not match! Dooming and closing streams.\n");
      // This cache is no good - doom it, and prepare for a new one.
      this.clearCache();
      this.pagePipe.outputStream.close();
      this.scriptPipe.outputStream.close();
      this.setDeferredResult(this.CACHE_RESULT_SCALARS.INVALIDATED);
      return;
    }

    let cachePageInputStream;

    try {
      cachePageInputStream = this._cacheEntry.openInputStream(0);
    } catch (e) {
      this.log.error("Failed to open main input stream for cache entry", e);
      this.pagePipe.outputStream.close();
      this.scriptPipe.outputStream.close();
      this.setDeferredResult(this.CACHE_RESULT_SCALARS.CORRUPT_PAGE);
      return;
    }

    this.log.trace("Connecting page stream to pipe.");
    lazy.NetUtil.asyncCopy(
      cachePageInputStream,
      this.pagePipe.outputStream,
      () => {
        this.log.info("Page stream connected to pipe.");
      }
    );

    let cacheScriptInputStream;
    try {
      this.log.trace("Connecting script stream to pipe.");
      cacheScriptInputStream =
        this._cacheEntry.openAlternativeInputStream("script");
      lazy.NetUtil.asyncCopy(
        cacheScriptInputStream,
        this.scriptPipe.outputStream,
        () => {
          this.log.info("Script stream connected to pipe.");
        }
      );
    } catch (e) {
      if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
        // For some reason, the script was not available. We'll close the pipe
        // without sending anything into it. The privileged about content process
        // will notice that there's nothing available in the pipe, and fall back
        // to dynamically generating the page.
        this.log.error("Script stream not available! Closing pipe.");
        this.scriptPipe.outputStream.close();
        this.setDeferredResult(this.CACHE_RESULT_SCALARS.CORRUPT_SCRIPT);
      } else {
        throw e;
      }
    }

    this.setDeferredResult(this.CACHE_RESULT_SCALARS.VALID_AND_USED);
    this.log.trace("Streams connected to pipes.");
  },

  /**
   * Called when we have received a the cache values from the "privileged
   * about content process". The page and script streams are written to
   * the nsICacheEntry.
   *
   * This writing is asynchronous, and if a write happens to already be
   * underway when this function is called, that latter call will be
   * ignored.
   *
   * @param {nsIInputStream} pageInputStream
   *   A stream containing the HTML markup to be saved to the cache.
   * @param {nsIInputStream} scriptInputStream
   *   A stream containing the JS hydration script to be saved to the cache.
   * @returns {Promise<undefined, Error>}
   *   When the cache has been successfully written to.

   *   Rejects with a JS Error if writing any part of the cache happens to
   *   fail.
   */
  async populateCache(pageInputStream, scriptInputStream) {
    await this.ensureCacheEntry();

    await new Promise((resolve, reject) => {
      // Doom the old cache entry, so we can start writing to a new one.
      this.log.trace("Populating the cache. Dooming old entry.");
      this.clearCache();

      this.log.trace("Opening the page output stream.");
      let pageOutputStream;
      try {
        pageOutputStream = this._cacheEntry.openOutputStream(0, -1);
      } catch (e) {
        reject(e);
        return;
      }

      this.log.info("Writing the page cache.");
      lazy.NetUtil.asyncCopy(pageInputStream, pageOutputStream, pageResult => {
        if (!Components.isSuccessCode(pageResult)) {
          this.log.error("Failed to write page. Result: " + pageResult);
          reject(new Error(pageResult));
          return;
        }

        this.log.trace(
          "Writing the page data is complete. Now opening the " +
            "script output stream."
        );

        let scriptOutputStream;
        try {
          scriptOutputStream = this._cacheEntry.openAlternativeOutputStream(
            "script",
            -1
          );
        } catch (e) {
          reject(e);
          return;
        }

        this.log.info("Writing the script cache.");
        lazy.NetUtil.asyncCopy(
          scriptInputStream,
          scriptOutputStream,
          scriptResult => {
            if (!Components.isSuccessCode(scriptResult)) {
              this.log.error("Failed to write script. Result: " + scriptResult);
              reject(new Error(scriptResult));
              return;
            }

            this.log.trace(
              "Writing the script cache is done. Setting version."
            );
            try {
              this._cacheEntry.setMetaDataElement(
                "version",
                Services.appinfo.appBuildID
              );
            } catch (e) {
              this.log.error("Failed to write version.");
              reject(e);
              return;
            }
            this.log.trace(`Version is set to ${Services.appinfo.appBuildID}.`);
            this.log.info("Caching of page and script is done.");
            resolve();
          }
        );
      });
    });

    this.log.trace("populateCache has finished.");
  },

  /**
   * Returns a Promise that resolves once the nsICacheEntry for the cache
   * is available to write to and read from.
   *
   * @returns {Promise<nsICacheEntry, string>}
   *   Resolves once the cache entry has become available.
   *
   *   Rejects with an error message if getting the cache entry is attempted
   *   before the AboutHomeStartupCache component has been initialized.
   */
  ensureCacheEntry() {
    if (!this._initted) {
      return Promise.reject(
        "Cannot ensureCacheEntry - AboutHomeStartupCache is not initted"
      );
    }

    return this._cacheEntryPromise;
  },

  /**
   * Clears the contents of the cache.
   */
  clearCache() {
    this.log.trace("Clearing the cache.");
    this._cacheEntry = this._cacheEntry.recreate();
    this._cacheEntryPromise = new Promise(resolve => {
      resolve(this._cacheEntry);
    });
    this._hasWrittenThisSession = false;
  },

  /**
   * Clears the contents of the cache, and then completely uninitializes the
   * AboutHomeStartupCache caching mechanism until the next time it's
   * initialized (which outside of testing scenarios, is the next browser
   * start).
   */
  clearCacheAndUninit() {
    if (this._enabled && this.initted) {
      this.log.trace("Clearing the cache and uninitializing.");
      this.clearCache();
      this.uninit();
    }
  },

  /**
   * Called when a content process is created. If this is the "privileged
   * about content process", then the cache streams will be sent to it.
   *
   * @param {number} childID
   *   The unique ID for the content process that was created, as passed by
   *   ipc:content-created.
   * @param {ProcessMessageManager} procManager
   *   The ProcessMessageManager for the created content process.
   * @param {nsIDOMProcessParent} processParent
   *   The nsIDOMProcessParent for the tab.
   */
  onContentProcessCreated(childID, procManager, processParent) {
    if (procManager.remoteType == lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE) {
      if (this._finalized) {
        this.log.trace(
          "Ignoring privileged about content process launch after finalization."
        );
        return;
      }

      if (this._firstPrivilegedProcessCreated) {
        this.log.trace(
          "Ignoring non-first privileged about content processes."
        );
        return;
      }

      this.log.trace(
        `A privileged about content process is launching with ID ${childID}.`
      );

      this.log.info("Sending input streams down to content process.");
      let actor = processParent.getActor("BrowserProcess");
      actor.sendAsyncMessage(this.SEND_STREAMS_MESSAGE, {
        pageInputStream: this.pagePipe.inputStream,
        scriptInputStream: this.scriptPipe.inputStream,
      });

      procManager.addMessageListener(this.CACHE_RESPONSE_MESSAGE, this);
      procManager.addMessageListener(this.CACHE_USAGE_RESULT_MESSAGE, this);
      this._procManager = procManager;
      this._procManagerID = childID;
      this._firstPrivilegedProcessCreated = true;
    }
  },

  /**
   * Called when a content process is destroyed. Either it shut down normally,
   * or it crashed. If this is the "privileged about content process", then some
   * internal state is cleared.
   *
   * @param {number} childID
   *   The unique ID for the content process that was created, as passed by
   *   ipc:content-shutdown.
   */
  onContentProcessShutdown(childID) {
    this.log.info(`Content process shutdown: ${childID}`);
    if (this._procManagerID == childID) {
      this.log.info("It was the current privileged about process.");
      if (this._cacheDeferred) {
        this.log.error(
          "A privileged about content process shut down while cache streams " +
            "were still en route."
        );
        // The crash occurred while we were waiting on cache input streams to
        // be returned to us. Resolve with null streams instead.
        this._cacheDeferred({ pageInputStream: null, scriptInputStream: null });
        this._cacheDeferred = null;
      }

      this._procManager.removeMessageListener(
        this.CACHE_RESPONSE_MESSAGE,
        this
      );
      this._procManager.removeMessageListener(
        this.CACHE_USAGE_RESULT_MESSAGE,
        this
      );
      this._procManager = null;
      this._procManagerID = null;
    }
  },

  /**
   * Called externally by ActivityStreamMessageChannel anytime
   * a message is broadcast to all about:newtabs, or sent to the
   * preloaded about:newtab. This is used to determine if we need
   * to refresh the cache.
   */
  onPreloadedNewTabMessage() {
    if (!this._initted || !this._enabled) {
      return;
    }

    if (this._finalized) {
      this.log.trace("Ignoring preloaded newtab update after finalization.");
      return;
    }

    this.log.trace("Preloaded about:newtab was updated.");

    this._cacheTask.disarm();
    this._cacheTask.arm();
  },

  /**
   * Stores the CACHE_RESULT_SCALARS value that most accurately represents
   * the current notion of how the cache has operated so far. It is stored
   * temporarily like this because we need to hear from the privileged
   * about content process to hear whether or not retrieving the cache
   * actually worked on that end. The success state reported back from
   * the privileged about content process will be compared against the
   * deferred result scalar to compute what will be recorded to
   * Telemetry.
   *
   * Note that this value will only be recorded if its value is GREATER
   * than the currently recorded value. This is because it's possible for
   * certain functions that record results to re-enter - but we want to record
   * the _first_ condition that caused the cache to not be read from.
   *
   * @param {number} result
   *   One of the CACHE_RESULT_SCALARS values. If this value is less than
   *   the currently recorded value, it is ignored.
   */
  setDeferredResult(result) {
    if (this._cacheDeferredResultScalar < result) {
      this._cacheDeferredResultScalar = result;
    }
  },

  /**
   * Records the final result of how the cache operated for the user
   * during this session to Telemetry.
   *
   * @param {number} result
   *   One of the result constants from CACHE_RESULT_SCALARS.
   */
  recordResult(result) {
    // Note: this can be called very early on in the lifetime of
    // AboutHomeStartupCache, so things like this.log might not exist yet.
    Glean.browserStartup.abouthomeCacheResult.set(result);
  },

  /**
   * Called when the parent process receives a message from the privileged
   * about content process saying whether or not reading from the cache
   * was successful.
   *
   * @param {boolean} success
   *   True if reading from the cache succeeded.
   */
  onUsageResult(success) {
    this.log.trace(`Received usage result. Success = ${success}`);
    if (success) {
      if (
        this._cacheDeferredResultScalar !=
        this.CACHE_RESULT_SCALARS.VALID_AND_USED
      ) {
        this.log.error(
          "Somehow got a success result despite having never " +
            "successfully sent down the cache streams"
        );
        this.recordResult(this._cacheDeferredResultScalar);
      } else {
        this.recordResult(this.CACHE_RESULT_SCALARS.VALID_AND_USED);
      }

      return;
    }

    if (
      this._cacheDeferredResultScalar ==
      this.CACHE_RESULT_SCALARS.VALID_AND_USED
    ) {
      // We failed to read from the cache despite having successfully
      // sent it down to the content process. We presume then that the
      // streams just didn't provide any bytes in time.
      this.recordResult(this.CACHE_RESULT_SCALARS.LATE);
    } else {
      // We failed to read the cache, but already knew why. We can
      // now record that value.
      this.recordResult(this._cacheDeferredResultScalar);
    }
  },

  QueryInterface: ChromeUtils.generateQI([
    "nsICacheEntryOpenallback",
    "nsIObserver",
  ]),

  /* MessageListener */

  receiveMessage(message) {
    // Only the privileged about content process can write to the cache.
    if (
      message.target.remoteType != lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
    ) {
      this.log.error(
        "Received a message from a non-privileged content process!"
      );
      return;
    }

    switch (message.name) {
      case this.CACHE_RESPONSE_MESSAGE: {
        this.log.trace("Parent received cache streams.");
        if (!this._cacheDeferred) {
          this.log.error("Parent doesn't have _cacheDeferred set up!");
          return;
        }

        this._cacheDeferred(message.data);
        this._cacheDeferred = null;
        break;
      }
      case this.CACHE_USAGE_RESULT_MESSAGE: {
        this.onUsageResult(message.data.success);
        break;
      }
    }
  },

  /* nsIObserver */

  observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "intl:app-locales-changed": {
        this.clearCache();
        break;
      }
      case "process-type-set":
      // Intentional fall-through
      case "ipc:content-created": {
        let childID = aData;
        let procManager = aSubject
          .QueryInterface(Ci.nsIInterfaceRequestor)
          .getInterface(Ci.nsIMessageSender);
        let pp = aSubject.QueryInterface(Ci.nsIDOMProcessParent);
        this.onContentProcessCreated(childID, procManager, pp);
        break;
      }

      case "ipc:content-shutdown": {
        let childID = aData;
        this.onContentProcessShutdown(childID);
        break;
      }
    }
  },

  /* nsICacheEntryOpenCallback */

  onCacheEntryCheck() {
    return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
  },

  onCacheEntryAvailable(aEntry) {
    this.log.trace("Cache entry is available.");

    this._cacheEntry = aEntry;
    this.connectToPipes();
    this._cacheEntryResolver(this._cacheEntry);
  },
};
