/* 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";

// Bug 1827382, as this module can be used from the worker thread,
// the following JSM may be loaded by the worker loader until
// we have proper support for ESM from workers.
const {
  startTracing,
  stopTracing,
  addTracingListener,
  removeTracingListener,
} = require("resource://devtools/server/tracer/tracer.jsm");

const { Actor } = require("resource://devtools/shared/protocol.js");
const { tracerSpec } = require("resource://devtools/shared/specs/tracer.js");

const { throttle } = require("resource://devtools/shared/throttle.js");

const {
  TYPES,
  getResourceWatcher,
} = require("resource://devtools/server/actors/resources/index.js");

const LOG_METHODS = {
  STDOUT: "stdout",
  CONSOLE: "console",
};

const CONSOLE_ARGS_STYLES = [
  "color: var(--theme-toolbarbutton-checked-hover-background)",
  "padding-inline: 4px; margin-inline: 2px; background-color: var(--theme-toolbarbutton-checked-hover-background); color: var(--theme-toolbarbutton-checked-hover-color);",
  "",
  "color: var(--theme-highlight-blue); margin-inline: 2px;",
];

const CONSOLE_THROTTLING_DELAY = 250;

class TracerActor extends Actor {
  constructor(conn, targetActor) {
    super(conn, tracerSpec);
    this.targetActor = targetActor;
    this.sourcesManager = this.targetActor.sourcesManager;

    // Flag used by CONSOLE_MESSAGE resources
    this.isChromeContext = /conn\d+\.parentProcessTarget\d+/.test(
      this.targetActor.actorID
    );

    this.throttledConsoleMessages = [];
    this.throttleLogMessages = throttle(
      this.flushConsoleMessages.bind(this),
      CONSOLE_THROTTLING_DELAY
    );
  }

  destroy() {
    this.stopTracing();
  }

  getLogMethod() {
    return this.logMethod;
  }

  startTracing(logMethod = LOG_METHODS.STDOUT) {
    this.logMethod = logMethod;
    this.tracingListener = {
      onTracingFrame: this.onTracingFrame.bind(this),
      onTracingInfiniteLoop: this.onTracingInfiniteLoop.bind(this),
    };
    addTracingListener(this.tracingListener);
    startTracing({
      global: this.targetActor.window || this.targetActor.workerGlobal,
    });
  }

  stopTracing() {
    if (!this.tracingListener) {
      return;
    }
    stopTracing();
    removeTracingListener(this.tracingListener);
    this.logMethod = null;
    this.tracingListener = null;
  }

  onTracingInfiniteLoop() {
    if (this.logMethod == LOG_METHODS.STDOUT) {
      return true;
    }
    const consoleMessageWatcher = getResourceWatcher(
      this.targetActor,
      TYPES.CONSOLE_MESSAGE
    );
    if (!consoleMessageWatcher) {
      return true;
    }

    const message =
      "Looks like an infinite recursion? We stopped the JavaScript tracer, but code may still be running!";
    consoleMessageWatcher.emitMessages([
      {
        arguments: [message],
        styles: [],
        level: "logTrace",
        chromeContext: this.isChromeContext,
        timeStamp: ChromeUtils.dateNow(),
      },
    ]);

    return false;
  }

  /**
   * Called by JavaScriptTracer class when a new JavaScript frame is executed.
   *
   * @param {Debugger.Frame} frame
   *        A descriptor object for the JavaScript frame.
   * @param {Number} depth
   *        Represents the depth of the frame in the call stack.
   * @param {String} formatedDisplayName
   *        A human readable name for the current frame.
   * @param {String} prefix
   *        A string to be displayed as a prefix of any logged frame.
   * @return {Boolean}
   *         Return true, if the JavaScriptTracer should log the frame to stdout.
   */
  onTracingFrame({ frame, depth, formatedDisplayName, prefix }) {
    const { script } = frame;
    const { lineNumber, columnNumber } = script.getOffsetMetadata(frame.offset);
    const url = script.source.url;

    // Ignore blackboxed sources
    if (this.sourcesManager.isBlackBoxed(url, lineNumber, columnNumber)) {
      return false;
    }

    if (this.logMethod == LOG_METHODS.STDOUT) {
      // By returning true, we let JavaScriptTracer class log the message to stdout.
      return true;
    }

    const args = [
      prefix + "—".repeat(depth + 1),
      frame.implementation,
      "⟶",
      formatedDisplayName,
    ];

    // Create a message object that fits Console Message Watcher expectations
    this.throttledConsoleMessages.push({
      filename: url,
      lineNumber,
      columnNumber,
      arguments: args,
      styles: CONSOLE_ARGS_STYLES,
      level: "logTrace",
      chromeContext: this.isChromeContext,
      sourceId: script.source.id,
      timeStamp: ChromeUtils.dateNow(),
    });
    this.throttleLogMessages();

    return false;
  }

  /**
   * This method is throttled and will notify all pending traces to be logged in the console
   * via the console message watcher.
   */
  flushConsoleMessages() {
    const consoleMessageWatcher = getResourceWatcher(
      this.targetActor,
      TYPES.CONSOLE_MESSAGE
    );
    // Ignore the request if the frontend isn't listening to console messages for that target.
    if (!consoleMessageWatcher) {
      return;
    }
    const messages = this.throttledConsoleMessages;
    this.throttledConsoleMessages = [];

    consoleMessageWatcher.emitMessages(messages);
  }
}
exports.TracerActor = TracerActor;
