/* 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, {
  NetworkUtils:
    "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
});

/**
 * The NetworkResponse class is a wrapper around the internal channel which
 * provides getters and methods closer to fetch's response concept
 * (https://fetch.spec.whatwg.org/#concept-response).
 */
export class NetworkResponse {
  #channel;
  #decodedBodySize;
  #encodedBodySize;
  #fromCache;
  #fromServiceWorker;
  #isCachedCSS;
  #isDataURL;
  #headersTransmittedSize;
  #status;
  #statusMessage;
  #totalTransmittedSize;
  #wrappedChannel;

  /**
   *
   * @param {nsIChannel} channel
   *     The channel for the response.
   * @param {object} params
   * @param {boolean} params.fromCache
   *     Whether the response was read from the cache or not.
   * @param {boolean} params.fromServiceWorker
   *     Whether the response is coming from a service worker or not.
   * @param {boolean} params.isCachedCSS
   *     Whether the response is coming from a cached css file.
   * @param {string=} params.rawHeaders
   *     The response's raw (ie potentially compressed) headers
   */
  constructor(channel, params) {
    this.#channel = channel;
    const {
      fromCache,
      fromServiceWorker,
      isCachedCSS,
      rawHeaders = "",
    } = params;
    this.#fromCache = fromCache;
    this.#fromServiceWorker = fromServiceWorker;
    this.#isCachedCSS = isCachedCSS;
    this.#isDataURL = this.#channel instanceof Ci.nsIDataChannel;
    this.#wrappedChannel = ChannelWrapper.get(channel);

    this.#decodedBodySize = 0;
    this.#encodedBodySize = 0;
    this.#headersTransmittedSize = rawHeaders.length;
    this.#totalTransmittedSize = rawHeaders.length;

    // See https://github.com/w3c/webdriver-bidi/issues/761
    // For 304 responses, the response will be replaced by the cached response
    // between responseStarted and responseCompleted, which will effectively
    // change the status and statusMessage.
    // Until the issue linked above has been discussed and closed, we will
    // cache the status/statusMessage in order to ensure consistent values
    // between responseStarted and responseCompleted.
    this.#status = this.#isDataURL ? 200 : this.#channel.responseStatus;
    this.#statusMessage =
      this.#isDataURL || this.#isCachedCSS
        ? "OK"
        : this.#channel.responseStatusText;
  }

  get decodedBodySize() {
    return this.#decodedBodySize;
  }

  get encodedBodySize() {
    return this.#encodedBodySize;
  }

  get headers() {
    return this.#getHeadersList();
  }

  get headersTransmittedSize() {
    return this.#headersTransmittedSize;
  }

  get fromCache() {
    return this.#fromCache;
  }

  get fromServiceWorker() {
    return this.#fromServiceWorker;
  }

  get mimeType() {
    return this.#getComputedMimeType();
  }

  get protocol() {
    return lazy.NetworkUtils.getProtocol(this.#channel);
  }

  get serializedURL() {
    return this.#channel.URI.spec;
  }

  get status() {
    return this.#status;
  }

  get statusMessage() {
    return this.#statusMessage;
  }

  get totalTransmittedSize() {
    return this.#totalTransmittedSize;
  }

  /**
   * Clear a response header from the responses's headers list.
   *
   * @param {string} name
   *     The header's name.
   */
  clearResponseHeader(name) {
    this.#channel.setResponseHeader(
      name, // aName
      "", // aValue="" as an empty value
      false // aMerge=false to force clearing the header
    );
  }

  /**
   * Set a response header
   *
   * @param {string} name
   *     The header's name.
   * @param {string} value
   *     The header's value.
   * @param {object} options
   * @param {boolean} options.merge
   *     True if the value should be merged with the existing value, false if it
   *     should override it. Defaults to false.
   */
  setResponseHeader(name, value, options) {
    const { merge = false } = options;
    this.#channel.setResponseHeader(name, value, merge);
  }

  setResponseStatus(options) {
    let { status, statusText } = options;
    if (status === null) {
      status = this.#channel.responseStatus;
    }

    if (statusText === null) {
      statusText = this.#channel.responseStatusText;
    }

    this.#channel.setResponseStatus(status, statusText);

    // Update the cached status and statusMessage.
    this.#status = this.#channel.responseStatus;
    this.#statusMessage = this.#channel.responseStatusText;
  }

  /**
   * Set the various response sizes for this response. Depending on how the
   * completion was monitored (DevTools NetworkResponseListener or ChannelWrapper
   * event), sizes need to be retrieved differently.
   * There this is a simple setter and the actual logic to retrieve sizes is in
   * NetworkEventRecord.
   *
   * @param {object} sizes
   * @param {number} sizes.decodedBodySize
   *     The decoded body size.
   * @param {number} sizes.encodedBodySize
   *     The encoded body size.
   * @param {number} sizes.totalTransmittedSize
   *     The total transmitted size.
   */
  setResponseSizes(sizes) {
    const { decodedBodySize, encodedBodySize, totalTransmittedSize } = sizes;
    this.#decodedBodySize = decodedBodySize;
    this.#encodedBodySize = encodedBodySize;
    this.#totalTransmittedSize = totalTransmittedSize;
  }

  /**
   * Return a static version of the class instance.
   * This method is used to prepare the data to be sent with the events for cached resources
   * generated from the content process but need to be sent to the parent.
   */
  toJSON() {
    return {
      decodedBodySize: this.decodedBodySize,
      headers: this.headers,
      headersTransmittedSize: this.headersTransmittedSize,
      encodedBodySize: this.encodedBodySize,
      fromCache: this.fromCache,
      mimeType: this.mimeType,
      protocol: this.protocol,
      serializedURL: this.serializedURL,
      status: this.status,
      statusMessage: this.statusMessage,
      totalTransmittedSize: this.totalTransmittedSize,
    };
  }

  #getComputedMimeType() {
    // TODO: DevTools NetworkObserver is computing a similar value in
    // addResponseContent, but uses an inconsistent implementation in
    // addResponseStart. This approach can only be used as early as in
    // addResponseHeaders. We should move this logic to the NetworkObserver and
    // expose mimeType in addResponseStart. Bug 1809670.
    let mimeType = "";

    try {
      if (this.#isDataURL || this.#isCachedCSS) {
        mimeType = this.#channel.contentType;
      } else {
        mimeType = this.#wrappedChannel.contentType;
      }
      const contentCharset = this.#channel.contentCharset;
      if (contentCharset) {
        mimeType += `;charset=${contentCharset}`;
      }
    } catch (e) {
      // Ignore exceptions when reading contentType/contentCharset
    }

    return mimeType;
  }

  #getHeadersList() {
    const headers = [];

    // According to the fetch spec for data URLs we can just hardcode
    // "Content-Type" header.
    if (this.#isDataURL) {
      headers.push(["Content-Type", this.#channel.contentType]);
    } else {
      this.#channel.visitResponseHeaders({
        visitHeader(name, value) {
          headers.push([name, value]);
        },
      });
    }

    return headers;
  }
}
