/* 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 Targets = require("resource://devtools/server/actors/targets/index.js");

const {
  PAUSE_REASONS,
  STATES: THREAD_STATES,
} = require("resource://devtools/server/actors/thread.js");

// Possible values of breakpoint's resource's `state` attribute
const STATES = {
  PAUSED: "paused",
  RESUMED: "resumed",
};

/**
 * Emit THREAD_STATE resources, which is emitted each time the target's thread pauses or resumes.
 * So that there is two distinct values for this resource: pauses and resumes.
 * These values are distinguished by `state` attribute which can be either "paused" or "resumed".
 *
 * Resume events, won't expose any other attribute other than `resourceType` and `state`.
 *
 * Pause events will expose the following attributes:
 * - why {Object}: Description of why the thread pauses. See ThreadActor's PAUSE_REASONS definition for more information.
 * - frame {Object}: Description of the frame where we just paused. This is a FrameActor's form.
 */
class BreakpointWatcher {
  constructor() {
    this.onPaused = this.onPaused.bind(this);
    this.onResumed = this.onResumed.bind(this);
  }

  /**
   * Start watching for state changes of the thread actor.
   * This will notify whenever the thread actor pause and resume.
   *
   * @param TargetActor targetActor
   *        The target actor from which we should observe breakpoints
   * @param Object options
   *        Dictionary object with following attributes:
   *        - onAvailable: mandatory function
   *          This will be called for each resource.
   */
  async watch(targetActor, { onAvailable }) {
    // When debugging the whole browser (via the Browser Toolbox), we instantiate both content process and window global (FRAME) targets.
    // But the debugger will only use the content process target's thread actor.
    // Thread actor, Sources and Breakpoints have to be only managed for the content process target,
    // and we should explicitly ignore the window global target.
    if (
      targetActor.sessionContext.type == "all" &&
      targetActor.targetType === Targets.TYPES.FRAME &&
      targetActor.typeName != "parentProcessTarget"
    ) {
      return;
    }

    const { threadActor } = targetActor;
    this.threadActor = threadActor;
    this.onAvailable = onAvailable;

    // If this watcher is created during target creation, attach the thread actor automatically.
    // Otherwise it would not pause on anything (especially debugger statements).
    // However, do not attach the thread actor for Workers. They use a codepath
    // which releases the worker on `attach`. For them, the client will call `attach`. (bug 1691986)
    const isTargetCreation = this.threadActor.state == THREAD_STATES.DETACHED;
    if (isTargetCreation && !targetActor.targetType.endsWith("worker")) {
      await this.threadActor.attach({});
    }

    this.isInterrupted = false;

    threadActor.on("paused", this.onPaused);
    threadActor.on("resumed", this.onResumed);

    // For top-level targets, the thread actor may have been attached by the frontend
    // on toolbox opening, and we start observing for thread state updates much later.
    // In which case, the thread actor may already be paused and we handle this here.
    // It will also occurs for all other targets once bug 1681698 lands,
    // as the thread actor will be initialized before the target starts loading.
    // And it will occur for all targets once bug 1686748 lands.
    //
    // Note that we have to check if we have a "lastPausedPacket",
    // because the thread Actor is immediately set as being paused,
    // but the pause packet is built asynchronously and available slightly later.
    // If the "lastPausedPacket" is null, while the thread actor is paused,
    // it is fine to ignore as the "paused" event will be fire later.
    if (threadActor.isPaused() && threadActor.lastPausedPacket()) {
      this.onPaused(threadActor.lastPausedPacket());
    }
  }

  /**
   * Stop watching for breakpoints
   */
  destroy() {
    if (!this.threadActor) {
      return;
    }
    this.threadActor.off("paused", this.onPaused);
    this.threadActor.off("resumed", this.onResumed);
  }

  onPaused(packet) {
    // If paused by an explicit interrupt, which are generated by the
    // slow script dialog and internal events such as setting
    // breakpoints, ignore the event.
    const { why } = packet;
    if (why.type === PAUSE_REASONS.INTERRUPTED && !why.onNext) {
      this.isInterrupted = true;
      return;
    }

    // Ignore attached events because they are not useful to the user.
    if (why.type == PAUSE_REASONS.ALREADY_PAUSED) {
      return;
    }

    this.onAvailable([
      {
        state: STATES.PAUSED,
        why,
        frame: packet.frame.form(),
      },
    ]);
  }

  onResumed() {
    // NOTE: resumed events are suppressed while interrupted
    // to prevent unintentional behavior.
    if (this.isInterrupted) {
      this.isInterrupted = false;
      return;
    }

    this.onAvailable([
      {
        state: STATES.RESUMED,
      },
    ]);
  }
}

module.exports = BreakpointWatcher;
