/* 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 EventEmitter = require("resource://devtools/shared/event-emitter.js");

const {
  bindActionCreators,
} = require("resource://devtools/client/shared/vendor/redux.js");
const {
  Connector,
} = require("resource://devtools/client/netmonitor/src/connector/index.js");
const {
  configureStore,
} = require("resource://devtools/client/netmonitor/src/create-store.js");
const {
  EVENTS,
} = require("resource://devtools/client/netmonitor/src/constants.js");
const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");

const {
  getDisplayedRequestById,
  getSortedRequests,
} = require("resource://devtools/client/netmonitor/src/selectors/index.js");

/**
 * API object for NetMonitor panel (like a facade). This object can be
 * consumed by other panels, WebExtension API, etc.
 *
 * This object doesn't depend on the panel UI and can be created
 * and used even if the Network panel UI doesn't exist.
 */
class NetMonitorAPI extends EventEmitter {
  constructor() {
    super();

    // Connector to the backend.
    this.connector = new Connector();

    // List of listeners for `devtools.network.onRequestFinished` WebExt API
    this._requestFinishedListeners = new Set();

    // Bind event handlers
    this.onPayloadReady = this.onPayloadReady.bind(this);
  }
  async connect(toolbox) {
    // Bail out if already connected.
    if (this.toolbox) {
      return;
    }

    this.toolbox = toolbox;

    // Configure store/state object.
    this.store = configureStore(
      this.connector,
      this.toolbox.commands,
      this.toolbox.telemetry
    );
    this.actions = bindActionCreators(Actions, this.store.dispatch);

    // Register listener for new requests (utilized by WebExtension API).
    this.on(EVENTS.PAYLOAD_READY, this.onPayloadReady);

    // Initialize connection to the backend. Pass `this` as the owner,
    // so this object can receive all emitted events.
    const connection = {
      toolbox,
      owner: this,
    };

    await this.connector.connect(connection, this.actions, this.store.getState);
  }

  /**
   * Clean up (unmount from DOM, remove listeners, disconnect).
   */
  destroy() {
    this.off(EVENTS.PAYLOAD_READY, this.onPayloadReady);

    this.connector.disconnect();

    if (this.harExportConnector) {
      this.harExportConnector.disconnect();
    }
  }

  // HAR

  /**
   * Support for `devtools.network.getHAR` (get collected data as HAR)
   */
  async getHar() {
    const {
      HarExporter,
    } = require("resource://devtools/client/netmonitor/src/har/har-exporter.js");
    const state = this.store.getState();

    const options = {
      connector: this.connector,
      items: getSortedRequests(state),
    };

    return HarExporter.getHar(options);
  }

  /**
   * Support for `devtools.network.onRequestFinished`. A hook for
   * every finished HTTP request used by WebExtensions API.
   */
  async onPayloadReady(resource) {
    if (!this._requestFinishedListeners.size) {
      return;
    }

    const {
      HarExporter,
    } = require("resource://devtools/client/netmonitor/src/har/har-exporter.js");

    const connector = await this.getHarExportConnector();
    const request = getDisplayedRequestById(
      this.store.getState(),
      resource.actor
    );
    if (!request) {
      console.error("HAR: request not found " + resource.actor);
      return;
    }

    const options = {
      connector,
      includeResponseBodies: false,
      items: [request],
    };

    const har = await HarExporter.getHar(options);

    // There is page so remove the page reference.
    const harEntry = har.log.entries[0];
    delete harEntry.pageref;

    this._requestFinishedListeners.forEach(listener =>
      listener({
        harEntry,
        requestId: resource.actor,
      })
    );
  }

  /**
   * Support for `Request.getContent` WebExt API (lazy loading response body)
   */
  async fetchResponseContent(requestId) {
    return this.connector.requestData(requestId, "responseContent");
  }

  /**
   * Add listener for `onRequestFinished` events.
   *
   * @param {object} listener
   *        The listener to be called it's expected to be
   *        a function that takes ({harEntry, requestId})
   *        as first argument.
   */
  addRequestFinishedListener(listener) {
    this._requestFinishedListeners.add(listener);
  }

  removeRequestFinishedListener(listener) {
    this._requestFinishedListeners.delete(listener);
  }

  hasRequestFinishedListeners() {
    return this._requestFinishedListeners.size > 0;
  }

  /**
   * Separate connector for HAR export.
   */
  async getHarExportConnector() {
    if (this.harExportConnector) {
      // Wait for the connector to be ready to avoid exceptions if this method is called
      // twice during its initialization.
      await this.harExportConnectorReady;
      return this.harExportConnector;
    }

    const connection = {
      toolbox: this.toolbox,
    };

    this.harExportConnector = new Connector();
    this.harExportConnectorReady = this.harExportConnector.connect(connection);

    await this.harExportConnectorReady;
    return this.harExportConnector;
  }

  /**
   * Resends a given network request
   *
   * @param {string} requestId
   *        Id of the network request
   */
  resendRequest(requestId) {
    // Flush queued requests.
    this.store.dispatch(Actions.batchFlush());
    // Send custom request with same url, headers and body as the request
    // with the given requestId.
    this.store.dispatch(Actions.sendCustomRequest(requestId));
  }
}

exports.NetMonitorAPI = NetMonitorAPI;
