/* 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, {
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});

/**
 * Listens for interactions in the child process and passes information to the
 * parent.
 */
export class InteractionsChild extends JSWindowActorChild {
  #progressListener;
  #currentURL;

  actorCreated() {
    this.isContentWindowPrivate =
      lazy.PrivateBrowsingUtils.isContentWindowPrivate(this.contentWindow);

    if (this.isContentWindowPrivate) {
      return;
    }

    this.#progressListener = {
      onLocationChange: (webProgress, request, location, flags) => {
        this.onLocationChange(webProgress, request, location, flags);
      },

      QueryInterface: ChromeUtils.generateQI([
        "nsIWebProgressListener2",
        "nsIWebProgressListener",
        "nsISupportsWeakReference",
      ]),
    };

    let webProgress = this.docShell
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebProgress);
    webProgress.addProgressListener(
      this.#progressListener,
      Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
        Ci.nsIWebProgress.NOTIFY_LOCATION
    );
  }

  didDestroy() {
    // If the tab is closed then the docshell is no longer available.
    if (!this.#progressListener || !this.docShell) {
      return;
    }

    let webProgress = this.docShell
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebProgress);
    webProgress.removeProgressListener(this.#progressListener);
  }

  onLocationChange(webProgress, request, location, flags) {
    // We don't care about inner-frame navigations.
    if (!webProgress.isTopLevel) {
      return;
    }

    // If this is a new document then the DOMContentLoaded event will trigger
    // the new interaction instead.
    if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
      return;
    }

    this.#recordNewPage();
  }

  #recordNewPage() {
    if (!this.docShell.currentDocumentChannel) {
      // If there is no document channel, then it is something we're not
      // interested in, but we do need to know that the previous interaction
      // has ended.
      this.sendAsyncMessage("Interactions:PageHide");
      return;
    }

    let docInfo = this.#getDocumentInfo();

    // This may happen when the page calls replaceState or pushState with the
    // same URL. We'll just consider this to not be a new page.
    if (docInfo.url == this.#currentURL) {
      return;
    }

    this.#currentURL = docInfo.url;

    if (
      this.docShell.currentDocumentChannel instanceof Ci.nsIHttpChannel &&
      !this.docShell.currentDocumentChannel.requestSucceeded
    ) {
      return;
    }

    this.sendAsyncMessage("Interactions:PageLoaded", docInfo);
  }

  async handleEvent(event) {
    if (this.isContentWindowPrivate) {
      // No recording in private browsing mode.
      return;
    }
    switch (event.type) {
      case "DOMContentLoaded": {
        this.#recordNewPage();
        break;
      }
      case "pagehide": {
        if (!this.docShell.currentDocumentChannel) {
          return;
        }

        if (!this.docShell.currentDocumentChannel.requestSucceeded) {
          return;
        }

        this.sendAsyncMessage("Interactions:PageHide");
        break;
      }
    }
  }

  /**
   * Returns the current document information for sending to the parent process.
   *
   * @returns {{ isActive: boolean, url: string, referrer: * }?}
   */
  #getDocumentInfo() {
    let doc = this.document;

    let referrer;
    if (doc.referrer) {
      referrer = Services.io.newURI(doc.referrer);
    }
    return {
      isActive: this.manager.browsingContext.isActive,
      url: doc.documentURIObject.specIgnoringRef,
      referrer: referrer?.specIgnoringRef,
    };
  }
}
